객체 관계 매핑에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일입니다. 객체의 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표
입니다.
연관관계 중에서 다대일(N:1)
단방향 관계를 가장 먼저 이해해야 합니다.
- 회원과 팀이 있습니다.
- 회원은 하나의 팀에만 소속될 수 있습니다.
- 회원과 팀은 다대일 관계입니다.
위의 그림을 보면 회원 객체와 팀 객체는 단방향 관계
입니다. 즉, Member -> Team의 조회는 가능하지만 Team -> Member를 접근하는 필드는 없습니다.
반면에 테이블 연관관계는 회원 테이블과 팀 테이블은 양방향 관계
입니다. 즉, 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있습니다.
테이블과 객체는 이러한 큰 간격이 존재합니다.
Member 객체와 Team 객체를 매핑하면 위와 같이 할 수 있습니다.
이 상태에서 Team과 Member로 코드를 작성해보면 위에서 볼 수 있듯이 뭔가 객체지향과는 맞지 않은 부분
이 존재합니다.
뭔가 조회하는 과정에서도 객체지향
스럽지 않고 난잡하다는 느낌을 받을 수 있습니다.
그래서 객체지향스럽게 설계하기 위해서는 위와 같이 Member
객체가 teamId가 아니라 Team 객체를 가지도록 설계를 해보겠습니다.
- 멤버가 N이고 팀이 1이기 때문에 Member 입장에서는
N:1
이기 때문에ManyToOne
어노테이션을 사용합니다. @JoinColumn
은 어떤 컬럼을 기준으로 JOIN 할지를 명시하는 어노테이션입니다.
이렇게 설계를 한 후에 위에서 작성했던 코드를 리팩터링 해보겠습니다.
Member에 TeamId가 아니라 Team을 저장하니까 확실히 아까보다는 좀 더 객체지향스럽고 코드도 깔끔해진 것을 볼 수 있습니다.
위에서 보면 SQL에서는 외래키
를 통해서 양쪽에서 참조할 수 있지만, 객체는 한쪽에서만 참조가 가능하다는 차이점이 있었습니다. 즉, 위의 예시로 보면 Member -> Team
으로는 참조할 수 있지만, Team -> Member
로는 참조할 수 없습니다. 즉, 연관관계를 하나 더 만들어야 합니다.
이렇게 양쪽해서 서로 참조하는 것을 양방향 연관관계
라고 하는데요. 정확히 말하면 양방향 관계가 아니라 서로 다른 단방향 관계 2개
입니다.
그래서 Team
에서 Member
를 참조하기 위해서 위와 같이 설정할 수 있습니다. 여기서 보아야 할 점은 mappedBy
라는 것인데요. 이것은 연관관계의 주인
이라는 것을 알아야 합니다.
위에서 말했던 것처럼 테이블은 외리캐 하나로 두 테이블의 연관관계를 관리
합니다. 하지만 엔티티는 단방향 2개가 있어야 양방향 관계
가 됩니다. 그리고 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 합니다.
이것을 연관관계의 주인
이라고 합니다.
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
연관관계의 주인만이 외래 키를 관리합니다.
주인이 아닌 쪽은 읽기만 가능합니다.
- 주인은 mappedBy 속성을 사용하지 않습니다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정합니다.
연관관계 주인은 Many
쪽에 주고, One
쪽에는 읽기 전용
으로 두는 것이 좋습니다. 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것입니다.
즉, 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 합니다.
정리하면, 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있습니다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못합니다.
위와 같이 Team을 통해서 Member를 추가했습니다. 이 상태로 실행을 해보겠습니다.
그러면 분명히 INSERT
쿼리도 2번이 실행된 것을 볼 수 있습니다. 그래서 DB에도 값이 잘 들어갔는지 확인해보겠습니다.
확인해보니 Member 테이블에 TEAM_ID가 null인 것
을 볼 수 있습니다. 위에서 분명히 INSERT 쿼리도 실행이 되었고 값을 넣어주었던 거 같은데 말이죠..
왜 이런가 생각해보면 Member가 연관관계의 주인이고, Team은 MappedBy가 지정된 읽기 전용
이기 때문에 Team을 통해서 Member를 저장하려 할 때는 제대로 저장이 되지 않는 것입니다. 그래서 이런 문제를 해결하려면 아래와 같이 수정하면 됩니다.
그래서 위와 같이 연관관계의 주인인 Member를 통해서 Team을 저장하도록 하면 제대로 값이 잘 들어가게 됩니다.
그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까요? 객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전합니다.
위의 코드처럼 한쪽 방향에만 값을 넣어주고 Team에서 Member를 호출하면 값이 제대로 출력이 될까? 할 수 있지만 제대로 출력이 됩니다.
JPA에서 Team의 속한 Member들을 호출할 때 위와 같은 쿼리를 생성하여 가져오기 때문에 Team에서 Member로의 값을 세팅하지 않아도 제대로 출력은 됩니다. 하지만 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있습니다.
예를들어 위와 같이 em.flush()
, em.clear()
를 하지 않는다면 1차 캐시에 존재하는 값 그대로 가져오게 될 것입니다. 즉, JPA에서 외래키를 사용하여 쿼리를 생성하지 못하기 때문에 Team에서 Member의 값을 가져올 때는 아무 것도 가져오지 못하게 됩니다.
양쪽의 값을 저장해야 할 때 하나는 까먹을 가능성이 존재합니다. 그래서 두 코드를 하나인 것처럼 만드는 것이 안전한데요.
Member에서 Team을 저장할 때 위와 같이 한번에 Team에서 Member로 저장하는 코드도 같이 하나의 메소드에 존재한다면 개발자가 값을 한쪽에만 세팅하는 실수가 없어질 것입니다.
단방향 매핑만으로도 이미 연관관계 매핑은 완료
를 해야 합니다.양방향 매핑은 반대 방향으로 조회기능이 추가된 것뿐
입니다.- JPQL에서 역방향으로 탐색할 일이 많습니다.
- 단방향 매핑을 잘해놓으면 필요할 때만 양방향 관계를 추가하면 됩니다.