[JPA] 기본값, 임베디드 타입
📌 Topic
- 💡 기본값 타입
- 💡 임베디드 타입
⚡ 1. 기본값 타입
✅ 1-1. 엔티티 타입
JPA는 데이터 타입을 최상위 레벨로 바라보았을 때 엔티티 타입과 값 타입 두 가지로 분리할 수 있다.
@Entity
public class Member {
//..
}
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
✅ 1-2. 값 타입
int num = 0;
String str = "str";
dobule db = 1.0;
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
⚡ 2. 값 타입 분류
✅ 2-1. 기본값 타입
- 자바 기본 타입(int, double, short..)
- 래퍼 클래스(Integer, Doble, Long..)
- String
✅ 2-2. 임베디드 타입
- embedded type, 복합 값 타입
✅ 2-3. 컬렉션 값 타입
- collection value type
- collection 안에 들어가는 값 타입을 의미한다.
⚡ 3. 기본값 타입
- 기본값 타입은 생명주기를 엔티티에 의존한다
- ex) 회원 엔티티를 삭제하면 이름, 나이 필드도 함께 삭제
- 값 타입의 value는 절대 공유하면 안된다
- 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨(side effect)
3-1. 자바의 기본 타입에서 예제를 살펴 보자.
✅ 3-1. 자바의 기본 타입
primitive type
public class ValueMain {
public static void main(String[] args) {
int a = 10; // 10
int b = a; // 10
a = 20;
System.out.println("a = " + a); // 20
System.out.println("b = " + b); // 10
}
}
public class ValueMain {
public static void main(String[] args) {
Integer a = new Integer(10);
Integer b = a
// a.setValue(20); // 만약 이런식으로 셋팅 할 수 있다면?
System.out.println("a = " + a); // 20
System.out.println("b = " + b); // 20
}
}
자바의 기본 타입은 절대 값을 공유하지 않는 특성을 가지고 있다. 또한 기본 타입의 경우 항상 값을 복사하여 명령을 수행한다.
또한 Integer와 같은 래퍼 클래스(Wrapper Class)나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경은 불가능하다.
⚡ 4. 임베디드 타입(복합 값 타입)
임베디드 타입 역시 기본값 타입에 속한다. 즉, 추적도 안되고 변경하면 끝이다.
- 새로운 값 타입을 직접 정의할 수 있음
- JPA에서는 임베디드 타입(embedded type)이라고 한다
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
- int, String과 같은 값 타입
✅ 4-1. 임베디드 타입
공통으로 묶어내는 방식을 임베디드 타입이라 지칭 할 수 있다.
💡 4-1-1. 임베디드 타입 사용 전
위 사진을 한번 살펴보자. 회원 엔티티는 이름, 근무 시작일, 종료일, 주소, 도시 주소, 번지, 우편번호 등의 필드를 가지고 있다. 일반적인 엔티티의 모습이지만 자세히 살펴보면 시작일(startDate), 종료일(endDate)를 하나의 단위로 묶을 수 있고, city, street, zipcode 역시 하나의 단위로 묶을 수 있다.
💡 4-1-2. 임베디드 타입 사용 후
이전 사진에서는 Member 엔티티에 모든 필드가 들어가 있었지만, 현재는 workPeriod, homeAddress와 같은 임베디드 타입을 선언하여 필드를 제어하고 있다. 또한 Long, String 타입처럼 여기서는 Period 타입과 Address 타입이 생성했다고 생각하면 된다.
✅ 4-2. 임베디드 타입을 통해 필드 분리
현재 Member 엔티티는 id, name, workPeriod, homeAddress 타입을 갖는다. 또한 각각의 workPeriod, homeAddress 타입은 개별적인 필드를 하나로 묶어 저장한다.
✅ 4-3. JPA에서 임베디드 타입 사용 방법
- @Embeddable
- 값 타입을 정의하는곳에 표시
- @Embedded
- 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
✅ 4-4. 임베디드 타입의 장점
임베디드 타입 역시 값 타입이다. 즉, 엔티티가 죽으면 값 타입도 다 죽는다.
임베디드 타입의 장점은 재사용이 가능하며 높은 응집도를 가질 수 있다. 또한 Period.isWork()처럼 해당 값 타입만 사용하는 의미있는 메소드 생성이 가능하며,
임베디드 타입에 포함한 모든 값 타입은 해당 값 타입을 소유한 엔티티에 의존한다.
✅ 4-5. 임베디드 타입과 테이블 매핑
우선 DB 입장에서 기본 값 타입을 사용하던, 임베디드 타입을 사용하던 딱히 변경 되어야 하는 부분은 없다. 즉, 임베디드 타입으로 사용이 된 필드를 매핑만 해주면 된다.
♻ 4-6. 임베디드 타입 실습
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
}
Member 엔티티의 필드 startDate, endDate..등을 추가한다.
후에 서버를 재기동하여 테이블 생성 결과를 확인해보자.
🖨 4-7. 출력 결과
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
endDate timestamp,
startDate timestamp,
street varchar(255),
USERNAME varchar(255),
zipcode varchar(255),
TEAM_ID bigint,
primary key (MEMBER_ID)
)
서버 기동 시 위에서 추가한 endDate, startDate 등의 필드가 테이블 생성 시 추가된다. 그렇다면 다음에는 해당 엔티티(Member)에 임베디드 타입을 적용해보자.
✅ 4-8. 임베디드 타입 적용
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
//.. Getter, Setter 중략
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Embeddable 어노테이션을 사용하여 JPA에게 값 타입임을 알려준다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
/* Before
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipcode;
*/
// 날짜 -> 하나의 단위(클래스)
@Embedded
private Period period;
// 주소 -> 상동
@Embedded
private Address homeAddress;
}
Member 엔티티에서는 임베디드 타입을 사용하기 위해 각각의 필드에 @Embedded 어노테이션을 추가한다.
🖨 4-9. 출력 결과
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
endDate timestamp,
startDate timestamp,
street varchar(255),
USERNAME varchar(255),
zipcode varchar(255),
TEAM_ID bigint,
primary key (MEMBER_ID)
)
두 개의 클래스를 생성하고 임베디드 타입을 사용하였으나, 출력 결과는 이전과 동일하다. 이렇듯 임베디드 타입을 사용하면 좀 더 ‘객체지향스럽게’ 클래스 설계를 할 수 있다.
🍃 4-2-1. 임베디드 타입 활용
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
MemberTest member = new MemberTest();
member.setUsername("ymkim");
member.setHomeAddress(new Address("city", "streer", "zipcode")); // Address 타입 셋팅
member.setPeriod(new Period()); // Period 타입 셋팅
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
위와 같이 Member의 값을 셋팅할 때 Period, Address는 간단히 객체만 생성하여 만들 수 있다.
✅ 4-2-2. 임베디드 타입과 테이블 매핑
간단한 모델링 구현 가능.
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블 형태는 동일하다.
- 임베디드 타입 사용 시 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능해진다.
- 추상화 가능.
- 잘 설계한 ORM Application은 매핑 테이블 개수보다, 클래스 수가 더 많다.
⚡ 5. 임베디드 타입과 연관관계
- Member 엔티티는 Address와 PhoneNumber를 갖는다.
- Address는 Zipcode(임베디드 타입)를 가지고 있다.
- PhoneNumber(임베디드 타입)는 PhoneEntity를 가지고 있다?
- 즉, 임베디드 타입은 다른 엔티티를 가질 수 있다.
✅ 5-1. @AttributeOverried: 속성 재정의
// 집 주소와 회사 주소를 구분해야 하는 경우 발생.
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: com.hello.jpatest
- 한 엔티티에서 같은 값 타입을 사용하는 경우?
- 컬럼명이 중복되면 위와 같은 예외가 발생한다.
- 어떻게 해결해야 하는가?
- @AttributeOverrides, @AttributeOverride를 사용하여 컬럼명 속성을 재정의.
// Member 엔티티
@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;
위와 같이 @AttributeOverrides, @AttributeOverride를 선언하여 중복이 되는 컬럼의 이름(name)을 다르게 변경하여 DB와 매핑 시켜야 한다.
🖨 5-2. 출력 결과
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
ZIPCODE varchar(255),
endDate timestamp,
startDate timestamp,
USERNAME varchar(255),
WORK_CITY varchar(255),
WORK_STREET varchar(255),
WORK_ZIPCODE varchar(255),
TEAM_ID bigint,
primary key (MEMBER_ID)
)
마지막으로 WORK_CITY, WORK_STREET, WORK_ZIPCODE가 따로 빠진 것을 확인 할 수 있다.
댓글남기기