[Spring Boot] JPA 강의 정리-24 (값 타입-컬렉션)

Posted by 김성철

SpringBoot - JPA 강의 정리-23(값 타입 - 컬렉션)

요약 정리

- 엔티티 타입의 특징  
	* 식별자가 있음  
	* 생명 주기 관리됨  
	* 공유할수 있음  
  
- 값 타입의 특징  
	* 식별자가 없음  
	* 생명 주기를 엔티티에 의존함  
	* 공유하지 않는 것이 안전함(복사해서 사용)  
	* 불변 객체로 만드는 것이 안전  
  
- 값 타입은 정말 값 타입이라 판단될 때만 사용  
- 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨  
- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티임  

값 타입 컬렉션

- 값 타입을 컬렉션에 담아서 사용하는 것을 말함  
- 값 타입을 하나 이상 저장할 때 사용  
- @ElementCollection, @CollectionTable 사용  
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없음  
- 컬렉션을 저장하기 위한 별도의 테이블이 필요함  
- 컬렉션 타입도 값 타입이기 때문에 본인 스스로의 라이프사이클이 없고 member(값타입을 지정한 객체)에 의존함  
- 값 타입은 별도의 update 가 없이 Member를 업데이트 하면됨 (1:N과 비슷함)  
- 컬렉션은 지연로딩임(LAZY)  
  
※ 값 타입 컬렉션은 영속성 전이(Cascade) + 고아객체 제거 기능을 필수로 가짐  
※ 실무에서는 추적할 필요가 없고, 업데이트 할 필요가 없을 때 사용  

매핑 방법

컬렉션 상단에 @ElementCollection 어노테이션 추가  
테이블명 지정을 위해 @CollectionTable(name="FAVORITE_FOOD" , joinColumns = @JoinColumn(name = "MEMBER_ID")) 추가  
name에 기재한 내용으로 테이블이 생성됨  
외래키 지정을 위해 joinColumns 설정  

매핑 예제

Member Entity  
=====================================================================  
@Entity  
public class MemberCollection {  
  
	@Id  
	@GeneratedValue  
	@Column(name="MEMBER_ID")  
	private Long id;  
  
	@Column(name="USERNAME")  
	private String username;  
  
	@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)  
	)  
=====================================================================  
  
- 테스트 코드 실행  
=====================================================================  
MemberCollection memberCollection = new MemberCollection();  
memberCollection.setUsername("멤버컬렉션테스트");  
memberCollection.setHomeAddress(new Address("용인시" , "명지로" , "15999"));  
memberCollection.getFavoriteFoods().add("치킨");  
memberCollection.getFavoriteFoods().add("피자");  
memberCollection.getFavoriteFoods().add("탕수육");  
memberCollection.getAddressHistory().add(new Address("용인시","김량장동","10000"));  
memberCollection.getAddressHistory().add(new Address("광주시","신현리","20000"));  
em.persist(memberCollection);  
tx.commit();  
=====================================================================  
  
