엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 예) (주문 엔티티)---(상품 엔티티)---(카테고리, 재고 엔티티)

그런데 객체는 참조를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다.

핵심 키워드

  • 방향: 단방향, 양방향이 있다.
    • 단방향: 회원과 팀이 관계가 있을 때, 회원->팀, 팀->회원 둘 중 한 쪽만 참조하는 것
      • 양방향: 회원->팀, 팀->회원 양쪽 모두 서로 참조하는 것
    • 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
  • 다중성: 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N) 다중성이 있다.
    • 다대일 관계: 회원과 팀 (여러 회원은 한 팀에 속하므로)
    • 일대다 관계: 팀과 회원 (한 팀에 여러 회원이 소속될 수 있으므로)
  • 연관관계의 주인: 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

단방향 연관관계

  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이
    • 객체 연관관계: 참조를 통한 연관관계는 언제나 단방향이다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다.
    • 테이블 연관관계: 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

객체 관계 매핑

매핑한 회원 엔티티
@Entity
public class Member {
    ...

    //연관관계 매핑
    @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;

    ...
}

@JoinColumn(name = "TEAM_ID"): 조인 컬럼은 외래 키를 매핑할 때 사용한다.

  • name 속성에는 매핑할 외래 키 이름을 지정한다. 이 어노테이션은 생략할 수 있다.
속성기능기본값
name매핑할 외래 키 이름필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName외래 키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL)외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. 

unique, nullable, insertable, updatable, columnDefinition, table

@Column의 속성과 같다. 

@ManyToOne: 다대일 관계라는 매핑 정보다.

속성기능기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다.true
fetch글로벌 페치 전략을 설정한다. 자세한 내용은 8장에서 설명한다.

@ManyToOne=FetchType.EAGER

@OneToMany=FetchType.LAZY

cascade영속성 전이 기능을 사용한다. 자세한 내용은 8장에서 설명한다. 
targetEntity연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. 

연관관계 사용

양방향 연관관계

이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가해서 양방향 연관관계를 매핑해보자.

  • 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.

    • JPA는 List를 포함해서 Collection, Set, Map 같은 다양한 컬렉션을 지원한다.

      양방향 연관관계 매핑

      매핑한 팀 엔티티
      @Entity
      public class Team {
      ...
      @OneToMany(mappedBy = "team")
      private List<Member> members = new ArrayList<Member>();
      ...
      }
  • mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.

연관관계의 주인

  • 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케잉션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐 이다.
  • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다.
  • 이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

양방향 매핑의 규칙: 연관관계의 주인

  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 주인은 mappedBy 속성을 사용하지 않고 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관게의 주인을 지정해야 한다.
  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.

연관관계의 주인은 외래 키가 있는 곳

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
  • 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다.

양방향 연관관계의 주의점

  • 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.

순수한 객체까지 고려한 양방향 연관관계

  • 그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?
  • 사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

연관관계 편의 메소드

  • 양방향 연관관계는 결국 양쪽 다 신경 써야 한다.

  • member.setTeam(team);
    team.getMembers().add(member);

    양방향 연관관계에서는 두 코드는 하나인 것처럼 사용하는 것이 안전하다.
    Member 클래스의 setTeam() 메소드를 수정해서 코드를 리팩토링해보자.

    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 -> member1 관계를 제거하지 않았다.

  • 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.

    public void setTeam(Team team) {
      //기본 팀과 관계를 제거
      if (this.team != null) {
          this.team.getMembers().remove(this);
      }
      this.team = team;
      team.getMembers().add(this);
    }
  • 이 코드는 객체에서 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지 보여준다. 반면에 관계형 데이터베이스는 외래키 하나로 문제를 단순하게 해결한다. 정리하자면 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.

정리

  • 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

양방향 매핑은 복잡하다. 비즈니스 로직의 필요에 따라 다르겠지만 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능(JPQL 쿼리 탐색 포함)이 필요할 때 양방향을 사용하도록 코드를 추가해도 된다.

연관관계의 주인을 정하는 기준

  • 단방향은 항상 외래 키가 있는 곳을 기분으로 매핑하면 된다.
  • 양방향은 연관관계의 주인이라는 이름으로 오해가 있을 수 있다.
  • 비즈니스 로직상 더 중요하다고 연관관계의 주인으로 선택하면 안 된다. 비즈니스 중요도를 배제하고 단순히 외래 키 관리자 정도의 의미만 부여해야 한다.
주의
  • 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다. (Lombok 라이브러리를 사용할 때도 자주 발생한다)
  • 예) Member.toString() -> getTeam() -> Team.toString() -> getMember() ...
  • 이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생하는데 JSON 라이브러리들은 보통 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공한다.
참고
  • 일대다를 연관관계의 주인으로 선택하는 것이 불가능한 것만은 아니다. 예를 들어 엔티티의 Team.members를 연관관계의 주인으로 선택하는 것이다. 하지만 성능과 관리 측면에서 권장하지 않는다. (6.2.1절 참고)

+ 따끈한 최근 게시물