일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 검증
- jpa 활용2 - api 개발 고급
- 스프링 mvc1 - 스프링 mvc
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch5
- 스프링 고급 - 스프링 aop
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch7
- 타임리프 - 기본기능
- 2024 정보처리기사 수제비 실기
- 스프링 mvc1 - 서블릿
- @Aspect
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch9
- 코드로 시작하는 자바 첫걸음
- 스프링 입문(무료)
- 게시글 목록 api
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch12
- 스프링 mvc2 - 타임리프
- 스프링 mvc2 - 로그인 처리
- Today
- Total
나구리의 개발공부기록
값 타입, 기본값 타입, 임베디드 타입, 값 타입과 불변 객체, 값 타입의 비교, 값 타입 컬렉션, 실전 예제 - 값 타입 매핑 본문
값 타입, 기본값 타입, 임베디드 타입, 값 타입과 불변 객체, 값 타입의 비교, 값 타입 컬렉션, 실전 예제 - 값 타입 매핑
소소한나구리 2024. 10. 14. 22:39출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
** 실전예제 전까지는 DB URL 설정을 test로 설정하여 실습
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
** 실전예제는 jdbc.url 설정을 jpashop으로 변경해서 진행
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/jpashop"/>
1. 기본값 타입
1) JPA의 데이터 타입
(1) 엔터티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적이 가능함
- ex) 회원 엔터티의 키나 나이 값을 변경해도 식별자로 인식이 가능함
(2) 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적이 불가능함
- ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체됨
2) 값 타입 분류
(1) 기본값 타입
- 자바 기본 타입(int, double ...)
- 래퍼 클래스(Integer, Long)
- String
(2) 임베디드 타입(embedded type, 복합 값 타입)
- 새로운 값 타입을 직접 정의
(3) 컬렉션 값 타입(collection value type)
- 자바의 컬렉션 처럼 값 타입을 하나 이상 저장 가능
3) 기본값 타입
- String name, int age 과같은 필드들
- 회원엔터티를 삭제하면 이름, 나이의 필드도 함께 삭제되듯이 생명주기를 엔터티에 의존함
- 회원 이름이 변경될 때 다른 회원의 이름도 함께 변경되는 사이드 이펙트가 나타나면 안되기 때문에 값 타입은 절대 공유하면 안됨
** 참고
- int, double 같은 기본 타입(primitive type)은 절대 공유가 되지 않음
- 기본값 타입은 항상 값을 복사하는 방식으로 동작하기 때문에 값 타입으로 사용 시 안전함
- Integer같은 래퍼클래스나 String같은 특수한 클래스도 공유가 가능한 객체이지면 불변객체이므로 값 타입으로 사용 시 안전함
2. 임베디드 타입(복합 값 타입)
1) 임베디드 타입
(1) 특징
- 새로운 값 타입을 직접 정의한 타입을 JPA는 임베디드 타입(embedded type)이라고 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 하며 int, String처럼 엔터티가 아니라 값 타입임
(2) 예시
- 회원 엔터티에 이름, 근무 시작일, 근무 종료일과 주소 정보인 도시, 번지, 우편번호를 가져야한다고 가정할 때 근무 관련 속성과 주소 관련 속성은 별도로 공통 속성으로 관리 할 수 있음
- 근무 시작일, 근무 종료일을 근무 기간으로 묶고 도시, 번지, 우편번호를 주소로 묶어서 별도의 클래스로 정의함
- 그럼 회원엔터티는 이름, 근무 기간, 주소를 가진다고 추상화하여 설명할 수 있고 기본 값 들을 묶어 새로 정의한 근무 기간과 주소는 임베디드타입으로 정의하여 엔터티에 종속되어 관리할 수 있음
2) 임베디드 타입 사용법과 장점
(1) 사용법
- @Embeddable: 값 타입을 정의하는 곳에 입력
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자가 필수임
(2) 장점
- 시스템 전체에서 임베디드타입으로 정의된 클래스를 재사용할 수 있고 비슷한 속성끼리 묶었기 때문에 클래스의 응집도가 높음
- Period.isWork()처럼 해당 값 타입만 사용하는 메소드를 만들어 객체지향적으로 설계가 됨
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔터티에 생명주기를 의존함
3) 입베디드 타입 적용 전
- Member엔터티와 테이블 구조
- 엔터티와 동일한 구조의 테이블이 생성됨
@Entity
public class Member extends BaseEntity {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String city;
private String street;
private String zipCode;
// ... getter, setter
}
4) 임베디드 타입 적용 후
- startDate, endDate -> Period(기간)이라는 클래스로 정의
- city, street, zipCode -> Address(주소)라는 클래스로 정의
- @Embedded(임베디드 타입을 사용하는 곳에 적용)와 @Embeddable(임베디드 타입으로 정의하는 곳에 사용)은 둘중에 하나만 적용해도 되지만 가독성을 위해 둘 다 사용하는 것을 권장함
- 임베디드 타입에는 기본생성자가 필수이므로 매개변수가 있는 생성자를 사용할 경우 꼭 기본생성자를 직접 생성해주어야 함
(1) Member
- 임베디드 타입으로 정의한 Period와 Address를 Member엔터티의 필드로 적용
@Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@Embedded // 임베디드 적용
private Period workPeriod;
@Embedded // 임베디드 적용
private Address homeAddress;
// ... getter, setter
}
(2) Address
- 기존에 Member에 있던 주소에 관련된 속성들을 임베디드타입으로 별도로 관리
package jpabook.jpashoptest.domain;
@Embeddable // Address 클래스를 임베디드타입으로 설정
public class Address {
private String city;
private String street;
private String zipCode;
// 기본생성자 필수, 매개변수 생성자, getter, setter
}
(3) Period
- 기존에 Member에 있던 근무 기간에 관련된 속성들을 임베디드타입으로 별도로 관리
package jpabook.jpashoptest.domain;
@Embeddable // 기본생성자 필수, getter, setter
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
// 기본생성자 필수, 매개변수 생성자, getter, setter
}
(4) 회원 생성 및 테이블 결과
- 새로운 회원을 생성하면 클래스는 3개로 설계했지만 테이블은 1개로 생성됨
// ...
Member member = new Member();
member.setUsername("member");
member.setHomeAddress(new Address("aaa","bbb","ccc"));
member.setWorkPeriod(new Period(now(), now().plusDays(30)));
em.persist(member);
tx.commit();
// ...
5) 임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔터티의 값일 뿐이기 때문에 임베디드 타입을 사용하기 전과 후의 매핑하는 테이블의 구조는 같음
- 객체와 테이블을 세밀하게 매핑하는것이 가능하며 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
- 실무에서 임베디드타입을 적절하게 필요한 곳에 적절히 사용하면 공통 속성을 별도로 묶어서 관리할 수 있는 등등의 여러 장점이 많음
6) 임베디드 타입과 연관관계
- 임베디드 타입에서 다른 임베디드 타입을 가질 수 있을 뿐만 아니라 엔터티의 FK의 값을 가지고있다면 연관관계 매핑을 통해 관계를 맺을 수 있음
(1) 만약 한 엔터티에서 같은 값 타입을 사용하면?
- 한 엔터티에서 같은 임베디드 타입을 중복해서 사용하게되면 PersistenceException 예외가 발생 됨
- 즉 컬럼명이 중복되었기 때문에 예외가 발생되므로 @AttributeOverrides, @AttributeOverride를 사용해서 컬럼명의 속성을 재정의 하면 됨
// Member Entity ...
@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;
(2) 임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null이됨
3. 값 타입과 불변 객체
- 값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 만든 개념임
- 값 타입은 단순하고 안전하게 다룰 수 있어야 함
1) 값 타입 공유 참조와 복사
(1) 임베디드 타입의 공유 참조 -> 적용하지 말것
- 임베디드 타입 같이 값 타입을 여러 엔터티에서 공유하면 사이드 이펙트가 발생하여 위험함
- 생성한 각 멤버 엔티티가 동일한 임베디드의 참조값을 가지고 있을 때, 개발자가 둘중 하나의 엔터티가 참조한 임베디드 타입의 값을 변경을 하게되면 둘다 변경이 됨
- 즉 member1만 변경하려고 시도했지만, member1, member2 둘다 주소 정보가 업데이트가 되어버리며 이런 버그는 잡기가 거의 불가능할 정도의 어려운 버그이기 때문에 이렇게 사용하면 절대 안됨
- 이러한 상황을 의도해서 적용하려고 할 수 있는데 그런 경우에는 주소를 임베디드타입을 사용하는 것이아니라 엔터티 타입으로 적용하여 연관관계 매핑을 통해 값을 공유해야 함
try {
Address address = new Address("aaa", "bbb", "ccc");
Member member = new Member();
member.setUsername("member");
member.setHomeAddress(address);
em.persist(member);
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
member1.getHomeAddress().setCity("member1시티변경");
tx.commit();
// ...
(2) 임베디드 타입 복사를 이용
- 참조를 공유하는 것을 대신하여 값(인스턴스)를 복사해서 사용해야 함
- 기존의 임베디드 타입의 값들을 인수로 새로운 임베디드객체를 생성하여 참조값을 저장한 참조변수를 사용하도록 하면, 각 임베디드타입은 별도의 참조값을 가지고 있기 때문에 각 멤버가 가진 임베디드의 값을 변경하여도 서로 영향이 없음
try {
Address address = new Address("aaa", "bbb", "ccc");
Member member = new Member();
member.setUsername("member");
member.setHomeAddress(address);
em.persist(member);
Address addressCopy = new Address(address.getCity(), address.getStreet(), address.getZipCode());
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(addressCopy);
em.persist(member1);
member1.getHomeAddress().setCity("member1시티변경");
tx.commit();
3) 객체 타입의 한계와 불변 객체
(1) 객체 타입의 한계
- 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이아니라 객체 타입으로 생성되는데, 객체 타입은 값을 대입하면 기본타입처럼 값을 복사하는 것이 아니라 참조 값을 직접 대입하게됨
- 위에서 알아 보았듯이 임베디드 타입은 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있음
- 그러나 이런 부분은 실수가 많이 발생할 수 여지가 있고 특히 실수를 방지할 수 있는 방법이 없음, 즉 실수로 인한 객체의 공유 참조를 피할 수가 없음
(2) 불변 객체
- 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있으므로 값 타입은 불변 객체(Immutable object)로 설계해야함
- 불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체임
- 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 불변 객체를 만들 수 있음
- 자바가 제공하는 대표적인 불변객체로 Integer, String 등이 있음
- 불변객체라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있음
(3) 불변 객체 예시
- 임베디드 타입에 세터를 제거하여 불변 객체로 생성 -> 이 외에도 불변객체를 만드는 방법은 다양하게 존재하니 검색하여 원하는 형태로 적용
- 값을 수정할 때는 불변성을 유지하기 위해 수정하고자 하는 임베디드 타입의 값만 새롭게 입력하고 다른 필드는 기존의 값을 가져와서 새롭게 주소를 생성한 뒤 수정 대상인 멤버 엔터티에 적용하면 됨
- 다른 방법으로 중간에 값을 바꾸는 방법도 있긴 하지만 이론적으로는 임베디드 객체를 전체를 새롭게 생성해서 입력해 주는 것이 맞음
// 임베디드인 Address에 세터를 제거
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
public Address() {
}
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipCode() {
return zipCode;
}
}
// 값을 수정하고자 할 때는 완전히 새로운 주소를 가져와서 교체해야함
Address address = new Address("aaa", "bbb", "ccc");
Member member = new Member();
member.setUsername("member");
member.setHomeAddress(address);
em.persist(member);
Address newAddress = new Address("newCity", address.getStreet(), address.getZipCode());
member.setHomeAddress(newAddress);
// ...
4. 값 타입 비교
(1) 비교 방식 2가지
- 동일성(identity)비교 : 인스턴스의 참조값을 비교 ( == 사용)
- 동등성(equivalence)비교 : 인스턴스의 값을 비교 (equals() 사용)
(2) 임베디드 값 타입을 오버라이딩
- 값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야함
- 즉, 기본값의 비교처럼 임베디드 객체의 값을 비교했을 때 그 안의 내용이 같으면 같다고 나와야 하기 때문에 equals와 hashcode를 오버라이딩하여 값을 비교하도록 변경해야 함
- equals와 hashcode의 오버라이딩은 IDE에서 지원하므로 기본적으로는 해당 기능을 사용하여 오버라이딩 하는 것을 권장함
(3) Address타입에 equals와 hashcode를 오버라이딩
- IDE마다 지원하는 equals와 hascode를 오버라이딩하는 기능을 사용하면 됨
- Use getters when available이라는 체크 항목을 체크하면 getter를 사용하여 오버라이딩 하는데, 직접 필드의 값으로 오버라이딩하면 JPA가 프록시로 동작할 때는 적용되지 않기 때문에 가능하면 해당 항목을 체크하여 오버라이딩 하는것을 권장함
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
// ... 기존 코드 생략
@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(getCity(), address.getCity())
&& Objects.equals(getStreet(), address.getStreet())
&& Objects.equals(getZipcode(), address.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipCode);
}
}
5. 값 타입 컬렉션
1) 구조 및 설명
- 값 타입을 하나 이상 저장할 때 사용하며 @ElementCollection, @CollectionTable을 사용하여 매핑함
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없으므로 컬렉션을 저장하기 위한 별도의 테이블이 필요함
- Member 엔터티에 favoriteFoods라는 필드가 Set 컬렉션으로 되어있고 addressHistory라는 필드가 List 컬렉션으로 정의가 되어있다고 가정
- 테이블의 구조는 각 컬렉션으로 구현된 필드가 각각의 테이블로 생성되어 MEMBER테이블과 다대일 연관관계가 맺는 모양으로 설계가 되어있어야 함
- 컬렉션을 구현한 테이블은 엔터티가 아니기 때문에 별도의 임의 식별자를 PK로 하지 않고 구성하는 속성을 묶어서 복합키로 구성해야함
2) 예제
(1) Member
- @ElementCollection 애노테이션으로 컬렉션들을 매핑
- @CollectionTable로 테이블과 매핑하며 name으로 테이블명을 변경할 수 있고 joinColumns로 외래키를 설정할 수 있음
- String타입의 경우 @Column으로 컬럼명을 변경할 수 있고 임베디드 타입은 @AttributeOverrides, @AttributeOverride으로 컬럼명을 변경하면 됨
@Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
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<>();
// ... 기존코드 생략
}
(2) 테이블 생성결과
- 컬렉션들이 각각의 테이블로 생성된 것을 로그로 확인할 수 있음
(3) Member 객체 저장
- Member객체를 생성하고, 이름과 주소를 설정
- 생성한 Member객체의 favoriteFoods 컬렉션에 값을 3개 입력
- 생성한 Member 객체의 addressHistory 컬렉션에 값을 2개 입력
- 실행하면 한번의 persist로 입력한 모든 객체가 각 테이블에 반영됨
- 즉, 컬렉션들은 별도의 엔터티가아닌 Member에 속해있는 값 타입이기 때문에 생명주기를 같이 가짐
// ...
try {
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homecity", "street", "100-100"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "100-100"));
member.getAddressHistory().add(new Address("old2", "street", "100-100"));
em.persist(member);
tx.commit();
// ...
(4) 조회
- 기존 코드에서 Member만 조회하면 Member만 찾는 쿼리만 실행됨
- 즉, 지연로딩을 기본값으로 사용하고 있으며 이런 컬렉션들은 즉시로딩으로 사용하면 문제를 일으킬 가능성이 높음
// ...
em.flush();
em.clear();
System.out.println("================= START ==================");
Member findMember = em.find(Member.class, member.getId());
- 조회된 멤버에서 addressHistory와 favoriteFoods를 접근하면 그때 각 테이블에 select 쿼리가 전송됨
// ...
System.out.println("================= START ==================");
Member findMember = em.find(Member.class, member.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);
}
(5) 수정 - 문제발생
- 임베디드타입의 값의 일부를 수정할때는 위에서 배운 것처럼 수정할 값을 입력 수 임배디드 객체를 새로 생성해서 교체해야함
- 일반적인 컬렉션 값 타입은 자동으로 수정할 수 있는 방법이 없어서 직접 remove로 지우고 add로 입력하여 수정을 해야함
- 임베디드 컬렉션 값 타입도 마찬가지로 임베디드 객체 전체를 삭제한뒤에 수정할 값을 입력하여 임베디드 객체를 생성해야함
- 이때 임베디드 타입에 꼭 equals와 hashcode가 제대로 오버라이딩 되어있어야 정상적으로 값의 삭제 및 추가가 진행됨
// ...
System.out.println("================= START ==================");
Member findMember = em.find(Member.class, member.getId());
// 임베디드타입의 값을 homecity -> newcity로 수정
// 값 타입은 필드 하나만 바꾸는 것이 아니라 완전히 Address를 새로 생성해야함
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newcity", a.getStreet(), a.getZipCode()));
// 컬렉션(Set<String>) 값 타입의 값을 치킨 -> 한식으로 수정
// 삭제 후 다시 입력해야함
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// 컬렉션(List<Address>) 값 타입의 값을 old1 -> new1으로 수정
findMember.getAddressHistory().remove(new Address("old1", "street", "100-100"));
findMember.getAddressHistory().add(new Address("new1", "street", "100-100"));
tx.commit();
//...
- 실행해보면 추가적인 persist 없이 자바의 컬렉션의 값을 수정하는 것처럼 변경하여도 DB의값들이 정상적으로 잘 반영이 되어있는 것을 확인할 수 있으나 쿼리를 보면 문제가 있음
- FAVORITE_FOOD를 삭제하고 생성하는 것은 원하는 대로 하나의 delete쿼리와 하나의 insert쿼리가 진행되어 정상적으로 수정이 된것으로 보임
- 그러나 ADDRESS 테이블의 수정 로그를 보면, delete쿼리가 한번 insert쿼리가 2번 발생했음
- delete쿼리로 ADDRESS의 테이블의 값을 전부 지워버리고 새로 등록할 주소와, 삭제 대상이 아닌 주소를 테이블에 다시 저장하는 방식으로 동작하고 있는 것을 확인할 수 있음
- 즉, 값 타입 컬렉션은 영속선 전이(Cascade) + 고아 객체 제거 기능을 가진 것처럼 동작하고 있음
- 만약, 주소의 필드가 10개 20개라면, 전부지우고 다시 전부 생성하는 것임
3) 값 타입 컬렉션의 제약 사항
- 값 타입은 엔터티와 다르게 식별자 개념이 없으므로 변경하게되면 추적이 어려움
- 값 타입 컬렉션을 매핑하는 테이블은 null입력 방지와 중복 저장 방지를 위해 모든 컬럼을 묶어서 기본키를 구성해야함
- 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔터티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장함
(1) @OrderColumn으로 해결하는 것은 권장하지 않음
- 문제가 발생되는 컬렉션 값 타입에 @OrderColumn을 적용하면 식별할 수 있는 컬럼을 추가시키기 때문에 update쿼리로 문제가 해결되는 것처럼 보이지만 OrderColumn으로 지정한 컬럼의 값이 중간에 비어있으면 null로 들어오게되는 등등의 여러가지 문제로 의도하지않은 방식으로 동작하는 경우가 발생함
- 이렇게 복잡하게 문제를 해결하는 것이아니라 다른 방법으로 문제를 해결해야함
// OrderColumn을 사용하여 문제를 해결할 수 있지만 해당 방법도 다른 문제가 발생됨
@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
4) 값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 사용하는 것을 고려해야함
- 일대다 관계를 위한 엔터티를 만들고 여기에서 값 타입을 사용하는 방식
- 매핑 옵션으로 영속성 전이 + 고아 객체 제거를 사용하여 값 타입 컬렉션 처럼 사용하도록 할 수 있음
(1) AddressEntity 생성
- 테이블의 이름을 ADDRESS로 설정하고 엔터티 타입으로 생성
- 필드로 임베디드타입인 Address를 가지고 있고 생성자로 값을 입력받아서 Address객체를 생성하도록 작성
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity() {}
public AddressEntity(String city, String street, String zipCode) {
this.address = new Address(city, street, zipCode);
}
// ... getter, setter
}
(2) Member 수정
- Address와 일대다 관계를 매핑하고 Cascade와 orphanRemoval옵션을 적용하여 영속성 전이와 고아 객체를 사용하도록 설정
- 외래키를 MEMBER_ID로 설정
@Entity
public class Member{
// ... 기존코드 생략
// @OrderColumn(name = "address_history_order")
// @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<>();
// ... getter, setter
}
(3) AddressEntity로 값을 추가
- update로그로 ADDRESS 테이블에 값이 반영되도록 변경이 됨
- DB의 ADDRESS 테이블에도 ID값이 생성되어 해당 값을 조회하여 자유롭게 CRUD를 할 수 있음
// ...
member.getAddressHistory().add(new AddressEntity("old1", "street", "100-100"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "100-100"));
// ...
(4) 값 타입 컬렉션은 언제 사용해야 할까?
- 셀렉트 박스나 체크박스처럼 옵션을 선택할 때처럼 정말 단순한 상황같이 추적이 필요없고 값이 바뀌어도 update할 필요가 없을 때 사용해야함
- 웬만하면 Entity로 사용하는 것을 권장하는데, 테이블의 값을 변경하지 않는다고해도 해당 테이블에서 DB쿼리가 시작해야 한다고하면 전부 Entity라고 봐야함
5) 정리
(1) 엔터티 타입의 특징
- 식별자가 있고 생명 주기가 관리되며 공유를 할 수 있음
(2) 값 타입의 특징
- 식별자가 없고 생명 주기를 엔터티에 의존함
- 공유하지 않는 것이 안전하며 공유를 해야할 때는 복사해서 사용하도록 불변 객체로 만드는 것이 안전함
** 참고
- 값 타입은 정말 값 타입이라 판단될 때만 사용해야 하며 실무에서 생각보다 값 타입을 사용할 일이 많지 않음
- 엔터티와 값 타입을 혼동해서 엔터티를 값 타입으로 만들면 안되며 식별자가 필요하고 지속적으로 값을 추적하고 변경해야 한다면 그것은 값 타입이 아니라 엔터티임
6. 실전 예제 - 값 타입 매핑
- 배운 내용을 예제로 적용해보기
1) 테이블 구조
- Member와 Delivery에서 사용하는 city, street, zipcode를 Address 클래스를 생성하여 값 타입으로 적용
- UML에서 스테레오타입(<< >>)으로 값 타입임을 표시
2) 코드
(1) Address 생성
- 임베디드타입으로 Address 클래스를 생성
- getter는 public, setter는 private으로 설정하여 setter을 외부에서 접근 못하도록 설정(불변객체로 설정)
- equals와 hashcode를 ide가 지원하는 기능으로 오버라이딩(getter로 생성하기 옵션 적용)
** 참고
- JPA에서는 프록시 때문에 equals, hashcode가 아니더라도 코드를 작성할 때 get을 해서 값을 얻고 메서드로 적용하는 방식으로 코드를 구성하는 것을 권장함
package jpabook.jpashop.domain;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
private void setStreet(String street) {
this.street = street;
}
private String getZipcode() {
return zipcode;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@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(getCity(), address.getCity())
&& Objects.equals(getStreet(), address.getStreet())
&& Objects.equals(getZipcode(), address.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
}
(2) Member, Delivery 수정
- 적용되어있던 city, street, zipCode 필드를 지우고 임베디드 타입인 Address를 필드값으로 적용
@Entity
public class Member {
@Id @GeneratedValue // GeneratedValue 전략을 기본값으로 설정(Auto)
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@Embedded
private Address address;
// 기존 코드 생략
}
@Entity
public class Delivery {
@Id @GeneratedValue
@Column(name = "DELIVERY_ID")
private Long id;
@Embedded
private Address address;
// 기존 코드 생략
}
** 참고
- 임베디드 타입은 의미있는 비즈니스 메소드를 정의하거나 각 필드에 길이, 입력타입 등의 제약조건을 각각 적용하면 해당 타입을 사용하는 엔터티에서 메소드를 사용하거나, 공통으로 제약조건을 적용하는 등의 장점이 있음