이 글은 “JPA 프로그래밍 입문 (최범균 저)” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “gunjuko92@gmail.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

Chapter4. 밸류와 @Embeddable

1. 밸류로 의미를 더 드러내기

밸류는 다음과 같은 특징을 가진다.

  • 밸류는 개념적으로 한 개의 값을 표현한다.
  • 다른 밸류 객체와 구분하기 위한 식별자를 갖지 않는다.
  • 자신만의 라이프사이클을 갖지 않는다. 밸류는 자신이 속한 객체가 생성될 때 함께 생성되고 삭제될 때 함께 삭제된다.

밸류 객체를 사용하는 이유는 값의 의미를 더 잘 드러내기 때문이다.

2. 밸류 클래스의 구현

밸류 클래스 구현

  • 생성 시점에 모든 프로퍼티를 파라미터로 전달받는다.
  • 읽기 전용 프로퍼티만 제공한다.
  • 각 프로퍼티의 값을 비교하도록 equals() 메소드를 재정의한다.
  • 각 프로퍼티의 값을 이용해서 해시코드를 생성하도록 hashCode() 메소드를 재정의한다.

3. @Embeddable 애노테이션을 이용한 밸류 매핑

Address 밸류 타입을 갖는 Hotel 엔티티를 테이블에 매핑하려면 다음과 같은 매핑 설정을 추가해야 한다.

  • 밸류 타입인 Address 클래스에 @Embeddable 애노테이션을 적용
  • Hotel 클래스는 @Embedded 애노테이션을 사용해서 밸류 타입을 매핑 설정한다.

@Embeddable 애노테이션은 대상 클래스가 다른 엔티티의 일부로 함께 저장될 수 있다는 것을 설정한다. @Embedded 애노테이션은 매핑 대상이 @Embeddable 클래스의 인스턴스라는 것을 설정한다.

3.1 null 밸류의 매핑 처리

  • @Embedded 애노테이션을 적용한 밸류 객체가 널인 경우엔 매핑된 모든 컬럼에 널이 들어간다.
  • @Embedded로 매핑한 컬럼의 모든 값이 널이면 조회할 때 매핑 대상도 널이 된다.

3.2 @Embeddable의 접근 타입

  • @Embedded로 매핑한 대상은 해당 엔티티의 접근 타입을 따른다.
    • 예를 들어 엔티티가 필드 접근 타입을 사용하면 @Embedded로 매핑한 객체를 처리할 때에도 필드 접근 타입을 사용한다.
  • @Embeddable이 속한 엔티티의 접근 방식에 상관없이 접근 타입을 고정하려면 @Access 애노테이션을 사용하면 된다.

4. @Entity와 @Embeddable의 라이프사이클

  • @Embedded로 매핑한 객체는 엔티티와 동일한 라이프사이클은 갖는다.
  • JPA는 @Entity로 매핑한 클래스와 @Embeddable로 매핑한 클래스를 서로 다른 테이블에 저장하는 방법을 제공하는데, 이 경우에도 @Embeddable의 라이프사이클은 엔티티를 따른다.

5. @AttributeOverrides를 이용한 매핑 설정 재정의

  • @AttributeOverrides 애노테이션과 @AttributeOverride 애노테이션을 사용하면 @Embedded로 매핑한 값 타입의 매핑 설정을 재정의할 수 있다.
@Entity
public class Sight {
  @Id
  private Long id;
  private String name;
  
  @Embedded
  private Address korAddress;
  
  @Embedded
  @AttributeOverrides({
    @AttributeOverride(name = "zipcode", column = @Column(name = "eng_zipcode")),
    @AttributeOverride(name = "address1", column = @Column(name = "eng_addr1")),
    @AttributeOverride(name = "address2", column = @Column(name = "eng_addr2"))
  })
  private Address engAddress;
}

@AttributeOverride 애노테이션은 개별 매핑 대상에 대한 설정을 재정의한다. name 속성으로 재정의할 대상을 설정하고 column 속성으로 변경할 매핑 컬럼을 지정한다. @AttributeOverrides 애노테이션은 여러 개의 @AttributeOverride를 설정할 때 사용한다.

6. @Embeddable 중첩

