Chap9
이 글은 김영한님의 jpa책을 보고 공부한 흔적입니다.
JPA-Cha9 값 타입
JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.
값 타입은 다음 3가지로 나눌 수 있다.
기본값 타입
자바 기본 타입(int, double)
래퍼 클래스 (Integer)
String
임베디드 타입(embedded type)
컬렉션 값 타입(collection value type)
기본값 타입
@Entity
@Data
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
Member에서의 String, int는 값 타입이다.
임베디드 타입(복합 값 타입)
임베디드 타입이란?
JPA에서는 새로운 값 타입을 직접 정의해서 사용하는 것.
@Embaddable과 @Embedded가 있다. 둘다 기본 생성자가 필수이다.
@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Temporal(TemporalType.DATE) java.util.Date startDate;
@Temporal(TemporalType.DATE) java.util.Date endDate;
private String city;
private String street;
private String zipcode;
}
@Entity
public class Member {
@Id @GenratedValue
private Long id;
private String name;
@Embedded Period workPerod;
@Embedded Address homeAddress;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE) java.util.Date startDate;
@Temporal(TemporalType.DATE) java.util.Date endDate;
}
@Embeddable
public class Address {
@Column(name="city")
private String city;
private String street;
private String zipcode;
}
임베디드 타입은 엔티티의 값일 뿐이다. 값이 속한 엔티티의 테이블에 매핑한다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블이 같다.
즉, 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하다.
결론적으로 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
@Entity
public class Member {
@Embedded Address address;
@Embedded PhoneNumber phoneNumber;
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
@Embedded Zipcode zipcode;
}
@Embbedable
public class Zipcode {
String zip;
String plusFour;
}
@Embbedable
public class PhoneNumber {
String areaCode;
String localNumber;
@ManyToOne PhoneServiceProvider provider
}
@Entity
public class PhoneServiceProvider {
@Id String name;
}
@AttributeOverride : 속성 재정의
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded Address companyAddress;
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", column=@Column(name = "COMPANY_CITY")),
@AttributeOverride(name="street", column=@Column(name = "COMPANY_STREET")),
@AttributeOverride(name="zipcode", column=@Column(name = "COMPANY_ZIPCODE"))
})
Address companyAddress;
}
임베디드 타입에 정희한 매핑정보를 재정의할 때 @AttributeOverride를 쓴다.
하지만 너무 많이 사용하면 엔티티코드가 지저분해진다는 단점이 있다.
임베디드 타입과 null
임베디드 타입이 null이면 메ㅐ핑한 컬럼 값은 모두 null이 된다.
member.setAddress(null);
em.persist(member);
회원 테이블의 주소와 관련된 CITY, STREET, ZIPCODE 컬럼 값이 모두 null이 된다.
값 타입과 불변 객체
값 타입의 공유 참조
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
member1.setHomeAddress(new Address("oldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity");
member2.setHomeAddress(address);
member1과 member2의 도시는 모두 NewCity로 들어간다.
이것은 바로 공유 참조에 의해 일어난 것이다.
자바의 기본타입은 값을 대입하는 것만으로도 값이 복사되지만, 임베디트 타입처럼 직접 정의한 값 타입은 객체 타입이다.
객체 타입은 값을 대입하면 항상 참조 값을 전달하기 때문에 address.setCity("NewCity")
처럼 회원1의 address 값을 공유해서 사용 했을때 결과로 둘다 City가 NewCity로 되는것을 볼 수 있다.
어떻게 해결해야할까?
책에서는 두가지 방법을 추천한다.
Clone을 만드는 방법
setter메소드를 모두 제거해서 객체의 값을 수정하지 못하게 만드는 방법, 즉 불변 객체를 만드는 방법이다.
Clone을 만드는 방법
@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddressClone implements Cloneable{
@Column
private String city;
private String street;
private String zipcode;
@Override
public Object clone() throws CloneNotSupportedException {
AddressClone clone = (AddressClone) super.clone();
return clone;
}
public AddressClone(String city) {
this.city = city;
}
}
public static void exClone(EntityManager em) throws CloneNotSupportedException {
MemberEmbeddClone member = new MemberEmbeddClone();
MemberEmbeddClone member1 = new MemberEmbeddClone();
member.setUsername("형준");
member1.setUsername("먕준");
AddressClone a = new AddressClone("old");
AddressClone b = (AddressClone) a.clone();
b.setCity("new");
member.setHomeAddress(a);
member1.setHomeAddress(b);
em.persist(member);
em.persist(member1);
em.flush();
}
불변 객체
불변 객체란? 한번 만들면 절대 변경할 수 없는 객체.
왜 만들까? 객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있기 떄문이다.
어떻게 만들까?
@Embeddable
public class Address {
private String city;
protected Address() {}
public Address(String city){this.city=city}
public String getCity() {
return city;
}
//setter는 만들지 않는다.
}
간단한 방법으로 생성자로만 값을 설정하고 수정자를 만들지 않으면 된다.
값 타입의 비교
동일성(Identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
동등성(Equivalence) 비교 : 인스턴스의 값을 비교, equals()사용
값 타입 컬렉션
@ElementCollection, @CollectionTable을 쓰면 된다.
@Entity
public class Member {
@Id @GenratedValue
private Long id;
@Embedded Address homeAddress;
@ElementCollection
@CollectionTalbe(name = "FAVORITE_FOODS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name ="FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<String>;
@ElementCollection
@CollectionTalbe(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
}
@Embeddable
public class Address {
@Column
private String city;
private String street;
private String zipcode;
}
Member member = new Member();
member.setHomeAddress(new Address("통영", "몽돌해수욕장", "660-123"));
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");
member.getAddressHistory().add(new Address("서울", "강남", "660-123"));
member.getAddressHistory().add(new Address("서울", "강북", "660-123"));
em.persist(member);
Member member = em.find(Member.class, 1L);
Address homeAddress = member.getHomeAddress();
Set<String> favoriteFoods = member.getFavoriteFppds();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood );
}
List<Address> addressHistory = member.getAddressHistory();
addressHistory.get(0);
Member member = em.find(Member.class, 1L);
member.setHomeAddress(new Address("새로운도시","신도시", "123456"));
Set<String> favoritFoods = member.getFavoritFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울","기존주소", "123456"));
addressHistory.setHomeAddress(new Address("새로운도시","신도시", "123456"));
값 타입 컬렉션의 제약사항
엔티티는 식별자가 있으므로 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수있다.
반면에 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이므로 값을 변경해버리면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다.
값이 변경되면 컬렉션이 남아있는 상황이 많은데 이것을 해결하기 위해서는
값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신에 일대다 관계를 고려하기도 한다.
일대다 매핑에 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 적용하여 쓸 수도 있다.
값 타입 컬렉션을 사용할 때에 기본 키는 모두 생성키(PK)로 들어가야 한다. (?)
정리
엔티티 타입과 값 타입의 특징은 다음과 같다.
엔티티 타입의 특징
식별자가 있다.
엔티티 타입은 식별자가 있고 식별자로 구별할 수 있다.
생명 주기가 있다.
생성하고, 영속화하고, 소멸하는 생명 주기가 있다.
em.persist(entity)로 영속화 한다.
em.remove(entity)로 제거한다.
공유할 수 있다.
참조 값을 공유할 수 있따. 이것을 공유 참조라 한다.
예를 들어 회원 엔티티가 있다면 다른 엔티티에서 얼마든지 회원 엔티티를 참조 할 수 있다.
값 타입의 특징
식별자가 없다.
생명 주익를 엔티티에 의존한다.
스스로 생명주기를 가지지 않고 엔티티에 의존한다. 의존하는 엔티티를 제거하면 같이 제거된다.
공유하지 않는 것이 안전하다.
엔티티 타입과는 다르게 공유하지 않는 것이 안전하다. 대신에 값을 복사해서 사용해야 한다.
오직 하나의 주인만이 관리해야 한다.
불변 객체로 만드는 것이 안전하다.
Last updated
Was this helpful?