JPA(Java Persistence API) 04 - 연관관계(기본)

연관관계 매핑의 핵심 키워드

  • 방향성(Direction) : 단방향, 양방향.
  • 다중성(Multiplicity) : 다대일, 일대다, 일대일, 다대다.
  • 연관관계의 주인(Owner) : 양방향 연관관계인 경우 연관관계의 주인을 지정해야 한다.

객체 연관관계와 테이블 연관관계

연관관계는 테이블 연관관계객체 연관관계로 분류할 수 있다. 테이블은 외래키로 연관관계를 맺으며 객체는 참조로 연관관계를 맺는다.

참조를 통한 객체 연관관계는 언제나 단방향 관계, 외래키를 통한 테이블 연관관계는 양방항 관계이다. 객체 연관관계 중 양방향 관계는 정확히 이야기하면 서로 다른 단방향 관계 2개라고 할 수 있다. 반면 테이블은 외래키 하나로 양방향 관계가 가능하다.

연관관계의 주인(외래키 관리자)

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

연관관계의 주인만이 데이터베이스 연관관계에 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있다. 반면 주인이 아닌 쪽은 읽기만 할 수 있다.

주의할 점은, 연관관계의 주인은 비즈니스 중요도가 아닌 외래키의 위치와 관련해서 결정한다는 것이다. 연관관계의 주인을 정한다는 것은 사실 외래키 관리자를 선택하는 것이며, 따라서 외래 키가 존재하는 테이블로 정해야 한다.

연관관계의 주인은 mappedBy 속성을 사용하지 않고, 주인이 아닌 쪽은 mappedBy 속성을 사용해서 연관관계의 주인을 지정해야 한다. mappedBy 속성에 지정되는 값은 연관관계의 주인인 엔티티의 참조 필드명이다.

다대일 연관관계

단방향

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "member")
public class Member {
// ...

@ManyToOne
@JoinColumn(name = "team_id")
private Team team;

// ...
}

@Entity
@Table(name = "team")
public class Team {
// ...
}

@ManyToOne

다대일 관계를 매핑할 때 사용한다.

  • optional : 기본값 true. false로 설정하면 연관된 엔티티가 반드시 있어야 한다.
  • fetch : 페치 전략을 설정한다.
  • cascade : 영속성 전이 기능을 설정한다.
  • targetEntity : 연관된 엔티티 타입 정보를 설정한다.

@JoinColumn

외래키를 매핑할 때 사용한다. 이 어노테이션은 생략할 수 있다.

  • name : 외래키의 이름을 지정한다. 생략할 경우 외래키를 찾을 때 기본 전략을 사용한다.
  • referencedColumnName : 외래키가 참조하는 대상 테이블의 컬럼명. 기본값은 참조 테이블의 기본키 컬럼명.
  • foreignKey : 외래키 제약조건을 DDL로 직접 지정할 수 있다.

@JoinColumn을 생략하면 외래키를 찾을 때 기본 전략이 있다. 기본 전략은 “필드명_참조테이블컬럼명”이다.

양방향

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name = "team")
public class Team {
// ...

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

// ...
}

다대일 양방향 연관관계는 다대일 단방향 연관관계에서 @ManyToOne을 적용한 반대쪽 엔티티에 @OneToMany를 적용하면 된다.

이 때 mappedaBy 속성을 통해 연관관계의 주인을 지정한다. 위의 예제에서 연관관계의 주인은 Member 엔티티의 team 필드다.

일대다 연관관계

일대다 단방향 관계에서 외래키는 항상 다쪽 테이블에 있기 때문에, 반대편 테이블의 외래키를 관리하는 형태다.

단방향

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "product_group")
public class ProductGroup {
// ...

@OneToMany
@JoinColumn(name = "product_group_id")
private List<Product> products;

// ...
}

@Entity
@Table(name = "product")
public class Product {
// ...
}

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블(Join Table) 전략을 기본으로 사용해서 매핑한다.

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

상황에 따라 다르겠지만 일반적으로 일대다 단방향 매핑보다는 다대일 양방향 매핑을 권장한다.

@OneToMany

일대다 관계를 매핑할 때 사용한다.

  • mappedBy : 연관관계의 주인이 아닌 엔티티에서 사용하며 연관관계의 주인을 지정해야 한다.
  • cascade : 다른 관계 매핑 어노테이션의 cascade 속성과 동일하게 사용.
  • fetch : 다른 관계 매핑 어노테이션의 fetch 속성과 동일하게 사용.
  • orphanRemoval : 고아 객체 제거 기능을 설정한다. 기본값 false.
  • targetEntity : 다른 관계 매핑 어노테이션의 targetEntity 속성과 동일하게 사용.

양방향

