엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 이때 자바의 객체는 참조(주소)를 사용해 관계를 맺고, 테이블은 외래 키를 사용해 관계를 맺는다. 이 둘은 앞에서 나왔듯이 특징이 꽤 다르기에 객체의 참조와 테이블의 외래 키를 매핑하는 것이 ORM(객체 관계 매핑)에서 가장 까다로운 부분이다.
키워드 정리
방향: 단방향과 양방향이 존재. 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
다중성: 다대일, 일대다, 일대일, 다대다 다중성이 존재한다.
연관관계의 주인: 객체를 양방향 연관관계로 만들 때에는 연관관계의 주인을 설정해줘야한다.
- 회원과 팀
- 회원은 하나의 팀에만 소속될 수 있음
- 회원과 팀은 다대일관계
즉, 하나의 팀에는 여러 회원이 속할 수 있지만, 각 회원은 하나의 팀에 속해야함.

Member.team 필드로 팀 객체와 연관관계를 맺음member.getTeam() 과 같이 팀을 알 수 있지만, 반대로 팀에서는 어떤 멤버들이 팀에 들어있는지 확인할 수 없음 TEAM_ID 외래 키를 통해 팀 테이블과 연관관계를 매음객체의 참조를 통한 연관관계는 항상 단방향이다. 즉, 이 연관관계를 양방향으로 만들기 위해서는 양쪽에 필드를 추가해 참조를 보관해야한다. 즉, 양방향 관계가 아니라 서로 다른 단방향 관계 2개라고도 할 수 있다. 이와 다르게 테이블은 외래 키 하나만으로 양방향으로 조인할 수 있다.
Member에서 회원 엔티티를 매핑하고, Team에서 팀 엔티티를 매핑.
Member.team 필드 사용MEMBER_TEAM_ID 외래키 컬럼 사용여기서 두 관계의 Member.team과 MEMBER_TEAM_ID를 매핑하는 것이 중요하다
@Entity
public class Member {
@Id
@Column (name="MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam (Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@Column
private String id;
private String name;
}
@ManyToOne다대일(N:1) 관계라는 매핑 정보로 연관관계 매핑 시에는 다중성을 나타내는 어노테이션이 필수적이다.
@JoinColumn(name="TEAM_ID")조인 컬럼은 외래 키를 매핑할 때 사용하며 name 속성에는 매핑할 외래 키 이름을 지정한다. 해당 어노테이션은 생략이 가능하고, 속성은 아래와 같다.
name: 매핑할 외래 키 이름referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명unique, nullable, insertable...등아래는 연관관계를 등록, 수정, 삭제, 조회하는 내용이다.
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설정
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설정
em.persist(member2);
회원 엔티티가 팀 엔티티를 참조하고 저장하면, JPA는 참조한 팀의 식별자를 외래키로 사용해 적절한 등록 쿼리를 생성한다.
연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 이다
객체 그래프 탐색
member.getTeam()을 사용해 member와 관련된 team 엔티티를 조회할 수 있음.
객체지향 쿼리(JPQL) 사용
JPQL도 조인을 지원하는데 SQL과 문법은 조금 다르다.
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");
for (Member member : resultList) {
System.out.println("[query] member.username="
+ member.getUsername());
}
from Member m join m.team t
:회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해 Member와 Team을 조인하였다. 이후 where절을 통해서 조인한 t.name을 검색조건으로 사용해 팀1에만 속한 팀을 검색한 것이다.
:teamName
: :로 시작하는 것은 파라미터를 바인딩받는 문법이다.
Team team2 = new Team("team2", "팀2");
em.persist(team2);
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
수정의 경우에는 다른 메소드가 없기 때문에 엔티티를 조회한 후에 엔티티의 값을 변경해두면 트랜잭션 커밋 시에 플러시가 일어나며 변경 감지 기능이 작동한다.
회원1을 팀에 소속하지 않도록 연관관계를 제거
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); //연관관계 제거
엔티티를 조회한 후 수정하여 연관관계를 제거한다.
연관된 엔티티를 삭제하기 위해서는 기존의 연관관계를 먼저 제거한 후에 삭제해야한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.
예를 들면 팀1에 회원1,2가 소속되어있다고 할 때, 팀1을 삭제하기 위해서는 이 둘 사이의 연관관계를 끊어줘야한다.
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);
위에서는 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 만들었다. 해당 장에서는 반대인 팀에서 회원으로 접근하는 관계를 추가해본다.
회원과 팀이 다대일 관계인데 반해, 팀과 회원은 일대다 관계이다. 여러 연관관계를 맺을 수 있기에 컬렉션을 사용해야한다.
팀1에 여러 명의 회원들이 속해있을 수 있기에, 이 여러 명의 회원들을 관리하기 위해서 리스트/컬렉션을 사용해야한다.
데이터베이스 입장에서는 원래부터 외래 키 하나로 양방향 조회가 가능하기에 추가할 부분이 없다.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany (mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
@OneToMany:팀과 회원은 일대다 관계이기에, 팀 엔티티에는 컬렉션인 List<Member> members를 추가하고 어노테이션 역시 위의 @OneToMany를 사용한다.
여기서 mappedBy 속성은 양방향 매핑일 경우 사용하는 것으로 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 위에서는 반대의 매핑이 Member.team이므로 team을 준다.
팀에서 회원 컬렉션(리스트)로 객체 그래프 탐색을 사용해 조회한 회원들을 출력하게된다
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // 팀->회원 객체 그래프 탐색
for (Member member : members) {
System.out.println("member.username = "+ member.getUsername());
}
mappedBy를 보면 회원 엔티티를 매핑할 때에는 사용하지 않은 반면, 팀 엔티티의 매핑에서만 사용하였다. 이 필요성을 해당 장에서 설명한다.
앞서 설명된 객체와 테이블의 연관관계 차이 때문에 JPA에서는 두 객체 연관관계 중 하나를 정해 테이블의 외래키를 관리하기 위한 연관관계의 주인을 정하게된다.
양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야한다.
이 주인을 정하기 위해 mappedBy 속성을 사용하게된다.
mappedBy 속성을 사용하지 않는다mappedBy를 통해 연관관계 주인을 저장해야함즉, 연관관계의 주인을 정하는 것 = 외래 키 관리자 설정과도 같다. 앞서서는 Member가 키 관리자이자 연관관계의 주인이다.
위의 코드에서는 회원 테이블이 외래키를 갖고 있기에 Member.team이 이 연관관계의 주인이 된다.
Team.members에는 mappedBy = "team" 속성을 사용해 주인이 아님을 설정mappedBy의 값은 연관관계 주인인 엔티티의 필드@OneToMany (mappedBy = "team")
private List<Member> members = new ArrayList<>();
양방향 연관관계에서 연관관계의 주인이 외래 키를 관리하기에 주인이 아닌 방향은 값을 설정하지 않아도, 데이터베이스에 외래 키 값이 정상 입력된다.
team1.getMembers().add(member1); //무시
즉, 위와 같은 코드가 필요하다고 생각되어도 결국 무시되고, 이 값은 외래 키 값에 영향을 주지 못한다.
연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않기에 주의해야한다.
이 경우 외래키에 널값이 입력된다.
어차피 연관관계의 주인이 아닌쪽에는 값을 저장해도 데이터베이스에 반영되지 않기에 저장할 필요가 없다. 하지만, 객체 관점에서는 양쪽 방향에 모두 값을 입력하는 것이 가장 안전하다.
...
member1.setTeam(team1); //연관관계: member1 -> team1
team1.getMembers().add(member1); // 연관관계: team1 -> member1
member2.setTeam(team1); //연관관계: member2 -> team1
team1.getMembers().add(member2); //연관관계: team1 -> member2
이렇게 양쪽 모두에 관계를 설정해준 경우에만 후에 필요에 의해 팀에 있는 인원 수를 출력한다거나, 팀에 있는 팀원 목록을 출력할 때 객체로 접근할 수 있다.
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
//양방향 연관관계 설정
member1.setTeam(team1); //연관관계 설정 member1 -> team1
team1.getMembers().add(member1); //연관관계 설정 team1 -> member1
em.persist(member1); //영속상태로 만듦.
양쪽에 연관관계를 위처럼 설정해야 순수 객체 상태에서도 동작하며, 테이블의 외래 키에도 정상 입력된다. 테이블의 외래 키 값은 연관관계 주인인 Member.team의 값이 사용된다.
양방향 연관관계에서는 결국 명시적으로 양쪽에 값을 써주고 관리하는 불편함이 있고, 실수의 여지가 많다.
member.setTeam(team);
team.getMembers().add(member);
양방향 관계에서 두 코드를 하나처럼 사용하는 것이 안전하기에 Member 클래스의 메소드를 수정해 코드를 리팩토링할 수 있다.
public class Member{
private Team team;
public void setTeam(Team team) {
this.team = team.
team.getMembers().add(this);
}
}
메소드 하나로 양쪽에 모두 값을 설정하도록 하면 실수나 빼먹을 가능성이 줄어든다.
앞선 setTeam() 메소드에서는 팀을 변경해주어도 이전 팀의 멤버에서 변경한 팀원을 조회할 수 있다는 문제가 존재한다.
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember(); //여전히 멤버1 조회 가능
즉, 팀을 변경할 때 관계를 제거하지 않았다. 연관관계를 변경할 때는 기존 연관관계를 삭제하는 코드를 추가해줘야한다. 따라서 앞의 setTeam()을 아래와 같이 수정해줘야한다.
public void setTeam(Team team) {
//이전의 연관관계 삭제 (팀이 있었다면 해당 팀의 컬렉션에서 멤버 삭제
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}