
출처 - 인프런 김영한님 자바 ORM 표준 JPA 프로그래밍 - 기본편
인강의 내용이 있기 때문에 출처를 밝힙니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
양방향 연관관계를 이해하는데 시간이 좀 오래걸렸다. 복습해도 새로워 늘 짜릿해...
이번에는 다대일 부터 다대다까지의 다양한 매핑을 복습해보자. 기록 시작 ~
연관관계 매핑 시 고려사항 3가지
엔티티의 연관관계를 매핑할 때는 3가지를 고려해야한다.
1. 다중성
N:1, 1:N, 1:1, N:M 인지 고려해 매핑을 해야하는데, DB Table 기준으로 다중성을 구분하자.
다중성 고려 시, 가끔 애매한 경우가 있다. 이럴 땐 반대의 관점에서 생각해보는 것도 좋다.
아래에서 서술하겠지만 N:M 은 실무에서 적용하지 않는다.
2. 방향성
앞선 포스팅에서 설계는 모두 단방향으로 끝내는 것이 좋다고 했다.
어짜피 DB 테이블은 방향성이란게 존재하지 않기 때문에 객체 관점에서 단방향으로 모두 설계를 끝내고, 대상 객체에서 주 객체를 조회하는 로직이 자주 나올 때 양방향 매핑을 적용하도록 하자. 양방향 매핑을 적용한다고 해도 DB 테이블에는 영향을 주지 않으니 안심해도 된다.
3. 연관관계의 주인
DB 테이블과 객체는 연관관계의 패러다임이 다르다.
주 객체와 대상 객체에 양방향 배핑을 적용한다고 했을 때, 테이블과 달리 객체는 두 곳에서 참조가 들어가기 때문에 외래 키를 관리할 곳을확실하게 정해주어야 한다. 즉, 연관관계의 주인을 명시하는게 필요하다.
다양한 연관관계 매핑 어노테이션
JPA 에서 지원하는 연관관계 매핑 어노테이션은 총 4가지가 존재한다.
@ManyToOne, @OneToMany, @OneToOne, @ManyToMany
하나씩 자세히 알아보자.
1. 다대일 [N : 1 / @ManyToOne]
매핑 시 가장 많이 사용하는 다중성이다. 외래 키가 있는 곳에 참조를 걸고, 키를 관리한다. 먼저 N : 1 단방향을 그림으로 살펴보자.
회원이 N, 팀이 1 인 연관관계에서 무조건 외래 키는 N 쪽으로 잡아줘야 한다. 즉, 회원에서 외래 키를 관리 !

N : 1 연관관계 양방향도 그림으로 살펴보자. 양방향 매핑이기 떄문에 외래 키를 관리하는 회원이 연관관계의 주인이 될 것 !
DB 테이블은 역시 변동없이 회원 쪽에서 외래 키를 관리한다.

@ManyToOne 어노테이션에서 사용할 수 있는 옵션을 잘 살펴보면 왜 외래 키를 N 쪽으로 잡아줘야 하는지 알게 되는데, mappedBy 옵션이 아예 존재하지 않는다. 즉, N : 1 관계에서 N 쪽은 무조건 외래 키를 관리하는 연관관계의 주인이 되어야 한다는 뜻이다.

2. 일대다 [1 : N / @OneToMany]
보통 N 쪽에서 외래 키를 관리하는데, 1: N 관계에선 반대쪽 테이블에 있는 외래 키를 관리하게 된다.
이 매핑 방법을 사용할 때는, @JoinColumn 어노테이션을 필수로 붙여줘야하는데, 붙여주지 않았을 때 JPA 에서 자동으로 연결 테이블을 만들고 JoinTable 전략을 기본으로 사용해 매핑하게 된다. 따라서 이런 상황을 피하기 위해선 무조건 @JoinColumn 을 사용해주자. 참고로 N : 1 매핑은 @JoinColumn 을 생략해도 자동으로 연결 테이블을 만들지 않는다.
사실 1 : N 단방향이든, 양방향이든 영한님께선 권장하지 않는 방법이라고 하신다.
아래 서술하겠지만, 테이블은 N 쪽에서 외래 키를 관리하는데 객체는 1 쪽에서 N 의 참조 값을 가지고 있기 때문에 쓸데없이 UPDATE 쿼리가 나가게 된다. JPA 쪽에서 억지로 맞추려는 느낌도 강하고 .. 아무튼 웬만해선 N : 1 로 매핑하고 공식 스펙으로 지원하는 거니까 이런게 있구나 정도만 알아두자.
1 : N 단방향 매핑을 그림으로 살펴보자.
DB 테이블 관점으로 N 쪽에서 외래 키 관리를 담당하고 있는데, 객체는 N 쪽이 주가 아닌 대상이 되어버린다.
즉, 테이블은 변함없이 N 쪽에서 외래 키를 관리하지만, 객체는 뒤집어진 형태라고 할 수 있다.

