값 타입 복사
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다
대신 값(인스턴스)를 복사해서 사용

객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 객체의 공유 참조는 피할 수 없다.
기본 타입과 객체 타입 비교
//기본 타입(primitive type)
int a = 10;
int b = a; //기본 타입은 값을 복사
b = 4;
//객체 타입
Address a = new Address("Old");
Address b = a; //객체 타입은 참조를 전달
b.setCity("new");
결론: 불변 객체를 사용하자
불변 객체
- 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
- 값 타입은 불변 객체로 설계해야함
- 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성지로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다.
참고: Integer, String은 자바가 제공하는 대표적인 불변 객체이다.
임베디드 타입 Address 클래스를 불변 객체로 만든 예시

만약에 값을 바꾸고 싶다면 바꾸고 싶은 값만 새로운 값을 넣어서 새로운 객체를 만든다. (아래 사진 참고)

값 타입 비교
값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
- 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야한다.
- 값 타입의 equals() 메소드를 적절하게 재정의하자. (주로 모든 필드에서 사용)


값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- @ElementCollection, @CollectionTable 사용
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요함
예시

addressHistory: List<Address> 처럼 Address라는 값 타입을 리스트에 담아서 사용
DB는 테이블 안에 컬렉션을 담을 수 있는 구조가 없다. → 별도의 테이블을 만들어야한다.
위 사진과 같이 Address에서 member_id, city, street, zipcode를 다 묶어서 PK로 만들어야한다.
식별자를 다로 넣어서 PK를 생성하면 Address는 값 타입이 아니라 엔티티가 되어버린다.
@Entity
puplic class Member {
@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> addresHistory = new ArrayList<>();
}


값 타입 컬렉션 사용
값 타입 저장 예제
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homecity", "street", "zipcode"));
//favorit insert문 나간다.
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
//Address insert 문 나간다
member.getAddresHistory().add(new Address("old1", "street1", "zipcode1"));
member.getAddresHistory().add(new Address("old2", "street2", "zipcode2"));
em.persist(member);



em.persist(member)만 했는데 값 타입 컬렉션도 자동으로 insert 된 걸 볼 수 있다. (컬렉션을 다른 테이블로 설정했음에도 불구하고)
컬렉션이 member 엔티티 생명주기에 의존한다.
값 타입 조회 예제
지연 로딩 전략 사용
System.out.println("===========START========");
Member findMember = em.find(Member.class, member.getId());

지연 로딩: member 객체 가져올 대 컬렉션은 가져오지 않는다
값 타입 수정 예제
참고: 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
/**
* 컬렉션 수정
* 치킨 -> 한식 타입이 String이므로 update안되고 아예 새롭게 값을 수정해줘야함
*/
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

Member에서 컬렉션 값을 수정하면 알아서 쿼리가 날아가서 수정해준다.
findMember.getAddresHistory().remove(new Address("old1", "street1", "zipcode1")); //equals hashcode이용해서 값 찾아서 remove
findMember.getAddresHistory().add(new Address("newCity", "street1", "zipcode1"));

