Home 4. 밸류와 @Embeddable
Post
Cancel

4. 밸류와 @Embeddable

01. 밸류로 의미 더 드러내기

Hotel 클래스는 주소 자체를 의미하는 address 데이터를 갖고 있다. 그리고 Address 클래스는 zipcode, address1, address2를 데이터로 갖고 있어 따로 유추하지 않아도 주소가 우편번호, 주소1, 주소2로 구성된다는 것을 알 수 있다.

Address 클래스 같은 타입을 value라고 부르는데 value의 특징은 다음과 같다.

  • value는 개념적으로 한 개의 값을 표현한다. (Address 클래스는 세 개의 데이터로 구성되어 있지만 개념적으로 한 개의 주소를 나타낸다.)
  • 식별자를 갖지 않는다.
  • 자신만의 라이프사이클을 갖지 않는다.(자신이 속한 객체가 생성될 때 함께 생성되고 삭제될 때 함께 삭제됨)

value 객체를 사용하는 이유는 값의 의미를 더 잘 드러내기 때문이다. zipcode, address1, address2 의 세 데이터보다 Address 클래스가 주소의 의미를 더 잘 드러낸다.

02. 밸류 클래스의 구현

기본 타입 값은 1)값을 비교한다. 2)값 자체는 바뀌지 않는다. 라는 특징을 갖고 있다. 밸류 클래스도 값으로 활용하기 위해 기본 타입 값 특징을 적용해볼 수 있다. 그러기 위해 다음과 같이 구현해야 한다.

  • 생성 시점에 모든 프로퍼티를 파라미터로 받는다.
  • 읽기 전용 프로퍼티만 제공한다.
  • 각 프로퍼티의 값을 비교하도록 equals() 메서드를 재정의한다.
  • hashCode() 메서드를 재정의 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
public class Hotel{
    @Id
    private String id;

    //…

    private Address address;
    public Address getAddress() {
        return address;
    }

    //주소를 변경하려면 다음 코드 처럼 Address 객체를 새로 할당한다.
    public void changeAddress(Address newAddress){
        this.address = newAddress;
    }
}

03. @Embeddable 애노테이션과 @Embedded 애노테이션을 이용한 밸류 매핑

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

  • 밸류 타입인 Address 클래스에 @Embeddable 애노테이션을 적용한다.
  • Hotel 클래스는 @Embedded 애노테이션을 사용해서 밸류 타입을 매핑 설정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//대상 클래스가 다른 엔티티의 일부로 함께 저장될 수 있다는 것을 설정
@Embeddable
public class Address {
  private String zipcode;
  private String address1;
  private String address2;
}

@Entity
public class Hotel{
  @Id
  private String id;
  private String name;
  @Eumerated(EnumType.STRING)
  private Grade grade;

  //@Embeddable 클래스의 인스턴스라는 것을 설정한다.
  @Embedded
  priavate Address address;
}

//사용 예제
Hotel hotel = em.find(Hotel.class, "H100-10");
Address address = hotel.getAddress();
//address.getAddress1();
//address.getAddress2();
//address.getZipcode();

Hotel의 address도 매핑 대상이므로 트랜잭션 범위 안에서 address가 변경되면 UPDATE 쿼리를 실행해서 변경 내역을 DB에 반영한다.

3.1 null 밸류의 매핑 처리

1
2
3
4
5
6
public Hotel(String id, String name, Grade grade, Address address){
  this.id = id;
  this.name = name;
  this.grade = grade;
  this.address = address;
}

위와 같은 생성자가 정의되어 있을 때 address 파라미터를 null로 주고 저장하면 Address와 매핑된 세 개의 칼럼에 모두 null 값이 할당된다. 그리고 조회할 때 매핑 대상도 null이 된다.

3.2 @Embeddable의 접근 타입

기본적으로 @Embedded로 매핑한 대상은 해당 엔티티의 접근 타입을 따른다. 예) 위 예제에서 Hotel 클래스는 필드 접근 타입을 사용하므로 @Embedded로 매핑한 Address를 처리할 때에도 필드 접근 타입을 사용한다.

반대로 Hotel 클래스에서 프로퍼티 접근 타입을 사용하면 Address 프로퍼티도 프로퍼티 접근 타입을 따른다. 그래서 Address 클래스도 프로퍼티 처리를 위한 get/set 메서드를 정의해야 한다.

만약 Address 클래스에 항상 필드 접근 방식을 사용해서 처리하려는 경우 Address 클래스에 @Access 애노테이션을 사용해서 @Embeddable의 접근 타입을 고정하면 된다.

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

@Embedded로 매핑한 객체는 엔티티와 동일한 라이프사이클을 갖는다. 즉 엔티티를 저장하고 수정하고 삭제할 때 엔티티에 속한 @Embeddable 객체도 함께 저장되고 수정되고 삭제된다.

위 예에서는 @Entity로 설정한 객체와 @Embeddable로 설정한 객체를 한 테이블에 매핑하여 엔티티의 라이프 사이클을 따르는 것이 당연해 보였다. JPA는 @Entity로 매핑한 클래스와 @Embeddable로 매핑한 클래스를 서로 다른 테이블에 저장하는 방법도 제공하는데 이때도 엔티티의 라이프사이클을 따른다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Embeddable
public class Address {
    private String zipcode;
    private String address1;
    private String address2;
}

@Entity
public class Sight {
    @Id @GeneratedValue(stratrgy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Embedded
    private Address korAddress;

    @Embedded
    private Address engAddress;
}

위의 경우 영어주소, 한글주소의 테이블 칼럼의 이름이 같아진다. 그래서 초기화 과정에서 에러가 발생하게 된다. 이때 사용할 수 있는 설정이 @AttributeOverrides이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
public class Sight {
    @Id @GeneratedValue(stratrgy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Embedded
    private Address korAddress;

    @Embedded
    //매핑 대상이 여러개인 경우 s가 붙은 AttributeOverrides를 사용한다.
    @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;
}
  • korAddress.zipcode -> zipcode
  • korAddress.address1 -> address1
  • korAddress.address2 -> address2
  • engAddress.zipcode -> eng_zipcode
  • engAddress.address1 -> eng_addr1
  • engAddress.address2 -> eng_addr2

06. @Embeddable 중첩

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Embaddable
public class ContactInfo{
    @Column(name="ct_phone")
    private String phone;

    @Column(name="ct_email")
    private String email;

    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name="zipcode", column=@Column(name="ct_zipcode")),
        @AttributeOverride(name="address1", column=@Column(name="ct_addr1")),
        @AttributeOverride(name="address2", column=@Column(name="ct_addr2")),
    })
    private Address address;
}

//중첩된 @Embeddable 클래스에 대한 설정 재정의
@Entity
public class City{
    //...

    @Embedded
    @AttributeOverrides({
        //address.
        @AttributeOverride(name="address.zipcode", column=@Column(name="city_zip")),
        @AttributeOverride(name="address.address1", column=@Column(name="city_addr1")),
        @AttributeOverride(name="address.address2", column=@Column(name="city_addr2"))
    })
    private ContactInfo contactInfo;

}

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

지금까지 예제는 엔티티와 밸류 객체를 한 테이블에 저장했다. 하지만 밸류 객체를 반드시 같은 테이블에 저장해야 하는 것은 아니다. 엔티티와 밸류를 서로 다른 테이블에 저장하는 예로 기본 정보와 상세 정보를 들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//밸류이므로 주요키를 갖지 않는다.
@Embeddable
public class SightDetail {
  @Column(name="hours_op")
  private String hoursOfOperation;
  private String holidays;
  private String facilities;
}

@Entity
//밸류를 저장할 테이블 지정
@SecondaryTable(
  name="sight_detail",
  pkJoinColumns=@PrimaryKeyJoinColumn(
    name="sight_id", referenceColumnName="id")
)
public class Sight{
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private Long id;
  privats String name;

  //...

