- 엔티티 타입
- @Entity
- 데이터가 변해도 식별자를 이용해 추적하는 것이 가능하다. 즉 컬럼의 데이터가 변경되더라도 @Id로 데이터를 추적하는 것이 가능하다.
- 값 타입
- int, Integer, String 같이 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있기 때문에 변경되더라도 추적이 불가능하다.
값 타입 분류
- 기본값 타입 : 자바 기본 타입 (프리미티브 타입), 래퍼 클래스, String
- 임베디드 타입 (Embeddet Type, 복합 값 타입) : 커스텀 클래스 같은 형태
- ex) Position (x, y), Address (city, street, zipcode)
- 컬렉션 값 타입 : 기본 값타입 , 임베디드 타입을 컬렉션에 넣은 형태
기본값 타입
- 생명주기를 엔티티에 의존한다.
- 회원 엔티티를 삭제하는 순간 가지고 있는 나이, 이름 같은 값 타입도 함께 삭제된다.
- 값 타입은 공유하면 안된다.
- 회원 이름 변경 시 다른 회원의 이름도 변경되면 안된다.
- 자바의 프리미티브 타입은 값을 복사해서 사용하기 때문에 공유되지 않는다.
int a = 5;
int b = a;
a = 10;
// 결과 a : 10 , b : 5 -> 값 자체를 복사해서 사용하기 때문에 공유하지 않는다.
- 래퍼클래스, String 클래스 같은 경우는 참조 값을 이용해 공유하는 것은 가능하지만 변경하는 것이 불가능하다.
임베디드 타입
- 새로운 값 타입을 직접 정의하는 것이 가능하다.
- 주로 기본 값 타입을 모아서 만든다.
- 공통되는 속성을 하나의 클래스로 추출하고 값 타입으로 생성해 사용한다.
- 값 타입으로 추출한 클래스에는 @Embeddable을 붙여서 값 타입임을 명시해준다.
- 값 타입을 이용하는 엔티티에서는 @Embedded를 이용해 주입해준다. (@Embeddable을 값 타입 클래스에 붙여놓은 경우 생략이 가능하지만 작성하는 것이 권장된다.)
- 값 타입으로 추출한 클래스는 반드시 기본 생성자를 가져야한다.
// As-Is Member
// 근무 기간을 하나의 값 타입으로 추출
private LocalDateTime startDate;
private LocalDateTime endDate;
// 주소 정보를 하나의 값 타입으로 추출
private String city;
private String street;
private String zipcode;
// To-Be Member
@Embedded
private Period period;
@Embedded
private Address address;
@Embeddable
public class Address {
// 주소 정보
private String city;
private String street;
private String zipcode;
}
@Embeddable
public class Period {
// 근무 기간
private LocalDateTime startDate;
private LocalDateTime endDate;
}
- 모든 값 타입은 소유한 엔티티에 생명주기를 의존한다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 동일하다.
- 응집도가 높은 컬럼을 가지고 해당 컬럼들을 이용해 의미있는 메서드를 만듬으로 써 재사용하는 것이 가능하다.
- 값 타입은 내부에 다른 엔티티 값을 가질 수 있다.
@Embeddable
public class Period {
// 근무 기간
private LocalDateTime startDate;
private LocalDateTime endDate;
private Member member; // 엔티티 값을 가질 수 있다.
}
- 한 엔티티 내에서 중복되는 값 타입을 가질 수 있는 경우가 있다. 이 때 컬럼 명이 중복될 수 있다.
- @AttributeOverrides, @AttributeOveride를 이용한다.
// 주소 정보 값 타입
@Embedded
private Address address;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),
@AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
@AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE")),
})
private Address homeAddress;
- 임베디드 타입 값이 null이면 매핑한 컬럼 값은 모두 null이된다.
값 타입과 불변 객체
- 값 타입을 여러 엔티티에서 공유할 수 있지만, 다른 엔티티들이 하나의 값 타입 참조를 가지는 경우 하나의 값이 변경되면 모두 변경되게 된다.
Address address = new Address("seoul", "road", "12345");
// 동일한 주소를 갖는 경우
Member member = new Member();
member.setUsername("zayson");
member.setAddress(address);
em.persist(member);
Member member1 = new Member();
member1.setUsername("zayson2");
member1.setAddress(address); // Address에 대해 동일한 참조를 가지고 있다.
em.persist(member1);
member1.getAddress().setCity("Bundang"); // update 쿼리가 2번 날아간다.
Member findMember = em.find(Member.class, member1.getId());
findMember.getAddress().setCity("Busan"); // 이 경우도 영속성 컨텍스트가 현재 관리하고 있기 때문에 동일하게 두개의 값이 변경된다
- 값 타입의 실제 인스턴스 값을 공유하는 것 보다 인스턴스 값을 복사해서 사용하는 것이 해결책이 될 수 있다.
Address address = new Address("seoul", "road", "12345");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
- 직접 정의한 값 타입은 기본 타입이 아니라 객체 타입이기 때문에 참조 값을 직접 대입하는 것을 막을 수 없다.
member1.setAddress(member.getAddress());
Address copyAddress = address;
- 값 타입은 생성 시점 이후에 값을 변경할 수 없도록 불변 객체로 설계해야한다.
- 생성자로만 값을 생성하고, Setter를 만들지 않는다.
- 값 타입을 변경하고 싶은 경우 새로 생성해서 값을 교체한다.
// 값 타입을 변경하고 싶은 경우
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member.setAddress(newAddress);
- 프리미티브 타입의 경우 값을 비교하는 것이 가능하지만 임베디드 된 값 타입의 경우 데이터는 동일하다고 해도 참조가 다르기 때문에 다른 값이다.
- 모든 값 타입은 값의 동등성 비교가 가능해야 하기 때문에 .equals()와 hashCode()를 오버라이딩 해 동등성 비교를 구현할 수 있다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode)
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
💡 동일성 비교 : 인스턴스의 참조 값을 비교하는 것으로 ==을 사용한다.
💡 동등성 비교 : 인스턴스의 값 자체를 비교하는 것으로 .equals()를 사용한다.
값 타입 컬렉션
- 값 타입 컬렉션의 경우 별도의 테이블로 관리해야한다. (1:N 테이블로 표현된다.)
- Set, List와 같이 기본적인 자료구조를 이용할 수 있다.
- 값 타입 컬렉션 매핑
- @ElementCollection, @CollectionTable을 이용해 매핑한다.
@ElementCollection // 값 타입 컬렉션을 나타낸다.
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "FODD_NAME")) // 값 타입 컬렉션의 테이블 명 지정
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS")
private List<Address> addressHistory = new ArrayList<>();
- 값 타입 컬렉션 데이터 세팅
- persist(member)를 한 번만 호출해도 관련된 값 타입 컬렉션 값들이 테이블에 각각 삽입된다.
- Favorite_Food insert 쿼리 3번 호출, AddressHistory 쿼리 2번 호출
- 값 타입 컬렉션의 경우 엔티티의 라이프 사이클이 맞춰진다. 값 타입, 값 타입 컬렉션은 자신의 라이프 사이클을 갖고 있지 않고 엔티티에 의존된다.
- Cascade + 고아 객체 제거 기능을 필수로 가진다.
member.getFavoriteFoods().add("chicken");
member.getFavoriteFoods().add("pizza");
member.getFavoriteFoods().add("rice");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
- 값 타입 컬렉션 조회
- 엔티티 조회 시에 관련된 값 타입 컬렉션은 함께 조회 쿼리가 날아가지 않는다. 즉, 값 타입 컬렉션은 지연 로딩이다.
- 값 타입 컬렉션을 지정하는 @ElementCollection에 fetchType의 default가 LAZY로 되어있다.
- 1:1로 매핑되어 있는 (엔티티, 테이블에 포함되어 있는) 값 타입은 조회 쿼리가 함께 나간다.
for (Address address : findMember.getAddressHistory()) {
System.out.println("address = " + address.getCity()); // 값 타입 컬렉션은 지연로딩
}
for (String favoriteFood : findMember.getFavoriteFoods()) {
System.out.println("favoriteFood = " + favoriteFood);
}
- 값 타입의 경우 immutable해야 하므로 값 자체를 새로 생성해서 갈아 끼워준다.
// 좋지 않음
findMember.getHomeAddress().setCity("new");
// 새로운 인스턴스로 갈아치워야한다.
Address oldAddress = findMember.getHomeAddress(); findMember.setHomeAddress(new Address("new", oldAddress.getStreet(), oldAddress.getZipcode()));
- 값 타입 컬렉션도 변경할 값 지운 후 새롭게 삽입 (Delete And Insert)
// 값 타입 컬렉션 수정
findMember.getFavoriteFoods().remove("chicken");
findMember.getFavoriteFoods().add("korean");
- 값 타입 컬렉션을 수정하는 경우 (커스텀 클래스) 엔티티와 관련된 값 타입 컬렉션이 모두 삭제되고 새로 수정할 값과 기존에 남아있는 값이 다시 삽입된다. (Delete All → 기존 삭제되지 않은 데이터, 변경할 데이터 새로 Insert)
- 값 타입의 경우 자신의 식별자가 따로 존재하지 않고 연관 엔티티가 라이프 사이클을 관리하기 때문에 중간에 값이 변경되더라도 해당 값 타입의 변경을 추적하는 것이 어렵다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어 하나의 기본키로 구성해야한다. (Not Null, 중복 저장 X)
- 실무에서는 값 타입 컬렉션 대신 1:N 관계를 고려한다.
// As is
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
// To Be (값 타입 컬렉션을 엔티티로 승격 후 OneToMany 매핑)
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
}
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // 값 타입은 (Cascade + orphanRemoval을 가진 형식)
private List<AddressEntity> addressHistory = new ArrayList<>();
- 값 타입 컬렉션이 엔티티로 변경되면서 식별자가 생겼고, 이로 인해 값이 변경되더라도 값의 변경을 추적하는 것이 가능하다. (엔티티 승격)
값 타입 VS 엔티티
엔티티 | 값 타입 |
식별자 O | 식별자 X |
자체적인 라이프 사이클 | 라이프 사이클을 엔티티에 의존 |
값을 공유 | 값을 공유하지 않는 것이 안전 |
- 식별자를 통해 값의 변경을 추적해야 한다면 값 타입을 사용하지 않고 엔티티로 승격해서 사용한다.
- 엔티티와 값 타입을 혼동해 엔티티 → 값 타입으로 만들면 안된다.
📄 References
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
반응형
'Backend > JPA' 카테고리의 다른 글
페치 조인 (Fetch Join) (0) | 2022.08.03 |
---|---|
JPQL 기본 (0) | 2022.07.29 |
영속성 전이 (Cascade), 고아 객체 (0) | 2022.07.21 |
즉시 로딩과 지연 로딩 (0) | 2022.07.21 |
프록시 (0) | 2022.07.20 |