개요
JPA의 값 타입은 크게 기본값 타입, 임베디드 타입이 있고 이들로 이루어진 컬렉션을 값 타입 컬렉션이라고 합니다.
이번 게시글에서는 이 타입들에 대해서 알아보겠습니다.
1. 엔티티 타입 vs 값 타입
1.1 엔티티 타입
- @Entity 어노테이션으로 정의된 객체
- 내부 데이터 즉, 속성이 변하더라도 식별자로 인해 지속해서 추적이 가능한 객체
- 사람 엔티티의 나이와 몸무게가 변하더라도 식별자로 인식 가능
1.2 값 타입
- Primitive Type, Reference Type처럼 단순히 값으로 사용하는 자바 기본 타입 혹은 객체
- 식별자가 없고 값만 존재
- 따라서 값이 변하면 추적 불가
- 숫자 1을 2로 변경할 경우 완전히 다른 값으로 대체
2. 값 타입
2.1 기본값 타입
- int와 String 같은 타입
- 생명주기를 엔티티에 전적으로 의존
- 계정을 삭제할 경우 이름, 이메일, 나이와 같은 정보 모두 같이 삭제됨
- 값 타입은 공유하면 side effect 발생하므로 절대 공유하면 안됨
- A 회원의 잔고 변경 시 다른 회원의 잔고도 변경되면 안됨
- int와 double과 같이 NULL 타입을 허용하지 않은 primitive type 같은 경우 항상 값을 복사하므로 공유될 일이 없음
- 하지만, Integer, String과 같이 NULL 타입을 허용하는 reference type 같은 경우 레퍼런스 즉, 참조를 복사하므로 같은 인스턴스를 공유하므로 공유를 할 수 없도록 별도 작업을 해줘야 함
- 변경 자체를 불가능하게 setter를 생성하지 않거나 private으로 선언해서 side effect 방지
2.2 임베디드 타입
- 클래스와 같이 새로운 값 타입을 직접 정의한 타입
- 주로 기본 값 타입을 조합해서 만든 타입을 임베디드 타입이라고 함
- 직원 엔티티가 이름, 근무 시작일, 근무 종료일, 주소에 기입된 도시, 주소에 기입된 번지, 주소에 기입된 우편번호를 가진다고 가정
- 근무 시작일, 근무 종료일은 비슷한 성격을 가지고 도시, 번지, 우편번호 또한 비슷한 성격을 가짐
- 따라서, 각각을 조합해 임베디드 타입을 생성 가능
2.2.1 임베디드 타입을 사용하면서 생기는 장점
- 재사용 가능 및 높은 응집도
- 해당 값만 사용하는 의미 있는 메서드 즉 행위를 만들 수 있으므로 객체지향적으로 설계가 가능
- 객체는 데이터뿐만 아니라 메서드라는 기능 혹은 행위까지 가지고 있으므로 묶는 게 유리함
- 이때, DB 입장에서는 바뀔 게 없으므로 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 동일
- 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능하고 용어 공통화 및 코드 공통화가 가능
- 따라서, 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
- 임베디드 타입을 포함한 모든 값 타입은 해당 엔티티에 생명주기를 의존하므로 관리하기 용이함
2.2.2 임베디드 타입 사용 방법
- @Embedabble 어노테이션을 값 타입을 정의하는 곳에 지정
- @Embedded 값 타입을 사용하는 곳에 지정
- 기본 생성자는 필수로 있어야 함
2.2.3 임베디드 타입 지정 전
@Entity
@Getter
class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
private LocalDateTime workStartAt;
private LocalDateTime workEndAt;
private String city;
private String street;
private String zipCode;
}
2.2.4 임베디드 타입 지정 후
@Entity
@Getter
class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@Embedded
private WorkTime workTime;
@Embedded
private Address address;
}
@Embeddable
@Getter
class WorkTime {
private LocalDateTime workStartAt;
private LocalDateTime workEndAt;
}
@Embeddable
@Getter
class Address {
private String city;
private String street;
private String zipCode;
}
2.2.5 하나의 엔티티 내 동일한 임베디드 타입을 여러 개 사용해야 할 경우?
- 위 Address 임베디드 타입 같은 경우 하나의 엔티티 내 여러 개 사용 가능
- 집 주소와 근무지 주소를 속성으로 가질 경우
- 이럴 경우 칼럼명이 중복되어 에러가 발생
- 그렇다면 어떻게 해결할 수 있을까?
- @AttributeOverride 어노테이션을 통해 칼럼명을 변경해주면 됨
- 여러 개의 칼럼명을 재정의하기 위해서는 @AttributeOverrides 사용
- 하나의 칼럼명만 재정의하는 경우 @AttributeOverride 사용
3. 값 타입과 불변 객체
- 앞서 언급했듯이 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 side effect가 발생할 수 있으므로 위험함
- 따라서, 인스턴스의 값을 복사해서 사용하는 것이 안전
- 하지만, 객체 타입의 경우 primitive type과 다르게 무조건 복사가 아닌 reference 즉, 참조값을 복사
- 정리하자면 객체 타입은 참조 값을 직접 대입하는 것을 막을 수 없으므로 컴파일 단계에서 방지 불가능하므로 객체의 공유 참조를 피할 수 없음
// primitive type
int a = 1;
int b = a;
b = 2;
결과 값: a = 1, b = 2
// 객체 타입
Address address = new Address("Old Address");
Address newAddress = a; // 객체 타입은 참조를 복사
newAddress.setCity("New Address");
결과 값: address.getCity(): "New Address", newAddress.getCity(): "New Address"
따라서, 객체 타입의 경우 불변 객체로 선언하여 수정할 수 없게 만들어 side effect를 원천 차단해야 함
- 정리를 하자면 객체 값 타입은 immutable object로 설계해야 함
- immutable object란 생성 시점 이후 절대 값을 변경할 수 없는 객체
- String과 같은 객체 타입
- String의 경우 값이 변경될 경우 주소 값도 변경되므로 불변 객체
- 불변 객체의 경우 생성자로만 값을 설정하고 getter만 제공해야 함
- setter는 선언하지 않거나 private으로 지정해야 함
- 정리를 하자면, 객체 타입의 경우 불변 객체로 선언해야 하고 이 말은 즉슨 값을 변경하기 위해서는 새로운 객체를 생성해야 함
3.1 객체 값 타입을 비교하는 방법
- 값 타입을 비교할 때는 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함
- 여기서 우리는 동일성(identity)과 동등성(equivalence)의 차이를 알아야 함
- 동일성은 인스턴스의 참조 값을 비교하므로 == 기호를 통해 비교
- 동등성은 인스턴스의 값을 비교하므로 equals() 메서드를 통해 값을 비교
- 따라서, 객체 값 타입의 경우 equals() 메서드를 통해 값을 비교해야 함
- 즉, 객체를 선언할 때 equals() 메서드를 적합하게 재정의하고 equals() 메서드를 사용하기 위해서는 hash() 메서드 또한 구현해줘야 함
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
private String city;
private String street;
private String zipCode;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
// JPA에서는 proxy 때문에 getter로 해야함
return Objects.equals(getCity(), address.city)
&& Objects.equals(getStreet(), address.street)
&& Objects.equals(getZipcode(), address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipCode);
}
}
4. 값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용하는 컬렉션
- @ElementCollectoin, @CollectionTable 어노테이션 사용
- DB 같은 경우 JSON 타입으로 저장하지 않는 이상 컬렉션을 같은 테이블 내 저장할 수 없음
- 따라서, 컬렉션을 저장하기 위한 별도의 테이블이 필요함
- 값 타입 컬렉션을 조회 시 즉시 로딩 전략을 취할 경우 실행 속도가 엄청 느려질 가능성 높음
- 따라서, 지연 로딩 전략을 취함
- 값 타입의 경우 엔티티에 종속적이므로 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 지닌다고 생각해도 됨
- 관련해서는 이전 게시글 https://jaimemin.tistory.com/1920 참고
4.1 값 타입 컬렉션의 제약사항
- 앞서 언급했듯이 값 타입은 식별자 개념이 없기 때문에 변경 시 추적이 어려움
- 따라서, 값 타입 컬렉션에 변경 사항이 발생할 경우 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하는 과정을 거쳐야 함
- 실행 속도에 큰 영향을 미침
- 또한, 값 타입 컬렉션을 매핑하는 테이블은 모든 칼럼을 묶어서 기본키를 구성해야 하는 제약사항이 있음
- NULL X, 중복 저장 X
4.2 값 타입 컬렉션 대안
- 값 타입 컬렉션을 사용하는 대신 OneToMany 관계를 사용하는 것이 실무에서 나을 수도 있음
- 일대다 관계를 위한 엔티티를 별도로 생성하고 여기에서 사용하려던 값 타입을 사용
- 영속성 전이 및 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용하면 됨
값 타입 컬렉션 엔티티 예시
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Address {
private String city;
private String street;
private String zipCode;
@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);
}
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ADDRESS")
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
}
샘플 멤버를 통한 예시
5. 최종 정리
엔티티 타입
- 식별자가 있으므로 값이 변경되더라도 식별할 수 있음
- 엔티티가 생명주기를 직접 관리
- 공유해도 무방함
값 타입
- 식별자가 없으므로 값이 변경되면 추적 불가능
- 생명주기를 엔티티에 의존
- 공유할 경우 side effect 발생할 수 있으므로 복사해서 사용하는 것이 안전
- 객체 타입의 경우 참조값을 복사하므로 이런 케이스는 복사하는 것이 적합하지 않음
- 따라서, 값을 오직 생성자에서 설정을 해서 불변 객체(immutable object)로 선언하는 것이 중요
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
반응형
'DB > JPA' 카테고리의 다른 글
[JPA] JPQL 추가 정리 (0) | 2021.10.18 |
---|---|
[JPA] JPQL 간단 정리 (0) | 2021.10.16 |
[JPA] 프록시와 연관관계 관리 정리 (0) | 2021.09.14 |
[JPA] @MappedSuperclass (0) | 2021.09.07 |
[JPA] 상속관계 매핑 (2) | 2021.09.07 |