Post

Springboot Jpa - 다양한 연관관계 매핑

DB에는 다양한 연관관계가 있는데, 이를 어떻게 JPA에 적용할 수 있을까?

Springboot Jpa - 다양한 연관관계 매핑

연관관계 매핑시 고려사항 3가지

다중성
4가지 케이스가 존재(다대다, 일대다, 다대일, 일대일)
헷갈릴 수 있는데, JPA에서 제공되는 annotation은 모두 DB와 매핑하기 위한 것이므로, DB관점에서의 다중성을 기준으로 고민하면된다.
만약, 이 경우에는 다중성 중 어느부분이지? 와 같이 헷갈린다면, 반대의 경우를 중점으로 생각해보자! 다중성은 대칭성이 있다! 다대다 - 다대다, 일대일 - 일대일, 일대다 - 다대일
다대다 관계는 실무에서 쓰면 안좋다. (아래 설명)
단방향, 양방향
테이블은 이전 글에서 설명한 것처럼 방향의 개념이 없고, 외래 키 하나로 양쪽 조인이 가능하다.
참조용 필드가 있는 쪽으로만 참조 가능한데, 한쪽만 참조를 하면 단방향이고, 이 단방향을 양쪽에 설정하면 양방향(정확히는 양방향 처럼 보이는)이다.
연관관계의 주인
객체의 양방향 관계는 2곳이라, 둘 중 한 곳에서 외래키를 관리할 곳을 지정해야한다.
연관관계의 주인(=외래키를 관리할 곳)은 외래 키를 관리하는 객체로 하면, 주인의 반대편은 외래 키에 영향을 주지않고, 단순 조회만 할 수 있게 된다.

이 부분이 아직 명확하지 않다면, 이전 글을 보도록 하자.

다대일 [N:1]

단방향

다대일 단방향의 설계 가장 많이 사용되는 다대일 단방향 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
public class Member{
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Team team;
    
    @Column(name = "USERNAME")
    private String username;
}

다대일 단방향 구조 코드

  • 가장 많이 사용하는 연관관계
  • 다대일의 반대는 일대다

양방향

다대일 양방향의 설계 다대일의 단방향 구조를 양방향으로 변경시

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Team{
    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
    
    @Column(name = "NAME")
    private String name;
}

다대일 양방향 구조 코드

  • 외래 키가 있는 쪽이 연관관계의 주인
  • 양쪽을 서로 참조하도록 개발
    위 예시의 단방향 구조에서 한쪽 방향만 더 추가, Teammembers를 추가
  • 단방향에서 양방향으로 변경시 DB 테이블에 영향을 주지 않는다. 외래 키 관리를 단방향 설계시 방식으로 유지하기 때문에

일대다 [1:N]

단방향

일대다 단방향의 설계 일대다 단방향 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
public class Member {
  @Id
  @GeneratedValue
  @Column(name = "MEMBER_ID")
  private Long id;

  @Column(name = "USERNAME")
  private String username;
}

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

  @OneToMany
  @JoinColumn(name = "TEAM_ID")
  private List<Member> members = new ArrayList<>();

  @Column(name = "NAME")
  private String name;
}

다대일 단방향 구조 코드, Member.team삭제 & Team.membersmappedBy가 아닌 @JoinColumn을 이용

  • 표준 스펙에서는 지원하는 모델이지만, 권장하지 않는 모델
    • DB 설계는 N에서 외래키를 관리해야하는데, 이러한 방식은 1에서 관리되기 때문에 (N에서 외래키를 관리시, 데이터의 중복이 발생한다.)
  • 연관관계의 주인은 1이 연관관계의 주인이다.
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조다.
  • 이런 관계라면, 유지보수가 어려워지기 때문에 약간의 트레이드 오프를 감수하더라도 1:N 의 구조로 간다.
    유지보수가 어려워지는 이유 - 1의 데이터를 수정하면(=Team.members를 수정하면) N의 테이블을 수정시, 쿼리로그만 보고 직관적으로 문제로 생각하게 된다. Team.members 추가시, MEMBER테이블에 유저를 추가하고, update를 통해 팀의 정보가 MEMBERTEAM_ID가 반영된다.
    트레이드 오프 - 객체지향적으로 손해가 발생할 수 있다. 비즈니스 로직이 Member.getTeam()을 하지 않거나 Team.members()를 더 많이 하는데, 1:N 모델을 이용하지 않고 차라리 N:1에서 양방향을 설정한다.
  • @JoinColumn을 추가하지 않으면 조인 테이블 방식(1과 N 사이에 관계를 관리하는 테이블을 생성해 사용)을 사용하게 된다.