단순히 N : 1 매핑의 반대 개념이라고 생각할 수 있는데, 이 방법은 치명적인 단점이 존재한다. 코드로 살펴보자.
팀 객체에 회원 리스트가 있기 때문에 7번 라인의 로직 자체는 틀린게 아니다.
그런데, DB 테이블 관점에서 봤을 때 팀 테이블에 회원을 저장할 수가 없는데 어떻게 동작하는 걸까?
1 Member member = new Member();
2 member.setName("memberA");
3 em.persist(member);
4
5 Team team = new Team();
6 team.setName("teamA");
7 team.getMembers().add(member);
8 em.persist(team);
이 경우에 JPA 쪽에서 찾은 해답은 대상 쪽으로 UPDATE 쿼리를 날리는 것이다.
DB 테이블에서는 외래 키가 N 쪽 즉, 여기서는 회원 테이블이 가지고 있기 때문에 INSERT 쿼리로 한 번에 처리가 불가능하기 때문에 이런 UPDATE 가 한 번 더 들어갈 수 밖에 없는 것이다. 따라서 영한님 추천으로는 1 : N 단방향 매핑을 사용하기보단, 객체가 손해를 보더라도 N : 1 양방향 매핑을 추천하셨다.

1 : N 의 단방향을 알아봤으니, 이제 1 : N 양방향 매핑을 알아보자.
앞서 언급했듯 @ManyToOne 어노테이션에는 따로 mappedBy 속성이 없다. 그럼 양방향이 아예 불가할까 ?
방법은 상대에서도 @joinColumn을 걸어 주면된다. 대신 이렇게 되면 두 가지 속성을 바꿔주면 된다.
아래처럼 삽입, 수정을 false 로 만들어버리면 읽기 전용으로 바꿀 수 있다.
하지만 이 방법은 권장되지 않는다. 읽기전용으로 사용하기 위해 이렇게 사용할 수 있다 정도만 알아두자.
... 중략
@ManyToOne(name = "team_id")
@JoinColumn(name = "team_id", insertable = false, updateable = false)
private Team team;
... 중략
3. 일대일 [1 : 1 / @OneToOne]
1 : 1 관계는 특이하게도 주, 대상 어디에든 외래 키 관리 역할을 맞길 수 있다.
단방향, 양방향 매핑 방법 자체도 N : 1 과 거의 유사하고, 외래 키가 있는 곳이 연관관계의 주인이 된다.
외래 키가 있는 곳은 유니크 제약조건이 추가되어야 한다. (필수는 아님)
회원과 사물함이 있고 한 명의 회원은 한 개의 사물함을 가질 수 있고 그 반대도 동일하다고 가정해보자.
그러면 회원에서 외래 키를 관리해야 할까, 사물함에서 회원 키를 관리해야 할까 ?
아래 예제에서 주 테이블, 대상 테이블이란 용어가 나오는데, 상대 객체를 참조하는 쪽이 회원이면 회원이 주 테이블이 되고, 사물함이 대상 테이블이 된다. 이렇게 일단 잡고 예제를 보자. 두 가지 방법 무엇이든 상관없지만 Trade-Off 를 잘 고려해서 선택해야 한다.
조금 더 자세히 알아보자.
가. 대상 테이블 사물함에 외래 키가 있는 경우 (사물함이 연관관계의 주인)
만약 미래에 회원이 여러 사물함을 가질 수 있도록 요구 사항이 변경된다면 ?
이 때 사물함에 외래 키가 있다면, 사물함이 N 으로 변경되므로 외래 키를 그대로 사용하되 유니크 제약조건만 삭제하면 끝난다.
즉, 변경이 굉장히 간단해진다. 대신 이렇게 대상 테이블에 외래 키가 있다면 JPA 에서 단방향 매핑을 지원하지 않기 때문에 양방향으로 매핑해야 한다. 또한 프록시 기능의 한계로 지연 로딩으로 설정하더라도 항상 즉시 로딩되게 되는 치명적인 단점이 존재한다.


나. 주 테이블 회원에 외래 키가 있는 경우 (회원이 연관관계의 주인)
반대로 회원 쪽에 외래 키가 있다면, 위 가정에 대해 변경이 좀 복잡해질 수도 있다.
다만 개발자 입장으로 조회 같은 경우 주 테이블을 굉장히 많이 조회하는데, 어짜피 외래 키가 주 테이블에 존재하기 때문에 대상 테이블에 데이터가 있는지 없는지 쉽게 확인할 수 있어서 성능상 이점이 있다.
다만, 외래 키 값이 없다면 NULL 값이 테이블에 들어갈 수 있도록 허용해줘야 한다는 단점이 있다.
쉽게 말해서, 회원이 무조건 사물함을 하나 가져야 한다는 요구 사항이 없다면, 어떤 회원은 사물함을 가지고 있고, 다른 회원은 사물함이 없을 수도 있으니 NULL 값을 허용해줘야 한다는 것.
[주 테이블 외래키 단방향 매핑 예제]
주 테이블인 회원에서 외래 키를 관리한다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker
}

