JPA 게시글은 대부분 인프런의 김영한님의 강의인 '자바 ORM 표준 JPA 프로그래밍' 기반으로 내용을 정리했습니다.
임베디드 타입(복합 값 타입)
- 새로운 값 타입을 직접 정의할 수 있음
- JPA는 임베디드 타입(embedded type)이라 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
- int, String과 같은 타입 → 추적 불가능
예제(회원)
- 회원은 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
회원 엔티티를 보면 몇 개의 필드를 하나로 합쳐 공통화 할 수 있는 부분들이 보인다.
- workPeriod : startDate + endDate
- homeAddress : city + street + zipcode
뭔가 어려워보이지만 그냥 공통화 할 수 있는 부분을 따로 빼서 클래스화 한 것이다.
임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자가 필수이다.
임베디드 타입 장점
- 재사용 가능
- 높은 응집도
- Period.isWork()처럼 해당 값 타입만 사용하는 의미있는 메서드를 만들 수 있다. 즉 객체지향적이다.
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입과 테이블 매핑
DB 입장에서는 임베디드 타입을 사용하던 안하던 회원테이블은 변함없이 똑같다. 임베디드 타입을 사용하면 대신 매핑 작업이 추가로 들 뿐이다. 매핑 작업이 추가로 필요함에도 불구하고 임베디드 타입을 굳이 사용하는 이유는 객체 입장에서는 메서드, 즉 행위까지 생각해야하고 임베디드 타입을 사용하면 이런 부분에서 많은 이득을 가져갈 수 있다.
Member.class
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// 기간
@Embedded
private Period workPeriod;
// 주소
@Embedded
private Address homeAddress;
}
Period.class
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {
}
// getter/setter
}
Address.class
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
// getter/setter
}
출력 로그
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
USERNAME varchar(255),
endDate timestamp,
startDate timestamp,
primary key (MEMBER_ID)
)
사실 @Embeddable, @Embedded 둘 중 하나만 사용해도 알아서 적용되지만 웬만하면 두 개 다 사용하는 걸 권장한다. Member 객체에 기간과 주소와 관련된 필드를 다 적어도, 임베디드 타입을 사용해도 위 출력 로그가 동일하게 전송된다.
실행 클래스
Member member = new Member();
member.setUsername("member");
member.setHomeAddress(new Address("city", "street", "100"));
member.setWorkPeriod(new Period());
em.persist(member);
tx.commit();
// 출력 로그
Hibernate:
call next value for hibernate_sequence
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
Insert 쿼리가 전송되고, DB에도 값이 잘 저장되어 있는 모습을 볼 수 있다.
정리!
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
임베디드 타입과 연관관계
여기에서 특이한 점은 Value인 PhoneNumber가 Entity인 PhoneEntity를 가지고 있다는 점이다. 위에서 했던 예제에서 Address에서도 Member 객체를 불러올 수 있기 때문에 가능한 일이다.
@AttributeOverride : 속성 재정의
한 엔티티(클래스) 안에서 같은 값 타입을 사용할 수 있을까? 회원의 엔티티에 집 주소 외에 회사 주소를 저장해야한다고 가정해보자.
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
이렇게 필드를 만들고 애플리케이션을 실행해보면 에러가 발생한다.
이런 경우 @AttributeOverrides와 @AttibuteOverride로 해결할 수 있다.
@Embedded
@AttributeOverrides({@AttributeOverride(name = "city",
column = @Column(name = "work_city")),
@AttributeOverride(name = "street",
column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode",
column = @Column(name = "work_zipcode"))})
private Address workAddress;
임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null이다.
만약 Member 클래스에서 Preriod의 값을 null로 주었다면, Preriod 클래스 안에 있는 모든 필드엔 null 값이 저장된다.