양방향

일대다 단방향의 설계 일대다의 단방향 구조를 양방향으로 변경시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class Member {
  @Id
  @GeneratedValue
  @Column(name = "MEMBER_ID")
  private Long id;

  @Column(name = "USERNAME")
  private String username;
  
  @ManyToOne
  @JoinColumn(name = "TEAM_ID", inserable =false, updateable=false)
  private Team team;
}

일대다 양방향 구조 코드, Member만 변경

  • 표준 스펙에서 지원되는 방식은 아니다.
  • @JoinColumn을 사용해서 매핑하는데, inserable값과 updateable값을 false로 지정해 읽기 전용으로 가져온다.
    이 설정을 하지 않으면, 연관관계의 주인이 양쪽으로 되는 상황이되어 오류가 발생할 수 있으니 주의!!!

일대일 [1:1]

  • 반대의 관계도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
    둘 중 어느곳이든 한 곳에서 관리하면 된다.
  • 외래 키에 DB 유니크 제약 조건 추가
    안해도 되지만 그러면 application에서 관리를 엄청 잘해야한다.

주 테이블에서 외래 키 관리시

단방향

일대일 단방향의 설계 주 테이블에서 외래 키 관리시, 일대일의 단방향 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Locker{
  @Id @GeneratedValue
  private Long id;
  
  private String name;
}
@Entity
public class Member {
  @Id
  @GeneratedValue
  @Column(name = "MEMBER_ID")
  private Long id;
  
  @OneToOne
  @JoinColum(name = "LOCKER_ID")
  private Locker locker;
} 

일대일의 단방향 구조

  • 다대일 관계와 유사하게 설정하면 된다.

양방향

일대다 단방향의 설계 일대일의 단방향 구조를 양방향으로 변경시

1
2
3
4
5
6
7
8
9
10
@Entity
public class Locker{
  @Id @GeneratedValue
  private Long id;
  
  private String name;
  
  @OneToOne(mappedBy = "locker")
  private Member member;
}

일대일의 단방향 구조, Locker만 수정하면 된다.

  • 다대일 관계와 유사하게 mappedBy를 이용해 설정하면 된다.
  • 다대일 양방향과 같이 외래 키가 있는 곳이 연관관계의 주인이다.

대상 테이블에서 외래 키 관리시

단방향

일대일 단방향의 설계 주 테이블에서 외래 키 관리시, 일대일의 양방향 구조

  • 지원이 되지 않아 불가능

양방향

일대일 단방향의 설계 대상 테이블에서 외래 키 관리시, 일대일의 양방향 구조로 변경

  • 일대일 주 테이블의 외래 키 양방향과 매핑 방법이 같다.

정리

일대일 관계에서 외래 키의 관리 주체가 누구인지에 따라 장단점이 있고, 정해진 정답은 없다. 지금은 회명 1명당 1개의 라커를 갖지만, 이후 1명당 N개의 라커를 소유하게 되는 비즈니스 로직이 될 수도 있고, 여러명의 회원이 1개의 라커를 나눠서 가지게 되는 비즈니스 로직이 될 수도 있다. 이건 적용하는 곳의 비즈니스 로직과 이후 비즈니스 로직에 따라 DB를 설계하고 그에 맞춰 가는게 좋다. 그렇지만 양쪽의 가능성이 다 있거나, 아직까지 이후 비즈니스 로직이 어떻게 바뀔지 모르는 부분이라면 주 테이블에서 관리를 하도록 하는 편이 JPA로 일대일 관계를 다루기에 좀 더 편리하니 좋지 않을까 싶다. 아래에 각 케이스에 대해 장단점을 정리해보았다.

주 테이블에서 외래 키 관리
주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾음
객체지향 개발자 선호
JPA 매핑 편리
장점 - 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 되는 것이 좋다.
단점 - 값이 없으면 외래 키에 NULL값을 허용해야한다.
대상 테이블에 외래 키
대상 테이블에 외래 키가 존재
전통적인 DB 개발자 선호
장점 - 주 테이블과 대상 테이블이 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지할 수 있다.
단점 - 프록시 기능의 한계로, 지연로딩으로 설정해도 항상 즉시 로딩된다. Member 조회시 JPA가 값을 세팅할 때, MemberLocker에 데이터가 있어야하는지 없어야하는지를 MEMBER테이블만 보고 알 수가 없기 때문에 항상 LOCKER테이블에서 where로 해당 멤버의 ID를 지정해 조회한다.

다대다 [N:N]