[주 테이블 외래키 양방향 매핑 예제]
N : 1 양방향 매핑처럼 외래 키가 있는 회원이 연관 관계의 주인이 된다.
1. Member
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
private String name;
}
2. Locker
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
@OneToOne(mappedBy = "locker")
private Member member;
}

4. 다대다 [N : M / @ManyToMany]
RDB 는 DB 테이블 2개를 N : M 관계로 표현하지 못한다. 그럼 어떻게 N : M 을 사용할까?
테이블에서는 N : M 을 사용하기 위해선 테이블들 사이에 중간 연결 테이블을 추가해서 1 : N / N : 1 관계로 풀어내는 수 밖에 없다.
하지만 자바에서는 Collection 을 사용해 객체 2개로 N : M 관계를 표현할 수 있다.
머리가 굉장히 찌끈하다 .. ㅎㅎ 강의에서도 실무에서는 거의 사용하지 않는 방법이라고 하니 왜 사용하지 않는지 정도만 알아보자.
[N : M 매핑의 한계]
JPA 에서 @ManyToMany 어노테이션을 지원하기 때문에 이거 그냥 사용해도 되는 거 아닌가 ? 라고 생각할 수 있다.
하지만 해당 매핑을 실무에서 사용하기에는 한계가 명확하다고 하다.
일단 @ManyToMany 를 사용하면, JPA는 위에서 언급한 연결 테이블을 자동으로 생성하는데, 연결 테이블에는 두 테이블의 PK 값을 FK 로 가지고 있다. 보다싶이 회원의 PK, 물품의 PK 가 장동 생성된 연결 테이블에 들어간다.

연결 테이블이 생기는게 문제인가 ?
위에서 언급했는데, N : M 을 연결 테이블을 통해 1 : N / N : 1 관계로 풀어내야된다고 했다.
하지만 위의 연결 테이블은 JPA 에서 자동으로 생성해준것이기 때문에 단순 연결 그 이상 그 이하의 역할도 하지 않는다.
개발을 진행하다 연결 테이블에 주문 시간, 주문 수량 등 원하는 데이터를 더 넣을 수 없는 문제가 발생하는 것이다.
이런 문제 이외에도 개발자가 예상하지 못하는 쿼리가 나가는 등 골치아픈 문제가 생기기 때문에 @ManyToMany 는 지양하도록 하자 !
[한계의 극복]

근데 개발을 하다가 연결 테이블을 두고 1 : N / N : 1 관계로 풀어내야 할 때가 있다. 이럴 떈 어떻게 해야할까 ?
사실 @ManyToMany 어노테이션을 사용하지 말라는 거지 이런 식의 풀이를 사용하지 말라는 뜻은 아니다.
@ManyToOne, @OneToMany 어노테이션을 사용해 관계를 풀어내면 그만인 것. 그럼 연결 테이블은 어떻게 ?
연결 테이블 또한 JPA 에서 자동으로 만들어주는 그런게 아니고, 직접 만든 엔티티를 사용하면 된다. 직접 엔티티를 만들어서 연결 테이블로사용하게 되면, 단순 매핑 정보 이외에도 우리가 원하는 정보도 얼마든지 넣을 수 있기 때문에 한계를 극복할 수 있다. 즉, @ManyToMany 를 사용하지 말고, 엔티티로 승격시킨 연결 테이블을 만들고 @ManyToOne, @OneToMany 를 사용해 풀어내자 !
예제 코드를 보고 마무리하자.
MemberProduct 객체에서 매핑 정보 이외의 다른 값이 들어가면 의미있는 연결 테이블로 만들 수 있을 것이다.
1. 회원 객체
@Entity
public class Member {
... 중략
@OneToMany(mappedBy = "member")
public List<MemberProduct> memberProducts = new ArrayList<>();
... 중략
}
2. 상품 객체
@Entity
public class Product {
... 중략
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
... 중략
}
3. 연결 객체
@Entity
public class MemberProduct {
... 중략
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
.. 이외의 다양한 데이터 추가 가능
... 중략
}
'JPA' 카테고리의 다른 글
[인강 복습] JPA 기본편 연관관계 매핑 기초 (#4) (0) | 2022.07.05 |
---|---|
[인강 복습] JPA 기본편 요구사항 분석과 기본 매핑 까지 (#3) (0) | 2022.06.16 |
[인강 복습] JPA 기본편 영속 컨텍스트 까지 (#2) (0) | 2022.06.12 |
[인강 복습] JPA 기본편 JPA 시작하기 까지 (#1) (0) | 2022.06.10 |