DB/JPA

[JPA] 값 타입 정리

꾸준함. 2021. 9. 29. 00:06

개요

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) + 고아 객체 제거 기능을 필수로 지닌다고 생각해도 됨

 

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