Reference. 자바 ORM 표준 JPA 프로그래밍
책 목차 및 이전 글
더보기
들어가기 전 JPA 특징, Q&A
1. JPA 소개
3. 영속성 관리
4. 엔티티 매핑
4.1 - 4.3 @Entity, @Table, 다양한 매핑
4.4 - 4.5 데이터베이스 스키마 자동 생성, DDL 생성 기능
5. 연관관계 매핑 기초
6. 다양한 연관관계 매핑
7. 고급매핑
8. 프록시와 연관관계 관리
9. 값 타입
9.3 값 타입과 불변 객체
9.3.1 값 타입 공유 참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에 공유하면 위험
member1.setHomeAddress(new Address("OldCity")); Address address = member1.getHomeAddress(); address.setCity("NewCity"); //회원 1의 address 값을 공유해서 사용 member2.setHomeAddress(address);
- 회원 2의 주소만 "NewCity"로 변경되길 기대했지만 회원 1의 주소도 같이 변경
- 같은 address 인스턴스를 참조하기 때문에 영속성 컨텍스트는 속성이 변경된 것으로 판단
- 이런 부작용을 막으려면 값을 복사해서 사용
9.3.2 값 타입 복사
- 실제 인스턴스인 값을 공유하는 것은 위험, 대신에 값을 복사해서 사용
- 자바에서 객체에 값을 대입하면 항상 참조 값을 전달, 복사값 전달이 필요
//참조값을 전달 Address a = new Address("Old"); Address b = a; b.setCity("New"); //복사값을 전달 Address a = new Address("Old"); Address b = a.clone(); //복사해서 값을 넘김 b.setCity("New");
- 객체의 공유 참조는 피할 수 없고, 이를 해결하기 위해선 setter 같은 수정자 메소드를 모두 제거
9.3.3 불변 객체
- 객체를 불변하게 만들면 수정할 수 없으므로 부작용을 원천 차단
- 값 타입은 될 수 있으면 불변 객체로 설계
- 불변 객체를 구현하는 가장 간단한 방법은 생성자로만 값을 설정, 수정자는 X
- 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 방지
불변 객체 생성 및 사용
//불변 객체 생성
@Embeddable
public class Address {
private String city;
protected Address() {} //JPA에선 기본 생성자 필수
//생성자로 초기 값 설정
public Address(String city) {
this.city = city;
}
//setter 없이 getter만 설정
public String getCity() {
return city;
}
}
//불변 객체 사용
Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
9.4 값 타입의 비교
- 자바에서 제공하는 객체 비교는 2가지
- 동일성(Identity) 비교: 인스턴스의 참조 값을 비교, == 사용
- 동등성(Equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
- 객체를 비교하는 경우 a == b로 동일성을 비교하면 다른 인스턴스 이므로 결과는 거짓
Address a = new Address("서울시", "종로구", "1번지"); Address b = new Address("서울시", "종로구", "1번지"); a == b // false
- 객체 값 비교는
a.equals(b)
같이 동등성 비교가 필요, Addressequals()
재정의 필요equals()
메소드를 재정의할 때는 보통 모든 필드 값을 비교하도록 구현
equlas()
를 재정의하면hashCode()
도 재정의 하는 것이 안전 그렇지 않으면 컬렉션(HashSet
,HashMap
)이 정상 동작하지 않는 문제 발생
- 객체 값 비교는
9.5 값 타입 컬렉션
- 값 타입을 하나 이상 저장하려면 컬렉션에 보관 후
@ElementCollection
,@CollectionTable
사용@Entity public class Member { ... @ElementCollection @CollectionTable(name = "FAVORITE_FOODS", joinColumns = @JoinColumn(name = "MEMBER_ID")) @Column(name="FOOD_NAME") private Set<String> favoriteFoods = new HashSet<String>(); @ElementCollection @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID")) private List<Address> addressHistory = new ArrayList<Address>(); } @Embeddable public class Address { @Column private String city; ... }
- 관계형 데이터베이스의 테이블은 컬럼안에 컬렉션을 포함할 수 없음 별도의 테이블을 추가하고 @CollectionTable을 사용해서 추가 테이블을 매핑
- @CollectionTable을 생략하면 기본 값을 사용해서 매핑
(기본 값: {엔티티명}_{컬렉션 속성명},
addressHistory
는Member_addressHistory
으로 매핑)
9.5.1 값 타입 컬렉션 사용
값 타입 컬렉션 등록
Member member = new Member();
//임베디드 값 타입
member.setHomeAddress(new Address("통영","몽돌해수욕장","660111");
//기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");
//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울","강남","123123");
member.getAddressHistory().add(new Address("서울","강북","000000");
em.persist(member);
실행된 SQL
INSERT INTO MEMBER(ID, CITY, STREET, ZIPCODE) VALUES (1, '통영', '몽돌해수욕장', '660111');
INSERT INTO FAVORITE_FOODS(MEMBER_ID, FOOD_NAME) VALUES (1, '짬뽕');
INSERT INTO FAVORITE_FOODS(MEMBER_ID, FOOD_NAME) VALUES (1, '짜장');
INSERT INTO FAVORITE_FOODS(MEMBER_ID, FOOD_NAME) VALUES (1, '탕수육');
INSERT INTO ADDRESS(MEMBER_ID, CITY, STREET, ZIPCODE)
VALUES (1, '서울', '강남', '123123');
INSERT INTO ADDRESS(MEMBER_ID, CITY, STREET, ZIPCODE)
VALUES (1, '서울', '강북', '000000');
- 값 타입 컬렉션도 조회할 때 Fetch 전략 선택 가능, 기본은 LAZY
- @ElementCollection(fetch = FetchType.LAZY)
값 타입 컬렉션 수정
Member member = em.find(Member.class, 1L);
//임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시1", "123456"));
//기본 값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
//임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울", "강남", "123123");
addressHistory.add(new Address("서울", "강동", "123456");
- 임베디드 값 타입 컬렉션 수정할 경우 삭제 후 새로운 주소 등록
Address 값 타입은
equals
,hashcode
재정의가 필요 (remove에 필수)
9.5.2 값 타입 컬렉션의 제약사항
- 값 타입은 식별자라는 개념이 없어 값을 변경하면 저장된 원본 데이터 찾기가 어려움
- JPA 구현체들은 값 타입 컬렉션에 변경이 발생하면 연관된 데이터를 삭제 후 다시 저장
DELETE FROM ADDRESS WHERE MEMBER_ID = 100 INSERT INTO ADDRESS(MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100, ...) INSERT INTO ADDRESS(MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (100, ...)
- 값 타입 컬렉션이 매핑된 테이블에 데이터가 많으면 값 타입 컬렉션 대신 일대다 관계를 고려
- 값 타입 컬렉션은 모든 컬럼을 묶어서 기본 키로 구성이 필수
기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고 중복 저장도 할 수 없는 제약 발생
값 타입 컬렉션 대신 일대다 관계 사용
@Entity public class AddressEntity { @Id @GeneratedValue private Long id; @Embedded Address address; ... } //Member @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "MEMBER_ID") private List<AddressEntity> addressHistory = new ArrayList<AddressEntity>();
<정리>
엔티티 타입의 특징
- 식별자(@Id) 존재
- 생명 주기가 존재
- 생성하고, 영속화하고, 소멸하는 생성 주기가 존재
- 공유가 가능
- 참조 값을 공유 (공유 참조)
값 타입의 특징
- 식별자가 없음
- 생명 주기를 엔티티에 의존
- 의존하는 엔티티를 제거하면 같이 제거
- 공유하지 않는 것이 안전
- 공유하지 않는 것이 안전하며, 값을 복사해서 사용
- 오직 하나의 주인만이 관리
- 불변 객체로 만드는 것이 안전