기본값 타입
JPA는 데이터 타입을 최상위 레벨로 보면 엔티티 타입과 값 타입으로 분류한다.
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자(PK)로 지속해서 추적 가능
- 예) 회원 엔티티의 몸무게나 나이 값을 변경해도 식별자로 인식 가능
값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
값 타입 분류
- 기본값 타입
- 자바 기본 타입(int, double) -> primitive 타입
- 래퍼 클래스(Integer, Long)
- String
- 임베디드 타입(embedded type, 복합 값 타입)
- 컬렉션 값 타입(collection value type)
기본값 타입
- 예): String name, int age
- 생명주기를 엔티티의 의존
- 예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
- 회원 삭제 시 회원안에 있는 name, age 필드들도 함께 삭제된다는 의미
- 값 타입은 공유하면 안된다.
- 예) 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨
참고: 자바의 기본 타입은 절대 공유X
- int, double 같은 기본 타입(primitive type)은 절대 공유X
- 기본 타입은 항상 값을 복사함
- Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경이 불가능하다.
- 그래서 사이드 이펙트가 발생하지 않고 안전하게 개발할 수 있다.
임베디드 타입(복합 값 타입)
- 새로운 값 타입을 직접 정의할 수 있음
- JPA는 임베디드 타입(embedded type)이라 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
- int, String 처럼 임베디드 타입도 값 타입이다.
- 그래서 추적도 안되고 변경하면 끝난다.
예제
회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
이 엔티티에는 근무 시작일, 종료일을 공통으로 묶어서 클래스 타입으로 사용할 수 있다.
그리고 주소 도시, 주소 번지, 주소 우변번호 역시도 묶어서 클래스 타입으로 사용할 수 있다.

그래서 공통으로 묶을 수 있는 부분을 묶어서 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다라고 추상화해서 표현할 수 있다. 그래서 이렇게 클래스 타입으로 묶어낼 수 있는 게 embedded Type이다.

근무 시작일, 근무 종료일을 기간이라는 클래스로 묶는다.
주소 도시, 주소 번지, 주소 우편번호도 주소라는 클래스로 묶는다.

jpa에서의 임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입의 장점
재사용
- 시스템 전체에서 임베디드 타입의 클래스를 재사용 할 수 있다.
높은 응집도
- period, address 클래스는 응집도가 높다.
- 그래서 Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
- 이를 통해 상당히 객체 지향적으로 설계할 수 있다.
임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입과 테이블 매핑

임베디드 타입과 테이블을 어떻게 매핑하는지 살펴보자.
객체에서 임베디드 타입을 쓰든 안 쓰든 MEMBER 테이블은 똑같다.
대신에 위 이미지와 같이 매핑을 해줘야 한다.
DB는 데이터를 잘 관리하는 게 목적이기 때문에 위 회원 테이블과 같이 설계되는 게 맞다.
근데 객체에는 데이터뿐만 아니라 메서드라고 하는 기능, 행위도 존재한다.
그래서 위 Member 엔티티 객체와 같이 묶었을 때 가질 수 있는 이점이 많다.
엔티티 코드
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false)
private String name;
@Embedded
private Period period;
@Embedded
private Address address;
...
}
@Embeddable
@Getter
@Setter
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Embeddable
@Getter
@Setter
public class Address {
private String city;
private String street;
private String zipcode;
}
스키마 자동생성 실행결과
Hibernate:
create table Member (
MEMBER_ID bigint generated by default as identity,
TEAM_ID bigint,
createdDate timestamp(6),
endDate timestamp(6),
USERNAME varchar(255) not null,
city varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (MEMBER_ID)
)
테스트 코드
Member member = new Member();
member.setName("Hello");
member.setAddress(new Address("city", "street", "zipcode"));
member.setPeriod(new Period(LocalDateTime.now(), LocalDateTime.now()));
em.persist(member);
tx.commit();
테이블 결과

임베디드 타입과 테이블 매핑
임베디드 타입은 엔티티의 값일 뿐이다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
예를 들어 임베디드 타입을 사용한다고 해서 매핑되는 테이블이 바뀌는 것도 아니고 MEMBER 테이블에도 영향을 주지 않는다.
객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능해진다.
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
임베디드 타입과 연관관계
Member 엔티티는 Address랑 PhoneNumber라는 값 타입(임베디드)을 가지고 있다.
아래 이미지를 보면 Address 타입은 Zipcode 타입을 가지고 있다.
즉 임베디드 타입은 임베디드 타입을 가질 수 있다.
PhoneNumber 타입이 PhoneEntity 타입을 가지고 있다.
즉 임베디드 타입이 엔티티 타입을 가질 수 있다.

