728x90

깃허브 바로가기
이번 장에서 다루는 내용들이다.

  • 상속 관계 매핑
  • @MappedSuperclass
  • 복합 키와 식별 관계 매핑
  • 조인 테이블
  • 엔티티 하나에 여러 테이블 매핑하기

상속 관계 매핑

RDBMS에는 객체지향 언어처럼 상속이라는 개념이 없다.

대신 그림과 같이 슈퍼타입 서브타입 관계 라는 모델링 기법이 상속 개념과 유사하다.
ORM에서의 상속 관계 매핑은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

이 슈퍼타입 서브타입 논리 모델을 실제 테이블로 구현할 때는 3가지 방법중 하나를 선택할 수 있다.

  • 각각의 테이블로 변환
    • 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라고 한다.
  • 통합 테이블로 변환
    • 테이블을 하나만 사용하여 통합한다. JPA에서는 단일 테이블 전략이라고 한다.
  • 서브타입 테이블로 변환
    • 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략 이라고 한다.

조인 전략

조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 따라서 조회할 때 조인을 자주 사용한다.

❗️ 주의 사항

  • 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다.
    • 타입을 구분하는 컬럼을 추가해야 한다. 여기서는 DTYPE 컬럼을 구분 컬럼으로 사용한다.

예제 코드

@Entity
@Inheritance(strategy = InheritanceType.JOINED) //1
@DiscriminatorColumn(name = "DTYPE") //2
public abstract class Item {
    @Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;

    private String name;
    private int price;
}

@Entity
@DiscriminatorValue("A") // 3
public class Album extends Item{
    private String artist;
}

@Entity
@DiscriminatorValue("M") // 3
public class Movie extends Item{
    private String director;
    private String actor;
}
  • @Inheritance(strategy = InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 @Inheritance 를 사용해야 한다. 그리고 매핑 전략 중 조인 전략을 사용해서 JOINED를 사용
  • @DiscriminatorColumn(name = "DTYPE")
    • 부모 클래스에 구분 컬럼 지정.
    • 이 컬럼으로 자식 테이블 구분 가능.
    • 기본값이 DTYPE이므로 생략 가능하다.
  • @DiscriminatorValue("M")
    • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
    • 어떤 엔티티를 저장하면 구분 컬럼인 DTYPE에 Value 설정한 값이 들어간다.

기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn 을 사용하면 된다.

조인전략 정리

  • 장점
    • 테이블이 정규화됨
    • 외래 키 참조 무결성 제약조건 활용 가능
    • 저장공간 효율적으로 사용
  • 단점
    • 조회할 때 조인이 많이 사용되므로 성능 저하 우려됨
    • 조회 쿼리가 복잡함
    • 데이터를 등록할 INSERT SQL을 두번 실행한다.
  • 특징
    • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 Hibernate를 포함한 몇 구현체는 구분 컬럼 없이도 동작한다.
  • 관련 어노테이션
    • @PrimaryKeyJoinColumn
    • @DiscriminatorColumn
    • @DiscriminatorValue

단일 테이블 전략

이름 그대로 테이블을 하나만 사용
구분 컬럼으로 어떤 자식 데이터가 저장됐는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

❗️ 주의사항

자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.

사용법은 @Inheritance(strategy = InheritanceType.SINGLE_TABLE 을 사용하면 단일 테이블 전략을 사용한다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.

단일 테이블 전략 정리

  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
  • 특징
    • 구분 컬럼을 꼭 사용해야 한다.
    • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

구현 클래스마다 테이블 전략

자식 엔티티마다 테이블을 만든다. 그러면서 자식 테이블 각각에 필요한 컬럼이 모두 있다.

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) 를 사용
일반적으로 추천하지 않는 전략

  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적
    • not null 제약조건 사용 가능
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다(SQL의 UNION을 사용해야 한다)
    • 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 특징
    • 구분 컬럼을 사용하지 않는다.

추천하지 않는 방법이므로, 조인이나 단일 테이블 전략을 고려하자

@MappedSuperclass

상속 관계는 부모,자식 클래스 모두 DB테이블에 매핑 시켰는데 이것은 부모 클래스는 매핑하지 않고 상속받은 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용하는 것이다.
@Entity 와는 다르게 실제 테이블과 매핑되지 않는다.

@MappedSuperclass의 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 직접 사용할 일이 없으므로 추상 클래스로 만들자.

@MappedSuperclass 를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

복합 키와 식별관계 매핑

식별관계 vs 비식별관계

DB 테이블 사이의 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별, 비식별로 구분된다.

식별관계

식별 관계는 부모 테이블의 기본 키를 내려 받아 자식 테이블의 기본키 + 외래키로 사용하는 관계

비식별관계

비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
비식별 관계는 외래키에 NULL을 허용하는지에 따라 필수적, 선택적으로 갈린다.

  • 필수적 비식별 관계
    • 외래 키에 NULL 허용 ❌
    • 연관관계를 필수적으로 맺어야함
  • 선택적 비식별 관계
    • 외래 키에 NULL 허용 ⭕️
    • 연관관계를 맺을지 말지 선택 가능

최근 동향은 DB테이블 설계할 때 비식별 관계를 주로 사용하며 꼭 필요한 곳에만 식별 관계를 사용하는 추세이다. JPA는 모두 지원한다.

복합키 : 비식별 관계 매핑

식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 equals와 hashcode를 override해야 한다.
JPA는 복합키를 지원하기 위해 @IdClass, @EmbeddedId 2가지 방법을 제공하는데 전자는 관계형 DB에 가까운 방법이고 후자는 좀 더 객체지향에 가까운 방법이다.

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;

    @Id
    @Column(name = "PARENT_ID2")
    private String id2;

    private String name;
}

public class ParentId implements Serializable {
    private String id1;
    private String id2;

    public ParentId() {
    }

    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals(Object o) {...}

    @Override
    public int hashCode() {...}
}

@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야 한다.

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현해야 한다.
    • equals, hashCode를 구현해야 함.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

위와같이
Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.

복합키로 조회하는건 아래와 같다.

식별자 클래스인 ParentId를 사용하여 엔티티를 조회함.

@EmbeddedId

다음 조건을 만족해야 한다.

  • @Embeddable 어노테이션을 붙여주어야 함.
  • Serializable 인터페이스 구현해야 함.
  • equals, hashCode 구현해야 함
  • 기본 생성자가 있어야함
  • 식별자 클래스는 public이어야 한다.

복합키와 equals(), hashCode()

영속성 컨텍스트는 엔티티의 식별자로 키를 사용해서 엔티티를 관리한다. 식별자를 비교하는 것으로는 equals, hashCode가 있다. 식별자 객체의 동등성이 지켜지지 않으면, 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생한다.
그래서 복합키는 equals, hashCode를 필수로 구현해야 한다.

@IdClass와 식별관계

필요한 키만큼 JoinColumn으로 자식에서 필요하다면 불러오면 된다.

@EmbeddableId와 식별관계

@MapsId 사용
Id클래스의 변수값을 @MapsId("childId") 식으로 매핑

비식별 관계 구현

@Entity
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;
    ...
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;

    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}

식별관계 복합 키를 사용한 코드에 비하면 코드가 단순하다.

일대일 식별 관계

일대일 식별 관계는 자식 테이블의 기본 키값으로 부모 테이블의 기본 키 값만 사용한다.

식별, 비식별 관계의 장단점

DB설계 관점에서 보면 비식별 관계를 선호하는데 이유는 다음과 같다.

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다.
  • 비식별 관계의 기본 키는 대리 키를 사용하는데 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

ORM 신규 프로젝트 진행 시 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용❗️

그리고 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하자
이유 - 선택적 비식별 관계는 null허용이므로 외부 조인을 사용해야 한다.

조인 테이블

DB테이블의 연관관계 설계 방법

  • 조인 컬럼 사용(외래 키)
    • 테이블 간의 관계는 주로 조인 컬럼이라 부르는 외래 키 컬럼을 사용하여 관리
  • 조인 테이블 사용(테이블 사용)
    • 연관관계를 관리하는 조인 테이블을 추가하고 조인하려는 두 테이블의 외래 키를 가지고 연관관계를 관리함

일대일 조인

일대일 관계를 만드려면 조인 테이블의 외래 키 컬럼 각각에 유니크 제약조건을 걸어야 함.

@Entity
public class Parent {
    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;

    @OneToOne
    @JoinTable(name = "PARENT_CHILD", joinColumns = @JoinColumn(name = "PARENT_ID"), inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
    private Child child;
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
}

@JoinTable의 속성

  • name : 매핑할 조인 테이블 이름
  • joinColumns : 현재 엔티티를 참조하는 외래 키
  • inverseJoinColumns : 반대방향 엔티티를 참조하는 외래 키

일대다 조인 테이블

일대다 관계를 만드려면 조인 테이블의 컬럼중 N과 관련된 컬럼에 유니크 제약조건을 걸어야한다.

다대일 조인 테이블

일대다 에서 방향만 반대이므로 생략하도록 하겠다.

다대다 조인 테이블

다대다 관계를 만드려면 조인 테이블의 두 칼럼을 합해서 하나의 복합 유니크 제약조건을 걸어주어야 한다.


엔티티 하나에 여러 테이블 매핑

@SecondaryTable 을 사용해서 @Table 로 먼저 매핑해주고 난다음 추가로 매핑할 수 있다.

속성

  • @SecondaryTable.name : 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성

여러 테이블에 접목하려면 @Column(table = "두번째 테이블") 로 이어주면 된다.
더 많은 테이블을 매핑하려면 @SecondaryTables 사용

728x90

'JPA' 카테고리의 다른 글

[JPA] 값 타입  (0) 2022.08.05
[JPA] 프록시  (0) 2022.08.05
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
[JPA] 연관관계 매핑  (0) 2022.08.04
728x90

깃허브 바로가기
엔티티 연관관계를 매핑할때는 다음 3가지를 고려해야 한다.

  • 다중성
    • 다대일(@ManyToOne)
    • 일대다(@OneToMany)
    • 일대일(@OneToOne)
    • 다대다(@ManyToMany)
  • 단방향, 양방향
    • 테이블은 외래키 하나로 조인을 사용하여 양방향 쿼리가 가능한데 비해, 객체는 참조용 필드를 가지고 있어야만 연관된 객체 조회가 가능하다. 한쪽만 참조하는것이 단방향, 양쪽 서로 참조하는 것이 양방향이다.
  • 연관관계의 주인
    • 연관관계의 주인은 외래키 관리자이고, fk를 소유하고 있는 테이블을 기준으로 관리자 역할을 주면 된다고 생각한다.

다대일

다대일 관계의 반대 방향은 항상 일대다 관계이고 일대다 관계의 반대 방향은 항상 다대일 관계이다.
DB테이블의 1, N 관계에서 외래 키는 항상 N쪽에 있다.
따라서 객체의 양방향 관계에서 연관관계의 주인은 항상 N쪽이다.

다대일 양방향관계

양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.

아까도 말했듯이 1:N, N:1 연관관계는 항상 N에 외래 키가 있다.

양방향 연관관계는 항상 서로를 참조해야 한다.

양방향 연관관계는 항상 서로 참조해야 한다. 어느 한쪽만 하게되는 순간 양방향 연관관계는 성립되지 않는다.
편의 메소드는 한곳에만 작성하거나 양쪽에 작성을 할 수가 있는데 양쪽에 다 작성하게 되면 무한루프에 빠지므로 주의해야 한다.

일대다

다대일 관계의 반대 방향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수가 있으므로 Java Collection중 Collection, List, Set, Map 중에 하나를 사용해야 한다.

일대다 단방향

하나의 팀은 여러 멤버를 참조할 수 있는데 이런 관계를 일대다 관계라고 한다. 팀은 멤버를 참조하지만 멤버는 팀을 참조하지 않으면 둘의 관계는 단방향이다.

일대다 단방향 매핑의 단점

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점이다.
본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT 한번으로 끝낼 수가 있지만, 다른 테이블에 외래키가 존재한다면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

일대일

일대일 관계는 양쪽이 서로 하나의 관계만 갖는다.

일대일 관계의 특징

  • 일대일 관계는 그 반대도 일대일 관계
  • 테이블 관계에서 일대다, 다대일은 항상 다쪽이 외래키를 가진다. 반면 일대일 관계는 주 테이블이나 대상 테이블 중 어느 곳이나 외래키를 가질 수 있다.
    • 주 테이블에 외래 키
      • 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조함.
      • 장점 - 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
    • 대상 테이블에 외래 키
      • 전통적인 DB개발자들은 보통 대상 테이블에서 외래 키를 두는 것을 선호한다. 이 방법의 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

주의할점❗️

프록시를 사용할 때 외래 키를 직접 관리하지 않는 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩된다.

프록시에 해당하는 설명은 이후 포스팅에서 다루도록 하겠다.

다대다

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
그런데 객체는 테이블과는 다르게 객체 2개로 다대다 관계를 만들 수 있다.

@ManyToMany@JoinTable 을 사용해서 연결 테이블을 바로 매핑한 것이다.

@JoinTable의 속성은 아래와 같다.

  • @JoinTable.name
    • 연결 테이블을 지정한다.
  • @JoinTable.joinColumns
    • 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정한다.
  • @JoinTable.inverseJoinColumns
    • 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정한다.

매핑의 한계와 극복(연결엔티티 사용)

@ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 여러 가지로 편리하다.
연결 테이블에 컬럼을 추가하면 더 이상 @ManyToMany 를 사용할 수 없다.
다른 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.

JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 만들어야 한다. 그리고 엔티티에 @IdClass 를 사용해서 식별자 클래스를 지정하면 된다.
이 식별자 클래스의 특징은 다음과 같다.

  • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
  • Serializable을 구현해야 한다.
  • equals와 hashCode 메소드를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
  • @IdClass 를 사용하는 방법 외에 @EmbeddedId 를 사용하는 방법도 있다.

식별관계

부모 테이블의 기본키를 받아서 자신의 기본키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별 관계라고 한다.

정리

다대일, 일대다, 일대일, 다대다 연관관계 매핑에 관해 살펴봤는데 다대다 연관관계는 사용하기엔 JPA가 다 알아서 처리해주므로 편리했지만 연결 테이블에 필드가 추가된다고 하면 더는 사용할 수 없어서
실무에서 사용하기엔 다대일 매핑이 훨씬 괜찮아 보였다.

728x90

'JPA' 카테고리의 다른 글

[JPA] 프록시  (0) 2022.08.05
[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 연관관계 매핑  (0) 2022.08.04
[JPA] 엔티티 매핑  (0) 2022.08.04
728x90

깃허브 바로가기
엔티티들은 대부분 다른 엔티티와 연관이 있다.

연관관계 매핑 핵심 키워드

  • 방향 : 객체관계에서만 존재하고 실제 테이블 관계는 항상 양방향
    • 단방향 : 두 엔티티중 하나만 참조하는 것이
    • 양방향 : 두 엔티티가 서로 참조하는 것
  • 다중성 : 다대일(N:1), 일대일(1:1), 일대다(1:N), 다대다(N:M)
  • 연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

단방향 연관관계

Member와 Team이 있다고 가정할 때, 팀은 멤버를 여러명 가질 수 있지만, 멤버는 하나의 팀만 가질 수 있는 구조이다. 이것이 다대일관계이다.

public class Member {
    private String id;
    private String name;
    private Team team;
}

public class Team {
    private String id;
    private String name;
}

이런 관계로 있다고 한다면 멤버 객체와 팀 객체는 단방향 관계이다.
멤버는 Member.team 필드를 통해 팀을 알 수 있는데에 반해, 팀은 회원을 알 수가 없는 구조이다.

다시말해,
Get메소드를 추가한다면 member.getTeam() 으로 가능한데 반대는 불가하다는 것이다.

참조를 통한 연관관계는 언제나 단방향 구조이다. 이 객체간의 연관관계를 양방향으로 하고 싶다면 반대에도 필드를 추가해줘서 참조를 보관해야 한다.

그렇게 하려면 위 구조에서 Team을 아래와 같이 바꿔야 한다.

public class Team {
    private String id;
    private String name;
    private Member member;
}

객체는 양방향을 만드려면 서로 참조하게끔 단방향 연관관계를 2개 만들어야 하고, 테이블은 JOIN을 사용하기 때문에 외래 키를 사용하는 테이블의 연관관계는 양방향이다.

JPA매핑

멤버와 팀 객체들을 JPA를 사용하여 매핑해보면 아래와 같다.

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String name;

    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}
  • 객체 연관관계 : 멤버 객체의 Member.team 필드 사용

  • 테이블 연관관계 : 멤버 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용

  • @ManyToOne

    • 이름처럼 다대일 관계라는 매핑 정보들 함축하고 있다. 멤버와 팀은 다대일 관계이다.
      연관관계를 매핑할때 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name = "TEAM_ID") - 외래키를 매핑할 때 사용

    • name속성에는 매핑할 외래 키 이름을 지정. 멤버와 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하여 맞춰주면 된다. 근데 이 어노테이션은 생략 가능하다.

@JoinColumn의 속성

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다.
이 속성은 테이블을 생성할 때만 사용한다.
unique
nullable
insertable
updatable
columnDefinition
table
@Column의 속성과 같다.

@ManyToOne의 속성

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 페치 전략을 설정한다. • @ManyToOne = FetchType.EAGER
• @OneToMany = FetchType.LAZY
cascade 영속성 전이 기능을 사용한다.
targetEntity 연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않는다.
컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계들을 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래키 제약 조건으로 DB에서 오류가 발생한다.
팀을 삭제하려면 팀에 소속된 멤버 연관관계를 먼저 전부 제거해야한다.
그런 뒤에 em.remove(team)을 해줘야 한다.

양방향 연관관계

아까 말했듯 팀은 여러 멤버를 가질 수 있다. 그렇기 때문에 컬렉션을 사용해야한다.
JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

  • 멤버 → 팀 (Member.team)
  • 팀 → 멤버 (Team.members)

DB 테이블은 외래 키 하나로 양방향 조회가 가능하기 때문에 따로 설정해줄 것은 없다.

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

이렇게 설계하여도 테이블은 외래키 하나로 두 테이블의 연관관계를 관리한다.

엔티티를 양방향의 연관관계로 설정하면 객체 참조는 둘인데 외래키는 하나이다.
그래서 둘 사이에 차이가 발생
이러한 이유로 JPA에서는 연관관계중 하나를 정해 테이블의 외래키를 관리해야 한다.
이것을 연관관계의 주인이라고 한다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니라면 mappedBy 속성을 사용하여 속성의 값으로 연관관계의 주인을 지정해야 한다.

그래서 테이블에 외래 키가 존재하는 테이블에 연관관계 관리자 역할을 주어야한다.

주의점

양방향 연관관계는 연관관계의 주인이 외래 키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래 키 값이 정상 입력된다.

그렇기 때문에 주인이 아닌곳에서만 값을 할당한다면 null을 반환하게 되는 결과를 초래한다.
이 부분을 굉장히 주의깊게 사용하여야 할 것이라고 생각한다.

객체까지 고려하여 주인이 아닌 곳에도 값을 할당해줘야 추후에 테스트 코드를 작성했을때 size 0인것을 반환하는 오류를 막을 수 있다.

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자

정리

양방향 매핑은 단방향 매핑에 비해 복잡하다. 복잡한 것에 비해 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

그리고 양방향 매핑 시에는 무한루프에 빠지지 않게 조심해야 한다. 서로가 서로를 무한으로 호출하는지 생각하면서 사용하자.

728x90

'JPA' 카테고리의 다른 글

