BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(5)
연관관계 매핑 기초
- 엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
- 객체는 reference를 사용해서 관계를 맺고 테이블은 foreign key를 사용해서 관계를 맺는다.
- 객체의 연관관계는 단방향이고, 테이블의 연관관계는 양방향이다.
단방향 연관관계
- 다음과 같은 다대일(N:1) 단방향 관계의 예가 있다.
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.

객체 연관관계
-
회원 객체는
Member.team필드로 팀 객체와 연관관계를 맺는다. -
회원 객체와 팀 객체는 단방향 관계다.
- 회원은
team필드를 통해서 팀을 알 수 있지만 팀은 회원을 알 수 없다. - 즉,
team.getMember()가 불가능하다.
- 회원은
-
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- 양쪽에서 서로 참조하도록 필드를 추가해야 한다.
-
객체는 참조를 이용해서
member.getTeam()과 같이 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.
테이블 연관관계
-
회원 테이블은
TEAM_ID외래 키로 테이블과 연관관계를 맺는다. -
회원 테이블과 팀 테이블은 양방향 관계다.
- 회원 테이블의
TEAM_ID를 통해서 팀 테이블을 조인할 수 있고, 반대로도 조인할 수 있다.
- 회원 테이블의
-
데이터베이스는 다음과 같이 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다.
SELECT T.* FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID WHERE M.MEMBER_ID = "member1";
객체 관계 매핑
@Entity
public class Member {
...
// 연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@Column(name="TEAM_ID")
private String id;
}
-
@ManyToOne: 다대일(N:1) 관계라는 매핑 정보-
연관관계를 매핑할 때 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
-
optional:false로 설정하면 연관된 엔티티가 항상 있어야 한다. -
fetch: 글로벌 페치 전략을 설정한다. -
cascade: 영속성 전이 기능을 사용한다. -
targetEntity: 연관된 엔티티의 타입 정보를 설정한다.-
컬렉션을 사용해도 generic으로 타입 정보를 알 수 있기 때문에 거의 사용하지 않는다.
@OneToMany private List<Member> members; // generic으로 타입 정보를 알 수 있다.
-
-
-
@JoinColumn(name="TEAM_ID"): 외래 키를 매핑할 때 사용한다.-
name: 매핑할 외래 키 이름 지정 referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명nullable,insertable,updatable등의 DDL 제약 조건 속성은@Column속성과 동일하게 사용 가능하다.@JoinColumn을 생략하면 기본 전략을 사용하여 외래 키를 찾는다.- 기본 전략 : 필드명(
team) + _ + 참조하는 테이블의 컬럼명(TEAM_ID)
- 기본 전략 : 필드명(
-
연관관계 사용
저장
- JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1");
member1.setTeam(team1);
em.persist(member1);
-
JPA는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.
INSERT INTO TEAM(TEAM_ID, TEAM_NAME) VALUES('team1', "팀1"); INSERT INTO MEMBER(MEMBER_ID, TEAM_ID) VALUES('member1', 'team1');
조회
- 연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 이다.
- 객체 그래프 탐색
- 객체지향 쿼리 사용(JPQL)
객체 그래프 탐색
-
member.getTeam()과 같이 객체를 통해 연관된 엔티티를 조회할 수 있다.Member member = em.find(Member.class, "member1"); Team team = member.getTeam();
객체지향 쿼리 사용
-
JPQL에서 지원하는 조인 기능을 사용하여 연관된 엔티티를 조회할 수 있다.
-
팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 한다.
String jpql = "select m from Member m join m.team t where t.name=:teamName"; List<Member> resultList = em.createQuery(jpql, Member.class) .setParameter("teamName", "팀1") .getResultList();- 회원이 팀과 관계를 가지고 있는 필드(
m.team)를 통해서Member와Team을 조인했다. where절에서t.name을 검색조건으로 사용해서 팀1에 속한 회원만 검색했다.
- 회원이 팀과 관계를 가지고 있는 필드(
수정
Team team2 = new Team("team2", "팀2");
em.persist(team2);
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
-
위와 같이 팀 참조 값을 변경했을 때 실행되는 SQL은 다음과 같다.
UPDATE MEMBER SET TEAM_ID='team2', ... WHERE ID='member1'; -
단순히 불러온 엔티티의 값만 변경해두면 트랜잭션 커밋시에 flush가 일어나면서 dirty checking으로 변경사항을 데이터베이스에 자동으로 반영한다.
-
연관관계를 제거하는 작업도 팀 참조값을
null로 변경하는 것 외에 모두 동일하다.
양방향 연관관계
- 팀에서 회원으로 접근하는 관계를 추가하면 회원 -> 팀, 팀 -> 회원 의 양방향 접근이 가능해진다.
- 팀에서 회원은 일대다 관계이므로 컬렉션을 사용해야 한다.
- 따라서
Team.members를List컬렉션으로 추가한다.
- 따라서
- 테이블은 외래 키 하나로 양방향 관계를 조회할 수 있으므로 데이터베이스에 추가할 내용은 없다.
@Entity
public class Team {
...
@OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<Member>();
}
@OneToMany: 일대다(1:N) 관계라는 매핑 정보mappedBy: 양방향 매핑일 때 사용하는 반대쪽 매핑 필드 이름
연관관계의 주인
- 엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다.
- 서로 다른 2개의 단방향 연관관계를 어플리케이션 로직으로 묶어서 양방향 연관관계인 것처럼 보이게 할 뿐이다.
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
- 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다.
- 엔티티를 양방향으로 매핑하면 두 엔티티는 서로를 참조하므로 객체의 연관관계를 관리하는 포인트는 2곳이 된다.
- 즉, 객체의 참조는 둘인데 외래 키는 하나다.
- 둘 사이의 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데, 이것을 연관관계의 주인(owner)이라 한다.
양방향 매핑의 규칙 : 연관관계의 주인
- 양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
- 연관관계의 owner만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제)할 수 있다.
- owner가 아닌 쪽은 read만 가능하다.
- 어떤 연관관계를 owner로 정할지는
mappedBy속성을 사용한다.- owner는
mappedBy속성을 사용하지 않는다. - owner가 아니면
mappedBy속성을 사용해서 속성의 값으로 연관관계의 owner를 지정해야 한다.
- owner는
둘 중 어떤 것을 연관관계의 주인으로 정해야 할까?
- 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것이다.
- 예를 들어 회원 - 팀 연관관계의 경우,
MEMBER테이블에 있는TEAM_ID외래 키를 관리할 관리자를 선택해야 한다.Member엔티티에 있는Member.team을 주인으로 선택하면 자기 테이블(MEMBER)에 있는 외래 키를 관리하면 된다.Team엔티티에 있는Team.members를 주인으로 선택하면 물리적으로 다른 테이블에 있는 외래 키를 관리해야 한다.
연관관계의 주인은 외래 키가 있는 곳
- 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
- 즉, 회원 테이블이 외래 키를 가지고 있으므로
Member.team이 주인이 된다. - 주인이 아닌
Team.members에는mappedBy="team"속성을 이용해서 주인을 지정해야 한다.
- 즉, 회원 테이블이 외래 키를 가지고 있으므로
- 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
- 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지는 못한다.
양방향 연관관계의 주의점
-
양방향 연관관계를 설정했을 때 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하면 데이터베이스에 외래 키 값이 정상적으로 저장되지 않는다.
Member member1 = new Member("member1"); em.persist(member1); Team team1 = new Team("team1", "팀1"); team1.getMembers().add(member1); em.persist(team1);- 위 코드를 실행하면
MEMBER테이블의member1의TEAM_ID값은null이 될 것이다. - 연관관계의 주인이 아닌
Team.members에만 값을 저장했기 때문이다.
- 위 코드를 실행하면
-
연관관계의 주인만이 외래 키 값을 변경할 수 있으므로 주인에 값을 입력해주어야 데이터베이스에 값이 정상적으로 저장된다.
순수한 객체까지 고려한 양방향 연관관계
-
JPA에서는 연관관계의 주인에만 값을 입력해주면 데이터베이스에 외래 키가 저장되기 때문에
member.setTeam(team)과 같이 주인에만 값을 저장하면 된다. -
그러나 객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
- 양방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
Team team1 = new Team("team1", "팀1"); Member member1 = new Member("member1"); Member member2 = new Member("member2"); member1.setTeam(team1); member2.setTeam(team1); List<Member> members = team1.getMembers(); System.out.println("members.size = " + members.size()); // 출력 : members.size = 0- 위 코드는 JPA를 사용하지 않는 순수한 객체다.
Member.team에만 연관관계를 설정하고 반대방향은 연관관계를 설정하지 않았다.- 따라서
team.Members를 조회했을 때 원하는 결과를 얻지 못한다.
- 따라서
- 이런 경우를 방지하기 위해 양쪽 다 관계를 설정해야 한다.
team1.getMembers().add(member1); team1.getMembers().add(member2);- 이렇게 양쪽에 연관관계를 설정할 경우 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력된다.
연관관계 편의 메서드
-
양방향 연관관계는 양쪽 다 신경 써야 한다.
-
위처럼
member.setTeam(team)과team.getMembers().add(member)를 각각 호출하다 보면 둘 중 하나만 호출해서 양방향이 깨질 수 있다.- 두 코드를 하나인 것처럼 사용하는 것이 안전하다.
Member엔티티의setter를 리팩토링하여 편의 메서드를 작성할 수 있다.
public class Member { ... public void setTeam(Team team) { if(this.team != null) { this.team.getMembers().remove(this); } this.team = team; team.getMembers().add(this); } }- 기존에 해당 인스턴스가 다른 참조 값을 가지고 있었을 경우, 해당 연관관계를 끊고 새로운 연관관계를 연결해야 하므로
if문으로 참조 값을 확인하고 제거 한 후 새로운 연관관계를 매핑해준다.
정리
- 단방향 매핑은 언제나 연관관계의 주인이므로 단방향 매핑만으로 테이블과 객체의 연관관계의 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.