@AttributeOverride: 속성 재정의
한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복되서 오류가 발생한다.
@Embedded
private Period period;
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
@AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의 하면 된다.
@Embedded
private Period period;
@Embedded
private Address homeAddress;
@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
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
String이나 int 같은 것들은 자바에서 단순하고 안전하게 설계가 되어 있어 편하게 쓸 수 있다.
값 타입 공유 참조
임베디드 타입 같은 값 타입은 여러 엔티티에서 공유할 수 있다.
그래서 공유 시에는 부작용(side effect)이 발생할 수 있다.
예를 들어 회원1과 회원2가 같은 값 타입인 주소를 참조하고 있다고 하자.
이때 주소의 city 값을 OldCity에서 NewCity로 바꾸면 MEMBER 테이블에 회원1과 회원2의 레코드에서 city 값이 NewCity로 변경된다.

테스트 코드
Address address = new Address("city", "street", "zipcode");
Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);
테이블 결과

테이블 결과를 보면 member1과 member2가 똑같은 주소값을 가지고 있는 거를 볼 수 있다.
위 테스트 코드에서 맨 아래에 아래 코드를 추가하고 결과를 확인해 보자.
member1.getHomeAddress().setCity("newCity");
쿼리 실행결과
Hibernate:
/* update
for jpa_basic.ex1_hello_jpa.hellojpa.Member */update Member
set
createdBy=?,
createdDate=?,
city=?,
street=?,
zipcode=?,
lastModifiedBy=?,
lastModifiedDate=?,
USERNAME=?,
endDate=?,
startDate=?,
TEAM_ID=?
where
MEMBER_ID=?
Hibernate:
/* update
for jpa_basic.ex1_hello_jpa.hellojpa.Member */update Member
set
createdBy=?,
createdDate=?,
city=?,
street=?,
zipcode=?,
lastModifiedBy=?,
lastModifiedDate=?,
USERNAME=?,
endDate=?,
startDate=?,
TEAM_ID=?
where
MEMBER_ID=?
테이블 결과

member1.getHomeAddress().setCity("newCity");
예상했던 결과는 member1의 city값만 수정하는 거였다.
하지만 결과를 보면 member1과 member2의 city컬럼값을 수정하는 update 쿼리가 발생한다.
테이블 결과를 보면 둘의 city값이 바뀐 걸 확인할 수 있다.
이러한 사이드 이펙트는 굉장히 치명적이다.
값 타입 복사
결론적으로 값 타입의 실제 인스턴스인 값을 공유하는 것은 굉장히 위험하다.
그래서 대신에 값(인스턴스)을 복사해서 사용한다.

