[JPA 기본편] 9. 값 타입
JPA의 데이터 타입 분류
엔티티 타입
- @Entity로 정의하는 객체
- 데이터(속성)가 변해도 식별자로 지속해서 추적 가능
값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고, 값만 있으므로 변경 시 추적 불가
값 타입 분류
기본값 타입
: 자바 기본 타입(int, double), 래퍼 클래스(Integer, Long), String
- 생명주기를 엔티티에 의존 (회원을 삭제하면 이름, 나이 필드도 함께 삭제)
- 값 타입은 공유하면 안됨 (회원 이름 변경 시, 다른 회원의 이름도 함께 변경되면 안됨)
- int, double 같은 기본 타입은 항상 값을 복사하므로 절대 공유하면 안됨
- Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경은 불가함
/* 기본 타입 */
int a = 10;
int b = a; // a의 값을 단순 복사
b = 20; // b의 값을 수정해도 a는 바뀌지 않음
// a = 10, b = 20 (값을 공유하지 않고 그냥 단순 복사)
/* 객체 타입 */
Integer a = new Integer(10);
Integer b = a; // 주소값이 넘어감
a.setValue = 20; // a의 값을 바꿀 수 있다면 아래와 같은 결과가 나오겠지만 바꾸는 방법 자체가 없음(a.setValue 성립이 안됨)
// a = 20, b = 20 (래퍼런스 주소로 값을 공유)
임베디드 타입(embedded type, 복합 값 타입)
- 새로운 값 타입을 직접 정의할 수 있음
- int, String과 같은 값 타입으로 JPA는 임베디드 타입이라고 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입의 장점
- 재사용과 높은 응집도
- Period.isWork()처럼 해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있음
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
// === 아래 "임베디드 타입과 연관관계"의 좌측 그림 확인 ===
@Embeddable // 값 타입을 정의
@Getter @Setter
public class Peroid {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Peroid() {
}
}
@Embeddable // 값 타입을 정의
@Getter @Setter
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;
}
}
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded // 값 타입을 사용
private Peroid workPeroid;
@Embedded // 값 타입을 사용
private Address homeAddress;
}
// 실행 코드
Member member = new Member();
member.setUsername("hello");
member.setHomeAddress(new Address("city", "street", "1000")); // (도시명, 도로명, 집코드)
member.setWorkPeroid(new Peroid());
em.persist(member);
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이며 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션 = 매핑한 테이블 수 < 클래스 수
임베디드 타입과 연관관계
@AttributeOverride: 속성 재정의 (값 타입을 여러 개 사용하고 싶은 경우)
위 "임베디드 타입과 연관관계"의 우측 그림과 같이 한 엔티티에서 값은 값 타입을 사용하려면 컬럼 명이 중복되므로 @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의 한다.
@Entity
public class Member {
@Id @GenetratedValue
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name="city", colume=@colume(name="work_city")),
@AttributeOverride(name="zipcode", colume=@colume(name="work_zipcode")),
@AttributeOverride(name="zipcode", colume=@colume(name="work_zipcode"))
})
Address workAddress;
}
값 타입과 불변 객체
값 타입은 복잡한 객체를 조금이라도 단순화하려고 만든 개념이므로 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
하지만 아래의 좌측 그림과 같이 임베디드 타입의 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.(값 타입의 실제 인스턴스인 값을 공유하는 것은 위험)
따라서 우측 그림과 같이 대신 값(인스턴스)를 복사해서 사용해야 한다.
// === 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하는 경우 ===
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member1.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
member1.getHomeAddress().setCity("NewCity") // member1, member2의 도시명이 전부 변경됨
// === 대신 값(인스턴스)를 복사해서 사용하는 경우 ===
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
// 위 Address를 복사해서 사용
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipCode());
Member member2 = new Member();
member1.setUsername("member2");
member2.setHomeAddress(copyAddress); // 복사한 copyAddress를 사용
em.persist(member2);
member1.getHomeAddress().setCity("NewCity") // member1의 도시명만 변경되고, member2의 도시명은 유지됨
객체 타입의 한계
Primitive 타입(기본 타입)은 값을 복사하므로 문제가 없지만, 객체 타입은 참조 값을 직접 대입한다.(하나의 인스턴스를 공유) 따라서 객체의 공유 참조는 피할 수 없다.(한 객체를 수정하면, 다른 객체도 함께 수정될 수 있음)
→ 객체 타입을 수정할 수 없게 만들어서 부작용을 사전에 차단하면 된다. 무조건!!
즉, 값 타입을 불변 객체(immutable object: 생성 시점 이후에 절대 값을 변경할 수 없는 객체)로 설계하여 생성자로만 값을 설정하고, 수정자(Setter)를 만들지 않으면 된다.
만약, 값을 수정하고 싶다면 아래와 같이 새로 만들면 된다.
Address address = new Address("city", "street", "10000");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Address newAddress = new Address("NewCity", address.getStreet(), address.getZipCode()); // 새로 만들어라
member1.setHomeAddress(newAddress); // 새로 만든걸 적용
값 타입의 비교
값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 본다.
자바에서 기본 타입은 == 비교를 한다. 나머지는 참조값을 비교하므로 equals를 사용해야 한다.
- 동일성(identity) 비교 : 인스턴스의 참조 값 비교. == 사용
- 동등성(equivalence) 비교 : 인스턴스의 값 비교. equals() 사용. (equals() 메소드를 재정의(주로 모든 필드 사용)) 그냥 사용하면 equals의 기본값(return (this == obj);)으로 적용됨
컬렉션 타입(collection value type)
자바 컬렉션 안에 기본값 타입이나 임베디드 타입을 넣는 것으로 값 타입을 하나 이상 저장할 때 사용한다. → @ElementCollection, @CollectionTable 사용
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@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")
) // DB는 컬렉션을 같은 테이블에 저장할 수 없음 → 컬렉션을 저장하기 위한 별도의 테이블이 필요함!
private List<Address> addressHistory = new ArrayList<>();
}
값 타입의 수정하는 경우, 값 타입은 불변해야 안전하므로 수정하고 싶다면 새로 생성해야 함.(업데이트를 절대로 하면 안됨)
Member findMember = em.find(Member.class, member.getId());
// homeCity → newCity
// findMember.getHomeAddress().setCity("newCity"); // 값 수정이 안됨
Address a = findMember.getHomeAddress(); // 새로 만들어야 함
findMember.setHomeAddress(new Address ("새 도시명", a.getStreet(), a.getZipcode()));
// String타입의 favoriteFoods를 치킨 → 라면으로 변경하려면, 삭제 후 새로 생성
findMember.getFavoiteFood().remove("치킨"); // 삭제
findMember.getFavoiteFood().add("라면"); // 추가
// Address타입 addressHistory를 변경
findMember.getHomeAddress().remove(new Address ("이전 도시명", "도로명", "집코드"); // 삭제
findMember.getHomeAddress().add(new Address ("새 도시명", "도로명", "집코드"); // 추가
// 쿼리를 확인 시, Address 테이블 전체 삭제 후 데이터 다시 저장함
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없으므로 값을 변경하면 추적이 어렵다.
- 값 타입의 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 그래서 null과 중복이 안된다.
값 타입 컬렉션의 대안
실무에서는 일대다 관계를 사용해라! → 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
(영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용)
정리
- 엔티티 타입 → 식별자o, 생명 주기 관리, 공유
- 값 타입 → 식별자x, 생명 주기를 엔티티에 의존, 공유하지 않는게 안전(복사해서 사용), 불변 객체로 만드는 것이 안전
값 타입은 정말 값 타입이라 판단될 때만 사용해야 하며, 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면? 값 타입이 아닌 엔티티를 사용해야 한다.
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 9장을 듣고 정리한 내용입니다.