Posts [자바 ORM 표준 JPA 프로그래밍-기본편] 다양한 연관관계 매핑
Post
Cancel

[자바 ORM 표준 JPA 프로그래밍-기본편] 다양한 연관관계 매핑

본 포스팅은 인프러의 JPA 기본편을 수강하고 정리하는 내용입니다.


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

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

다중성

  • 다대일: @ManyToOne
  • 일대다: @OneToMany
  • 일대일: @OneToOne
  • 다대다: @ManyToMany

위의 관점이 헤깔릴땐 대칭적인 관점으로 보면 쉽다.

일대일의 반대는 일대일, 다대다의 반대는 다대다

일대다의 반대는 다대일, 다대일의 반대는 일대다

Note: 사실 다대다는 실무에서 쓰면 안되는 것이다.

단방향, 양방향

  • 테이블
    • 외래 키 하나로 양쪽 조인 가능
    • 사실 방향이라는 개념이 없음
  • 객체
    • 참조용 필드가 있는 쪽으로만 참조 가능
    • 한쪽만 참조하면 단방향
    • 양쪽이 서로 참조하면 양방향 (사실은 단방향이 2개 있는 것다. 이렇게 접근해야 밑에 있는 연관관계의 주인에 대해 이해할 수 있다)

연관관계의 주인

  • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
  • 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데
  • 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래 키를 관리할 곳을 지정해야함
  • 연관관계의 주인: 외래 키를 관리하는 참조
  • 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능

다대일 [N:1]

다대일 단방향

image

다대일 단방향 정리

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

다대일 양방향

image

다대일 양방향 정리

  • 외래 키가 있는 쪽이 연관관계의 주인
  • 양쪽을 서로 참조하도록 개발

일대다 [1:N]

먼저 위의 모델(일대다)은 권장하진 않지만 표준 스펙에서 지원은 하는 모델이다.

일대다 단방향

image

  • Team은 Member를 아는데 Member는 Team을 알고 싶지 않은 상황이다.
  • DB입장에선 무조건 다(Member)쪽에 외래키가 들어가게 된다. DB설계상 절대적이다.
  • 그러면 Team의 List<Member> 값을 바꼈을때 다른 테이블인 Member 테이블에 업데이트를 쳐줘야 한다.
  • 이게 일대다 단방향 매핑이다.
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

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

@Entity
public class Member {

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

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

    ...
}

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    
        EntityManager em = emf.createEntityManager();
       
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member member = new Member();
            member.setUsername("member1");
            em.persist(member);

            Team team = new Team();
            team.setName("teamA");
            team.getMembers().add(member); // team 엔티티를 저장하는데 member 테이블의 update 쿼리를 날림

            em.persist(team);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

team.getMembers().add(member); 호출시 추가적인 업데이트 쿼리가 발생한다는 성능상 단점이 있다.(치명적인 단점이라 하긴 그렇다.)

하지만 더 심각한점은 실무에서 테이블 수십개가 엮어서 돌아가는 상황에서 일대다 로 설계하면 운영이 힘들어진다. 그래서 이전과 똑같이 다대일 단방향 관계에 필요하면 양방향 추가하는 방식으로 접근하는게 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (USERNAME, MEMBER_ID) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, TEAM_ID) 
        values
            (?, ?)
Hibernate: 
    /* create one-to-many row hellojpa.Team.members */ update
        Member 
    set
        TEAM_ID=? 
    where
        MEMBER_ID=?

실제 위처럼 쿼리가 호출될텐데 insert를 두 번 호출하고 update를 한 번 호출한다? 오해 및 실수의 소지가 있다. Member에서 Team으로 참조가 필요없다하더라도 약간의 손해를 감수하며 다대일 양방향으로 가면 비슷하게 쓸수 있어 좋다.

일대다 단방향 정리

  • 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조 => 권장X
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함 => JoinTable)
    • 추가적으로 @JoinColumn의 name속성은 무조건 넣어주는게 좋다. 시스템에서 알아서 넣긴하지만 굉장히 지저분하다.
  • 일대다 단방향 매핑의 단점
    • 엔티티가 관리하는 외래 키가 다른 테이블에 있음
    • 연관관계 관리를 위해 추가로 UPDATE SQL 실행
  • 결론적으로 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자! (설계가 덜 깔끔해지는걸 감수하더라도 참조를 하나 더 넣자)

일대다 양방향

image

Member에서 Team으로 조회하고자 하면 약간의 야매로 가능하다.(스펙상 되는건 아님)

다쪽에 @JoinColumn(insertable = false, updatable = false) 으로 활용하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Member {

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

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

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false) // 읽기 전용이 되어버림
    private Team team;

    ...
}

일대다 양방향 정리

  • 이런 매핑은 공식적으로 존재X
  • @JoinColumn(insertable=false, updatable=false)
  • 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
  • 결론은 다대일 양방향을 사용하자
    • 설계라는게 테이블 한 두개 있는 것도 아니고 결국 단순해야 한다. 복잡하게 들어가면 다 힘들어진다.

일대일 [1:1]

일대일 관계

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 중에 외래 키 선택 가능
    • 주 테이블에 외래 키
    • 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

일대일: 주 테이블에 외래 키 단방향

image

  • Member 를 주테이블이라고 생각 (둘 중 어디에 넣어도 됨)

여기서 DBA입장에서의 딜레마와 개발자 입장에서의 딜레마가 존재한다.

