JPA의 데이터 타입 분류
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
값 타입 분류
기본값 타입
자바에서 제공하는 기본 타입
- 자바 기본 타입 (int, double)
- 래퍼 클래스 (Integer, Long)
- String
임베디드 타입 (embedded type, 복합 값 타입)
정의해서 사용하는 타입
컬렉션 값 타입 (collection value type)
자바 컬렉션에 기본값 타입이나 임베디드 타입을 넣은 것
기본값 타입
- 예) String name, int age
- 생명주기를 엔티티에 의존
- 예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제
- 값 타입은 공유하면 안됨 🙅🏻♀️
- 하나의 값 타입이 여러 곳에서 사용되면 안된다.
- 예) 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨
- 위와 같은 것을 side effect (부수 효과)라고 한다.
참고로 자바의 기본 타입 (primitive type)은 절대 공유되지 않는다.
- int, double 같은 기본 타입(primitive type)은 절대 공유되지 않으므로 안전하다 😀
(side effect : 내 이름을 바꿨는데, 다른 사람 이름 바뀌는 경우 없다)
- 기본 타입은 항상 값을 복사
- Integer같은 래퍼 클래스나 String같은 특수한 클래스는 공유 가능한 객체이지만 변경 X (불변객체)
임베디드 타입 (복합 값 타입)
- 새로운 값 타입을 직접 정의할 수 있음
- JPA는 임베디드 타입(embedded type)이라 함
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
- int, String과 같은 값 타입 (추적도 안되고 변경하면 끝난다)
예제
- 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.
- 비슷한 필드끼리 묶을 수 있다.
- 근무 시작일과 근무 종료일
- 주소 도시, 주소 번지, 주소 우편번호
- 위의 것들 클래스로 묶어서 시스템에서 공통으로 쓸 수 있지 않을까?
- 보통 저렇게 구체적으로 설명하지 않는다.
- 다음과 같이 추상화해서 설명한다.
- 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다.
- 이처럼 묶어낼 수 있는게 바로 임베디드 타입
- 기본 값타입 묶어서 클래스 2개를 새로 정의한 것임
- 이것을 임베디드 타입이라고 한다
- JPA에서 이러한 값타입을 어떻게 쓰는가?
임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
장점
- 재사용
- 높은 응집도
- Period.isWork() 처럼 해당 값 타입만 사용하는 의미있는 메서드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이다.
- 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 테이블 입장에서는 임베디드 타입 사용하든 말든 바뀔 것이 없다.
- 테이블은 데이터를 잘 관리하는 것이 목적이기 때문에 위처럼 설계되는 것이 맞다.
- 하지만 객체는 데이터(상태) 뿐만아니라 메서드(행위)까지 들고있다.
- 따라서 임베디드 타입으로 묶었을 때 가질 수 있는 이득이 많다.
- 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
- 프로젝트를 하다보면 기간 값이나 좌표값 같은 것은 클래스로 뽑으면 그 안에 메서드 같은 것 만들어두면 활용할 수 있는 것이 되게 많다.
- 또 설계적인 관점에서도 임베디드 타입으로 묶어놓는 것이 설명하기도 좋고 깔끔하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다. (값 타입으로 비슷한 것끼리 잘 묶어놨기 때문 😋)
- 막 어마어마하게 많이 쓰지는 않지만 맞춰두면 얻을 수 있는 장점이 많다.
임베디드 타입과 연관관계
- 임베디드 타입이 임베디드 타입을 가질 수 있다. (Address ➡ Zipcode)
- 임베디드 타입이 엔티티 가질 수 있다. (PhoneNumber ➡ PhoneEntity)
- 크게 어렵지 않은게 임베디드 타입 입장에서는 그냥 엔티티의 FK만 들고 있으면 된다.)
@AttributeOverride : 속성 재정의
- 한 엔티티에서 같은 값타입을 사용하면?
- 컬럼명 중복됨
- @AttributeOverrides, @AttributeOverride를 사용해서 컬럼명 속성을 재정의
임베디드 타입과 null
- 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야한다.
값 타입 공유참조
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다!
- 부작용(side effect) 발생
- 이런 식으로 공유해서 사용하는 것을 의도한 것이라면, 값타입이 아니라 엔티티로 만들어야함
- 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.
- 대신 값(인스턴스)를 복사해서 사용
객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용은 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 하지만, 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 객체의 공유 참조는 피할 수 없다.
기본타입 (primitive type)
int a = 10;
int b = a;
b = 4;
객체 타입
Address a = new Address("Old");
Address b = a;
b.setCity("New");
불변 객체
- 객체 타입을 수정할 수 없게 만들면 부작용(side effect) 원천 차단
- 값 타입은 불변 객체(immutable object)로 설계해야한다.
- 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 된다. 👨🏻🎓
- 참조 : Integer, String은 자바가 제공하는 대표적인 불변 객체
불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.
그런데 혹시 값을 진짜 바꾸고 싶을 때는 어떻게 해야하나?
그냥 새로운 객체를 생성해서 넣어야한다 🤣 (그래도 side effect 없는게 더 중요!)
값 타입의 비교
- 값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
int a = 10;
int b = 10;
a == b;
Address a = new Address("서울시");
Address b = new Address("서울시");
a == b
- 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함
- 값 타입의 equals() 메서드를 적절하게 재정의 (주로 모든 필드 사용)
값 타입 컬렉션
- 값 타입을 컬렉션에 담아서 사용하는 것을 말함
- 이전에는 일대다 매핑에서 엔티티를 컬렉션에 넣어서 써보았다.
- 값 타입도 마찬가지로 컬렉션에 넣어서 사용할 수 있다.
- 값 타입을 하나 이상 저장할 때 사용
- 값 타입 매핑
- @ElementCollection, @CollectionTable 사용
데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
위의 테이블 보면 모든 컬럼들을 묶어서 PK로 사용하는 것을 볼 수 있다.
따로 PK를 준다면 그것은 값 타입이 아니라 엔티티가 되버린다.
사용
- 값 타입 저장 예제
- 값 타입 조회 예제
- 값 타입 수정 예제
- 값 타입은 setter등으로 수정하면 안된다. side effect 발생 예방
- 값 타입은 immutable 해야한다.
- 따라서 아예 제거하고 새로 생성해서 넣어야한다.
- 참고 : 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거기능을 필수로 가진다고 볼 수 있다.
제약 사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- PK값이 따로 없다.
- 전체 값이 그냥 PK이다.
- 따라서 값을 변경하면 추적이 어렵다.
- 매핑된 테이블 보면 외래키만 있고, 모든 컬럼을 PK로 사용함
- 그런데 이거는 내가 따로 제약조건 걸어준 것임 (PK없이는 운영할 수가 없으니까)
- 추적할 만한 식별자가 없기에 값이 변경되어도 추적하기 힘들다.
- 따라서 아예 통채로 다 지우고 새로 넣어야한다.
- 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 여기서 감온다... 쓰면 안된다는 감... 😅
- 물론 식별자 비슷한 orderColumn을 줄 수 있지만 너무 복잡하다.
- 따라서 아예 다르게 풀어야한다 (일대다 엔티티로 승격)
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해줘야한다. null입력 X, 중복저장 X
- PK로 할만한게 없으니까 이렇게라도 제약조건 걸어줘야한다.
- 따로 아예 상관없는 PK를 생성해주면 더이상 값 타입이 아니라 엔티티가 되버린다.
대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
- 일대다 단방향 매핑 사용 (특별한 경우니까!)
- 다른 테이블에 update 쿼리 나가는 것은 어쩔 수 없다. (일대다 단방향 매핑 특성)
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용
그럼 값타입 컬렉션 아예 안쓰나?
아니다 사용한다! 진짜 간단한 것에! 변경되도 추적할 필요없고, 크게 중요하지 않는.. 예를 들어 체크박스 (치킨 좋아하나, 족발 좋아하나) 이런 정보 저장해두는 정도에 사용한다. (굳이 엔티티까지 승격시킬 수준 아닐때)
정리
엔티티 타입 특징
- 식별자 존재
- JPA에 의한 생명주기 관리
- 공유
값 타입 특징
- 식별자 없음
- 생명주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
값 타입은 정말 값 타입이라 판단될 때만 사용
엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다! 🙅🏻♀️
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티!
해당 게시글은 인프런 김영한님의 <자바 ORM 표준 JPA 프로그래밍 - 기본편>을 듣고 정리한 내용입니다.