예시 코드
Address address = new Address("city", "street", "zipcode");
Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(address);
em.persist(member1);
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member1.getHomeAddress().setCity("newCity");
객체 타입의 한계
항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
자바 기본 타입에 값을 대입하면 값을 복사한다. 그래서 공유 자체가 불가능해 안정적이다.
하지만 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
그래서 객체의 공유 참조는 피할 수 없다.
기본 타입(primitive type)
int a = 10;
intb=a;//기본 타입은 값을 복사
b = 4;
객체 타입
Address a = new Address(“Old”);
Address b = a;//객체 타입은 참조를 전달
b.setCity(“New”)
불변 객체
참조값을 전달하는 걸 막을 수 있는 방법이 없다.
그래서 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단 할 수 있다.
값 타입은 불변 객체(immutable object)로 설계해야 한다.
불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체를 말한다.
생성자나 초기화 단계에서만 값을 생성하고 이후에 활용하는 단계에서는 값을 못 바꾸게 만드는 방식이다.
생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다.
참고: Integer, String은 자바가 제공하는 대표적인 불변 객체
불변 객체인데 값을 바꾸고 싶은 경우 아래와 같이 address를 새로 만들어서 넣어야 한다.
Address address = new Address("city", "street", "zipcode");
Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(address);
em.persist(member1);
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member1.setHomeAddress(newAddress);
정리하면 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
값 타입의 비교
값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 한다.
int a = 10;
int b = 10;
Address a = new Address(“서울시”)
Address b = new Address(“서울시”)
동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
값 타입의 equals() 메서드를 적절하게 재정의(주로 모든 필드 사용)
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Address address = (Address) object;
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
정리하면 자바의 primitive 타입들은 == 비교를 하면 되고 그 외 타입 특히 임베디드 타입 같은 경우에는 꼭 equals()를 사용해야 된다.
값 타입 컬렉션
값 타입 컬렉션이란 값 타입을 컬렉션에 담아서 쓰는 거를 말한다.
단순하게 값 타입이 하나인 경우에는 member의 필드 속성으로 해서 Member 테이블 안에 넣으면 된다.
문제는 관계형 데이터베이스는 내부적으로 테이블에 컬렉션을 담을 구조가 없다.
그래서 테이블에서 favoriteFoods라는 컬렉션을 담을 FAVORITE_FOOD이라는 별도의 테이블을 만들어야 한다.
주의할 점은 FAVORITE_FOOD의 컬럼들을 다 묶어서 PK를 만들어야 한다.
왜냐면 현재 값 타입 컬렉션이기 때문이다. FAVORITE_FOOD 테이블에 식별자 Id 같은 개념을 넣어서 PK로 사용하면 favoriteFoods는 값 타입이 아니고 엔티티가 되어버린다. 그래서 값 타입 컬렉션은 테이블에 컬럼들을 묶어서 PK로 구성하면 된다.
예시코드
@Entity
@Getter
@Setter
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME", nullable = false)
private String name;
@Embedded
private Period period;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
...
}
쿼리 결과
Hibernate:
create table ADDRESS (
MEMBER_ID bigint not null,
city varchar(255),
street varchar(255),
zipcode varchar(255)
)
Hibernate:
create table FAVORITE_FOOD (
MEMBER_ID bigint not null,
FOOD_NAME varchar(255)
)
Hibernate:
create table Member (
MEMBER_ID bigint generated by default as identity,
TEAM_ID bigint,
createdDate timestamp(6),
endDate timestamp(6),
lastModifiedDate timestamp(6),
startDate timestamp(6),
USERNAME varchar(255) not null,
city varchar(255),
createdBy varchar(255),
lastModifiedBy varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (MEMBER_ID)
)
값 타입 컬렉션 설명
- 값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용한다.
- @ElementCollection, @CollectionTable 사용해서 매핑하면 된다.
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- member 테이블에 컬렉션을 저장할 수 없다.
- 그래서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
값 타입 컬렉션 사용
참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 저장 예제
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
쿼리
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member */insert
into
Member (createdBy, createdDate, city, street, zipcode, lastModifiedBy, lastModifiedDate, USERNAME, endDate, startDate, TEAM_ID, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, default)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD (MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD (MEMBER_ID, FOOD_NAME)
values
(?, ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD (MEMBER_ID, FOOD_NAME)
values
(?, ?)
쿼리를 보면 member 테이블에 한번 address 테이블에 세 번 favorite_food 테이블에 두 번 쿼리가 나간다.
테이블

테이블에도 데이터가 잘 저장된 걸 볼 수 있다.
member만 persist()를 했는데 값 타입 컬렉션들도 자동으로 저장됐다.
왜냐면 값 타입이기 때문이다.
값 타입 컬렉션도 본인 스스로 라이프 사이클이 없다.
위 상황에서는 값 타입 컬렉션의 생명주기가 member에 소속된 거다.
그래서 이 값 타입들은 별도로 persist()하거나 업데이트할 필요 없다.
member에서 값을 수정하거나 저장하면 된다.
마치 일대다 연관관계에서 cascade의 all 옵션과 orphanremoval의 true 옵션을 사용하는 것과 비슷하다.
값 타입 조회 예제
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("족발");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("============ START =============");
Member findMember = em.find(Member.class, member.getId());
실행쿼리
============ START =============
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.endDate,
m1_0.startDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
쿼리를 보면 member만 가져왔다.
즉 값 타입 컬렉션들은 지연 로딩 전략을 사용한다.
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
위 코드를 추가하면 address 테이블과 favoriteFood 테이블에서 값을 가져온다.
Hibernate:
select
ah1_0.MEMBER_ID,
ah1_0.city,
ah1_0.street,
ah1_0.zipcode
from
ADDRESS ah1_0
where
ah1_0.MEMBER_ID=?
address = old1
address = old2
Hibernate:
select
ff1_0.MEMBER_ID,
ff1_0.FOOD_NAME
from
FAVORITE_FOOD ff1_0
where
ff1_0.MEMBER_ID=?
값 타입 컬렉션도 지연 로딩 전략 사용 값 타입 수정 예제
System.out.println("============ START =============");
Member findMember = em.find(Member.class, member.getId());
//findMember.getHomeAddress().setCity("newCity"); 이렇게 수정하면 안됨
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
System.out.println("============ ADDRESS ============");
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCIty", "street", "10000"));
실행쿼리
============ START =============
Hibernate:
select
m1_0.MEMBER_ID,
m1_0.createdBy,
m1_0.createdDate,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.lastModifiedBy,
m1_0.lastModifiedDate,
m1_0.USERNAME,
m1_0.endDate,
m1_0.startDate,
t1_0.TEAM_ID,
t1_0.createdBy,
t1_0.createdDate,
t1_0.lastModifiedBy,
t1_0.lastModifiedDate,
t1_0.name
from
Member m1_0
left join
Team t1_0
on t1_0.TEAM_ID=m1_0.TEAM_ID
where
m1_0.MEMBER_ID=?
Hibernate:
select
ff1_0.MEMBER_ID,
ff1_0.FOOD_NAME
from
FAVORITE_FOOD ff1_0
where
ff1_0.MEMBER_ID=?
============ ADDRESS ============
Hibernate:
select
ah1_0.MEMBER_ID,
ah1_0.city,
ah1_0.street,
ah1_0.zipcode
from
ADDRESS ah1_0
where
ah1_0.MEMBER_ID=?
Hibernate:
/* update
for jpa_basic.ex1_hello_jpa.hellojpa.Member */update Member
set
createdBy=?,
createdDate=?,
city=?,
street=?,
zipcode=?,
lastModifiedBy=?,
lastModifiedDate=?,
USERNAME=?,
endDate=?,
startDate=?,
TEAM_ID=?
where
MEMBER_ID=?
Hibernate:
/* one-shot delete for jpa_basic.ex1_hello_jpa.hellojpa.Member.addressHistory */delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.addressHistory */insert
into
ADDRESS (MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* delete for jpa_basic.ex1_hello_jpa.hellojpa.Member.favoriteFoods */delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
Hibernate:
/* insert for
jpa_basic.ex1_hello_jpa.hellojpa.Member.favoriteFoods */insert
into
FAVORITE_FOOD (MEMBER_ID, FOOD_NAME)
values
(?, ?)
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); 을 통해 수정 시 member 테이블을 수정한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
이 경우는 FAVORITE_FOOD 테이블에서 "치킨"을 삭제하고 "한식"을 저장하는 쿼리를 볼 수 있다.
System.out.println("============ ADDRESS ============");
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCIty", "street", "10000"));
문제는 이 부분이다.
FAVORITE_FOOD 테이블처럼 삭제 한번하고 저장 한번 할 줄 알았지만 실제 실행 쿼리를 봐보자.
실제 실행 쿼리를 보면 삭제 한번 발생하고 저장이 두 번 발생한다.
값 타입 컬렉션의 제약사항
값 타입은 엔티티와 다르게 식별자 개념이 없다.
그래서 값을 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 예를 들어 addressHistory의 값을 변경하면 DB에 있는 address 테이블에서 MEMBER_ID와 관련된 데이터를 다 지운다.
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. null 입력X, 중복 저장X
결론적으로는 사용하는 걸 추천하지 않는다.
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
- EX) AddressEntity
- 실무에선 이 방법을 많이 사용한다.
코드예시
// @ElementCollection
// @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
// private List<Address> addressHistory = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
정리
엔티티 타입의 특징
- 식별자O
- 생명주기 관리
- 공유
값 타입의 특징
- 식별자X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
값 타입은 정말 값 타입이라 판단될 때만 사용해야 한다.
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안 된다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
11. 객체지향 쿼리 언어(JPQL) - 2 (0) | 2024.08.13 |
---|---|
10. 객체지향 쿼리 언어(JPQL) - 1 (0) | 2024.08.13 |
8. 프록시와 연관관계 관리 (0) | 2024.08.11 |
7. 상속관계 매핑 (0) | 2024.08.10 |
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |