1. 임베디드 타입
주로 자바에서의 기본 값 타입(int, String)을 모아서 만들어서 복합 값 타입이라고도 한다.
예를들어 과일이라는 엔티티는 이름 가격 수량을 가진다고 가정을 했을 때 가격과 수량은 뭔가 공통으로 묶을 수 있지 않을까라는 생각을 해보자.
다음과 같이 Fruit라는 엔티티안에 PriceCount라는 복합 값 타입을 선언하는 걸 임베디드 타입이라고 부른다.
JPA에서는 @Embeddable, @Embedded라는 어노테이션으로 임베디드 타입을 선언한다.
@Embeddable: 값 타입을 정의하는 곳에 표시
@Embedded: 값 타입을 사용하는 곳에 표시
그리고 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null이다.
임베디드 타입의 장점
- 재사용이 용이해진다
- 높은 응집도를 가진다.
- 해당 임베디드 타입만의 사용하는 의미 있는 메소드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티의 생명주기에 의존한다.
예제 코드
@Entity
public class Fruit {
@Id @GeneratedValue
private Long id;
@Embedded
private PriceCount priceCount;
public Fruit() {
}
}
@Embeddable
public class PriceCount {
private int price;
private int count;
public PriceCount() {
}
public void validation(){
if (price <= 0 ){
throw new IllegalStateException();
}
if (count < 0 ){
throw new IllegalStateException();
}
}
// Getter Setter...
}
PriceCount 클래스에는 해당 클래스만 사용하는 의미 있는 메서드(validation( ))을 선언했다.
임베디드 타입은 엔티티의 값이므로 사용유무에 상관없이 매핑하는 테이블은 같다.
테이블은 다음과 같이 생성된다.
Hibernate:
create table Fruit (
count integer,
price integer,
id bigint not null,
primary key (id)
)
2. 값 타입과 불변객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
예를 들어 Fruit1과 Fruit2가 같은 PriceCount를 갖고 있다고 가정해보자.
만약 PriceCount가 새로운 PriceCount로 변경됐다고 할 때 그럼 참조하고 있던 Fruit1과 Fruit2의 PriceCount둘다 바뀌게 된다.
예제 코드를 통해 알아보자
PriceCount priceCount = new PriceCount(10, 20);
Fruit fruit1 = new Fruit(priceCount);
Fruit fruit2 = new Fruit(priceCount);
em.persist(fruit1);
em.persist(fruit2);
fruit1.getPriceCount().setCount(100);
fruit1과 fruit2를 생성 후 같은 PriceCount를 참조하도록 했다. 그 다음 fruit1의 PriceCount의 count를 변경하면 friut2의 PriceCount의 count도 같이 변경된다.
Hibernate:
/* update
for hellojpa.jpashop.domain.Fruit */update Fruit
set
count=?,
price=?
where
id=?
Hibernate:
/* update
for hellojpa.jpashop.domain.Fruit */update Fruit
set
count=?,
price=?
where
id=?
업데이트 쿼리가 두번 나가는 것을 볼 수 있다.
값 타입의 실제 인스턴스인 값을 공유하는 것은 매우 위험하다. 따라서 인스턴스를 복사해서 사용해야 한다.
PriceCount priceCount = new PriceCount(10, 20);
Fruit fruit1 = new Fruit(priceCount);
PriceCount priceCount2 = new PriceCount(10, 20);
Fruit fruit2 = new Fruit(priceCount2);
em.persist(fruit1);
em.persist(fruit2);
fruit1.getPriceCount().setCount(100);
항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 하지만 임베디드 타입은 객체 타입이다. 그래서 참조값을 직접 대입하는 것을 컴파일 레벨에서 막을 방법이 없다.
그래서 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단해야한다. 값 타입은 불변 객체로 설계해야한다.
(불변 객체: 생성 시점 이후에 절대 값을 변경할 수 없는 객체)
즉, 생성자로만 값을 설정하고 수정자(Setter)는 만들지 않으면 된다.
근데 내가 값을 바꾸고 싶다면 그냥 위 코드 처럼 새로운 인스턴스를 생성 후 다시 넣어주면 된다.
3. 값 타입 컬렉션
값 타입 컬렉션은 엔티티와 연관된 값 타입 데이터를 여러개 저장할 수 있는 컬렉션을 의미한다.
JPA에서 엔티티가 아닌 식별자가 없고 독립적으로 존재할 수 없을 때 사용한다. 예를들어 회원 주소, 연락처등이 이에 해당된다.
값 타입 컬렉션 저장
위 사진처럼 만약 Member가 존재하고, Favorite Food와 Address를 컬렉션으로 저장한다고 가정해보자.
@Entity
@NoArgsConstructor
@Getter
@Setter
public class V3Member {
@Id
@GeneratedValue
@Column(name = "V3Member_ID")
private Long id;
private String username;
@ElementCollection
@CollectionTable(
name = "ADDRESS_HISTORY"
, joinColumns = @JoinColumn(name = "V3Member_ID")
)
private List<Address> addressHistory = new ArrayList<>();
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD"
, joinColumns = @JoinColumn(name = "V3Member_ID")
)
@Column(name = "FOOD_NAME") // 예외적으로 가능
private Set<String> favoriteFoods = new HashSet<>();
}
사용법은 다음과 같다.
- @ElementCollection 어노테이션 선언
- @CollectionTable을 선언 후 테이블 명과 @JoinColums에 조인할 컬럼을 설정
Adress는 값 타입으로, favoriteFoods는 단순히 String으로 선언해줬다.
String만 저장하는 경우 column name을 지정해줄 수 있다.
이제 한번 저장을 해보자.
V3Member member = new V3Member();
member.setUsername("member1");
member.setAddress(new Address("city1", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("city2", "street2", "zipcode2"));
member.getAddressHistory().add(new Address("city3", "street3", "zipcode3"));
em.persist(member);
Hibernate:
/* insert for
jpa.practice.v3.V3Member */insert
into
v3member (city, street, zip_code, username, end_date, start_date, v3member_id)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert for
jpa.practice.v3.V3Member.addressHistory */insert
into
address_history (v3member_id, city, street, zip_code)
values
(?, ?, ?, ?)
// 이하 생략
Hibernate:
/* insert for
jpa.practice.v3.V3Member.favoriteFoods */insert
into
favorite_food (v3member_id, favorite_foods)
values
(?, ?)
로그를 보면 member가 저장이 될 때 값 타입 컬렉션도 같이 저장되는 것을 볼 수 있다.
저장한 것을 DB에 조회해보면 아래 사진과 같이 나온다.
여기서 중요한점은 값 타입 컬렉션을 따로 저장하지 않아도 Member가 저장될 때 자동으로 같이 저장되는 것이다.
즉, 라이프 사이클이 같다고 볼 수 있다.
값 타입 컬렉션은 영속성 전이(CASCADE)와 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 컬렉션 조회
이제 한번 조회해보자.
em.clear();
em.flush();
V3Member findMember = em.find(V3Member.class, member.getId());
System.out.println("findMember = " + findMember);
조회를 해보면 다음과 같은 로그가 찍힌다.
Hibernate:
select
vm1_0.v3member_id,
vm1_0.city,
vm1_0.street,
vm1_0.zip_code,
vm1_0.username,
vm1_0.end_date,
vm1_0.start_date
from
v3member vm1_0
where
vm1_0.v3member_id=?
여기서 알 수 있는점은 값타입 컬렉션은 지연 로딩 전략을 사용한다는 점이다.
값 타입 컬렉션 수정
이제 값 타입 컬렉션 수정을 해보겠다. 단순히 String으로 선언한 favorite_food의 경우에는 단순히 삭제하고 추가를 하면 된다.
em.flush();
em.clear();
V3Member findMember = em.find(V3Member.class, member.getId());
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
Hibernate:
/* delete for jpa.practice.v3.V3Member.favoriteFoods */delete
from
favorite_food
where
v3member_id=?
and food_name=?
Hibernate:
/* insert for
jpa.practice.v3.V3Member.favoriteFoods */insert
into
favorite_food (v3member_id, food_name)
values
(?, ?)
로그에서 볼 수 있듯이 Delete 쿼리 실행 후 Insert 쿼리가 실행 되는 것을 볼 수 있다. 컬렉션의 값만 바뀌어도 JPA에서 알아서 쿼리를 실행 해준다.
이제 값 타입 컬렉션을 수정 해보자. 컬렉션에서 컬렉션 안에 담긴 객체중, 어떤게 같은 객체인지 어떻게 알고 그 객체를 삭제 할까?
정답은 값 타입 안에 Object 객체의 equals와 hashcode메서드를 오버라이드해서 알 수 있다.
Address.java
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
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);
}
한번 실행해보자.
findMember.getAddressHistory().remove(new Address("city3", "street3", "zipcode3"));
findMember.getAddressHistory().add(new Address("newcity3", "newstreet3", "newzipcode3"));
중요한점은 로그 기록이다.
Hibernate:
/* one-shot delete for jpa.practice.v3.V3Member.addressHistory */delete
from
address_history
where
v3member_id=?
Hibernate:
/* insert for
jpa.practice.v3.V3Member.addressHistory */insert
into
address_history (v3member_id, city, street, zip_code)
values
(?, ?, ?, ?)
Hibernate:
/* insert for
jpa.practice.v3.V3Member.addressHistory */insert
into
address_history (v3member_id, city, street, zip_code)
values
(?, ?, ?, ?)
쿼리를 보면 Address_history를 전부 삭제 시키고 컬렉션에 담겼던 두개의 값 타입이 Insert되는 것을 볼 수 있다.
값 타입은 엔티티와 다르게 식별자 개념이 없다. 그래서 값은 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
추적이 어렵기 때문에 이건 쓰면 안된다고 한다.
'JPA' 카테고리의 다른 글
[JPA] JPQL의 경로표현식이란? (0) | 2024.12.27 |
---|---|
[JPA] JPQL의 프로젝션, 페이징 (0) | 2024.12.24 |
[JPA] 프록시와 지연로딩, 즉시로딩 (0) | 2024.11.26 |
[JPA] 연관관계 매핑 (0) | 2024.11.25 |
[JPA] 엔티티 매핑 (0) | 2024.11.25 |