[JPA] 고급 매핑  (0) 2022.08.04
[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
[JPA] 엔티티 매핑  (0) 2022.08.04
[JPA] 영속성 관리  (0) 2022.08.04
728x90

MockMvc

MockMvc란 실제 객체와 비슷한데 Test를 할때 필요한 기능만 가지는 가짜 객체를 만들어 스프링MVC 동작을 재현할 수 있는 클래스이다.

build.gradle

testCompile('org.springframework.boot:spring-boot-starter-test')

의존성을 추가해준다.

가장 간단한 GET방식을 알아볼 것인데, Http Method의 post, put, patch, delete 들은 어디서 넣으면 테스트 할 수 있는지 별도의 주석으로 첨부하도록 하겠다.

Controller 추가

일단 Controller를 추가해준다.

@RestController
public class MockTestController{

    @GetMapping("/mockTest")
    public String mockTest(@RequestParam String name){
        return name + "님 안녕하세요.";
    }
}

요청 파라미터로 name값을 받아서 안녕하세요를 추가하여 반환해주는 컨트롤러를 작성했다.

Test Code 작성

MockTestController를 테스트하는 클래스를 만들어준다.

@ExtendWith(SpringExtension.class)
@WebMvcTest(MockTestController.class)
public class MockTestControllerTest{
    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("mockMvc 테스트") //이 DisplayName은 임의로 준 것이기 때문에 안써도 무방하다.
    void 테스트_GET() throws Exception{
        MultiValueMap<String, String> initDatas = new LinkedMultiValueMap<>();

        initDatas.add("name", "홍길동");

        mockMvc.perform(get("/mockTest") // get, post, delete, put, patch를 여기서 매칭시킴
               .params(initDatas))
               .andExpect(status().isOk())
               .andExpect(content().string("홍길동님 안녕하세요"))
               .andDo(print());
    }
}

MockMvc 메소드 정리

  • perform() : 요청을 전송할 때 사용함.
    • 결과 값으로 ResultAction 객체를 받고, 이 객체는 반환값을 검증이나 확인을 하는 andExpect() 메소드 제공 perform(RequestBuilder requestBuilder) - 공식문서 참조
  • get("/mockTest") : Http 메소드를 결정하는 곳(get(), post(), delete(), put(), patch())
    • 괄호 안에는 url 경로를 매칭시킨다.
    • contentType(MediaType type) : json parse
    • content(ObjectMapper) : 요청하는 컨텐츠 contentType으로 변경되어 body에 들어감
  • params(initDatas) : 키,값 파라미터 전달함. 한개일때 param(), 여러개일때 params()
  • andExpect() : 응답을 검증
    • status() : 상태코드를 나타냄. 뒤에 메소드 체이닝으로 ResultMatcher 메소드를 잇는다. 여기선 자주 쓰는것만 정리하겠다.
      • isOk() : HttpStatus 200
      • isCreated() : 201
      • isNotFound() : 404
      • isInternalServerError() : 500
      • is(int status) : HttpStatus 직접 할당
      • view()
        • 뷰 이름 검증
        • view().name("aaa") : 뷰 이름이 aaa인가
      • content() : 응답정보에 대한 검증
        • string() : 괄호안 문자를 포함하는지
  • andDo(print()) : test 응답 결과에 대한 모든 것을 출력한다.

이런식으로 mocking을 하여 테스트를 진행할 수 있다.
이것으로 http method를 다양하게 테스트 해볼 수 있게 되었다.

728x90

'Java' 카테고리의 다른 글

[JPA] 객체 지향 쿼리 심화  (0) 2022.08.05
Effective Java 1장  (0) 2022.08.05
Mock, Mockito  (0) 2022.08.04
[Java] Enum  (0) 2022.08.03
728x90

Mock

Mock이란 테스트 더블 이라고도 하며, 실제 사용되어야 하는 객체의 대역을 의미한다.
Mock객체는 대상의 행위를 검증하는데에 있어서 사용하기 때문에 객체가 가지고 있어야하는 기본 정보를 반드시 가지고 있다. 테스트를 해야하는 어떠한 객체를 검증해야 할 경우 그 객체가 가지고 있는 정보들을 미리 갖고 있어야 한다.

Mock객체를 직접 생성하는 경우에는 일일이 클래스를 만들기 어려울 것이다. 그러므로 우리는 Mockito라는 라이브러리를 이용한다.

Mockito란?

단위 테스트를 위한 Java Mocking Framework이다.
또한 Mockito는 Junit위에서 동작하며 Mocking과 Verification을 도와주는 Framework이다.

의존성 추가

build.gradle에 다음과 같이 추가해준다.

testImplementation org.mockito.mockito-all:1.9.5

기입 후에 build를 해주면 mockito 라이브러리가 추가되고 사용할 수 있게 된다.

개인적 견해

내가 생각할 때 Mockito는 if의 뜻과 비슷하다고 생각한다.
그래서 만약 XXX가 있다면~ XXX을 대입해달라 라는 느낌으로 생각하며 코드를 작성하니 이해하기가 되게 쉬웠다.

사람을 관리하는 저장소인 PersonRepository가 있다고 가정한다.

PersonRepository.java

public interface PersonRepository extends JpaRepository<Person, Long> {
    List<Person> findByName(String name);
}

PersonServiceTest.java

여기서 얘기했던 만약 ~가 있다면 방법으로 코드를 짜보았다.

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {
    @Mock
    private PersonRepository personRepository;

    @Test
    void getPeopleByName(){
        when(personRepository.findByName("lsj")) // 만약 lsj라는 이름이 있다면 가정
                             .thenReturn(Lists.newArrayList(new Person("lsj"))); //이 객체를 돌려줘

        List<Person> result = personService.getPeopleByName("lsj");

        assertThat(result.size()).isEqualTo(1);
        assertThat(result.get(0)).isEqualTo("lsj");
    }
}

이런식으로 존재하면 돌려달라는 식으로 코드를 진행하여
객체가 반환되었으면 assertThat으로 size가 1인지 그리고 내가 달라했던 lsj라는 문자열 이름값이 들었는지 확인하는 검증을 진행하였다.

지금은 단순한 Mock과 Mockito에 대해서만 다루었지만 다음 포스팅에서는 MockMvc를 다뤄보는 포스팅을 올리도록 하겠다.

728x90

'Java' 카테고리의 다른 글

Effective Java 1장  (0) 2022.08.05
MockMvc  (0) 2022.08.04
[Java] Enum  (0) 2022.08.03
[Java] Optional  (0) 2022.08.03
728x90

깃허브 바로가기
JPA에서 가장 중요한 것은 엔티티와 테이블 매핑을 정확히 하는 것이다.
매핑 어노테이션을 잘 숙지해야 한다.

  • 객체와 테이블 매핑 : @Entity, @Table
  • 기본 키 매핑 : @Id
  • 필드와 컬럼 매핑 : @Column
  • 연관관계 매핑 : @ManyToOne, @JoinColumn

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
@Entity가 붙은 클래스는 JPA가 관리하는 것으로 엔티티라고 부른다.

속성 기능 기본값
name JPA에서 사용할 엔티티 이름을 정한다. 보통 기본값인 클래스 이름을 사용한다. 만약 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야 한다. 설정하지 않으면 클래스 이름 그대로 사용

@Entity 적용시 주의 사항

  • 기본 생성자는 필수(파라미터가 없는 public or protected 생성자)
    • Lombok에서는 @NoArgsConstructor 사용
  • final 클래스, enum, interface, inner 클래스에는 사용할 수 없다.
  • 저장할 필드에 final을 사용하면 안된다.

생성자가 하나도 없을 때 자바는 기본 생성자를 생성하지만, 생성자 오버로딩을 했을 때는 개발자가 직접 기본 생성자를 만들어줘야 한다.

@Table

@Table은 엔티티와 매핑할 테이블을 지정. 생략시 매핑한 엔티티 이름을 테이블 이름으로 사용

속성 기능 기본값
name 매핑할 테이블 이름 엔티티 이름을 사용
catalog catalog 기능이 있는 DB에서 catalog매핑
schema schema 기능이 있는 DB에서 schema 매핑
uniqueConstraints DDL 생성시 유니크 제약조건을 만듦. 2개 이상의 복합 유니크 제약조건도 만들 수 있다. 스키마 자동생성기능을 사용해서 DDL을 만들때만 사용

다양한 매핑

  • @Enumerated : 자바의 enum을 사용할 때
  • @Temporal : 자바의 날짜타입 매핑
  • @LOB : CLOB, BLOB 타입 매핑

DB 스키마 자동생성

JPA는 DB스키마를 자동 생성하는 기능을 지원.

application.yaml

spring:
  jpa:
    hibernate:
      ddl-auto: create

어플리케이션 실행 시점에 DB테이블 자동으로 생성

옵션 설명
create 기존 테이블을 삭제하고 새로 생성, DROP + CREATE
create-drop create 속성에 추가로 어플리케이션을 종료할때 DDL 제거, DROP + CREATE + DROP
update DB 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정
validate DB 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경로를 남기고 어플리케이션을 실행하지 않는다. 이것은 DDL을 수정하지 않음.
none 자동 생성 기능을 사용하지 않으려면 ddl-auto 속성을 제거하거나 유효하지 않은 옵션 값을 주면 된다.(none은 유효하지 않은 옵션값)

sql보기

spring:
  jpa:
    properties:
      hibernate:
        show_sql: true

DDL생성 기능

@Column 매핑정보의 nullable속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있다. 자동 생성되는 DDL에 문자 크기를 length옵션으로 지정할 수 있다.

@Table 에 유니크 제약조건을 만들어주는 uniqueConstraints 속성은 유니크 제약조건을 말 그대로 걸어주는 것인데 이 속성들은 DDL을 자동 생성할때만 사용되고 JPA 실행 로직에는 영향을 주지 않는다.

직접 DDL을 만들면 사용할 이유가 없는 속성들이다.
그럼에도 이 기능을 사용한다면 엔티티만 보고도 다양한 제약조건을 파악할 수 있다는 장점이 존재

기본키 매핑

@Entity
public class Member{
    @Id
    @Column(name= "ID")
    private String id;

JPA에서 제공하는 DB 기본키 생성 전략

  • 직접 할당 : 기본 키를 어플리케이션에서 직접 할당
  • 자동 생성 : 대리 키 사용 방식
    • IDENTITY : 기본 키 생성을 DB에 위임
    • SEQUENCE : DB시퀀스를 사용하여 기본 키 할당
    • TABLE : 키 생성 테이블을 사용

자동생성 전략이 다양한 이유는 DB벤더마다 지원하는 방식이달라서 이다.
SEQUENCE나 IDENTITY 전략은 사용하는 DB에 의존

기본키를 할당하려면 @Id 사용, 자동생성 전략을 이용하려면 @GeneratedValue를 사용하면 된다.

IDENTITY 전략

기본 키 생성을 DB에 위임하는 전략
MySQL, Postgre, SQL Server, DB2 에서 사용

@Id
@GeneratedValue(strategy = GenertaionType.IDENTITY)
private Long id;

IDENTITY는 데이터를 DB에 insert한 후에 기본 키 값을 조회할 수 있음.
그러므로 엔티티에 식별자 값을 할당하려면 추가로 DB를 조회해야함

SEQUENCE 전략

유일한 값을 순서대로 생성하는 전략
Oracle, Postgre, DB2, H2 에서 사용 가능
@SequenceGenerator 에 시퀀스 생성기를 name이값에 등록하고
sequenceName 속성 이름으로 DB 시퀀스를 매핑함
그러면서
@GenerateValue(strategy = Generation.Type.SEQUENCE, generator = "SequenceGenerator의 name값")
사용하면 시퀀스 생성기 할당됨

@SequenceGenerator

속성 기능 기본값
name 식별자 생성기 이름 필수
sequenceName DB에 등록된 시퀀스 이름 hibernate_sequence
initialValue DDL 생성시에 사용, 시퀀스 DDL 생성할때 처음 시작하는 수 지정 1
allocationSize 시퀀스 한번 호출에 증가하는수(성능최적화에 사용) 50
catalog, schema 데이터베이스 catalog, schema이름

DDL ex - create sequence [sequenceName] start with [initialValue] increment by [allocationSize]

TABLE 전략

키생성 전용 테이블을 하나 만들고 이름과 값으로 사용할 컬럼 만든후 시퀀스를 흉내내는 전략

create table MY_SEQUENCES {
    sequence_name varchar(255) not null,
    next_val bigint,
    primary key (sequence_name)
}

@GeneratorValue(generator = "BOARD_SEQ_GENERATOR")

@TableGenerator

속성 기능 기본값
name 식별자 생성기 이름 필수
table 키생성 테이블명 hibernate_sequences
pkColumnName 시퀀스 컬럼명 sequence_name
valueColumnName 시퀀스 값 컬럼명 next_val
pkColumnValue 키로 사용할 값 이름 엔티티 이름
initialValue 초기 값, 마지막으로 생성된 값이 기준 0
allocationSize 시퀀스 한번 호출에 증가하는 수(성능 최적화) 50
catalog, schema DB catalog, schema 이름
uniqueConstraints(DDL) 유니크 제약 조건 지정

AUTO 전략

선택한 DB의 방언에 따라 Identity, Sequence, Table 전략중 하나 자동으로 설정

기본키 매핑 정리

영속성 컨텍스트는 엔티티를 식별자 값으로 구분하기 때문에 엔티티를 영속 상태로 만드려면 식별자 값은 반드시 포함해야 하는 사실. em.perist()를 호출한 직후 발생하는일을 정리하면 아래와 같다.

  • 직접 할당
    • em.persist() 를 호출하기 전에 애플리케이션에서 직접 식별자 값 할당
    • 없다면 예외발생
  • SEQUENCE
    • DB 시퀀스에서 식별자 값 획득 후 영속성 컨텍스트에 저장
  • TABLE
    • DB 시퀀스 생성용 테이블에서 식별자 값 획득 후 영속성 컨텍스트 저장
  • IDENTITY
    • DB에 엔티티를 저장한 다음 식별자 값 획득 후 영속성 컨텍스트 저장
    • 테이블에 데이터를 저장해야 식별자 값 획득이 가능

레퍼런스

JPA에서 제공하는 필드와 컬럼 매핑용 어노테이션 정리

분류 매핑 어노테이션 설명
필드와 컬럼 매핑
@Column 컬럼을 매핑
@Enumerated 자바 enum 타입 매핑
@Temporal 날짜 타입 매핑
@Lob CLOB, BLOB 타입 매핑
@Transient 특정 필드를 DB에 매핑하지 않음
기타 @Access JPA가 엔티티에 접근하는 방식 지정

@Column

속성 기능 기본값
name 필드와 매핑할 테이블 컬럼이름 객체의 필드이름
nullable(DDL) null값 허용 여부 설정. false설정 시 DDL조건에 not null제약조건 추가됨 true
unique(DDL) 한 컬럼에 간단히 유니크 제약조건 걸 때 사용, 두 컬럼 이상 사용시 @Table.uniqueConstraints사용
ColumnDefinition(DDL) DB 칼럼 정보 직접 줄 수 있음 필드의 자바 타입과 방언 정보를 사용해서 적절한 컬럼 타입 생성
length(DDL) 문자 길이 제약조건, String타입에만 사용 255
precision, scale(DDL) BigDecimal타입에서 사용 precision은 소수점 포함 전체자리수, scale은 소수 자리수, float이나 double타입에는 적용 안됨 precision = 19, scale= 2

안쓰는 것은 구글링을 해서 찾아야겠다.

@Enumerated

  • EnumType.ORDINAL은 enum정의된 순서대로 0, 1 순서로 DB에 저장
    • 장점 : DB에 저장되는 데이터 크기가 작음
    • 단점 : 이미 저장된 enum의 순서를 변경할 수 없음
  • EnumType.STRING은 enum 이름 그대로 DB에 저장
    • 장점 : 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전
    • 단점 : DB에 저장되는 데이터 크기가 ORDINAL에 비해 큼

@Temporal

TemporalType필수 지정

  • value
    • TemporalType.DATE: 날짜, DB date 타입 매핑
    • TemporalType.TIME: 시간, DB time 타입 매핑
    • TemporalType.TIMESTAMP: 날짜와 시간, DB timestamp 타입과 매핑

@Lob

지정할 수 있는 속성이 없음.
대신 매핑하는 필드타입이 문자라면 CLOB으로 매핑 나머지는 BLOB으로 매핑

@Transient

매핑하지 않는 필드, 그렇기 때문에 DB에 저장하지도 않고 조회하지도 않음.
객체에 임시로 어떤 값을 보관하고 싶을 때 사용함.

@Access

엔티티 데이터에 접근하는 방식 지정

  • 필드 접근 : AccessType.FILED로 지정.
    • 필드에 직접 접근, 접근 권한이 private여도 접근 가능
  • 프로퍼티 접근 : AccessType.PROPERTY 로 지정. 접근자(Getter) 사용.

@Access 를 설정하지 않으면 @Id 위치를 기준으로 접근방식 설정

@Id 가 필드에 있으면 @Access(AccessType.FILED) 로 설정한 것과 같음.
그래서 생략 가능

728x90

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계 매핑  (0) 2022.08.04
[JPA] 연관관계 매핑  (0) 2022.08.04
[JPA] 영속성 관리  (0) 2022.08.04
[JPA] JPA 스터디 2장  (0) 2022.08.04
728x90

깃허브 바로가기
이번 발표는 내가 진행하였다. 그래서 이해하기가 더욱 더 쉬웠다.
JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계부분과 매핑한 엔티티를 실제 사용하는 두가지 부분으로 나눌 수 있다.
여기서는 매핑한 엔티티를 엔티티 매니저를 통해 사용하는 것을 알아보자.

영속성 관리

엔티티 매니저는 엔티티를 저장, 수정, 삭제, 조회 등등 엔티티와 관련된 모든 일을 처리한다.
말그대로 엔티티를 관리하는 것이다.

엔티티 매니저 팩토리와 엔티티 매니저

DB를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성한다.
팩토리를 만드는 것으로 생성할 때 비용이 아주 많이 든다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("");

교재에서는 이것을 통해 META-INF/persistence.xml에 persistence-unit name에 매핑된 값을createEntityManager에 넣어준다.

필요할때마다 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하면 된다.
팩토리에서 매니저를 생성하므로, 비용이 작다.

EntityManager em = emf.createEntityManager();

엔티티 매니저 팩토리는 이름 그대로 엔티티 매니저를 만드는 공장이다.
공장을 만드는 것은 상당한 비용이 든다.
그래서 하나만 만들어서 어플리케이션 전체에서 공유하도록 설계가 되어있다.
그렇기 때문에 공장에서 엔티티 매니저를 생성하는 비용은 작다고 하는 것이다.

엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드간에 공유를 해도 되지만,
엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성문제가 발생하므로 스레드 간에 절대 공유해서는 안된다.
일반적인 웹 어플리케이션

그림에서 하나의 EntityManagerFactory에서 다수의 엔티티 매니저를 생성했다. EntityManager1은 아직 DB connection을 하지 않는다. 엔티티 매니저는 DB 연결이 꼭 필요한 시점까지 connection을 얻지 않는다.
보통 트랜잭션을 시작할 때 커넥션을 획득한다.
Hibernate를 포함한 JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀도 만드는데
xml설정은 J2SE에서 사용한 방식이다.

영속성 컨텍스트란?

JPA를 이해하는데에 있어서 가장 중요한 용어가 바로 영속성 컨텍스트 다. 해석하자면 엔티티를 영구 저장하는 환경이다.
EntityManager로 엔티티를 저장하거나 조회하면 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.

em.persist(member);

persist()를 단순히 회원 엔티티를 저장한다고 표현했지만 사실 persist()는 엔티티 매니저를 이용하여 회원 엔티티를 영속성 컨텍스트에 저장한다는 말이다.

영속성 컨텍스트는 물리적인 것이 아니라 논리적인 개념에 가깝기 때문에 볼 수 없다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어진다. 그리고 생성된 매니저를 통해 영속성 컨텍스트에 접근하고 관리할 수가 있다.

엔티티의 생명주기

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태

엔티티 생명주기

비영속

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

엔티티 객체를 생성한 상태. 이 때는, 순수한 객체 상태이며 아직 저장하지 않은 것이다.
따라서 영속성 컨텍스트나 DB와 전혀 관련이 없다. 이것을 비영속 상태라고 한다.

영속

em.persist(member);

엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장한다. 영속성 컨텍스트가 관리하는 엔티티를 영속 상태라고 한다. persist()를 사용하는 순간부터 영속상태에 들어가게 되었다.

결국 영속상태 라는 것은 영속성 컨텍스트에 의해 관리된다 라는 뜻이다.

더불어 em.find()나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태이다.

준영속

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다.
특정 엔티티를 준영속 상태로 만들려면 detach()를 호출하면 된다.
close()를 호출해서 영속성 컨텍스트를 닫거나 clear()를 호출해서 영속성 컨텍스트를 초기화해도 영속성 컨텍스트가 관리하던 영속 상태의 엔티티는 준영속 상태가 된다.

삭제

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다.
remove()를 사용한다.

영속성 컨텍스트의 특징

영속성 컨텍스트와 식별자 값

엔티티를 식별자 값(@Id로 테이블의 PK와 매핑한 값)으로 구분한다.
따라서 영속 상태는 식별자 값이 반드시 있어야 한다. 없으면 예외가 발생한다.

영속성 컨텍스트와 DB저장

JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데
이를 flush라 한다.

영속성 컨텍스트가 엔티티를 관리할때의 장점

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

엔티티 조회

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 한다.
영속 상태의 엔티티는 모두 1차캐시에 저장된다. 내부에 Map이 하나 있는데 키는 @Id로 매핑한 식별자고
값은 엔티티의 인스턴스이다.

// 엔티티 생성(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//엔티티를 영속
em.persist(member);

이코드를 실행하면 1차 캐시에 회원 엔티티를 저장한다. 회원 엔티티는 아직 데이터베이스에 저장되지 않았다.

1차 캐시의 키는 식별자 값이라고 설명했다. 식별자 값은 데이터베이스 기본키와 매핑이 되었다고도 언급했다.
그렇기 때문에 영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스의 기본키 값이다.

Member member = em.find(Member.class, "member1");

find()메소드를 보면 첫번째 매개변수는 엔티티 클래스 타입이고, 두번째 매개변수는 엔티티의 식별자 값이다. find()를 호출하면 1차에서 엔티티를 찾고 만약 찾는 엔티티가 1차 캐시에 없으면 데이터베이스에서 조회한다.

데이터베이스에서 조회

find()를 호출했는데 엔티티가 1차 캐시에 없으면 DB에서 조회하여 엔티티를 생성한다.
생성한 다음 1차 캐시에 저장한 후, 영속 상태의 엔티티를 반환해준다.

영속 엔티티의 동일성 보장

객체를 두개 생성하는데 식별자가 같은 인스턴스를 조회하면 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다.

영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.

  • 동일성
    • 실제 인스턴스가 같다. 참조 값을 비교하는 == 비교의 값이 같다.
  • 동등성
    • 실제 인스턴스가 다를 수 있지만, 인스턴스가 가지고 있는 값이 같다. equals() 메소드로 확인한다.

엔티티 등록

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);

//커밋하는 순간 데이터베이스에 INSERT SQL을 보냄
transaction.commit();

persist()만 계속 진행하였을 경우 커밋 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 모아둔다. 이후 트랜잭션을 커밋할 때 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라고 한다.

엔티티 수정

엔티티의 데이터만 변경했는데 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경감지라고 한다.
스냅샷 : JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사하여 저장해둔다.

  1. 커밋하면 엔티티 매니저 내부에서 플러시가 먼저 호출됨
  2. 엔티티와 스냅샷을 비교하여 변경된 엔티티 탐색
  3. 변경된 엔티티가 있으면 수정쿼리를 생성하여 쓰기 지연 sql저장소에 보낸다
  4. 쓰기지연 저장소의 sql을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션을 커밋한다.

변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용
JPA의 기본 전략은 엔티티의 모든 필드를 업데이트한다.

  • 모든필드를 사용하면 수정 쿼리가 항상 같다.
  • 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.

저장되는 내용이 너무 크면 수정된 데이터만 사용하는
Hibernate의 확장 기능인 @DynamicUpdate를 사용하면 된다.

삭제도 마찬가지로 remove()를 호출해서 사용하는데 쓰기지연 SQL에 모아둔 후에
트랜잭션을 커밋하여 플러시를 호출하면 삭제 쿼리를 DB에 전달한다. remove는 호출하는 순간
영속성 컨텍스트에서 제거시킨다.

플러시

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 역할을 수행

실행했을 때 일어나는 일

  • 변경 감지가 동작하여 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 수정된 엔티티를 탐색
    • 여기서 수정된 엔티티는 수정쿼리를 쓰기 지연 SQL저장소에 등록함
  • 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송

플러시 하는 방법

  • em.flush() 직접 호출
    • 강제로 하는 것이기 때문에 테스트나 다른 프레임워크와 JPA를 사용할때 사용
  • 트랜잭션 커밋시 자동으로 호출
    • 트랜잭션만 커밋하면 DB에 반영되지 않음. 반영해야 하기 때문에 JPA는 자동으로 호출해준다.
  • JPQL 쿼리 실행 시 플러시가 자동 호출
    • JPA와 같이 사용했을 때, 엔티티로 조회하던 객체들은 쓰기지연에 남아있으므로 결과가 조회되지 않는다.
      그래서 플러시를 하여 내용을 DB에 반영해야 한다.

플러시 모드 옵션

  • FlushModeType.AUTO : 커밋이나 쿼리를 실행할때 (기본값)
  • FlushModeType.COMMIT : 커밋할 때만 플러시

준영속

준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

  • em.detach(entity) : 특정엔티티만 준영속 상태로 전환
  • em.clear() : 영속성 컨텍스트를 완전히 초기화
  • em.close() : 영속성 컨텍스트를 종료

detach를 호출하는 순간 1차 캐시부터 쓰기지연 SQL저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다.

준영속 상태의 특징

  • 거의 비영속 상태에 가까움
    • 영속성 컨텍스트가 관리하지 않으므로 1차캐시, 쓰기지연, 변경 감지, 지연 로딩 등 어떠한 기능도 동작하지 않는다.
  • 식별자 값을 가지고 있다.
    • 비영속은 식별자 값이 없을수 있지만 이미 한번 영속되었던 상태이므로 반드시 식별자 값을 가지고 있음.
  • 지연 로딩을 할 수 없다.
    • 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할때 영속성 컨텍스트를 통해 불러오는 방법이 지연로딩인데 준영속은 컨텍스트가 더는 관리하지 않기 때문에 지연로딩에 문제가 발생한다.

병합

준영속 상태의 엔티티를 다시 영속상태로 변경할 때 사용
merge()메소드는 준영속 상태의 엔티티를 받아 그 정보로 새로운 영속 상태의 엔티티를 반환 한다.

병합은 비영속 엔티티도 영속 상태로 만들 수 있다.
병합은 준영속, 비영속을 신경 쓰지 않는다. 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합, 없으면 새로 생성해서 병합. 그래서 병합은 save or update 기능 수행

정리

  • 엔티티 매니저는 엔티티 매니저 팩토리에서 생성.
    • 영속성 컨텍스트는 엔티티 매니저를 통해 접근이 가능
  • 영속성 컨텍스트는 애플리케이션과 DB사이에 객체를 보관하는 가상DB같은 역할
    • 이로 인해 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 사용할 수 있다.
  • 영속성 컨텍스트에 저장한 엔티티는 플러시 시점에 DB에 반영되는데 일반적으로 트랜잭션을 커밋할 때 영속성 컨텍스트가 플러시 된다.
  • 영속성 컨텍스트가 관리하는 엔티티는 영속 상태의 엔티티인데, 컨텍스트가 더이상 관리하지 못하면 그 엔티티는 준영속 상태의 엔티티가 된다. 이 엔티티는 컨텍스트의 관리를 더는 받지 못하므로 영속성 컨텍스트가 제공하는 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능 등을 사용할 수 없다.
728x90

'JPA' 카테고리의 다른 글

[JPA] 연관관계 매핑  (0) 2022.08.04
[JPA] 엔티티 매핑  (0) 2022.08.04
[JPA] JPA 스터디 2장  (0) 2022.08.04
[JPA] JPA 스터디 1장  (0) 2022.08.04

728x90

2장 요약정리
처음 2장은 바로 설치부터 maven 의존성, 라이브러리 설정을 진행한다.
테이블도 생성하는데 이부분은 생략하도록 하겠다.

어노테이션 정리

  • @Entity
    • 이 클래스를 테이블과 매핑한다고 JPA에게 알려줌.
    • 이렇게 @Entity가 사용된 클래스를 엔티티 클래스라고 함
  • @Table
    • 엔티티 클래스에 매핑할 테이블 정보를 알려줌
    • @Table(name = "테이블명") 으로 매핑가능함
    • name속성을 생략하면 클래스 이름을 테이블 이름으로 매핑한다
  • @Id
    • 엔티티 클래스의 필드를 테이블의 기본 키에 매핑한다.
    • @Id가 사용된 필드를 식별자 필드라고 한다.
  • @Column
    • 필드를 칼럼에 매핑한다
    • 매핑 어노테이션이 없는 경우 생략하면 필드명을 사용하여 컬럼명으로 매핑
    • 대소문자를 구분하는 DB를 사용한다면 @Column(name = '칼럼명')으로 명시적 매핑을 해야함

JPA표준 속성

  • JDBC드라이버 : javax.persistence.jdbc.driver
  • 데이터베이스 접속 ID : javax.persistence.jdbc.user
  • 데이터베이스 접속 비밀번호 : javax.persistence.jdbc.password
  • 데이터베이스 접속 url : javax.persistence.jdbc.url

hibernate 속성

  • hibernate.dialect: 데이터베이스 방언 설정 (가장 중요)

이름이 javax.persistence로 시작하는 속성은 JPA 표준 속성으로 특정 구현체에 종속되지 않음
hibernate로 시작하는 속성은 하이버네이트 전용 속성으로 하이버네이트에서만 사용가능

데이터베이스 방언

JPA는 특정 데이터베이스에 종속적이지 않은 기술이다. 그래서 데이터베이스를 쉽게 교체할 수 있다.
그런데 DB마다 sql문법, 함수가 조금씩 다르다.

  • 데이터 타입 - Mysql : VARCHAR, Oracle : VARCHAR2
  • 다른함수 - 문자열 자르는 함수:SUBSTRING Oracle : SUBSTR()
  • 페이징처리 - Mysql : LIMIT, Oracle : ROWNUM

위와 같이 sql 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능을 JPA에서는 방언이라고 한다
이런 종속되는 기능을 사용하면 나중에 데이터베이스 교체가 어렵다.

JPA가 제공하는 표준 문법에 맞춰 JPA를 사용하고, 특정 데이터베이스에 의존적인 SQL은 데이터베이스 방언이 처리해준다.

  • H2: org.hibernate.dialect.H2Dialect
  • Oracle 10g : org.hibernate.dialect.Oracle10gDialect
  • MySQL : org.hibernate.dialect.MYSQL5InnoDBDialect

JPA의 코드는 크게 3가지 부분으로 나뉜다

  • 엔티티 매니저 설정
  • 트랜잭션 관리
  • 비즈니스 로직

build.gradle 설정

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'

JPA의존성과 null체크 유효성검사에 대한 의존성을 추가했다.

엔티티 매니저 설정

EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");

이 엔티티 매니저 팩토리는 데이터베이스 커넥션 풀도 생성할수 있으므로 엔티티 매니저 팩토리를 생성하는 비용이 아주 크다.
엔티티 매니저 팩토리는 어플리케이션 전체에서 딱 한번만 생성하고 공유하여 사용해야 한다.

EntityManager em = emf.createEntityManager();

JPA의 기능 대부분은 이 엔티티 매니저가 제공한다. 대표적으로 이 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 CRUD가 가능하다. 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로
스레드 간에 공유하거나 재사용해서는 안된다.

사용이 끝난 엔티티 매니저는 반드시 종료하여야 한다.

em.close();
emf.close(); //엔티티 매니저 팩토리도 종료하여야 한다.

CRUD

  • 등록 : em.persist('등록할객체');
  • 수정 : 객체.set메소드
  • 삭제 : em.remove(객체)
  • 조회 : 객체 변수명 = em.find(객체.class, 객체에 해당하는 pk)

JPQL

기본적인 CRUD는 JPA를 사용하여 엔티티 객체 중심으로 개발하고 데이터베이스에 대한 처리는 JPA에 맡긴다
그래서 SQL문을 전혀 개발자 자신이 사용하지 않았다.
문제는 검색하는 쿼리이다. JPA는 객체중심으로 개발하므로 검색할 때에도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.
JPA는 JPQL(Java Persistence Query Language)라는 쿼리 언어로 문제를 해결해준다.

JPQL은 SQL을 추상화한 JPA의 객체지향 쿼리 언어이다. SQL문법과 많이 유사해서 select, from, where, group by, having, join 등을 사용할 수 있다.

  • JPQL은 엔티티 객체를 대상으로 쿼리한다. 클래스와 필드를 대상으로 쿼리
  • SQL은 DB 테이블을 대상으로 쿼리한다.

em.createQuery("query String")를 통하여 JPQL 쿼리를 날리면 된다.

여기서 Query String은 엔티티 객체를 다루는 것이지 절대 테이블이 아니다!!!!!

JPQL은 DB 테이블을 전혀 알지 못한다.

  • 순서
    • em.createQuery("QueryString")
    • 쿼리객체 생성된 후 쿼리 객체의 getResultList() 메소드를 호출

정리

JPA를 사용하기 위하여 개발 환경 설정부터 진행하였다. 지금까지의 장점은
내가 지금 진행하고있는 게시판 api 프로젝트에서 사용한 CRUD가 이 JPA로 인해 엄청 편하다는 사실
덕분에 SQL에 의존적인 개발을 피할 수 있고 좀 더 객체지향 관점에서 코드를 볼 수있게 된 것같다.

다음 3주차에서는 영속성 관리에 대해 알아볼 것이다.

728x90

'JPA' 카테고리의 다른 글

[JPA] 엔티티 매핑  (0) 2022.08.04
[JPA] 영속성 관리  (0) 2022.08.04
[JPA] JPA 스터디 1장  (0) 2022.08.04
[JPA] MariaDB Charset 오류 해결  (0) 2022.08.04

+ Recent posts