본문 바로가기

Dev Book Review/자바 ORM 표준 JPA 프로그래밍

Chapter5. 연관관계 매핑 기초

Entity들은 대부분 다른 Entity와 연관관계가 있다. 이때 객체는 참조를 사용하여 관계를 맺지만, 테이블은 외래 키를 사용해서 관계를 맺는다. ORM에서 가장 어려운 부분은 이러한 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.

 

연관관계 매핑을 이해하기 위한 핵심 키워드는 다음과 같다.

  • 방향 : 단방향/양방향이 있다. 방향은 객체관계에만 존재하고, 테이블 관계는 항상 양방향이다.
  • 다중성 : 다대일/일대다/일대일/다대다가 있다.
  • 연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야만 한다.

1. 단방향 연관관계

다대일 단방향 연관관계

객체 연관관계

회원 객체는 Member.team 필드를 통해서 팀 객체와 단방향 연관관계를 맺는다. 따라서 회원은 member.getTeam()을 통해 자신의 팀을 알 수 있지만, 팀은 필드가 없기 때문에 자신에게 속한 회원을 알 수 없다. 만약 객체 간의 연관관계를 양방향으로 만들고 싶다면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 객체에서의 양방향 연관관계는 서로 다른 단방향 관계 2개이다.

 

테이블 연관관계

회원 테이블은 TEAM_ID 외래 키를 통해 팀 테이블과 연관관계를 맺는다. 이때 회원 테이블과 팀 테이블은 항상 양방향 관계이다. 각 테이블은 외래 키를 통해서 JOIN하여 다른 테이블의 정보를 알 수 있다.

 

<JPA를 사용해서 단방향 연관관계 매핑>

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;
  }
}
@Entity
public class Team {

  @Id
  @Column(name="TEAM_ID")
  private String id;

  private String name;
}

위에서 살펴봤던 객체 연관관계와 테이블 연관관계를 JPA를 사용해서 매핑했다. 다시 말해서 Member.team(객체)과 MEMBER.TEAM_ID(테이블)를 매핑하는 것이다.

 

이때 새롭게 등장한 연관관계 매핑 어노테이션은 다음과 같다.

  • @ManyToOne : 다대일(N:1) 관계라는 매핑 정보다. 연관관계를 매핑할 때 다중성을 나타내는 어노테이션을 필수로 사용.
  • @JoinColumn(name="TEAM_ID") : 외래 키를 매핑할 때 사용한다. name 속성에는 외래 키 이름을 지정한다.
더보기

1. @ManyToOne

@ManyToOne 어노테이션은 다대일 관계에서 사용한다.

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch global fetch 전략을 설정한다. @ManyToOne=FetchType.EAGER
@OneToMany=FetchType.LAZY
cascade 영속성 전이 기능을 사용한다.  
targetEntity 연관된 엔티티의 타입 정보를 설정한다. (이 기능은 거의 사용하지 않는다)  

 

2. @JoinColumn

@JoinColumn은 외래 키를 매핑할 때 사용한다.

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약 조건을 직접 지정할 수 있다.
이 속성은 테이블을 생성할 때만 사용.
 
unique
nullable
insertable
updatable
columnDefinition
table
@Column의 속성과 같다.  

2. 연관관계 사용

1) 연관관계 저장

public void testSave() {

  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);
}
INSERT INTO TEAM (TEAM_ID, NAME) VALUES ('team1', '팀1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1')
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', 'team1')

회원 엔티티는 팀 엔티티를 참조하고 저장했다. 이때 JPA는 참조한 팀의 식별자 값을 외래키로 사용해서 적절한 INSERT 쿼리를 생성한다. 위 경우 생성되는 SQL에서는 회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값인 team1이 입력된다. 참고로 JPA에서 Entity를 저장할 때 연관된 모든 Entity는 영속 상태여야 한다.

 

2) 연관관계 조회

연관관계가 있는 Entity를 조회하는 방법은 크게 2가지다.

  • 객체 크래프 탐색 (객체 연관관계를 사용한 조회)
  • 객체지향 쿼리 사용 (JPQL)

자세한 설명은 다음 포스팅에서 한다!

 

3) 연관관계 수정

private static void updateRelation(EntityManager em) {

  Team team2 = new Team("team2", "팀2");
  em.persist(team2);

  Member member = em.find(Member.class, "member1");
  member.setTeam(team2);
}

이전 포스팅에서 설명했었지만 Entity의 값 수정은 트랜잭션을 커밋할 때 flush()가 일어나면서 변경 감지 기능이 동작한다. 이후 변경사항을 데이터베이스에 자동으로 반영한다. 연관관계를 수정할 때도 마찬가지로, 참조하는 대상만 변경하면 JPA가 알아서 처리한다.

 

4) 연관관계 제거

 

private static void deleteRelation(EntityManager em) {

  Member member1 = em.find(Member.class, "member1");
  member.setTeam(null);
}

연관관계를 null로 설정하여 제거한다.

 

member1.setTeam(null);
member2.setTeam(null);
em.remove(team1);

