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

+ Recent posts