객체 관계 매핑 ORM 에서 가장 어려운 부분이 바로 객체 연관 관계와 테이블 연관관계를 매핑하는 일이다.
방향(Direction):
- [ 단방향, 양방향 ]이 있다.
- 한 쪽만 참조하는 것을 단방향 관계라 하고, 양쪽 모두 서로 참조하는 것을 양방향 관계라 한다.
- 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
다중성(Multiplicity):
[ 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) ] 다중성이있다.
예를 들어 회원과 팀이 관계가 있을 때 여러 회원은 한 팀에 속하므로 회원과 팀은 다대일 관계다.
반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계다.
연관관계의 주인(Owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
5.1 단방향 연관관계
객체 연관관계와 테이블 연관관계의 가장 큰 차이 참조를 통한 연관관계는 언제나 단방향이다.
객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
- 결국 연관관계를 하나 더 만들어야 한다.
이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라 한다.
- 하지만 정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
객체 연관관계 vs 테이블 연관관계 정리
- 객체는 참조(주소) 로 연관관계를 맺는다.
- 객체 참조
- 단방향
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- 테이블은 외래 키로 연관관계를 맺는다.
- 조인(join)
- 양방향
- 객체는 참조(주소) 로 연관관계를 맺는다.
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.
Member.team
과MEMBER.TEAM_ID
를 매핑하는 것이 연관관계 매핑이다.@ManyToOne
: 이름 그대로 다대일(N:1) 관계라는 매핑 정보다.- 회원과 팀은 다대일 관계다.
- 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을필수로 사용해야 한다.
@JoinColumn(name="TEAM_ID")
: 조인 컬럼은 외래 키를 매핑할 때 사용한다.- name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAMLID
- 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다. 이 어노테이션은 생략할 수 있다.
5.2 연관관계 사용
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
5.2.1 저장
5.2.2 조회
[ 연관관계가 있는 엔티티를 조회 하는 방법 ]
객체 그래프 탐색(객체 연관관계를 사용한 조회)
객체지향 쿼리 사용; JPQL
JPQL도 조인을 지원한다.
조인해서 검색조건을 사용하면 된다.
:teamName
과 같이:
로 시작하는 것은 파라미터를 바인딩받는 문법이다.
5.2.3 수정
엔티티의 값만 변경해두면 트랙잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동하고, 변경사항을 DB에 자동 반영한다.
5.2.4 연관관계 제거
5.2.5 연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.
5.3 양방향 연관관계
- 팀에서도 회원으로 접근할 수 있도록 하자
- 회원 -> 팀 (Member.team)
- 다대일, N:1
- 팀 -> 회원 (Team.members)
- 일대다, 1:N
- 여러 건과 연관계를 맺을 수 있으므로, 컬렉션 사용
- 테이블은 외래 키 하나로 양방향 조회가 가능
A JOIN B
,B JOIN A
둘 다 가능
5.4 연관관계의 주인
엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다.
- 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
반면에 데이터베이스 테이블은 앞서 설명했듯이 외래 키 하나로 양쪽이 서로 조인할 수 있다.
- 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다.
그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?
- JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데, 이를 연관관계의 주인(Owner) 이라 한다.
5.4.1 양방향 매핑의 규칙: 연관관계의 주인
- 양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리 (등록, 수정, 삭제)
- 반면에 주인이 아닌 쪽은 읽기만 할 수있다.
- 어떤 연관관계를 주인으로 정할지는
mappedBy
속성을 사용하면 된다.- 주인은
mappedBy
속성을 사용하지 않는다. - 주인이 아니면
mappedBy
속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
- 주인은
- 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
- 회원 테이블에 있는
TEAM_ID
외래 키를 관리할 관리자를 선택해야 한다. - 회원 엔티티에 있는
Member.team
을 주인으로 선택하면, 자기 테이블에 있는 외래 키를 관리하면 된다. - 하지만 팀 엔티티에 있는
Team.members
를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다.
- 회원 테이블에 있는
비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하면 안 된다.
비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
5.4.2 연관관계의 주인은 외래 키가 있는 곳
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다.
다 쪽인
@ManyToOne
은 항상 연관관계의 주인이 되므로mappedBy
를 설정할 수 없다.따라서
@ManyToOne
에는mappedBy
속성이 없다.
양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다.
예를 들어
Member.toString()
에서getTeam()
을 호출하고Team.toString()
에서getMember()
를 호출하면 무한 루프에 빠질수 있다.이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생, 그리고
Lombok
이라는 라이브러리를 사용할 때도 자주 발생JSON 라이브러리들은 보통 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공한다.
5.5 양방향 연관관계 저장
- 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.
- 양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다.
- 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.
5.6 양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
5.6.1 순수한 객체까지 고려한 양방향 연관관계
- 그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?
- 사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
- 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
- 객체까지 고려하면 이렇게 양쪽 다 관계를 맺어야 한다.
Member.team
: 연관관계의 주인, 이 값으로 외래 키를 관리한다.Team.members
: 연관관계의 주인이 아니다. 따라서 저장 시에 사용되지 않는다.
5.6.2 연관관계 편의 메소드
- 양방향 관계에서 두 코드(회원 -> 팀, 팀 -> 회원)는 하나인 것처럼 사용하는 것이 안전하다.
- 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.
5.6.3 연관관계 편의 메소드 작성 시 주의사항
- 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
Reference
- 자바 ORM 표준 JPA 프로그래밍 (김영한) https://product.kyobobook.co.kr/detail/S000001766367