@Embeddable로 지정한 클래스에 또 다른 @Embeddable 타입을 중첩해서 매핑할 수 있다.

7. 다른 테이블에 밸류 저장하기

  • 밸류 객체를 반드시 같은 테이블에 저장해야 하는 것은 아니다.
  • @Entity 클래스에 @SecondaryTable로 밸류를 저장할 테이블 지정
  • @Entity 클래스에 @AttributeOverride로 컬럼의 테이블 이름을 재정의
@Entity
@SecondaryTable(
  name = "sight_detail",
  pkJoinColumns = @PrimaryKeyJoinColumn(name = "sight_id", referencedColumnName = "id")
)
public class Sight {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  
  @Embedded
  @AttributeOverrides({
    @AttributeOverride(name = "hoursOfOperation", column = @Column(name = "hours_op", table = "sight_detail")),
    @AttributeOverride(name = "holiday", column = @Column(table = "sight_detail")),
    @AttributeOverride(name = "facilities", column = @Column(table = "sight_detail"))
  })
  private SightDetail detail;
}
  • @SecondaryTable은 데이터의 일부를 다른 테이블로 매핑할 때 사용
    • name : 테이블의 이름을 지정
    • pkJoinColumns : @SecondaryTable로 지정한 테이블에서 @Entity와 매핑되는 테이블의 주요키를 참조할 때 사용할 컬럼을 설정한다. @PrimaryKeyJoinColumn의 name 속성은 @SecondaryTable의 컬럼 이름을 지정하고, referencedColumnName 속성은 @Entity의 @Id와 매핑되는 컬럼 이름을 지정한다.
  • @AttributeOverride를 이용해서 어떤 값을 sight_detail 테이블에서 읽어올지 설정한다.
  • detail 필드가 널인 경우엔 sight 테이블에 대해서만 insert 쿼리를 실행한다.
  • @EntityManager.find()로 엔티티를 조회하면 @SecondaryTable로 매핑한 테이블을 레프트 조인으로 조회한다.
  • Sight를 조회할 때 SightDetail에 매핑되는 sight_detail 테이블에 데이터가 존재하지 않으면 해당 매핑 대상의 값은 널이 된다.

아래와 같이 SightDetail과 매핑할 테이블 설정을 SightDetail에서 직접 설정해도 된다.

@Embeddable
public class SightDetail {
  @Column(name = "hours_op", table = "sight_detail")
  private String hoursOfOperation;
  @Column(name = "holidays", table = "sight_detail")
  private String holidays;
  @Column(name = "facilities", table = "sight_detail")
  private String facilities;
}

7.1 다른 테이블에 저장한 @Embeddable 객체 수정과 쿼리

sight_detail 테이블에 데이터 존재 여부 Sight의 detail 필드 변경 sight_detail 테이블에 대한 실행 쿼리
존재함 새 객체 할당 update
존재함 null 객체할당 delete
존재하지 않음 새 객체 할당 insert
존재함 기존 SightDetail 객체의 set 메소드를 이용 변경 update
존재하지 않음 null 쿼리 실행하지 않음

8. @Embeddable의 복합키

  • 테이블의 주요키가 두 개 이상의 컬럼으로 구성된 복합키이고 이 복합키를 엔티티의 식별자에 매핑해야 할 경우, @Embeddable 타입을 복합키에 매핑할 식별자 타입으로 사용할 수 있다.
  • 일반 밸류 클래스와 비교해서 복합키로 사용할 밸류 클래스는 다음의 차이가 있다.
    • 값 비교를 위한 equals() 메소드와 hashCode() 메소드를 알맞게 구현해야 한다.
    • Serializable 인터페이스를 상속해야 한다.
@Embeddable
public class MonChargeId implements Serializable {
  @Column(name = "hotel_id")
  private String hotelId;
  @Column(name = "year_mon")
  private String yearMon;
  
  public MonChargeId() {}
  
  public MonChargeId(String hotelId, String yearMon) {
    this.hotelId = hotelId;
    this.yearMon = yearMon
  }
  
  // equals, hashCode 메소드 구현 생략
}

@Entity
@Table
public class MonthCharge {
  @Id
  private String MonthChargeId id;
  
  // 생략
}