favorit_food 테이블에서는 지정한 값만 delete되고 insert 문도 한개만 나가는데 address 테이블에서는 테이블에 있는 데이터를 통째로 지우고 다시 새로운 데이터와 기존 데이터를 넣어준다.
→ member PK = 1에 historyAddress(컬렉션) 2개 넣었고, 값을 변경하기 위해 remove했다. 그러면 member PK = 1인 값이 모두 사라지고 값을 변경해서 add 할 때 변경된 값 + 기존 값 이 두 값을 다시 넣어주기 때문에 insert 쿼리문에 2개 나갔다.
Q. 왜 favorit_food 테이블에서는 해당 값만 삭제가 가능한가? https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic&unitId=21716&tab=community&category=questionDetail&q=817247
A. addressHistory와 favoriteFoods 가 담고 있는 값이 다르기 때문이다.
List<Address> adressHistory; 와 같이 콜렉션이 값 객체를 들고 있는 경우와
Set<String> favoriteFoods; 처럼 문자열을 들고 있는 경우의 차이로 보시면 됩니다.
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. (Address 테이블의 데이터가 다 삭제되고 다시 갈아끼우는 현상이 나타나는 이유)
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함
- jpa가 자동으로 해주지 않아서 DDL 생성시 따로 설정해 줘야하는 부분이다.
- null 입력X, 중복 저장X
- address의 모든 필드를 묶어서 하나의 PK로 지정해야 한다.
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
- 일대다 관계를 위한 엔티티를 만들고, 여기에 값 타입을 사용
- 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용할 수 있다.
AddressEntity를 만들어서 값 타입 컬렉션처럼 사용하는 예시
@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);
}
}
@Entity
public class Member {
...
@OneToMany(cascade = ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addresHistory = new ArrayList<>();
...
}
값 타입 컬렉션은 언제 사용하는가?
체크 박스 사용할 경우
추적이 필요하지 않는 값인 경우, 값이 변경돼도 update하지 않아도 되는것들
예를 들어서 치킨, 피자, 족발이 있을 때 어떤 걸 선택할 것인가
값 타입은 정말 값 타입이라 판단될 때만 사용하자
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티이다.
주소가 다 사라져도 이력은 남겨야 될 수 도 있는 상황? https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic&unitId=21716&tab=community&category=questionDetail&q=298231
값 타입은 단독으로 사용될 수 없다. 어딘가에 항상 소속된다. 주소 이력(AddressHistory)이 값 타입이라면 주소(Address)에 모두 소속 → 따라서 주소가 모두 삭제되면 값 타입인 주소 이력도 모두 삭제된다.
만약에 주소를 삭제해도 주소 이력을 남기고 싶다면 주소 이력을 엔티티(AddressEntity)로 사용해야 한다.
정리
엔티티 타입의 특징
- 식별자가 있다.
- 생명주기 관리
- 공유할 수 있다.
값 타입의 특징
- 식별자 없다.
- 생면 주기를 엔티티에 의존
- 공유하지 않는 것이 안전 (복사해서 사용)
- 불변 객체로 만드는 것이 안전하다.
'JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2023.11.11 |
---|---|
갑 타입 (1) | 2023.11.10 |
영속성 전이: CASCAED (0) | 2023.11.09 |
즉시 로딩과 지연 로딩 (2) | 2023.11.09 |
프록시 (1) | 2023.11.09 |
값 타입 복사
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다
대신 값(인스턴스)를 복사해서 사용

객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 객체의 공유 참조는 피할 수 없다.
기본 타입과 객체 타입 비교
//기본 타입(primitive type) int a = 10; int b = a; //기본 타입은 값을 복사 b = 4; //객체 타입 Address a = new Address("Old"); Address b = a; //객체 타입은 참조를 전달 b.setCity("new");
결론: 불변 객체를 사용하자
불변 객체
- 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
- 값 타입은 불변 객체로 설계해야함
- 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성지로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다.
참고: Integer, String은 자바가 제공하는 대표적인 불변 객체이다.
임베디드 타입 Address 클래스를 불변 객체로 만든 예시

만약에 값을 바꾸고 싶다면 바꾸고 싶은 값만 새로운 값을 넣어서 새로운 객체를 만든다. (아래 사진 참고)

값 타입 비교
값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
- 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야한다.
- 값 타입의 equals() 메소드를 적절하게 재정의하자. (주로 모든 필드에서 사용)


값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- @ElementCollection, @CollectionTable 사용
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요함
예시

addressHistory: List<Address> 처럼 Address라는 값 타입을 리스트에 담아서 사용
DB는 테이블 안에 컬렉션을 담을 수 있는 구조가 없다. → 별도의 테이블을 만들어야한다.
위 사진과 같이 Address에서 member_id, city, street, zipcode를 다 묶어서 PK로 만들어야한다.
식별자를 다로 넣어서 PK를 생성하면 Address는 값 타입이 아니라 엔티티가 되어버린다.
@Entity
puplic class Member {
@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> addresHistory = new ArrayList<>();
}


값 타입 컬렉션 사용
값 타입 저장 예제
Member member = new Member(); member.setUsername("member1"); member.setHomeAddress(new Address("homecity", "street", "zipcode")); //favorit insert문 나간다. member.getFavoriteFoods().add("치킨"); member.getFavoriteFoods().add("족발"); member.getFavoriteFoods().add("피자"); //Address insert 문 나간다 member.getAddresHistory().add(new Address("old1", "street1", "zipcode1")); member.getAddresHistory().add(new Address("old2", "street2", "zipcode2")); em.persist(member);



em.persist(member)만 했는데 값 타입 컬렉션도 자동으로 insert 된 걸 볼 수 있다. (컬렉션을 다른 테이블로 설정했음에도 불구하고)
컬렉션이 member 엔티티 생명주기에 의존한다.
값 타입 조회 예제
지연 로딩 전략 사용
System.out.println("===========START========"); Member findMember = em.find(Member.class, member.getId());

지연 로딩: member 객체 가져올 대 컬렉션은 가져오지 않는다
값 타입 수정 예제
참고: 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
/** * 컬렉션 수정 * 치킨 -> 한식 타입이 String이므로 update안되고 아예 새롭게 값을 수정해줘야함 */ findMember.getFavoriteFoods().remove("치킨"); findMember.getFavoriteFoods().add("한식");

Member에서 컬렉션 값을 수정하면 알아서 쿼리가 날아가서 수정해준다.
findMember.getAddresHistory().remove(new Address("old1", "street1", "zipcode1")); //equals hashcode이용해서 값 찾아서 remove findMember.getAddresHistory().add(new Address("newCity", "street1", "zipcode1"));

favorit_food 테이블에서는 지정한 값만 delete되고 insert 문도 한개만 나가는데 address 테이블에서는 테이블에 있는 데이터를 통째로 지우고 다시 새로운 데이터와 기존 데이터를 넣어준다.
→ member PK = 1에 historyAddress(컬렉션) 2개 넣었고, 값을 변경하기 위해 remove했다. 그러면 member PK = 1인 값이 모두 사라지고 값을 변경해서 add 할 때 변경된 값 + 기존 값 이 두 값을 다시 넣어주기 때문에 insert 쿼리문에 2개 나갔다.
Q. 왜 favorit_food 테이블에서는 해당 값만 삭제가 가능한가? https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic&unitId=21716&tab=community&category=questionDetail&q=817247
A. addressHistory와 favoriteFoods 가 담고 있는 값이 다르기 때문이다.
List<Address> adressHistory; 와 같이 콜렉션이 값 객체를 들고 있는 경우와
Set<String> favoriteFoods; 처럼 문자열을 들고 있는 경우의 차이로 보시면 됩니다.
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. (Address 테이블의 데이터가 다 삭제되고 다시 갈아끼우는 현상이 나타나는 이유)
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함
- jpa가 자동으로 해주지 않아서 DDL 생성시 따로 설정해 줘야하는 부분이다.
- null 입력X, 중복 저장X
- address의 모든 필드를 묶어서 하나의 PK로 지정해야 한다.
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
- 일대다 관계를 위한 엔티티를 만들고, 여기에 값 타입을 사용
- 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용할 수 있다.
AddressEntity를 만들어서 값 타입 컬렉션처럼 사용하는 예시
@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);
}
}
@Entity
public class Member {
...
@OneToMany(cascade = ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addresHistory = new ArrayList<>();
...
}
값 타입 컬렉션은 언제 사용하는가?
체크 박스 사용할 경우
추적이 필요하지 않는 값인 경우, 값이 변경돼도 update하지 않아도 되는것들
예를 들어서 치킨, 피자, 족발이 있을 때 어떤 걸 선택할 것인가
값 타입은 정말 값 타입이라 판단될 때만 사용하자
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티이다.
주소가 다 사라져도 이력은 남겨야 될 수 도 있는 상황? https://www.inflearn.com/course/lecture?courseSlug=ORM-JPA-Basic&unitId=21716&tab=community&category=questionDetail&q=298231
값 타입은 단독으로 사용될 수 없다. 어딘가에 항상 소속된다. 주소 이력(AddressHistory)이 값 타입이라면 주소(Address)에 모두 소속 → 따라서 주소가 모두 삭제되면 값 타입인 주소 이력도 모두 삭제된다.
만약에 주소를 삭제해도 주소 이력을 남기고 싶다면 주소 이력을 엔티티(AddressEntity)로 사용해야 한다.
정리
엔티티 타입의 특징
- 식별자가 있다.
- 생명주기 관리
- 공유할 수 있다.
값 타입의 특징
- 식별자 없다.
- 생면 주기를 엔티티에 의존
- 공유하지 않는 것이 안전 (복사해서 사용)
- 불변 객체로 만드는 것이 안전하다.
'JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2023.11.11 |
---|---|
갑 타입 (1) | 2023.11.10 |
영속성 전이: CASCAED (0) | 2023.11.09 |
즉시 로딩과 지연 로딩 (2) | 2023.11.09 |
프록시 (1) | 2023.11.09 |