일대다 양방향 매핑은 존재하지 않으며 대신 다대일 양방향 매핑을 사용해야 한다. 정확히는 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Table(name = "product")
public class Product {
// ...

@ManyToOne
@JoinColumn(name = "product_group_id", insertable = false, updatable = false)
private ProductGroup productGroup;

// ...
}

하지만 일대다 양방향 매핑이 완전히 불가능한 것은 아닌데, 위와 같이 반대편 엔티티에 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하는 방법이 있다.

일대일 연관관계

일대일 관계는 어느 테이블이나 외래키를 가질 수 있다. 일대일 관계는 주 테이블이나 대상 테이블 중 누가 외래키를 가질지 선택해야 한다. 아래 예제는 주 테이블에 외래 키를 가지는 예제다.

단방향

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@Table(name = "member")
public class Member {
// ...

@OneToOne
@JoinColumn(name = "member_detail_id")
private MemberDetail memberDetail;

// ...
}

@Entity
@Table(name = "member_detail")
public class MemberDetail {
// ...
}

@OneToOne

일대일 관계를 매핑할 때 사용한다.

  • mappedBy : 다른 관계 매핑 어노테이션의 mappedBy 속성과 동일하게 사용.
  • optional : 다른 관계 매핑 어노테이션의 optional 속성과 동일하게 사용.
  • cascade : 다른 관계 매핑 어노테이션의 cascade 속성과 동일하게 사용.
  • fetch : 다른 관계 매핑 어노테이션의 fetch 속성과 동일하게 사용.
  • orphanRemoval : 다른 관계 매핑 어노테이션의 orphanRemoval 속성과 동일하게 사용.
  • targetEntity : 다른 관계 매핑 어노테이션의 targetEntity 속성과 동일하게 사용.

양방향

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name = "member_detail")
public class MemberDetail {
// ...

@OneToOne(mappedBy = "memberDetail")
private Member member;

// ...
}

주 테이블이 외래키를 관리하므로 mappedBy 속성으로 연관관계의 주인을 설정해야 한다. 위의 예제에서 연관관계의 주인은 Member 엔티티의 memberDetail 필드다.

다대다 연관관계

다대다 관계의 경우 테이블 연관관계로 표현할 때 중간 관계 테이블을 생성하여 일대다, 다대일 관계로 풀어낸다.

단방향

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Table(name = "cart")
public class Cart {
// ...

@ManyToMany
@JoinTable(name = "cart_product",
joinColumns = @JoinColumn(name = "product_id"),
inverseJoinColumns = @JoinColumn(name = "cart_id"))
private List<Product> products;

// ...
}

@ManyToMany

다대다 관계를 매핑할 때 사용한다.

  • mappedBy : 다른 관계 매핑 어노테이션의 mappedBy 속성과 동일하게 사용.
  • targetEntity : 다른 관계 매핑 어노테이션의 targetEntity 속성과 동일하게 사용.
  • cascade : 다른 관계 매핑 어노테이션의 cascade 속성과 동일하게 사용.
  • fetch : 다른 관계 매핑 어노테이션의 fetch 속성과 동일하게 사용.

@JoinTable

연결 테이블을 지정하는데 사용한다.

  • name : 연결 테이블명을 지정한다.
  • joinColumns : 현재 엔티티와 매핑할 조인 컬럼 정보를 지정한다.
  • inverseJoinColumns : 반대 엔티티와 매핑할 조인 컬럼 정보를 지정한다.

양방향

1
2
3
4
5
6
7
8
9
10
@Entity
@Table(name = "product")
public class Product {
// ...

@ManyToMany(mappedBy = "products")
private List<Cart> carts;

// ...
}

역방향 엔티티에도 @ManyToMany를 사용하는데, 다대다 관계에서는 연관관계의 주인을 양쪽 중 원하는 곳에 지정하면 된다. 마찬가지로 mappedBy 속성으로 연관관계의 주인을 지정하는데, 위의 경우는 Cart 엔티티의 products 필드를 연관관계의 주인으로 지정한 것이다.

다대다 관계의 한계

@ManyToMany를 사용하여 연결 테이블을 자동으로 생성하면 여러 가지 편리한 점이 있으나 실무에서 사용하기에는 한계가 있다. 보통 연결 테이블에는 양쪽 테이블의 아이디만 담는 것이 아니라 부가적인 컬럼이 필요한데 이 경우에는 @ManyToMany를 사용할 수 없다.

따라서 이런 경우에는 연결 테이블과 별도로 매핑되는 연결 엔티티를 만들고 양쪽 테이블과의 관계는 일대다, 다대일 관계로 풀어야 한다.

위 내용은 김영한님의 자바 ORM 표준 JPA 프로그래밍를 읽으며 개인적으로 요약 및 정리하는 내용이다.
자세한 내용이 알고 싶으면 김영한님의 자바 ORM 표준 JPA 프로그래밍을 직접 읽어보길 추천한다.