728x90
- 핵심
- 임베디드 타입(복합 값 타입)
- 값 타입 컬렉션
1. JPA 데이터 타입 분류
- 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 PK(식별자)로 지속해서 추적 가능
- 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
2. 값 타입의 분류
- 기본값 타입
- 자바 기본 타입(int, double)
- 래퍼 클래스(Integer, Long)
- String
- 임베디드 타입(embedded type, 복합 값 타입)
- 컬렉션 값 타입(collection value type)
3. 기본값 타입
- 생명주기를 엔티티에 의존 - 회원 삭제시 이름, 나이 필드도 함께 삭제
- 값 타입은 공유하면 안된다 - cf> 엔티티 타입은 공유해도 된다.
3.1. 자바의 기본 타입은 절대 공유되지 않는다.
- 기본 타입은 항상 값을 복사한다. (기본형의 특징. 변수에 할당된 메모리주소에 값이 그대로 들어간다.)
- 래퍼 클래스(Integer), String 같이 특수 클래스는 참조 변수를 사용해서 값이 공유가 되지만 값 변경이 불가하여 공유로 인한 변경 문제가 발생하지 않는다.
- 래퍼 클래스
- String처럼 메모리에 값을 저장하고 해당 주소를 래퍼클래스 변수에 넣는다.
- 그래서 참조변수를 공유를 할 수 있다.
- 하지만 해당 참조변수 내부의 실제 값을 변경할 수 값없기 때문에 값을 변경하게 되면
- 새로운 메모리 주소에 새 값을 넣고, 새 값이 들어간 메모리 주소를 래퍼 클래스의 변수에 넣는다.
- 참고로 jvm은 java runtime 시 -128~127까지의 Interger를 위한 값을 미리 생성해놓기 때문에 성능 최적화가 가능하다.
- String
- 동일하다. 변경불가한 class 이다. 변경하고 싶으면 stringBuffer 사용하면 된다.
package hellojpa.chap9;
public class ValueMain {
public static void main(String[] args) {
Integer a = new Integer(100);
Integer b = a;
a = 1222;
System.out.println("a = " + a); //1222
System.out.println("b = " + b); //100
int c = 10;
int d = c;
c = 100;
System.out.println("c = " + c); //100
System.out.println("d = " + d); //10
}
}
4. 임베디드 타입(복합 값 타입)
- 새로운 값 타입을 직접 정의 가능
- JPA는 임베디드 타입이라고 한다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고 한다.
- 값 타입이다. == 공유되면 안된다. 변경시 추적이 불가하기 때문
4.1. 예시
- 모델링
4.2. 임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
- 임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
- Address
package hellojpa.chap9;
@Embeddable
public class AddressChap9 {
private String city;
private String street;
@Column(name = "ZIPCODE")
private String zipcode;
// 해당 type도 기본 생성자가 필수
public AddressChap9() {}
// 불변 객체 1 - setter 없애기
// public void setCity(String city) { this.city = city; }
// 불변 객체 2 - private
private void setStreet(String street) { this.street = street; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressChap9 that = (AddressChap9) o;
return Objects.equals(city, that.city) && Objects.equals(street, that.street) && Objects.equals(zipcode, that.zipcode);
}
@Override
public int hashCode() {return Objects.hash(city, street, zipcode);}
}
- Period
package hellojpa.chap9;
@Embeddable
public class PeriodChap9 {
private LocalDateTime startDate;
private LocalDateTime endDate;
// 해당 type도 기본 생성자가 필수
public PeriodChap9() {}
}
- Member
package hellojpa.chap9;
@Entity
public class MemberChap9 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded // 기간 Period - @Embeddable만 있으면 @Embedded 안넣어도 되는데 넣는 걸 추천
private PeriodChap9 workPeriod;
@Embedded // 주소 Address
private AddressChap9 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 AddressChap9 workAddress;
}
4.3. 임베디드 타입 장점
- 재사용
- 높은 응집도
- Address.isHome() 처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
4.5. 임베디드 타입과 연관관계
- 임베디드 타입으로 엔티티를 가질 수 있다.
4.6. @AttributeOverride : 속성 재정의
- 한 엔티티에서 같은 임베디드 타입을 사용시 컬럼 명이 중복된다.
- @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의
package hellojpa.chap9;
@Entity
public class MemberChap9 {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded // 기간 Period - @Embeddable만 있으면 @Embedded 안넣어도 되는데 넣는 걸 추천
private PeriodChap9 workPeriod;
@Embedded // 주소 Address
private AddressChap9 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 AddressChap9 workAddress;
}
4.7. 임베디드 타입과 null
- 임베디드 타입과 값이 null이면 매핑한 컬럼 값은 모두 null
5. 값 타입과 불변 객체
- 임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험
- side effect 발생 - 이걸 의도했어라고 변명하면 안되는게 그렇게 써야하는 경우면 엔티티로 따로 뽑아 써야한다.
- 값타입 참조를 공유하므로 변경시 모든 것이 변경됨 -> 새로 복사본을 만들어서 사용해야함 (깊은 복사)
- 문제는 개발자가 참조 값을 직접 대입하는 것을 막을 방법이 없다. -> 아예 직접 넣는 방식을 막아버림 (불변 객체)
- setter 없애기, private으로 두기
- 해시코드, equals 오버라이딩 하기 - 값 타입 비교의 문제점을 막기 위함
package hellojpa.chap9;
@Embeddable
public class AddressChap9 {
private String city;
private String street;
@Column(name = "ZIPCODE")
private String zipcode;
// 해당 type도 기본 생성자가 필수
public AddressChap9() {}
public AddressChap9(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// 불변 객체 1 - setter 없애기
// public void setCity(String city) { this.city = city; }
// 불변 객체 2 - private
private void setStreet(String street) { this.street = street; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressChap9 that = (AddressChap9) o;
return Objects.equals(city, that.city) && Objects.equals(street, that.street) && Objects.equals(zipcode, that.zipcode);
}
@Override
public int hashCode() { return Objects.hash(city, street, zipcode); }
}
package hellojpa.chap9;
public class JpaChap9Share {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
AddressChap9 shareAddress = new AddressChap9("city", "street", "10000");
MemberChap9 member = new MemberChap9();
member.setUsername("member1");
member.setHomeAddress(shareAddress);
em.persist(member);
MemberChap9 member2 = new MemberChap9();
member2.setUsername("member2");
member2.setHomeAddress(shareAddress);
em.persist(member2);
// 공유 참조의 위험성
// member.getHomeAddress().setCity("newCity"); // 해당 참조를 사용하는 모든 멤버의 값이 변경되는 버그 발생
// 값 타입 복사
AddressChap9 copyAddress = new AddressChap9(shareAddress.getCity(), shareAddress.getStreet(), shareAddress.getZipcode());
MemberChap9 member3 = new MemberChap9();
member3.setUsername("member3");
member3.setHomeAddress(copyAddress);
em.persist(member3);
// 영향 없음
// member.getHomeAddress().setCity("newCity"); // member3은 영향을 받지 않는다.
// 문제는 개발자들이 직접 값을 변경하게 막을 수 있는 방법이 없다. 참조주소를 사용하는 것이기 때문 -> 불변객체로 수정자체를 막음
// member.getHomeAddress().setCity("newCity"); // setter를 없애거나 private으로 둔다. 외부에서 사용을 막음
// 대신 불변 객체 값을 변경하고 싶을 때는 new Address()를 새로만들어서 통으로 넘겨야한다.
AddressChap9 newAddress = new AddressChap9("NewCity", shareAddress.getStreet(), shareAddress.getZipcode());
member.setHomeAddress(newAddress);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
6. 값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용
- @ElementCollection, @CollectionTable 사용
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
- 컬렉션을 저장하기 위한 별도의 테이블이 필요
6.1. 값 타입 컬렉션 사용
- 저장
- 조회 - 지연로딩 전략 사용 (@ElementCollection에서 설정 가능)
- 수정 -> 문제가 많기 때문에 엔티티 승계 작업 필요
- 값 타입 컬렉션은 영속성 전이(all), 고아 객체 제거 기능을 필수로 가진다.
6.2. 값 타입 등록, 조회, 수정-문제점
- @ElementColletion : 값 타입 collection 명시 및 지연로딩 및 즉시로딩 설정 가능
- @CollectionTable
- name : collection table의 이름
- joinColumn : 설계상 "collection : member = n : 1" 이므로 @JoinColumn 존재
- <String>처럼 예외적으로 하나의 컬럼만 존재할 경우 @Column으로 컬럼명 변경 허용
- 등록
- db상에서는 collection을 담는 기능이 없기 때문에 collection을 table로 분리해서 저장
- db상에서는 collection을 담는 기능이 없기 때문에 collection을 table로 분리해서 저장
- 조회
- default인 지연로딩 수행
- 임베디드타입은 그런 것이 필요없는데 객체 입장에서 분리된 것이지 db상에서는 동일 테이블에 존재하는 컬럼이기 때문
- 수정
- 임베디드때처럼 list의 요소를 찾아서 제거하고 변경요소를 넣는 코드를 작성해도 아래와 같은 문제 발생
Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert - 성능에 매우 안좋을 듯하다. 결론은 사용하지 말 것
- 임베디드때처럼 list의 요소를 찾아서 제거하고 변경요소를 넣는 코드를 작성해도 아래와 같은 문제 발생
- JpaChap9Collection
package hellojpa.chap9;
public class JpaChap9Collection {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
AddressChap9 address = new AddressChap9("city", "street", "10000");
MemberChap9Collection member = new MemberChap9Collection();
member.setUsername("member1");
member.setHomeAddress(address);
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new AddressChap9("old1", "street", "10000"));
member.getAddressHistory().add(new AddressChap9("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("=== START ===");
// 값 타입 Collection은 지연 로딩
// 임베디드는 지연로딩할 필요가 없음 - 실제 db에서는 동일 table의 값이 뭉태기로 있는 것이기 때문
MemberChap9Collection findMember = em.find(MemberChap9Collection.class, member.getId());
List<AddressChap9> addressHistory = findMember.getAddressHistory();
for (AddressChap9 a : addressHistory) System.out.println("address = " + a.getCity());
em.flush();
em.clear();
System.out.println("=== 값 타입 컬렉션 수정 ===");
MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());
/* 잘못된 방식. 참조 공유기 때문에 setter 사용하면 안됨 */
// findMember2.getHomeAddress().setCity("newCity")
/* 올바른 방식*/
AddressChap9 homeAddress = findMember2.getHomeAddress();
findMember2.setHomeAddress(new AddressChap9("newCity", homeAddress.getStreet(), homeAddress.getZipcode()));
// 치킨 -> 한식
findMember2.getFavoriteFoods().remove("치킨");
findMember2.getFavoriteFoods().add("한식");
// 주소변경 old1 -> changeOld1
// 문제점 : Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert
// 결론 : 쓰지 말고 아예 다른 방식으로 수행 할 것
findMember2.getAddressHistory().remove(new AddressChap9("old1", "street", "10000"));
findMember2.getAddressHistory().add(new AddressChap9("changeOld1", "street", "10000"));
// 다른 방식 : 엔티티로 값을 승계 할 것
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- Member
package hellojpa.chap9;
@Entity
public class MemberChap9Collection {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded // 기간 Period
private PeriodChap9 workPeriod;
@Embedded // 주소 Address
private AddressChap9 homeAddress;
/* 값타입 Collection
* db상에서는 collection을 담는 기능이 없기 때문에 collection을 table로 분리해서 저장
* @ElementColletion : 값 타입 collection 명시
* @CollectionTable
* name : collection table의 이름
* joinColumn : 설계상 "collection : member = n : 1" 이므로 @JoinColumn 존재
* 예외적으로 하나의 컬럼만 존재할 경우 @Column으로 컬럼명 변경 허용
*
*/
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection // fetch = lazy default로 설정되어 있다.
@CollectionTable(name = "ADDRESS_HISTORY", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<AddressChap9> addressHistory = new ArrayList<>();
}
6.3. 값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두
다시 저장한다. - @OrderColumn 같은 걸로 해결 할 수 있는데 너무 복잡하다. - 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력X, 중복 저장X
6.3. 수정 문제점 해결 - 엔티티 승계
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
- EX) AddressEntity
- AddressEntity
package hellojpa.chap9;
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
@Embedded
private AddressChap9 address;
public AddressEntity() {}
public AddressEntity(String city, String street, String zipcode) {
this.address = new AddressChap9(city, street, zipcode);
}
}
- MemberChap9Collection
package hellojpa.chap9;
@Entity
public class MemberChap9Collection {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded
private PeriodChap9 workPeriod;
@Embedded
private AddressChap9 homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
/*
@ElementCollection // fetch = lazy default로 설정되어 있다.
@CollectionTable(name = "ADDRESS_HISTORY", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<AddressChap9> addressHistory = new ArrayList<>();
*/
// 엔티티로 승계 - pk를 가지게 된다.
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
- JpaChap9Collection
package hellojpa.chap9;
public class JpaChap9Collection {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
tx.begin(); // tx 시작
try {
AddressChap9 address = new AddressChap9("city", "street", "10000");
MemberChap9Collection member = new MemberChap9Collection();
member.setUsername("member1");
member.setHomeAddress(address);
/* 값타입 컬렉션
member.getAddressHistory().add(new AddressChap9("old1", "street", "10000"));
member.getAddressHistory().add(new AddressChap9("old2", "street", "10000"));
*/
/* 값타입 컬렉션 엔티티 승계 */
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
/* 값 타입 컬렉션 수정 문제점
System.out.println("=== 값 타입 컬렉션 수정 ===");
MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());
// 2. 주소변경 old1 -> changeOld1
// 문제점 : Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert
// 결론 : 쓰지 말고 아예 다른 방식으로 수행 할 것
findMember2.getAddressHistory().remove(new AddressChap9("old1", "street", "10000"));
findMember2.getAddressHistory().add(new AddressChap9("changeOld1", "street", "10000"));
*/
// 다른 방식 : 엔티티로 값을 승계 할 것
System.out.println("=== 값 타입 컬렉션 엔티티 승계 수정 ===");
MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());
findMember2.getAddressHistory().remove(0); // 현재 pk까지 들어가서 new()로 찾아서 제거할 수 없다. 다른 방식을 찾아야 한다.
findMember2.getAddressHistory().add(new AddressEntity("changeOld1", "street", "10000"));
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
7. 정리
- 엔티티 타입의 특징
- 식별자O
- 생명 주기 관리
- 공유
- 값 타입의 특징
- 식별자X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전