DB에서 다대다 관계 DB에서 다대다 관계를 일대다,다대일의 관계로 해결하는 방법

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
연결 테이블을 추가해서 일대다와 다대일의 관계로 풀어내야한다.

객체의 다대다 관계를 BD의 일대다, 다대일로 변경 객체의 다대다 관계를 BD의 일대다, 다대일로 변경

그런데 객체는 컬렉션을 이용해 2개의 객체만으로 다대다 관계가 가능하다. 그래서 딜레마의 현상이 생기는데, 이를 JPA가 지원하는 @ManyToMany를 이용해서 DB의 일대다, 다대일 관계로 매핑되도록 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;

@Entity
public class Member {
  @Id
  @GeneratedValue
  private Long id;

  @ManyToMany
  @JoinTable(name = "MEMBER_PRODUCT")
  private List<Product> products = new ArrayList<>();
}

@Entity
public class Product {
  @Id
  @GeneratedValue
  private Long id;

  @ManyToMany(mappedBy = "products")
  private List<Member> members = new ArrayList<>();
}
  • 9 @ManyToMany를 이용해, 다대다 연결
  • 10 @JoinTable을 이용해, DB에서의 중간 테이블 지정
  • 20~21 만약 양방향으로 진행한다면, Product에도 참조 추가 및 mappedBy 설정

한계

한계 다대다 매핑의 한계

  • 편리해보이지만 실무에서는 연결 테이블이 단순히 연결만하고 끝나는 비즈니스 로직이 없기 때문에 사용할 수 없다.
  • 주문시간이나 수량과 같은 데이터가 연결 테이블에 추가가 된다.

극복

다대다 매핑의 한계를 극복하는 방법 다대다 매핑의 한계를 극복하는 방법

  • 연결 테이블용 엔티티를 추가한다. (= 연결 테이블을 엔티티로 승격한다.)
  • DB에 맞게 같이 일대다, 다대일 관계로 변환한다.

상속관계

8-1.png DB의 슈퍼타입-서브타입 논리 모델과 객체의 상속 구조

객체에는 상속관계가 있고, DB에는 상속관계가 없지만 비슷하게 슈퍼타입과 서브타입 관계라는 비슷한 관계가 있다. 그래서 객체의 상속 구조와 DB의 슈퍼서브타입의 관계를 매핑하는것을 상속관계 매핑이라고 한다.

DB의 슈퍼타입 서브타입 논리 모델을 물리 모델로 구현하는 방법

  • 조인 전략: 각각 테이블로 변환해 실제 데이터 조회시 조인해서 가져오는 방법
  • 단일 테이블 전략: 통합 테이블로 변환
  • 구현 클래스마다 테이블 전략: 서브타입 테이블로 변환

이중 어떤 방식으로 DB가 설계 되더라도, JPA는 매핑할 수 있도록 지원해준다.

조인 전략

조인 전략 조인 전략의 구조

ITEM 테이블에서는 공통된 정보(id, 이름, 가격)을 가지고, 각 실제 아이템 종류에 맞게 테이블을 구성하는 방식이다. ITEM 테이블에서는 각 아이템이 어느 정보에 대한 내용인지를 알려주는 DTYPE을 가지고있어, 이걸 JOIN할 테이블을 지정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item {
  @Id @GeneratedValue
  private Long id;
  private int price;
  private String name;
}
@Entity
public class Album extends Item{
	private String artist;
}
@Entity
public class Book extends Item{
  private String author;
  private String isbn;
}
@Entity
public class Movie extends Item{
  private String director;
  private String actor;
}
  • 2 조인 전략시에 설정하려면 JOINED 라고 설정하면 된다.
  • 3 조인 전략시, ITEM테이블에 어떤 데이터인지 구분하는 컬럼을 추가하는 설정이다.
    컬럼의 이름을 따로 지정하지 않으면, DTYPE이름으로 설정되며, 각 class 이름이 들어가게 되는데 이것은 @DiscriminatorValue로 변경할 수 있다.
  • 4Item 클래스를 abstract class로 만들어 테이블이 단독으로 추가되는 일이 없도록 한다.
    추상 클래스가 아니여도 되지만, 단독으로 추가되는 일이 없도록 하기 위함이다.
  • 11,15,20 Book, Album, MoiveItem을 상속을 받는다.

장점

  • DB가 정규화가 되어있다.
  • 외래 키 참조 무결성 제약조건 활용가능하다.
    주문 테이블에서 ITEM을 참조할 때, ITEM 테이블만 참조하면 된다.
  • 저장공간 효율화

단점

  • 조회시 조인을 많이 사용, 성능 저하
  • 조회 쿼리가 복잡함
  • 데이터 저장시 INSERT SQL 2번 호출

