연관관계 매핑 기초
단방향 연관관계
순수한 객체 연관관계
순수한 객체 모델에서는 클래스 간의 연관관계를 필드를 통해 나타낸다. 예를 들어, Member 객체가 Team 객체와 연관관계를 가질 때, Member 클래스는 Team 객체를 참조하는 필드를 가질 수 있다.
public class Member {
private Long id;
private String name;
private Team team;
}
public class Team {
private Long id;
private String name;
}
테이블 연관관계
관계형 데이터베이스에서는 이러한 연관관계를 외래 키(Foreign Key)를 통해 관리한다. 위의 객체 모델을 데이터베이스에 매핑하면, MEMBER 테이블에는 TEAM_ID라는 외래 키 컬럼이 생성되어 TEAM 테이블과의 연관관계를 나타내게 된다.
CREATE TABLE MEMBER (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255),
TEAM_ID BIGINT,
FOREIGN KEY (TEAM_ID) REFERENCES TEAM(ID)
);
CREATE TABLE TEAM (
ID BIGINT PRIMARY KEY,
NAME VARCHAR(255)
);
객체 관계 매핑
JPA에서는 객체 간의 연관관계를 테이블의 외래 키와 매핑할 수 있다. 이때, @ManyToOne이나 @OneToMany 같은 어노테이션을 사용하여 연관관계를 설정한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
@JoinColumn
@JoinColumn 어노테이션은 외래 키를 매핑할 때 사용하는데, 데이터베이스의 외래 키 컬럼을 지정하고, 해당 컬럼과 연관된 엔티티를 매핑한다.
- name: 외래 키 컬럼명을 지정
- referencedColumnName: 참조하는 테이블의 컬럼명을 지정한다. (기본 값은 참조하는 테이블의 기본 키)
@ManyToOne
@ManyToOne 어노테이션은 다대일(N:1) 관계를 나타낸다. 하나의 Team은 여러 Member와 연관될 수 있지만, 하나의 Member는 하나의 Team에만 소속될 수 있다. 이는 데이터베이스에서 외래 키를 통해 표현된다.
연관관계 사용
이미 눈치를 챘을 수 있겠지만, java 객체에서의 연관관계와 데이터베이스에서의 연관관계는 동일하지 않다. 데이터베이스는 외래 키를 통해 양방향 연관관계가 되지만 java 객체의 연관관계는 서로다른 두 단방향 연관관계가 합쳐져 양방향으로 보이게 하는 수법(?)을 사용한다. 밑에서 연관관계를 설정한 후, 이를 실제 애플리케이션에서 어떻게 사용할 수 있는지 알아보자.
저장
JPA를 사용해 연관관계를 설정한 엔티티를 저장할 때는, 연관된 엔티티를 먼저 저장한 후, 연관관계가 있는 엔티티를 저장해야 한다. 이때 JPA는 INSERT 쿼리를 생성하여 MEMBER 테이블과 TEAM 테이블에 데이터를 저장한다.
Team team = new Team();
team.setName("Team A");
em.persist(team);
Member member = new Member();
member.setName("Member 1");
member.setTeam(team);
em.persist(member);
조회
연관된 엔티티를 조회할 때는 JPA가 자동으로 연관된 엔티티를 가져온다.
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
수정
연관관계를 수정하려면 연관된 엔티티의 참조를 변경하면 된다. 이때 JPA는 변경된 내용을 반영하기 위해 UPDATE 쿼리를 실행한다.
Team newTeam = new Team();
newTeam.setName("Team B");
em.persist(newTeam);
member.setTeam(newTeam);
연관관계 제거
연관관계를 제거하려면 연관된 엔티티의 참조를 null로 설정한다. 이 경우 JPA는 외래 키 컬럼을 null로 업데이트한다.
member.setTeam(null);
연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 먼저 해당 엔티티와의 연관관계를 제거한 후 삭제해야 한다. 직접적으로 연관관계를 제거하지 않으면, 데이터베이스의 무결성 제약 조건으로 인해 오류가 발생할 수 있다.
member.setTeam(null);
em.remove(team);
양방향 연관관계
양방향 연관관계는 서로를 참조하는 두 개의 엔티티 간의 관계다. 예를 들어, Member가 Team을 참조하고, Team도 Member를 참조하는 경우다.
양방향 연관관계 매핑
양방향 연관관계를 매핑할 때는 @OneToMany와 @ManyToOne 또는 @OneToOne 어노테이션을 함께 사용한다. 한 쪽은 연관관계의 주인이 된다. mappedBy 속성은 연관관계의 주인이 아닌 쪽에 사용되며, 주인의 필드를 지정한다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
일대다 컬렉션 조회
양방향 연관관계에서 일대다(1:N) 컬렉션을 조회하면 JPA는 필요에 따라 JOIN 쿼리를 사용하여 데이터를 가져온다. 이때, Team 엔티티에 속한 모든 Member들이 함께 조회된다.
Team team = em.find(Team.class, teamId);
List<Member> members = team.getMembers();
연관관계의 주인
연관관계에서 어느 한 쪽은 연관관계의 주인이 되어 데이터베이스의 외래 키를 관리한다. 주인이 아닌 쪽은 연관관계를 단순히 읽기만 할 수 있다. 즉, 외래 키를 관리하는 엔티티가 주인이라고 생각하면 쉽다.
양방향 매핑의 규칙: 연관관계의 주인
양방향 연관관계에서는 반드시 한 쪽이 연관관계의 주인이 되어야 하며, 주인만이 외래 키를 업데이트할 수 있다. 주인은 주로 @ManyToOne 쪽이 된다.
연관관계의 주인은 외래 키가 있는 곳
연관관계의 주인은 외래 키가 위치한 엔티티다. 주인이 아닌 쪽은 mappedBy 속성을 사용하여 주인을 지정한다.
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private List<Member> members;
}
양방향 연관관계 저장
양방향 연관관계를 저장할 때는 두 엔티티 간의 연관관계를 모두 설정해야 한다. 왜냐하면 앞서 언급했 듯이 서로 다른 두 단방향을 합쳐서 양방향처럼 보이게 하기 때문이다. 어느 한 방향에서만 설정해서는 절대 안된다. 밑에 예제처럼 member에서도 team을 설정하고, team에서도 members에 member를 추가해야한다는 이야기다.
Team team = new Team();
team.setName("Team A");
em.persist(team);
Member member = new Member();
member.setName("Member 1");
member.setTeam(team);
team.getMembers().add(member);
em.persist(member);
양방향 연관관계의 주의점
양방향 연관관계를 사용할 때는 주의할 점이 있다. 순수한 객체 모델과의 불일치 문제, 연관관계 편의 메소드, 무한 루프 등의 문제를 고려해야 한다.
순수한 객체까지 고려한 양방향 연관관계
양방향 연관관계를 설정할 때, 순수한 객체 모델에서도 연관관계가 올바르게 설정되도록 주의해야 한다.
public void setTeam(Team team) {
this.team = team;
if (!team.getMembers().contains(this)) {
team.getMembers().add(this);
}
}
연관관계 편의 메소드
연관관계 설정을 더 쉽게 하기 위해 연관관계 편의 메소드를 작성할 수 있다.
public void setTeam(Team team) {
if (this.team != null) { // 기존에 이미 팀이 존재한다면
this.team.getMembers().remove(this); // 관계를 끊는다.
}
this.team = team;
team.getMembers().add(this);
}
연관관계 편의 메소드 작성 시 주의사항
연관관계 편의 메소드를 작성할 때는 무한 루프를 방지하고, 객체 간의 연관관계가 일관되게 유지되도록 주의해야 한다. 잘못된 편의 메소드는 데이터의 일관성을 해칠 수 있다. 예를 들어, 양방향 관계에서 한쪽만 설정되고 다른 쪽이 설정되지 않는다면 객체 간의 관계가 불완전하게 설정될 수 있다.
public void removeMember(Member member) {
this.members.remove(member);
member.setTeam(null);
}