9장 값 타입
JPA에서 데이터 타입은 엔티티(Entity) 타입과 값(value) 타입으로 분류할 수 있다.
엔티티 타입
@Entity
어노테이션으로 정의- 영속성 컨텍스트 내에서 식별자를 통해 지속적으로 추적가능한 객체
- 생명 주기가 있음 - 생성, 영속화, 소멸하는 생명 주기 존재
값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체
- 식별자가 없음
- 생명 주기를 엔티티에 의존
- 값 타입은 3가지로 나눌 수 있다.
- 기본값 타입 (basic value type)
- 자바 기본 타입(int, double, ...)
- 래퍼 클래스(Integer, ...)
- String
- 임베디드 타입 - JPA에서 사용자가 직접 정의한 값 타입
- 컬렉션 값 타입 - 하나 이상의 값 타입 저장시 사용
1. 기본값 타입
@Entity
public class Member { // 엔티티 타입
@Id @GeneratedValue
private Long id;
private String name; // 값 타입
private int age; // 값 타입
- 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.
- 값 타입은 공유하면 안 된다.
- String name(이름)을 A엔티티와 B엔티티가 공유하게 되었을 때, A의 name값이 바뀌면 B의 네임값도 바뀌는 불상사가 일어날 수 있다.
- int, double 같은 primitive type은 절대 공유되지 않는다 = 값에 의한 복사 (call by value)
- wrapper 클래스는 공유되므로 주의할것.
- 공유하지 않는 것이 안전하다
- 값을 복사해서 사용한다
- 오직 하나의 주인만이(엔티티) 관리해야 함
- 불변 객체로 만드는 것이 안전함 (immutable)
2. 임베디드 타입
새로운 값 타입을 직접 정의해서 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
@Embedded Period workPeriod; // 임베디드 타입
}
@Embeddable
public class Period {
@Temporal(TeporalType.DATE) java.util.Date startDate;
...
}
임베디드 타입 어노테이션
- @Embeddable : 값 타입을 정의하는 곳에 표시(클래스 명 위에)
- @Embedded : 값 타입을 사용하는 곳에 표시(엔티티 내의 필드)
✔️ 둘 중 하나는 생략해도 된다.
- 임베디드 타입은 기본 생성자가 필수
- 값 타입에, 값 타입을 위한 이름있는 메서드를 넣을 수 있다.
- 값 타입은 엔티티의 필드로 존재하며 엔티티의 생명주기에 의존하므로 엔티티와 임베디드 타입의 관계를
컴포지션(composition)
관계라고 한다.
하이버네이트는 임베디드 타입을 컴포넌트(compoenents) 라고 한다.
2.1 테이블 매핑
임베디드 타입은 엔티티의 값이기 때문에 속한 엔티티의 테이블에 매핑한다.
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수 보다 클래스의 수가 더 많다
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
2.2 임베디드타입과 연관관계
임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.
- 엔티티는 공유될 수 있으므로 참조한다고 표현
- 값 타임은 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로 포함한다고 표현.
@Entity
public class Member {
@Embedded Address address; // 임베디드 타입 포함
@Embedded PhoneNumber phoneNumber; // 임베디드 타입 포함
}
@Embeddable
public class Address {
String street;
String city;
@Embeddable Zipcode zipcode; // 임베디드 타입 포함
...
}
@Embeddable
public class Zipcode {
String zip;
...
}
@Embeddable
public class PhoneNumber {
@ManyToOne PhoneServiceProvider provider // 엔티티 참조
...
}
@Entity
public class PhoneServiceProvider {
@Id String name;
...
}
https://blog.kakaocdn.net/dn/3P3cb/btrNVhNRBCp/7dTNA74qhGDrPHTrtN6AU0/img.png
값 타입 Address가 값 타입 Zipcode 포함, 값 타입 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조
2.3 속성 재정의
@AttributeOverride : 임베디드 타입에 정의한 매핑 정보를 재정의한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@Embedded Address companyAddress; // 주소 추가
}
테이블에 매핑하는 컬럼명이 중복된다.
- @AttributeOverride 를 이용해서 매핑 정보를 재정의한다.
@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; // 주소 추가
}
-> 실행하면 다음과 같은 생성 SQL이 만들어 진다.
생성 SQL
CREATE table member {
...
city VARCHAR(255),
street VARCHAR(255),
zipcode VARCHAR(255),
company_city VARCHAR(255),
company_street VARCHAR(255),
company_zipcode VARCHAR(255),
...
}
@AttritubeOverride
- 속성을 재정의 한다.
- 엔티티에 설정한다.
- 키나 인덱스 설정등도 추가로 할 수 있다.
2.4 null
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이다.
3. 값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
3.1 공유 참조
임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 안 된다.
공유 참조로 인한 side effect 발생할 수 있으므로 값을 복사해서 사용해야 함
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용 -> 회원1의 주소도 변경
member2.setHomeAddress(address);
값 타입은 이러한 부작용을 막으려면 값(인스턴스, 객체)을 복사해서 사용해야 한다.
3.2 갑 타입 복사
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();
newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);
임베디드 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 기본 타입 - 값을 복사해서 전달 (call by value)
- 객체 타입 - 참조 값을 전달 (call by reference)
Address a = new Address("OldCity")
Address b = a.clone(); // 항상 복사해서 넘겨야 한다.
// Address b = a // 이렇게 참조만 넘기면 공유 참조의 부작용이 발생한다.
b.setCity("NewCity");
객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다.
하지만 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다.
객체의 공유 참조는 피할 수 없다.
-> 그러므로 객체의 수정자(setter)를 제거한다.
- 이렇게 하면 공유 참조를 해도 값을 변경하지 못하므로 부작용의 발생을 막을 수 있다 .
⇒ 객체의 값을 수정하지 못하게 막는다.
ex) setter와 같은 수정자 메소드를 제거한다.
3.3 불변 객체 (Immutable Object)
부작용을 원천 차단하기 위해서 값 타입은 되도록 불변 객체로 설계해야 한다.
불변 객체의 값은 조회 ⭕️, 수정 ❌
불변 객체도 결국 객체이다.
⇒ 인스턴스의 참조 값 공유를 피할 수 없다.
⇒ 하지만 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않는다.
불변 객체 만들기!
ex) 생성자로만 값 설정, 세터 X
만약 값을 수정해야한다면 새로운 객체를 생성해서 사용해야 한다.
4. 값 타입의 비교
int a = 10;
int b = 10;
Address a = new Address("가", "나", "다");
Address b = new Address("가", "나", "다");
- 동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
- 동등성 비교 : 인스턴스의 값을 비교, equals() 사용
값 타입을 비교할 때는 a.equals(b)를 이용해서 동등성 비교를 해야 한다.
- 직접 값 타입 정의할 경우 equals() 메소드 재정의해줘야 함
- equals() 재정의 시 hashCode() 도 재정의 해야함
- 만약 재정의를 하지 않는다면, 컬렉션(HashSet, HashMap)등이 정상 동작하지 않는다.
- 코드를 열어보면 이퀄스 앤 해시코드가 어떤 역할을 하고 있는지 알 수 있다.
5. 값 타입 컬렉션
값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection
, @CollectionTable
어노테이션을 사용한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Address homeAddress;
@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>();
}
- @ElementCollection : 값 타입 컬렉션
- @CollectionTable : 컬렉션 테이블 매핑 (테이블정보를 주는것 ex) name="ADDRESS" @joincolumn)
- 문제는, 단순하게 값타입이 하나일때는 멤버에 필드 속성으로 멤버 테이블안에 넣으면 되지만, 컬렉션은 DB에 들어갈 수가 없다.
- 관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션(리스트)을 포함할 수 없기 때문에 별도의 테이블을 추가해야 한다.
- 멤버입장에서는 컬렉션은 결국 일대다(1:N) 라서 별도의 테이블을 만들어 삽입해야 관리가 관리가 된다
- favoriteFoods 같은경우는 food_name , member_id 묶어서 pk가 이루어지고
address 3가지 zipcode street city member_id묶어서 pk를 만들어야된다
식별자를 넣어서 pk를쓰게되면 이건 entity가 되버린다. - 별도의 테이블을 만들게 되면 @AttributeOverride를 사용해서 재정의할 수 있다.
5.1 값 타입 컬렉션 사용
값 타입 컬렉션 등록
Member member = new Member();
// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));
// 기본 값 타입 컬렉션
member.getFavoriteFoods().add("r");
member.getFavoriteFoods().add("s");
// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("1"));
member.getAddressHistory().add(new Address("2"));
em.persist(member);
- 값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가진다.
- 값을바꾸거나 하면 값타입들은 persist나 update 할필요가없고 값을 바꾸면 알아서 업데이트가된다.
Member member = em.find(Member.class, 1L);
// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));
// 기본 값 타입 컬렉션
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove(r); // String 타입을 수정할 수 없다.
favoriteFoods().add("s");
// 임베디드 값 타입 컬렉션
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("1")); // 값 타입은 불변하므로 삭제 후 새로 등록한다.
addressHistory.add(new Address("2"));
5.2 값 타입 컬렉션의 제약사항
값 타입 컬렉션에 보관된 값들은 별도의 테이블에 보관되므로 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.
이 문제를 해결하기 위해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생
하면
- 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제한다.
- 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.
- delete, insert 쿼리가 자주 발생할 수 있다. -> 큰 단점
- 따라서 값 타입 컬렉션이 매핑된 테이블에
데이터가 많다
면 값 타입 컬렉션 대신 새로운 엔티티를 만들어서일대다 관계
로 설정한다.- 일대다 관계가 된다면 모든 데이터를 삭제하고 다시 삽입하는 많은 쿼리 작업이 사라진다.
- 추가로 영속성 전이(cascade) + 고아 객체 제거(orphanremoval) 기능을 적용한다.
- 그러면 값 타입 컬렉션처럼 사용 가능.
(실무에서는 상황에 따라서 일대다로만들고
cascade orphanremoval 설정해서 값 타입컬렉션 처럼사용)
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키로 구성해야된다
그러므로 null입력은 안되고 ,중복저장도 안된다.
참고
- 값 타입 : http//en.wikipedia.org/wiki/Value_object
- 값 타입과 엔티티에 대한 내용 : aeternum.egloos.com/1380433, 도메인 주도 설계(위키북스,2011)을 읽어보길 권장
'스터디 > JPA 프로그래밍 스터디' 카테고리의 다른 글
JPA 프로그래밍 스터디 13장 정리 (0) | 2022.10.18 |
---|---|
JPA 프로그래밍 스터디 10장 정리 (0) | 2022.10.14 |
JPA 프로그래밍 스터디 8장 정리 (0) | 2022.10.01 |
JPA 프로그래밍 스터디 7장 정리 (1) | 2022.10.01 |
JPA 프로그래밍 스터디 6장 정리 (0) | 2022.10.01 |