  @Embedded
  //칼럼의 테이블 이름을 재정의
  @AttributeOverride({
    @AttributeOverride(
      name="hoursOfOperation",
      column=@Column(name="hours_op", table="sight_detail")),
    @AttributeOverride(
      name="holidays",
      column=@Column(ntable="sight_detail")),
    @AttributeOverride(
      name="facilities",
      column=@Column(ntable="sight_detail")),
  })
  private SightDetail detail;

  //getter, setter
}

@SecondaryTable은 데이터의 일부를 다른 테이블로 매핑할 때 사용한다. name은 테이블 명을 pkJoinColumns는 @SecondaryTable로 지정한 테이블에서 @Entity와 매핑되는 테이블의 주요키를 참조할 때 사용할 칼럼을 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sight sight = new Sight("경복궁",
    new Address("03045", "서울시 종로구", "세종로 1-1"),
    new Address("03045", "Jongno-gu Seoul", "1-1, Sejong-ro")
);
sight.setDetail(new SightDetail("09~17시", "매주 화요일", "안내 설명"));

EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();

try{
  transaction.begin();
  entityManager.persist(sight);   //SightDetail은 sight_detail 테이블에 저장
  transaction.commit();
}catch(Exception ex) {
  transaction.rollback();
  throw ex;
}finally{
  entityManager.close();
}
1
2
3
4
5
6
Sight sight = new Sight("경복궁",
    new Address("03045", "서울시 종로구", "세종로 1-1"),
    new Address("03045", "Jongno-gu Seoul", "1-1, Sejong-ro")
);

entityManager.persist(sight); //detail이 null이면 sight_detail 테이블에는 insert 하지 않음

EntityManager.find()로 엔티티를 조회하면 @SecondaryTable로 매핑한 테이블을 레프트 조인으로 조회한다.

그리고 SightDetail의 경우 Sight에만 포함되므로 Address와 달리 포함되는 엔티티에 따라 컬럼 설정이 달라지지 않는다. SightDetail와 매핑할 테이블 설정을 SightDetail에 직접 설정해도 된다.

1
2
3
4
5
6
7
8
9
10
11
//SightDetail @Column에 table 명을 지정하면 Sight 클래스는 @AttributeOverride 애노테이션을 사용해서 테이블을 지정할 필요가 없다.
@Embeddable
public class SightDetail {
  @Column(name="hours_op", table="sight_detail")
  private String hoursOfOperation;
  @Column(table="sight_detail")
  private String holidays;
  @Column(table="sight_detail")
  private String facilities;
}

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

1
2
3
Sight sight = entityManager.find(Sight.class, 1L);
sight.setDetail(new SightDetail("오전9시~오후5시", "연중무휴", "10대 주차가능"));
transaction.commit();

위 예에서 Sight 객체를 조회했을 때 detail이 존재하느냐 여부에 따라 실행되는 쿼리가 달라진다. find()로 읽어온 Sight의 detail이 null이 아니면 detail 테이블의 데이터를 update 하고, null이면 insert 한다.

08. @Embeddable과 복합키

테이블의 주요키가 두 개 이상의 컬럼으로 구성된 복합키이고, 이 복합키를 엔티티의 식별자에 매핑해야 하는 경우 @Embeddable 타입을 복합키에 매핑할 식별자 타입으로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//복합키 밸류 클래스
@Embeddable
public class MonChangeId implements Serializable {
  @Column(name="hotel_id")
  priavate String hotelId;

  @Column(name="year_mon")
  private String yearMon;

  //....

  //equals, hashcode 구현
}

@Entity
@Table(name="month_change")
public class MonthCharge{
  @Id
  priavate MonChangeId id;
  @Column(name="change_amt")
  priavate int chargeAmount;
  @Column(name="unpay_amt")
  priavate int unpayAmount;
}

//복합키를 사용하므로 find() 메서드로 조회시 식별자 값에 복합키 객체를 전달한다.
MonthCharge monthCharge = em.find(MonthCharge.class, new MonChargeId("H100-10", "201608"));

복합키로 사용할 밸류 클래스는 값 비교를 위한 equals() 메서드와 hashCode() 메서드를 알맞게 구현해야 한다. 그리고 Serializable 인터페이스를 상속해야 한다.

This post is licensed under CC BY 4.0 by the author.