- 테스트 코드 실행 쿼리 , 생성한 Set 과 List 객체만큼 쿼리가 실행됨  
=====================================================================  
Hibernate:  
	/* insert org.hello.jpa.collection.MemberCollection  
		*/ insert  
		into  
			MemberCollection  
			(city, street, zipcode, USERNAME, MEMBER_ID)  
		values  
			(?, ?, ?, ?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.addressHistory */ insert  
		into  
			ADDRESS  
			(MEMBER_ID, city, street, zipcode)  
		values  
			(?, ?, ?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.addressHistory */ insert  
		into  
			ADDRESS  
			(MEMBER_ID, city, street, zipcode)  
		values  
			(?, ?, ?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.favoriteFoods */ insert  
		into  
			FAVORITE_FOOD  
			(MEMBER_ID, FOOD_NAME)  
		values  
			(?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.favoriteFoods */ insert  
		into  
			FAVORITE_FOOD  
			(MEMBER_ID, FOOD_NAME)  
		values  
			(?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.favoriteFoods */ insert  
		into  
			FAVORITE_FOOD  
			(MEMBER_ID, FOOD_NAME)  
		values  
			(?, ?)  
=====================================================================  
  
- DB에 저장된 내용  
=====================================================================  
SELECT * FROM MEMBERCOLLECTION ;  
MEMBER_ID  	CITY  	STREET  	ZIPCODE  	USERNAME  
1	용인시	명지로	15999	멤버컬렉션테스트  
(1 행, 0 ms)  
  
SELECT * FROM ADDRESS ;  
MEMBER_ID  	CITY  	STREET  	ZIPCODE  
1	용인시	김량장동	10000  
1	광주시	신현리	20000  
(2 행, 0 ms)  
  
SELECT * FROM FAVORITE_FOOD ;  
MEMBER_ID  	FOOD_NAME  
1	탕수육  
1	치킨  
1	피자  
(3 행, 0 ms)  
  
=====================================================================  
  
- 저장된 내용 조회  
=====================================================================  
MemberCollection memberCollection = new MemberCollection();  
memberCollection.setUsername("멤버컬렉션테스트");  
memberCollection.setHomeAddress(new Address("용인시" , "명지로" , "15999"));  
memberCollection.getFavoriteFoods().add("치킨");  
memberCollection.getFavoriteFoods().add("피자");  
memberCollection.getFavoriteFoods().add("탕수육");  
memberCollection.getAddressHistory().add(new Address("용인시","김량장동","10000"));  
memberCollection.getAddressHistory().add(new Address("광주시","신현리","20000"));  
em.persist(memberCollection);  
  
em.flush();  
em.clear();  
  
MemberCollection findMember = em.find(MemberCollection.class , memberCollection.getId());  
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);  
}  
=====================================================================  
  
- 조회 쿼리(지연로딩으로 작동함) (member insert 부분은 위에 작성했기에 주석처리함)  
=====================================================================  
Hibernate:  
	select  
		membercoll0_.MEMBER_ID as member_i1_7_0_,  
		membercoll0_.city as city2_7_0_,  
		membercoll0_.street as street3_7_0_,  
		membercoll0_.zipcode as zipcode4_7_0_,  
		membercoll0_.USERNAME as username5_7_0_  
	from  
		MemberCollection membercoll0_  
	where  
		membercoll0_.MEMBER_ID=?  
Hibernate:  
	select  
		addresshis0_.MEMBER_ID as member_i1_0_0_,  
		addresshis0_.city as city2_0_0_,  
		addresshis0_.street as street3_0_0_,  
		addresshis0_.zipcode as zipcode4_0_0_  
	from  
		ADDRESS addresshis0_  
	where  
		addresshis0_.MEMBER_ID=?  
address : 용인시  
address : 광주시  
Hibernate:  
	select  
		favoritefo0_.MEMBER_ID as member_i1_4_0_,  
		favoritefo0_.FOOD_NAME as food_nam2_4_0_  
	from  
		FAVORITE_FOOD favoritefo0_  
	where  
		favoritefo0_.MEMBER_ID=?  
favoriteFood : 탕수육  
favoriteFood : 치킨  
favoriteFood : 피자  
  
=====================================================================  

값 타입 컬렉션의 제약 사항

- 값 타입은 엔티티와 다르게 식별자 개념이 없음  
- 값은 변경하면 추적이 어려움  
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장함.  
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함 (Null 입력 X , 중복 저장 X)  

값 타입 컬렉션 대안

- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다(1:N) 관계를 고려  
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용  
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용  
ex) AddressEntity  

값 변경 예제

- 값을 업데이트 하는 것이 아닌 전부다 제거 후 남은 값의 갯수만큼 insert를 함.  
- @OrderColumn(name="address_history_number") 등으로 사용할수 있지만,, 안좋음  
  
- Set<String> 의 값을 변경  
=====================================================================  
MemberCollection findMember = em.find(MemberCollection.class , memberCollection.getId());  
//아래와 같이 수정하면 안됨, 사이드 이펙트 발생  
//findMember.getHomeAddress().setCity("서울특별시");  
//새로운 인스턴스 생성 후 완전히 교체해줘야 함  
Address oldAddress = findMember.getHomeAddress();  
findMember.setHomeAddress(new Address("서울특별시",oldAddress.getStreet(),oldAddress.getZipcode()));  
  
//변경하려는 값을 지우고 새로 넣어야 함  
//컬렉션의 값만 변경해도 JPA가 알아서 값을 변경해줌 (영속성 전이가 되는것처럼 작동함)  
findMember.getFavoriteFoods().remove("치킨");  
findMember.getFavoriteFoods().add("비빔밥");  
=====================================================================  
  
- List<Address> 의 값을 변경  
=====================================================================  
//equals 비교로 해당값을 똑같이 가지고 있는 객체를 제거  
findMember.getAddressHistory().remove(new Address("용인시","김량장동","10000"));  
findMember.getAddressHistory().add(new Address("용인시","역북동","15900"));  
  
//기존 Address("용인시","김량장동","10000")  
//변경 Address("용인시","역북동","15900")  
=====================================================================  
  
- 실행 쿼리문  
=====================================================================  
Hibernate:  
	/* delete collection org.hello.jpa.collection.MemberCollection.addressHistory */ delete  
		from  
			ADDRESS  
		where  
			MEMBER_ID=?  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.addressHistory */ insert  
		into  
			ADDRESS  
			(MEMBER_ID, city, street, zipcode)  
		values  
			(?, ?, ?, ?)  
Hibernate:  
	/* insert collection  
		row org.hello.jpa.collection.MemberCollection.addressHistory */ insert  
		into  
			ADDRESS  
			(MEMBER_ID, city, street, zipcode)  
		values  
			(?, ?, ?, ?)  
=====================================================================  

컬렉션 사용 대안(Entity 사용)

Address를 AddressEntity로 변경  
=====================================================================  
package org.hello.jpa.collection;  
  
import org.hello.jpa.member.domain.embedded.Address;  
  
import javax.persistence.Entity;  
import javax.persistence.GeneratedValue;  
import javax.persistence.Id;  
import javax.persistence.Table;  
  
@Entity  
@Table(name="ADDRESS")  
public class AddressEntity {  
  
	@Id  
	@GeneratedValue  
	private Long id;  
  
	private Address address;  
  
	public AddressEntity() {  
	}  
  
	public AddressEntity(Address address) {  
		this.address = address;  
	}  
  
	public AddressEntity(String city, String street, String zipcode) {  
		this.address = new Address(city,street,zipcode);  
	}  
  
	public Long getId() {  
		return id;  
	}  
  
	public void setId(Long id) {  
		this.id = id;  
	}  
  
	public Address getAddress() {  
		return address;  
	}  
  
	public void setAddress(Address address) {  
		this.address = address;  
	}  
}  
  
=====================================================================