03. Value Type
- -
이번 포스트에서는 JPA에서 사용되는 데이터 타입의 종류에 대해 알아보자.
데이터 타입
JPA는 크게 Entity 타입과 Value 타입 2가지 데이터 타입을 지원한다.
타입 | 설명 |
Entity 타입 | @Entity로 선언된 객체의 타입 |
Value 타입 | Entity의 구성 요소로 단순히 값으로 사용되는 타입 - Basic 타입: 기본형 및 Wrapper 타입, String - Embedded 타입: 복합 값 타입으로 별도의 클래스로 구성(ex: Address, Phone, Email, ...) - Collection 값 타입: 여러 가지 내용을 가지는 타입 |
Value 타입 중 Basic 타입이야 별 특별한 내용이 없고 여기서는 약간 생소한 Embedded 타입과 Collection에 대해 알아보자.
Embedded 타입
Embedded 타입
Embedded 타입이란 Entity의 속성 중 복합 값 타입으로 필요에 따라 생성한 사용자 정의 클래스를 말한다.
예를 들어 email을 id(String)와 domain(String)으로 구성할 때 데이터베이스 차원에서는 이 둘을 구분해서 별개의 컬럼으로 작성해야겠지만 객체 입장에서는 Email이라는 새로운 객체 타입으로 관리하는 것이 일반적이다. 이때 이 둘을 하나로 묶어 만든 Email 타입을 Embedded 타입이라고 한다.
Embedded 타입의 작성
Embedded 타입을 만들기 위해서는 @Embedded 와 @Embeddable 애너테이션을 사용한다.
먼저 새롭게 정의하는 Embedded 타입에는 @Embeddable을 사용한다.(이 클래스는 Embed 될 수 있다.!)
@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@ToString
@Builder
@Embeddable
public class Email {
@Column(length = 100)
private String emailId;
private String emailDomain;
}
Embedded 타입을 만들때 주의 사항은 반드시 기본 생성자와 setter/getter가 필요하다. 그리고 그 Embedded 타입을 사용하는 곳에서는 @Embedded를 사용한다.
@Entity
@Table(name = "member")
public class Member extends BaseEntity {
. . .
@Enumerated(EnumType.STRING)
private Gender gender;
@Embedded
private Email email;
}
참고로 이 둘 중 하나는 생략할 수 있다.
Embedded 타입은 새로운 테이블을 만들지는 않고 @Embeddable 타입의 각 속성들이 Entity 테이블을 구성하는 컬럼이 된다.
Hibernate:
create table member (
create_time timestamp(6),
mno bigint not null,
update_time timestamp(6),
email_id varchar(100),
id varchar(100) not null unique,
name varchar(100) not null,
email_domain varchar(255),
pass varchar(100) not null check(length(pass)>5),
gender enum ('F','M'),
primary key (mno)
)
중복되는 속성의 변경 처리
예를 들어 Member가 하나 이상의 Email을 포함한다면 어떨까? 회사에서 사용하는 공식 Email과 개인적으로 사용하는 Email이 있다고 생각해보자.
자바에서는 변수 명을 이용해서 처리할 수 있지만 데이터베이스 입장에서는 둘 다 동일한 이름의 테이블 컬럼이 필요하기 때문에 중복이 발생한다. 이때는 @MappedSuperclass와 마찬가지로 @AttributeOverride 또는 @AttributeOverrides를 사용할 수 있다.
@Entity
@AttributeOverride(name = "created", column = @Column(name = "created_date"))
public class Member extends BaseEntity {
@Embedded
private Email personalEmail;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "emailId", column = @Column(name = "office_email_id")),
@AttributeOverride(name = "emailDomain", column = @Column(name = "office_email_domain")),
})
private Email officeEmail;
}
값 타입의 Collection
값 타입의 Collection
하나 이상의 값 타입을 저장할 때는 Collection을 이용할 수 있다. 예를 들어 Member가 가지고 있는 별명들을 저장하려면 하나의 컬럼에 여러 정보가 저정되어야 하는데 이는 제1정규화의 원칙인 도메인 원자값의 조건에 만족하지 않게 된다.
이때 값 타입의 Collection을 사용할 수 있다. (결국 별도의 테이블에 저장하고 join 해서 가져온다.)
@ElementCollection과 @CollectionTable
@ElementCollection은 basic type이나 Embedded type의 Collection을 선언할 때 사용한다. 그리고 이 정보를 별도의 테이블로 관리하기 위해서 @CollectionTable을 사용한다.
@ElementCollection은 '값을 언제 조회할 것인가?'에 대한 설정을 위한 fetch 속성을 가진다. 기본 값은 FetchType.LAZY이다.
FetchType에 대한 설명은 뒤에 다루기로 한다. 일단 LAZY는 실제 그 데이터가 사용되는 시점이고 다른 속성은 EAGER가 있다.
@Target( { METHOD, FIELD })
@Retention(RUNTIME)
public @interface ElementCollection {
FetchType fetch() default FetchType.LAZY;
}
@CollectionTable에는 Collection 내용을 저장할 테이블을 설정한다. 주로 사용되는 속성은 name과 joinColumns인데 생략하면 기본 값으로 name은 "부모클래스이름_속성명" 이 사용되고 joinColumns에는 "속성명"이 사용된다. 어차피 이렇게 기본 값을 사용할 경우 @CollectionTable은 생략 할 수 있다.
Collection 내용이 저장될 테이블은 Entity에 종속적이다. 따라서 Entity가 삭제될 경우 컬렉션 테이블의 내용도 자동으로 삭제된다.(내부적으로 CascadeType.ALL이 적용된다.)
@Target( { METHOD, FIELD })
@Retention(RUNTIME)
public @interface CollectionTable {
String name() default ""; // 연결하려는 테이블 이름
JoinColumn[] joinColumns() default {}; // join에 사용될 부모 테이블의 join 컬럼 이름
...
}
@ElementCollection 적용
@Entity
@Table(name = "member")
public class Member extends BaseEntity {
...
@Embedded
private Email email;
@ElementCollection
private List<String> nicknames;
}
위와 같이 Entity를 작성했을 경우 생성되는 테이블 정보는 아래와 같다.
값 타입 컬렉션 주의 사항
값 타입 컬렉션은 단순한 값들이 저장되는 테이블로 특별한 P.K가 없이 단지 부모 테이블의 P.K로만 관리가 된다.
create table member_nicknames (
member_mno bigint not null,
nicknames varchar(255)
);
alter table if exists member_nicknames add constraint FKba6c6xm2f95evdxsl59075hb9
foreign key (member_mno) references member;
P.K가 없기 때문에 값을 추가/수정할 때 부모자료를 기준으로 일단 전체를 삭제 후 다시 필요한 내용들을 추가한다.
Member m = Member.builder().mno(100L).id("hong").name("hong gil dong").pass("1234")
.email(new Email("hong", "doding.com"))
.nicknames(new ArrayList<>(List.of("hong"))).build();
mrepo.saveAndFlush(m); // member: insert 1건, member_nicknames: insert 1건
m.getNicknames().add("jang"); // 별명으로 jang 추가
mrepo.saveAndFlush(m); // member_nicknames: delete 1건(hong), insert 2건(hong, jang)
따라서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많거나 빈번히 수정이 일어난다면 값 타입 컬렉션 대신 P.K를 갖는 테이블을 생성하고1:N 관계를 만드는 것을 고려하는 것이 좋다.
'Spring Model > 02. 객체 매핑과 P.C' 카테고리의 다른 글
06. 엔티티의 상태 관리 (0) | 2022.04.12 |
---|---|
05. EntityManager와 Persistence Context (0) | 2022.03.19 |
04.자동 키 매핑 전략 (0) | 2020.06.07 |
02. OR-Mapping과 상속 (0) | 2020.06.02 |
01. Hello Spring Data JPA (0) | 2020.05.30 |
소중한 공감 감사합니다