[JPA] 임베디드 타입(embedded type)

사명기·2019년 12월 9일
8

임베디드 타입(복합 값 타입)

새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입(embedded type)이라 합니다.
중요한 것은 직접 정의한 임베디드 타입도 int, String처럼 값 타입이라는 것입니다.

임베디드 타입을 사용하지 않았을 때와 사용했을 때를 비교해 봅시다.

// 임베디드 타입 사용하지 않았을 때
@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  private String name;
  
  // 근무 기간
  @Temporal(TemporalType.DATE)
  Date startDate;
  @Temporal(TemporalType.DATE)
  Date endDate;
  
  // 집 주소 표현
  private String city;
  private String street;
  private String zipcode;
  // ...
}

위는 평범한 회원 엔티티입니다.

누군가에게 이 엔티티를 설명하라면 이렇게 이야기할 것입니다.

"회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편 번호를 가진다."

하지만 이런 설명은 단순히 정보를 풀어둔 것 뿐이고, 근무 시작일과 우편 번호는 서로 아무 관련이 없습니다. 따라서 다음과 같이 설명하는게 더 정확합니다.

"회원 엔티티는 이름, 근무 기간, 집 주소를 가진다."

회원이 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으면 응집력만 떨어뜨립니다.

대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해질 것입니다.

// 임베디드 타입 사용
@Entity
public class Member {
  
  @Id @GeneratedVAlue
  private Long id;
  private String name;
  
  @Embedded
  private Period workPeriod;	// 근무 기간
  
  @Embedded
  private Address homeAddress;	// 집 주소
}
// 기간 임베디드 타입
@Embeddable
public class Peroid {
  
  @Temporal(TemporalType.DATE)
  Date startDate;
  @Temporal(TemporalType/Date)
  Date endDate;
  // ...
  
  public boolean isWork (Date date) {
    // .. 값 타입을 위한 메서드를 정의할 수 있다
  }
}
@Embeddable
public class Address {
  
  @Column(name="city") // 매핑할 컬럼 정의 가능
  private String city;
  private String street;
  private String zipcode;
  // ...
}

임베디드 타입 사용하는 방법

  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시
  • 임베디드 타입은 기본 생성자가 필수

임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입 관계를 UML로 표현하면 컴포지션(composition) 관계가 됩니다.

하이버네이트는 임베디드 타입을 컴포넌트(components)라 합니다.

<회원-컴포지션 관계 UML (출처: 김영한님 강의)>

임베디드 타입의 장점

  • 재사용
  • 높은 응집도
  • Period 객체의 isWork() 메서드처럼 해당 값 타입만 사용하는 의미있는 메서드를 만들 수 있습니다.

임베디드 타입과 테이블 매핑

<임베디드 타입을 사용한 회원-테이블 매핑 (출처: 김영한님 강의)>

임베디드 타입은 엔티티의 값일 뿐입니다.
따라서 값이 속한 엔티티의 테이블에 매핑합니다. 예제에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같습니다.

임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게(fine-grained) 매핑하는 것이 가능합니다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많습니다.

임베디드 타입과 연관관계

임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있습니다.

@Entity
public class Member {
  
  @Embedded
  Address address;	// 임베디드 타입 포함
	@Embedded
  PhoneNumber phoneNumber;	// 임베디드 타입 포함

  // ...
}


@Embeddable
public class Address {
  String street;
  String city;
  String state;
  @Embedded
  Zipcode zipcode; // 임베디드 타입 포함
}


@Embeddable
public class Zipcode {
  String zip;
  String plusFour;
}


@Embeddable
public class PhoneNumber {
 
  String areaCode;
  String localNumber;
  
  @ManyToOne
  PhoneServiceProvider provider;	// 엔티티 참조
}


@Entity
public class PhoneServiceProvider {
  @Id
  String name;
 	// ...
}

값 타입인 Address가 값 타입인 Zipcode를 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조합니다.

위 코드에 대한 연관관계는 다음과 같습니다.

![image-20191209220434702](/Users/conatuseus/Library/Application Support/typora-user-images/image-20191209220434702.png)

@AttributeOverride: 속성 재정의

임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 됩니다.

예를 들어 회원에게 주소가 하나 더 필요하면 어떻게 해야 할까요?

// 같은 임베디드 타입을 가지고 있는 회원
@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  private String name;
  
  @Embedded
  Address homeAddress;
  
  @Embedded
  Address companyAddress;
}

위 코드의 문제점은 테이블에 매핑하는 컬럼명이 중복되는 것입니다. 이때는 아래와 같이 @AttributeOverrides를 사용해서 매핑정보를 재정의해야 합니다.

@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  private String name;
  
  @Embedded
  Address homeAddress;
  
  @Embedded
  @AttributeOverrides({
    @AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
    @AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
    @AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
  })
  Address companyAddress;
}

생성된 테이블은 다음과 같습니다.

CREATE TABLE MEMBER (
	COMPANY_CITY varchar(255),
  COMPANY_STREET varchar(255),
  COMPANY_ZIPCODE varchar(255),
  city varchar(255),
  street varchar(255),
  zipcode varchar(255),
  ...
)

@AttributeOverride 를 사용하면 어노테이션을 너무 많이 사용해서 엔티티 코드가 지저분해집니다. 다행히도 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않습니다.

@AttributeOverrides는 엔티티에 설정해야 합니다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 합니다.

임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 됩니다.

member.setAddress(null); // null 입력
em.persist(member);

회원 테이블의 주소와 관련된 CITY, STREET, ZIPCODE 컬럼 값은 모두 null이 됩니다.

1개의 댓글

comment-user-thumbnail
2022년 5월 12일

많이 배우고 갑니다~~

답글 달기