DBA 입장에서의 딜레마는 다음과 같다. Member와 Locker 어디든 외래키를 둬도 문제는 없다. 하지만 장기적인 미래의 관점에서 나중에 하나의 Member이 여러 개의 Locker를 가질 수 있다고 할 때 Locker에 두는게 여러 개를 insert할 수 있어 편리하다. 만약 Member에 외래키가 있다면 코드나 기능을 많이 변경해야 될 것이다. 반대로 하나의 Locker가 여러 개의 Member를 가질 수 있다면 반대가 될 것이다.

개발자 입장에서의 딜레마는 다음과 같다. Member의 Locker가 있는게 성능도 그렇고 여러 가지로 유리하다. 예를 들어 Member는 거의 필수적으로 조회되는데 조인없이 DB쿼리 한 방으로 Member를 가져왔을 때 Locker가 있냐 없냐를 판단하는게 쉬워진다. DBA 의 선호에 맞춰 잘 가져가면 된다.

일대일: 주 테이블에 외래 키 단방향 정리

다대일(@ManyToOne) 단방향 매핑과 유사

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity
public class Member {

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

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

    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    private Team team;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

    ...
}

일대일: 주 테이블에 외래 키 양방향

image

위의 Member클래스가 있다고 할 때 반대편 Locker에 Member를 가리킬 필드를 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker")
    private Member member;
}

일대일: 주 테이블에 외래 키 양방향 정리

  • 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 적용

일대일: 대상 테이블에 외래 키 단방향

image

반대쪽 사이드에 MEMBER_ID 는 관리 할 수 없다. 지원도 안되고 방법이 없다.

일대일: 대상 테이블에 외래 키 단방향 정리

  • 단방향 관계는 JPA 지원X
  • 양방향 관계는 지원

일대일: 대상 테이블에 외래 키 양방향

image

Locker의 Member 필드를 연관 관계의 주인으로 잡아서 매핑해버리면 된다.

사실 일대일 주 테이블에 외래 키 양방향과 매핑 방법은 같다.

Note: 일대일 관계는 내 엔티티의 외래 키를 직접 관리해야 한다.

일대일 정리

  • 주 테이블에 외래 키
    • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
    • 객체지향 개발자 선호
    • JPA 매핑 편리
    • 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
    • 단점: 값이 없으면 외래 키에 null 허용
  • 대상 테이블에 외래 키
    • 대상 테이블에 외래 키가 존재
    • 전통적인 데이터베이스 개발자 선호
    • 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
    • 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명)
      • image
      • Member를 조회한다해도 어차피 Locker테이블을 조인으로 조회해야 Locker테이블을 채울지 말지를 결정짓게 된다. 그러므로 JPA는 한 번 조인으로 조회 후 즉시 로딩하게 된다.
      • JPA가 Member의 Locker 멤버 변수에 프록시를 넣거나 Null을 넣거나 판단을 해야하는데 무조건 조인을 통해 조회해야만 하므로 무조건 즉시로딩하게 된다.

다대다 [N:M]

  • 실무에서 안쓰는걸 추천하다.
  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결(중간) 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함

image

객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능하다.

image

  • @ManyToMany 사용
  • @JoinTable로 연결 테이블 지정
  • 다대다 매핑: 단방향, 양방향 가능
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
26
27
28
29
30
31
32
33
34
35
36
37
38
@Entity
public class Member {

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

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

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

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

만약 양방향으로 변경한다하면 아래와 같이 Product 클래스에 Member에 대한 참조를 추가해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<b>();
    ...
}

다대다 매핑의 한계(중요!)

편리해 보이지만 실무에서 사용 하지 않는 2가지 이유가 있다.

1) 매핑정보만 들어가고 추가적인 데이터를 넣을 수 없음

  • 연결 테이블이 단순히 연결만 하고 끝나지 않음
  • 주문시간, 수량 같은 데이터가 들어올 수 있는데 이런 데이터들을 쓸수가 없음

2) 중간 테이블이 숨겨져 있기에 내가 생각하지 못한 쿼리가 이상하게 나간다

  • Member랑 Product를 조회할려면 쿼리가 중간 테이블이 들어가고 조인이 되서 나와야하는데 예상치도 쿼리가 나올 것이다..

image

다대다 한계 극복(중요!)

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  • @ManyToMany -> @OneToMany, @ManyToOne

image

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Entity
public class Member {

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

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

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProducts = new ArrayList<>();

    ...
}

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

    private String name;

    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProdcuts = new ArrayList<>();

    ...
}

@Entity
public class MemberProduct {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name ="MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name ="PRODUCT_ID")
    private Product product;

    private int count;
    private int price;
    
    private LocalDateTime orderDatetime;
}

추가적으로

image

위의 이미지처럼 MEMBER_IDPRODUCT_ID 두 개로 PK를 잡을 수도 있다. 하지만 왠만하면 별도의 pk를 별도로 생성할 것을 추천한다. JPA 에서 복합키를 사용하면 Composite아이디를 별도로 만들어줘야하는데 조금 귀찮다. 그런걸 떠나서라도 DB설계 관점에서 새로운 id를 따는게 장단이 있다.

애플리케이션을 계속 개발하다보면 id라는게 어디 종속되는식으로 딱딱 걸리면 시스템을 유연성있게 다루기 쉽지 않아진다.

This post is licensed under CC BY 4.0 by the author.

[자바 ORM 표준 JPA 프로그래밍-기본편] 실전 예제2 - 연관관계 매핑 시작

[자바 ORM 표준 JPA 프로그래밍-기본편] 실전 예제3 - 다양한 연관관계 매핑