[JPA] 값 타입

Bam·2025년 5월 23일
0

Spring

목록 보기
65/73
post-thumbnail

JPA의 데이터 타입은 크게 엔티티 타입, 값 타입으로 나누어집니다.

이 중 엔티티 타입은 그동안 우리가 계속 다뤘던 @Entity가 붙는 객체입니다. 엔티티에 대한 내용은 해당 포스트에서 확인하실 수 있습니다. 여기서는 값 타입에 대한 내용을 다루려고 하기 때문에 엔티티 타입은 따로 이야기하지 않겠습니다.


값 타입

값 타입은 다시 기본값 타입, 임베디드 타입, 컬렉션 값 타입으로 나뉩니다.

이 중에서 기본값 타입은 자바 언어가 제공하는바 언어가 제공하는 기본형 타입, Wrapper 클래스, String을 말하기 때문에 여기서는 따로 설명하지 않겠습니다.

또한 컬렉션 타입도 자바의 컬렉션을 이용하려 하나 이상의 값 타입을 지정할 때 사용하는 것이므로 해당 포스트를 참조하시면 됩니다.

임베디드 타입

임베디드 타입은 기본형 타입 외에 프로그래머가 직접 정의해서 사용하는 타입을 의미합니다.

다음과 같은 학생 객체를 정의했습니다.

@Entity
public class Student {
	
    @Id
    @GenerateValue
    private Long sId;
    
    //소속 학과
    private String department;
    private Integer year;
    
    //주소
    private String city;
    private String street;
}

위 학생 객체는 소속 학과와 주소에 대해 너무 상세한 정보(필드)들을 가지고 있는 상태입니다. 소속 학과와 주소를 각 타입으로 정의하면 객체가 좀 더 깔끔해지고 재사용성도 올라가겠죠?

이때 사용하는 것이 임베디드 타입 정의입니다. 사실 새로운 개념은 아니고 예전에 복합 키 매핑을 이야기하면서 복합 키를 위한 새로운 식별자 클래스를 정의했었죠? 그때 등장한 @Embedded가 바로 임베디드 타입을 복합 키 식별자로 사용했던 것 입니다.

임베디드 타입을 정의하기 위해서는 다음 두 가지 어노테이션을 이용합니다.

  • @Embeddable: 임베디드 타입을 정의하는 클래스에 사용
  • @Embedded: 임베디드 타입을 사용하는 곳에 사용

또한 반드시 기본 생성자를 작성해야 합니다.

그러면 소속 학과와 주소에 대한 임베디드 타입을 정의해보겠습니다.

@Embeddable
public class Major {
	
    private String department;
    private Integer year;
    
    //기본 생성자 필수
}

@Embeddable
public class Address {

	private String city;
    private String street;
    
    //기본 생성자 필수
}

각 임베디드 타입 내에서 메소드, @Column을 정의할 수도 있습니다.

위와 같이 정의한 타입을 Student에 적용시키면 다음과 같습니다.

@Entity
public class Student {
	
    @Id
    @GenerateValue
    private Long sId;
    
    //소속 학과
    @Embedded
    private Major major;
    
    //주소
    @Embedded
    private Address address;
}

임베디드 타입NULL값이 오면 매핑된 컬럼 값들도 NULL이 옵니다.

예를들어 major에 null이 들어오면 department, year 모두 null이 됩니다.

@AttributeOverride

임베디드 타입에 정의한 매핑 정보를 재정의 하기 위해 @AttributeOverride를 사용합니다.

다음과 같이 복수 전공이 추가 되는 경우에 그냥 추가하면 Major 컬럼명이 중복되어 오류가 발생하기 때문에 @AttributeOverride를 사용해서 매핑 정보를 재정의합니다.

@Entity
public class Student {
	
    @Id
    @GenerateValue
    private Long sId;
    
    @Embedded
    private Major mainMajor;
    
    @Embedded
    @AttributeOverrides({
    	@AttributeOverride(
    		name = "department",
        	column = @Column(name = "minor_department")
        ),
        @AttributeOverride(
        	name = "year",
            column = @Column(name = "minor_year")
        )
    })
    private Major minorMajor;
}

값 타입 활용

불변 객체로 설계하라

임베디드 값 타입은 엔티티들이 공유하게 되면 안됩니다.

학생 A가 "컴퓨터공학"으로 입학했습니다. 그 후 학생 B가 입학해서 major.department에 "물리학" 값을 넣었더니 학생 A의 department도 "물리학"으로 변경되면 원하지 않은 동작이 발생하게 됩니다.

자바 기본형은 값을 복사하지만 임베디드 타입과 같은 값 타입은 객체 타입이기 때문에 참조 값을 사용하여 값을 조작하게 됩니다.

MyObject a = new MyObject("a");
MyObject b = a.clone();
b.setDate("b");

//위 코드의 실행 결과는 a.data = "b", b.date = "b"

위와 같은 문제점을 해결하기 위해 값 타입을 설계할 때는 불변 객체 Immutable Object로 설계하는 것이 권장됩니다.

불변 객체를 만드는 방법들에는 다음과 같은 방법들이 있습니다.

  • final 클래스 정의
  • private final 필드
  • setter 메소드 사용 금지 (생성자에서만 필드 초기화)
  • record 사용

record는 정의하기만 해도 불변 객체를 생성할 수 있습니다.
나머지 final 클래스, private final 필드, setter 메소드 금지는 세 방식을 상황에 따라 조합하면서 불변 객체를 정의하게 됩니다. 각 방식을 단독으로 사용하는 경우는 불변 객체임을 보장할 수 없습니다.

record는 Java 14에서 추가된 문법으로 자세한 내용은 이 포스트를 참조해주세요.

또한 값 타입인 String, Wrapper 클래스는 불변 객체입니다.

값 타입 비교

값 타입은 값 자체가 아닌 참조하는 주소를 통해 동등성 비교를 수행하므로 equals()를 사용해서 비교를 수행해야합니다.

따라서 임베디드 타입같은 타입을 정의할 때는 equals(), hashCode()를 함께 재정의하는 것이 좋습니다.

값 타입 컬렉션

컬렉션에 값 타입을 저장하고자 하는 경우 @ElementCollection, @CollectionTable을 사용합니다.

@Entity
public class Student {
	
    @Id
    @GenerateValue
    private Long sId;
    
    //소속 학과
    @Embedded
    private Major major;
    
    //주소
    @ElementCollection
    @CollectionTable(
    	name = "address",
        joinColumns = @JoinColumn(name = "s_id")
    )
    private List<Address> addressList = new ArrayList<>();
}

값 타입 컬렉션에 저장된 값들은 별도의 테이블을 갖습니다. 문제는 값 타입은 식별자를 갖지 않기 때문에 값 타입 변경이 발생하면 원본 값 타입을 찾기가 어려워집니다.

그래서 JPA(구현체)는 값 타입 컬렉션에 변경 사항이 생기면 값 타입 컬렉션이 매핑된 테이블의 모든 데이터를 삭제하고 다시 저장하는 과정을 거치게 됩니다.

이러한 추가 과정이 발생하기 때문에 데이터가 많아질 것으로 예상이 된다면 값 타입 대신 새로운 엔티티를 생성하고 일대다 연관관계를 지정하는 것이 좋습니다.

0개의 댓글