만약 관련된 엔티티를 제거하고 싶다면 기존에 있던 연관관계를 먼저 제거해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다. 

3. 양방향 연관관계

다대일 양방향 연관관계

이제부터는 팀->회원으로 접근하는 관계를 추가하여 다대일 양방향 연관관계로 매핑한다.

 

객체 연관관계

회원과 팀은 다대일 관계, 팀과 회원은 일대다 관계이다. 이때 팀은 여러 명의 회원과 관계를 맺을 수 있기 때문에 컬렉션을 사용해야 한다. 따라서 Team.members를 List 컬렉션으로 추가했다. (JPA는 List, Collection, Set, Map 같은 다양한 컬렉션을 지원한다)

 

테이블 연관관계

데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 따라서 데이터베이스에 추가할 내용은 전혀 없다.

 

<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;
  }
}
@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>();
}

 

public void biDirection() {

  Team team = em.find(Team.class, "team1");
  List<members> members = team.getMembers();

  for(Member member : members) {
  	System.out.println("member.username=" + member.getUsername());
  }
}

4. 연관관계의 주인

이전 포스팅에서 언급했었지만 엄연하게 따지면 객체에는 양방향 연관관계가 존재하지 않는다. 따라서 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향처럼 사용한다. 또한 테이블 연관관계는 외래 키 하나만으로 양쪽이 서로 JOIN이 가능하기 때문에 외래 키 하나만으로 양방향 연관관계를 맺는다.

 

둘 중 하나를 연관관계의 주인으로 선택해야 한다.

이때 우리가 Entity 간의 연관관계를 양방향으로 매핑하면 서로 다른 두 곳에서 객체의 연관관계를 관리하게 된다. 다른 말로 설명하면 객체의 참조는 둘인데 외래 키는 하나이다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?

 

이때 우리는 연관관계의 주인을 설정하게 된다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반대로 주인이 아닌 쪽은 읽기만 가능하다.

 

연관관계의 주인 설정

class Team {

  @OneToMany(mappedBy="team")
  private List<Member> members = new ArrayList<Member>();
}

위 테이블을 살펴봤을 때 외래 키는 회원 테이블에 존재한다. 이때 Member.team을 연관관계의 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 된다. 하지만 Team.members를 연관관계의 주인으로 선택하면 전혀 다른 테이블의 외래 키를 관리해야 한다. 따라서 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 Member.team이 주인이 된다. 주인이 아닌 Team.members에는 mappedBy 속성을 사용해서 주인이 아님을 설정한다. mappedBy 속성의 값으로는 연관관계의 주인인 team을 주면 된다.

 

참고로 데이터베이스 테이블의 다대일/일대다 관계에서는 항상 다쪽이 외래 키를 가진다. @ManyToOne은 항상 연관관계의 주인이 되기 때문에 mappedBy를 설정할 수 없고 속성 자체도 존재하지 않는다.

5. 양방향 연관관계 저장

public void testSave() {

  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);
}

이제 우리는 양방향 연관관계를 매핑했고, 연관관계의 주인도 선택했기 때문에 team1.getMembers().add(member1)과 같은 코드를 작성하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다. 사실 어차피 작성하더라도 Team.member는 연관관계의 주인이 아니기 때문에 외래 키에 아무런 영향을 주지 않는다. 결론적으로 EntityManager는 연관관계의 주인에 입력된 값을 사용해서 외래 키를 관리한다.

6. 양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인이 아닌 곳에만 값을 입력하는 것이다. 연관관계의 주인만이 외래 키 값을 변경할 수 있기 때문에 이럴 경우 데이터베이스 외래 키 값은 null로 입력된다.

 

또한 연관관계의 주인에만 값을 저장하고, 주인이 아닌 곳에는 값을 저장하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다. 예를 들어 JPA를 사용하지 않고 Entity에 대한 테스트 코드를 작성했을 때 원하는 결과가 나오지 않을 수 있다. 따라서 객체 관점에서는 양쪽 방향에 모두 값을 입력하는 게 가장 안전하다.

 

public class Member {

  private Team team;

  public void setTeam(Team team) {
    if (this.team != null) {
    	this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers.add(this);
  }
}

양방향 연관관계는 결국 양쪽 다 신경을 써야 하는데 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다보면 실수로 둘 중 하나만 호출하여 양방향이 깨질 수 있다. 따라서 양방향 관계에서는 두 코드를 하나인 것처럼 사용하는 게 안전하다. 이를 한번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라고 한다.

 

또한 연관관계를 변경할 때 기존의 연관관계가 존재한다면 제거하는 게 안전하다. 사실 제거하지 않아도 데이터베이스 외래 키를 변경하거나, 새로운 영속성 컨텍스트에서 getMembers()를 호출했을 땐 문제가 되지 않는다. 하지만 연관관계를 변경하고 영속성 컨텍스트가 아직 살아있는 상태에서 getMembers()를 호출하면 제거됐어야 할 회원이 반환된다. 따라서 애초에 제거하는 게 안전하다.