5장_ 연관관계 매핑 기초
Updated:
객체 관계 매핑(ORM) 에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.
이를 이해하기 위한 핵심 키워드를 먼저 보고, 하나씩 이해해보자
- 방향(Direction): 단방향, 양방향
- 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
- 연관관계의 주인(owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다
1. 단방향 연관관계
회원과 팀의 관계를 통해 다대일 단방향 관계를 알아보자
- 회원과 팀이 있다
- 회원은 하나의 팀에만 소속될 수 있다
- 회원과 팀은 다대일 관계다
위 예시를 객체 연관관계로 봤을때는
- 회원 객체는 Member.team 필드(맴버변수)로 팀 객체와 연관관계를 맺는다
- 객체는 참조(주소)로 연관관계를 맺는다
- 회원 객체와 팀 객체는 단방향 관계다
- 회원은 팀을 알 수 있지만, 반대로 팀은 회원을 알 수 없다
테이블 연관관계로 봤을때는
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다
- 테이블은 외래 키로 연관관계를 맺는다
- 회원 테이블과 팀 테이블은 양방향 관계다
- TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있다
객체 간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 볼 수 있지만,
정확히 이야기하면 양방향 관계가 아니라 서로 다른 단방향 관계 2개로 볼 수 있다.
이제 두 관계를 JPA로 매핑해보면 다음과 같다.
@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;
}
// Getter, Setter ...
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
// Getter, Setter ...
}
코드에 사용된 연관관계 매핑 어노테이션을 자세히 알아보자
- @JoinColumn
- @JoinColumn은 외래 키를 매핑할 때 사용한다
- name
- 매핑할 외래 키 이름
- 기본값은 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
- referencedColumnName
- 외래 키가 참조하는 대상 테이블의 컬럼명
- 기본값은 참조하는 테이블의 기본 키 컬럼명
- foreignKey(DDL)
- 외래 키 제약조건을 직접 지정할 수 있다
- 이 속성은 테이블을 생성할 때만 사용한다
- unique, nullable, insertable, updatable, columnDefinition, table
- @Column의 속성과 같다 (지난 포스트 참고)
- @ManyToOne
- @ManyToOne 어노테이션은 다대일 관계에서 사용한다
- optional
- false로 설정하면 연관된 엔티티가 항상 있어야 한다
- 기본값은 true
- fetch
- 글로벌 페치 전략을 설정한다 (8장에서 정리)
- 기본값은 @ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
- cascade
- 영속성 전이 기능을 사용한다 (8장에서 정리)
- targetEntity
- 연관된 엔티티의 타입 정보를 설정한다
- 이 기능은 거의 사용하지 않는다
- 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다
// targetEntity 예시
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다
@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다
2. 연관관계 사용
연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.
- 객체 그래프 탐색(객체 연관관계를 사용한 조회)
- 객체지향 쿼리 사용(JPQL)
수정은 em.update() 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면
트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.
그리고 변경사항을 테이터베이스에 자동으로 반영한다.
삭제는 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.
member1.setTeam(null); // 회원1 연관관계 제거
member2.setTeam(null); // 회원2 연관관계 제거
em.remove(team); // 팀 삭제
3. 양방향 연관관계
위에서 본 회원과 팀의 관계를 양방향 관계로 매핑해보자
회원 엔티티는 변경할 필요가 없고, 팀 엔티티만 변경하면 된다.
@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>();
// Getter, Setter ...
}
팀과 회원은 일대다 관계다. 따라서 팀 엔티티에 컬렉션을 추가했다.
그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
4. 연관관계의 주인
양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
어떤 연관관계를 주인으로 정할지는 아까 살펴본 mappedBy 속성을 사용하면 된다.
연관관계의 주인을 정하는 기준에 대해서 혼동이 된다면, 하나만 기억하면 된다.
단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다.
하지만 양방향은 연관관계의 주인이라는 이름으로 인해 오해가 있을 수 있다.
비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하면 안 된다.
비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.
5. 양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
객체 관점에서 볼때 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
member.setTeam(team);
team.getMembers().add(member);
하지만 각각 호출하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있으므로 아래처럼 코드를 리팩토링하는게 좋다.
이를 연관관계 편의 메소드라 한다. 하나의 호출로 두 코드를 실행시켜주는 것이다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
하지만 이렇게 개발하면 연관관계가 변경될 때 오류가 발생하게 된다.
member1.setTeam(teamA); // 1
member1.setTeam(teamB); // 2
Member findMember = teamA.getMember(); // member1이 여전히 조회된다
teamB로 변경할 때 teamA 관계를 제거하지 않았기 때문이다.
연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
public void setTeam(Team team) {
// 기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
그런데!
기존 팀과 관계를 제거하는 코드가 없어도 외래 키를 변경하는 데는 문제가 없다.
왜냐면 Team.member는 연관관계의 주인이 아니기 때문이다. 새로운 영속성 컨텍스트에서는 관계가 끊어져 있으므로 아무것도 조회되지 않는다.
문제는 관계를 변경하고 영속성 컨텍스트가 아직 살아있는 상태에서 teamA의 getMembers()를 호출하면 member1이 반환된다는 점이다.
따라서 변경된 연관관계는 앞서 설명한 것처럼 관계를 제거하는 것이 안전하다.
이처럼 객체에서 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지 볼 수 있다.
객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 된다.
Leave a comment