값 타입은 말 그대로 단순한 수치 정보이다.
JPA에서 타입을 크게 분류하면 엔티티 타입과 값 타입이 존재한다.
@Entity로 정의하는 객체는 엔티티 타입, 자바의 기본 타입이나 단순히 수치로 사용하는 타입을 값 타입이라 할 수 있다.
실제 우리가 값 타입을 활용할 수 있는 부분이 어디일까?
데이터베이스에서 여러 칼럼들을 모아 하나의 추상적인 객체로 표현할 수 있는 경우, 이를 애플리케이션 단에서 객체지향적으로 사용하고 싶다면 우리는 임베디드 타입을 고려해볼 수 있다.
복합 값타입이라고도하며 관련된 속성을 하나로 묶어 객체로 표현하는 것이다.
이는 또 다른 장점을 가지고 있는데, 클래스가 가지고있는 상태에 대해 책임을 질 수 있는 클래스가 존재하기 때문에
객체지향적으로 사용할 수 있다는 것이다.
@Embedded, @Embeddable 어노테이션을 붙여 사용할 수 있는데 객체를 사용하는 방법은 자유롭다고 해도 분명 주의사항이 존재한다.
값 타입과 불변 객체
값 타입을 여러 엔티티에서 공유할 때 값을 복사해서 사용해야한다. 이는 복합 값 타입에도 적용된다.
문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
자바 기본 타입에 값을 대입하면 값을 복사한다.
하지만 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다. 즉 객체의 공유 참조는 피할 수 없다.
이러한 문제는 객체 타입을 수정할 수 없게 불변 객체로 만들어야 한다.
공개적인 setter를 제거하고 별도의 임베디드 값 타입을 생성하여 동일 타입의 엔티티들에게 넣어주어야 의도치 않은 사이드 이펙트를 방지할 수 있다.
값 타입의 비교
단순하다. equals, hashcode를 재정의하여야 한다.
임베디드 타입이 복합 값 타입이라해도 근본적으로 자바의 객체이다. 따라서 동등성 비교 시 equals와 hashcode를 재정의하여 비교해야지 올바른 값을 도출할 수 있다.
값 타입 컬렉션
이러한 값 타입들을 컬렉션으로 관리해야 한다면 어떻게 해야 할까?
우선 정말 값 타입이 맞는지 생각해보자. 추적할 필요 없고 별도의 식별자가 존재하지 않고 특정 엔티티에 완전히 종속되는지 확인해야 한다. 또한 크기도 고려사항 중 하나이다.
값 타입을 컬렉션으로 사용하기 위해선 데이터베이스의 PK를 값들의 복합키로 정하여 처리한다.
데이터베이스 관련 이론을 공부할 때, 정규화를 공부할 때 여러 값을 하나의 칼럼에 지정하는 부분에 대한 이론이 있는데 이럴 경우 별도의 테이블을 고려하라는 조언이 많다.
최근 데이터베이스는 combination으로 json을 저장하도록 지원해주지만 이는 그리 좋은 방법인지는 잘 모르겠다.
따라서 컬렉션을 저장하기 위해선 별도의 테이블이 필요하다.
매핑은 @ElementCollection으로 한다. 또한 @CollectionTable(name = "name")으로 테이블 명을 지정한다.
@CollectionTable( joinColumns = @JoinColumn(name = "pk"))을 사용하면 pk값을 외래 키로 잡게 된다.
값 타입 컬렉션은 Cascade + 고아 객체 제거 기능을 필수로 가진다.
재밌는 점은 값 타입 컬렉션은 지연 로딩의 대상으로 default 설정이 되어 있다.
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "ar_type", joinColumns = @JoinColumn(name = "id"))
private Set<ArType> arTypeSets = new HashSet();
값 타입 컬렉션은 식별자 개념이 없다.
그 이유는 주인 엔티티의 PK + 값 타입들을 복합적으로 구성한 키를 사용하기 때문이다.
만약 여기서 컬렉션 중 하나의 값을 변경했다면 해당 컬렉션이 저장된 테이블에 JPA는 어떻게 접근해야 할까?
변경되지 전의 값을 캐시 한 후에 해당 값 전부 사용해 해당 테이블의 조건문을 통해 접근하여 update문을 날려줘야 하나?
모든 엔티티에 사용되는 값 타입을 캐시 해야 한다고 생각해보자. 말이 안 된다. 결국 변경되는 시점에는 해당 값 타입의 테이블에 값 타입을 추적할 수 없다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 값 타입의 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 이는 성능상 문제가 발생할 수 있다.
단순 update문 하나를 기대했지만 delete문 한 개, 컬렉션에 저장된 모든 값 타입을 insert, 추가된 값 insert 이는 사소한 작업이 커질 수 있는 여지를 가지고 있는 것이다.
실무에서는 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
그리고 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용한다.
저도 이번에 실무에서 파일 버전에 대한 값 타입 컬렉션이 필요해서 값 타입 컬렉션을 사용할까 고려해보았습니다.
이 과정에서 값의 추가와 제거는 의도한 대로 잘 동작하지만 수정하는 과정에서 전체 delete 이후 insert 하는 쿼리를 보고 한계가 존재한다는 걸 깨달았습니다.
이를 극복하고자 @OrderColumn으로 컬렉션의 인덱스를 저장하여 관리하면 변경을 추적할 수 있어 update 문만을 사용할 수 있었는데 인덱스 기반으로 동작한다는 것이 생각보다 위험하여 포기했습니다.
이때 연관관계에 대해 고민할 수 있는데 이렇게 특별한 경우에는 일대다 단방향으로 고려해도 되고 아님 양방향으로 처리해도 되어서 저는 양방향으로 고려했습니다.
추가적인 update문이 나가는 것을 원치 않기 때문이죠.
마지막으로 중요한 엔티티와 값 타입을 혼동하지 않는 팁을 하나 알려드리자면
1. 식별자가 필요하다. ( 식별자를 통해 접근할 가능성이 존재한다.)
2. 값을 지속적으로 추적해야 한다. ( 해당 식별자로 접근하여 해당 값을 추가하거나 제거할 수 있다)
이는 관리가 필요한 엔티티이다.
값 컬렉션 타입을 사용한다는 것은 정말 추적이 필요 없이 갈아 끼워도 될 때 매우 간단한 경우에만 사용해야 한다.
예를 들면 개수가 매우 작은 종류의 값들을 중복해서 주인 엔티티가 가질 수 있는 경우?
사실 나는 테이블이 추가적으로 필요한 상황이면 그냥 엔티티로 승격시킨다. 조금 더 고려해야 할 상황이 많지만
엔티티를 사용하는 편이 더 안정감을 주는 것 같다.
'데이터 접근 기술 > JPA' 카테고리의 다른 글
DTO와 Entity (0) | 2022.12.11 |
---|---|
상속관계 매핑 (0) | 2022.12.04 |
OSIV (0) | 2022.10.31 |
JPQL Pagination (0) | 2022.10.30 |
벌크연산 (0) | 2022.10.29 |
댓글