단일 테이블 전략

단일 테이블 전략 DB의 슈퍼타입-서브타입 논리 모델과 객체의 상속 구조

논리 모델을 한 테이블로 전부 합치는 방식으로, 앨범, 영화, 책의 모든 정보를 하나의 테이블에 넣어서 관리하는 방식이다. ITEM 테이블에서는 각 아이템이 어느 정보에 대한 내용인지를 알려주는 DTYPE을 가지고있어, 이걸로 데이터의 타입이 무엇인지 구분한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Item {
  @Id @GeneratedValue
  private Long id;
  private int price;
  private String name;
}
@Entity
public class Album extends Item{
	private String artist;
}
@Entity
public class Book extends Item{
  private String author;
  private String isbn;
}
@Entity
public class Movie extends Item{
  private String director;
  private String actor;
}
  • 2 단일 테이블 전략시에 설정하려면 SINGLE_TABLE 라고 설정하면 된다.
  • 3 Item 클래스를 abstract class로 만들어 테이블이 단독으로 추가되는 일이 없도록 한다.
    추상 클래스가 아니여도 되지만, 단독으로 추가되는 일이 없도록 하기 위함이다.
    @DiscriminatorColumn을 설정하지 않아도, 이 값이 없으면 하나의 테이블이라서 구분할 수가 없으므로 자동적으로 들어가게 된다.

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
  • 조회 쿼리 단순

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어 상황에 따라서는 오히려 조회성능이 느릴 수 있다. 이런상황이 되기 위해서는 임계점을 넘어야 한다.(잘 발생하지 않는 경우)

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

구현 클래스마다 테이블 전략 DB의 슈퍼타입-서브타입 논리 모델과 객체의 상속 구조

공통된 정보도 각 종류마다 테이블을 만들어 각자 가지고 있도록 하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
  @Id @GeneratedValue
  private Long id;
  private int price;
  private String name;
}
@Entity
public class Album extends Item{
	private String artist;
}
@Entity
public class Book extends Item{
  private String author;
  private String isbn;
}
@Entity
public class Movie extends Item{
  private String director;
  private String actor;
}
  • 2 단일 테이블 전략시에 설정하려면 TABLE_PER_CLASS 라고 설정하면 된다.
  • 3 반드시 Item 클래스를 abstract class로 만들어 테이블이 단독으로 추가되는 일이 없도록 한다.
    @DiscriminatorColumn을 설정해도 각 테이블로 관리되기 때문에 동작하지 않는다.
단점
객체 지향적인 관점에서 볼 때 각 실제 상품은 Item으로도 조회가 가능해야하는데, 그렇게 할 때 ITEM을 상속받는 객체와 매핑된 모든 테이블을 합쳐 모두 조회한다. 만약 상품의 PK만 아는 경우라고 할 때, 상품의 정보를 조회하기 위해서는 3개의 테이블을 UNION연산 후 조회해 성능이 나쁘다.

장점

  • 서브 타입을 명확하게 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

단점

  • 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
  • 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
  • 자식 테이블을 통합해서 쿼리하기 어려움
    정산이나, 결제 등의 상품에 대한 통합적인 로직을 적용하기 어렵고, 상품의 종류가 추가될 때마다 로직을 수정해야함

MappedSuperclass

만약 사내에서 개발시, 모든 DB 테이블에는 생성일, 생성유저, 수정일, 수정유저가 들어간다고 해보자. 이러면 Entity로 사용되는 모든 클래스에 각 컬럼에 맞는 내용을 추가해야한다. 이처럼 공통 매핑 정보가 필요할 때 사용되는 것이 @MappedSuperClass이다.

MappedSuperClass.png 이 클래스에는 공통적으로 사용되는 속성만 정의하여 중복 코드를 줄이도록한다. 이 설정을 한다고 해서 상속관계 매핑이 되는것도 아니고, @MappedSuperClass를 사용한 클래스는 엔티티도 테이블과 매핑되는것도 아니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@MappedSuperclass
public abstract class BaseEntity {
  private String createBy;
  private LocalDateTime createDate;
  private String updateBy;
  private LocalDateTime updateDate;
}
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item extends BaseEntity{
  @Id @GeneratedValue
  private Long id;
  private int price;
  private String name;
}
  • 1~7 공통으로 필요한 컬럼들을 정의하고 abstract 클래스로 정의한다.
  • 10 모든 Item에 적용될 수 있도록, extends 받는다.
특징
부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
조회, 검색 불가(em.find(BaseEntity) 불가)
직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
@Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능
This post is licensed under CC BY 4.0 by the author.