[자바 ORM 표쥰 JPA 프로그래밍] 18일차 - Value Type
지금까지는 JAVA에서 제공해주었던 원천 자료형들을 사용하여 엔티티를 구성하였는데
객체 지향 언어인 자바에서는 다양한 객체로 엔티티들을 제공되어야한다.
JPA에서는 각 엔티티들을 정의해주는 데이터들의 타입을 크게 엔티티 타입과 값 타입으로 나눌 수 있다.
1. Entity Type
엔티티 타입은 @Entity 로 정의 했던 객체들을 말한다.
이는 embedded type (복합 값 타입) 이라고도 불리며 객체로 사용한다.
JAVA에서 클래스를 작성하는 것과 동일하게 각종 원시자료원시나 클래스클을 조합하여 좀 더 객체지향스럽게 엔티티를 구성한다.
@Data
@Entity
public class Member {
@Id
@GeneratedValue
private long id;
private String name;
@Embedded // 임베디드 타입을 사용하려할때는 @Embedded 나 해당 객체에 @Embeddable 을 하나이상 명시해야한다.
private Period period; // 엔티티 필드를 객체로 분리하여 객체지향에 걸맞게 사용
@Embedded
private Address address;
}
@Embeddable
public class Address {
@Column(name = "CITY") // 임베디드 객체에서도 컬럼을 명시할 수 있다.
private String city;
private String street;
private String zipcode;
}
@Embeddable
public class Period {
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
}
엔티티에서 필드를 객체를 사용하여 좀더 JAVA 스러운 객체지향에 걸맞도록 사용되었다.
사용방법은 @Embedded 를 필드로 명시하거나 클래스에 @Embeddable 을 작성하여 만들어준다.
임베디드 타입의 객체는 그대로 테이블과 매핑이 되며 이를 통해 테이블을 초기화하여도 동일하게 반영된다.
하나에 엔티티에 동일한 임베디드 타입을 사용할 경우 필드 명 중복으로 인하여 에러가 발생할 수 있다.
@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"))
})
private Address companyAddress;
@AttributeOverrides 어노테이션을 사용하여 별도로 명시해주면 된다.
2. 임베디드 타입의 불변
이미 JAVA를 했던 사람들이라면 알고 있겠지만 JAVA에서는 깊은 복사와 얕은 복사라는 특징이 있다.
Member member = new Member();
member.setName("멤버");
System.out.println(member.getName()); // "멤버" 출력
Member copyMember = member;
copyMember.setName("복사멤버");
System.out.println(copyMember.getName()); // "복사멤버" 출력
System.out.println(member.getName()); // "복사멤버" 출력
너무 뻔한거지만 Member 인스턴스를 생성하고 setter를 통해 안의 member.name 필드에 "멤버"라는 값을 넣고
member 인스턴스를 대입연산자를 사용해 copyMember 를 복사(?) 하여 똑같이 setter 를 통해 copyMember.name의 필드에 대입연산자로 "복사멤버" 라고 넣어주었다.
하지만 우리는 알고있다싶히 member.name 필드의 값도 변경되었다는걸 알수있다.
이는 얕은복사의 특징인데 객체를 대입연산자로 사용하여 복사하는 경우 참조주소를 복사하기 때문에 실제 값은 member와 copyMember 모두 공유하고있다.
물론 clone() 를 사용하여 아예 인스턴스를 복제 하여 사용한다면 문제는 없다.
Member copyMember = (Member) member.clone();
하지만 이러면 근본적인 해결책이 되지않는다.
개발자가 실수로 member의 필드 값을 변경해버리면 오류를 잡기가 매우매우 힘들어진다.
그렇기 때문에 아예 변경할수 있는 수단을 차단하는 것이 맞다.
@Embeddable
public class Address {
@Column(name = "CITY") // 임베디드 객체에서도 컬럼을 명시할 수 있다.
private String city;
private String street;
@Embedded
private Zipcode zipcode;
public Address() {
}
public Address(String city, String street, Zipcode zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public Zipcode getZipcode() {
return zipcode;
}
}
// 임베디드 타입들은 무조건 생성자를 통해서만 값을 입력할수 있게 설정한다.
Address address = new Address("시티", "거리", new Zipcode());
// address.setCity("변경"); // 아예 setter를 차단하는 것
member.setAddress(address);
그렇기 때문에 임베디드 타입들은 setter 를 생성하지 않고 생성자를 통해서만 값을 넣을수 있도록 하는것이 좋다.
3. Collection Type
JAVA에서 컬렉션 여러개의 값을 저장하려고 할때 List, Set, Map과 같은 컬렉션 타입을 주로 사용하게 되는데
관계형 데이터베이스의 테이블에서는 컬럼안에 이러한 컬렉션들을 저장할 수 있는 방법이 없다.
그래서 JPA는 컬렉션을 저장하기 위하여 따로 테이블을 추가여 관리해준다.
@Data
@Entity
public class Member{
@Id
@GeneratedValue
private long id;
private String name;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOODS", // 테이블을 추가하여 MEMBER_ID와 매핑
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> addresseHistory = new ArrayList<Address>();
}
@Data
@Embeddable
public class Address {
private String city;
private String street;
}
엔티티내의 필드 타입을 컬렉션으로 하고(위 코드에서는 Set과 List로 함) @ElementCollection 어노테이션을 지정하였다.
그리고 @CollectionTable 어노테이션과 속성 값들을 주었는데
만약 이 어노테이션을 지정하지 않는다면 기본값으로 테이블이 생성된다. {엔티티이름}_{컬렉션 속성 이름}
그리고 코드를 실행하여 테이블 초기화가 된다면 아래와 같은 테이블이 생성된다.
이러한 컬렉션 타입도 사용하는 방법은 JAVA에서 컬렉션 타입에 add하는 방식으로 추가해주면 된다.
Member member = new Member();
member.getFavoriteFoods().add("음식1");
member.getFavoriteFoods().add("음식2");
member.getFavoriteFoods().add("음식3");
member.getAddresseHistory().add(new Address("서울","강남"));
member.getAddresseHistory().add(new Address("서울","강북"));
em.persist(member);
당연하지만 INSERT 나 DELETE 또한 컬렉션을 사용하여 관리해줄 수 있다.
final Member member = em.find(Member.class, 1l);
final List<Address> addresseHistory = member.getAddresseHistory();
for (int i = 0; i < 100; i++) {
addresseHistory.add(new Address("시티"+i, "거리"+i));
}
4. Collection Type 의 주의점
Entity Type에 비해 Value Type의 Collection Type의 경우 식별할 수 있는 방법이 없기 때문에 보관이 어렵다는 문제점을 갖고있다.
이 때문에 JPA는 Collection Type에서 변경사항을 감지한다면 모든 컬렉션의 모든 데이터들을 제거 후 다시 재입력하는 방식으로 작동한다.
// MEMBER_ID 1인 값을 조회
final Member member = em.find(Member.class, 1l);
final List<Address> addresseHistory = member.getAddresseHistory();
// MEMBER_ID 의 컬렉션을 찾아 INSERT
addresseHistory.add(new Address("2", "3"));
기존에 값이 있는 상태로 위와 같이 List 컬렉션에 add 를 한다면
일반적으로는 INSERT가 한건이 나올거라 생각하지만 JPA는 모든 컬렉션값을 DELETE 후 다시 INSERT를 하게된다.
-- SELECT SQL
-- 하지도 않는 DELETE SQL이 실행됨
Hibernate:
/* delete collection jpa.entity.Member.addresseHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row jpa.entity.Member.addresseHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street)
values
(?, ?, ?)
Hibernate:
/* insert collection
row jpa.entity.Member.addresseHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street)
values
(?, ?, ?)
이처럼 아예 새로 DELETE -> INSERT 를 거치기 때문에 컬렉션 데이터가 많으면 많을 수록 성능저하가 발생할 수 밖에 없다.
이러한 문제는 CASCADE(영속성 전이) + ORPHAN REMOVE(고아 객체 제거) 를 사용하여 값 타입 컬렉션 처럼 사용하게 만들 수 있다.
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private long id;
// Address 임베디드 타입을 필드로 정의
@Embedded
private Address address;
}
@Data
@Entity
public class Member{
@Id
@GeneratedValue
private long id;
private String name;
// CASCADE을 ALL, ORPHANREMOVAL를 TRUE 로 정의
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addresseHistory = new ArrayList<AddressEntity>();
}
// MEMBER_ID 1인 값을 조회
final Member member = em.find(Member.class, 1l);
final List<AddressEntity> addresseHistory = member.getAddresseHistory();
// 컬렉션 값을 추가
AddressEntity addressEntity = new AddressEntity();
addressEntity.setAddress(new Address("추가", "추가2"));
addresseHistory.add(addressEntity);