JPA 의 데이터 타입
1. 엔티티 타입
- @Entity 로 정의하는 객체
- 식별자 (id) 를 가지고 있어서 데이터가 변해도 식별자로 추적가능
2. 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
값 타입 분류
1. 기본값 타입
자바에서 제공하는 기본 데이터 타입을 의미한다. 자바의 primitive type, wrapper class, String 등을 포함하는 값 타입이다.
String name, int age 와 같이 엔티티에 정의되어 있는 기본 타입의 값들이다. 이러한 타입의 값들은 다른 엔티티와 공유해서는 안된다. 값 복사를 통해서 사용해야 한다. 그 이유는 공유된 값들이 변경됨으로 발생할 수 있는 사이드 이펙트 때문이다. 예를들어 name 을 공유하는 경우 한 회원의 이름을 변경하였더니 다른 회원의 이름도 변경되게 된다.
자바의 primitive 타입의 경우에는 값을 공유하여도 참조형 객체처럼 객체 자체가 공유되는 것이 아니라 값만 복사되게 된다. 그렇기 때문에 의도한 대로 동작하지 않을 수 있다. 이와같은 부수효과 때문에 기본값 타입은 매번 복사해서 사용해야한다.
기본 값 타입의 생명주기는 엔티티에 의존한다.
2. 임베디드 타입
사용자가 새롭게 클래스를 정의하여 만든 값 타입이다. JPA 에서는 임베디드 타입이라고하고 기본 값 타입을 모아서 만들었기 때문에 복합 값 타입이라고도 한다.
int, String 과 같은 기본 값 타입들을 클래스에 정의하여 사용자가 원하는 값 타입을 만든다. 해당 클래스에는 @Embeddable 어노테이션을 사용하여 정의한다. 그리고 해당 타입을 사용하는 엔티티에서는 해당 타입의 값에 @Embedded 어노테이션을 사용한다. 임베디드 타입의 클래스에는 기본 생성자가 필수로 구현되어 있어야 한다.
임베디드 타입은 클래스로 정의하지만 @Entity 어노테이션이 붙은 엔티티가 아니라 단순한 값이다. 그렇기 때문에 임베디드 타입을 추가하거나 변경하여도 테이블이 변경되지는 않는다. 그리고 기본 값 타입과 같이 생명주기는 엔티티에 의존한다.
@Embeddable
class Address {
private String city;
private String street;
private String zipcode;
// ...
}
@Entity
class Member {
@Embedded
private Address address;
// ...
}
만약 한 엔티티에 같은 임베디드 타입이 여럿 존재하여서 컬럼명이 중복되는 경우 에러가 발생한다. 이때는 @AttributeOverride 어노테이션을 사용하여 속성 컬럼을 재정의한다.
만약 임베디드 타입의 값이 null 인 경우에는 매핑한 컬럼의 값도 모두 null 이 된다.
3. 컬렉션 값 타입
값 타입을 하나 이상 저장할 때 사용하는 값 타입이다. 리스트 등으로 구현되는데 아래의 값 타입 컬렉션 항목에서 자세하게 다룬다.
불변객체
- 객체 타입의 공유
임베디드 타입은 자바 객체이기 때문에 같은 값을 여러 엔티티에서 참조하도록 하여 공유할 수 있다. 하지만 이러한 경우 값 변경으로 인해서 해당 값을 공유하는 다른 엔티티에 영향을 줄 수 있다. 이러한 문제를 해결하기 위해서 값을 사용할 때는 값 복사를 통해 사용해야 한다.
객체 타입의 값을 인스턴스를 그대로 공유하는 것은 위험하기 때문에 값 복사 방식으로 사용한다. 새로운 인스턴스를 생성하고 값을 복사하는 방식으로 사용하도록 한다.
하지만 이 방법에도 문제가 있다. 엔티티 객체 인스턴스를 값 복사 방법으로 새로운 객체를 만든다고 생각해보자. 자바에서 기본 타입은 값을 대입하면 값을 복사하기 때문에 값 복사 방식을 사용하여 새로운 객체를 만들 수 있다. 하지만 객체 타입은 값을 대입하면 참조 값이 저장되기 때문에 객체 참조 공유를 막을 수 없다.
int a = 10;
int b = a;
b = 11;
// a = 10, b = 11
Address a = new Address("123");
Address b = a;
b.setAddress("456");
// a = b = Address("456")
이러한 문제를 해결하기 위해서 객체 타입을 수정할 수 없는 불변 객체로 생성하여 사용하도록 한다.
- 불변객체 (immutable object)
불변 객체는 생성 이후에는 값을 수정할 수 없는 객체를 의미한다. 엔티티의 객체를 불변객체로 설계하여 이후에 값이 바뀌면서 발생하는 사이드 이펙트를 방지하도록 한다.
자바에서는 Integer 나 String 같은 타입이 기본적으로 제공되는 불변객체 타입이다. 만약 임베디드 값 타입과 같이 새롭게 정의한 타입의 경우 생성자만 구현하고 setter 를 구현하지 않도록 하여 값 변경을 막는다.
만약 불변객체의 값 변경이 필요한 경우에는 값을 수정하는 것이 아니라 변경되는 값으로 새로운 객체를 생성해서 저장하도록 한다. 이러한 방식이 손이 더 갈 수 있지만 사이드 이펙트를 방지에서 더 좋은 방식이다.
값 타입 비교
값 타입을 비교할 때는 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다. 참조형 인스턴스의 경우 서로 다른 주소값을 가지고 있지만 인스턴스가 가지고 있는 값들이 같은 경우에는 같은 값으로 인식해야 한다. 이러한 값 비교를 동등성 비교라고 한다.
- 동일성 비교 (identity): 인스턴스 참조 값을 비교. == 사용
- 동등성 비교 (equivalence): 인스턴스 값 비교. equals() 사용
각 타입의 값들에 대해서 동등성 비교를 사용하기 위해서 각 타입에 맞게 equals() 를 오버라이딩하여 재정의 해주어야 한다.
값 타입 컬렉션
값 타입을 하나 이상 저장할 때 사용한다. 자바의 컬렉션에 데이터를 담아서 저장한다. 이전에 일대다 매핑을 통해 엔티티를 컬렉션으로 저장했던 것과 같이 값 타입 또한 컬렉션을 사용하여 저장할 수 있다.
값 타입 컬렉션을 매핑 시에는 @ElementCollection, @CollectionTable 등의 어노테이션을 사용한다.
- @ElementCollection: 값 타입들을 저장하는 컬렉션임을 지정한다.
- @CollectionTable(name = "TABLE_NAME"): 매핑할 테이블을 지정한다.
데이터베이스는 컬렉션을 엔티티와 같은 테이블에 저장할 수 없다. 그렇기 때문에 해당 컬렉션의 값 타입을 저장하는 별도의 테이블을 생성해야 한다. 해당 테이블에서는 값 타입의 모든 값, 컬럼들이 묶여서 하나의 PK 로 사용된다. 따로 PK 를 주어서 사용한다면 값 타입이 아니라 엔티티가 된다.
값 타입 컬렉션 또한 엔티티가 아닌 값 타입이기 때문에 모든 생명주기가 엔티티 객체에 의존한다. 그렇기 때문에 값 타입 컬렉션을 따로 persist() 하지 않고 엔티티 객체만 저장하여도 값 타입 컬렉션 객체도 같이 업데이트 된다. 이는 영속성 전이 (Cascade) + 고아 객체 제거 기능을 필수로 가지는 것과 같다.
또한 객체 조회 시에 지연 로딩 전략을 사용하기 때문에 엔티티를 조회할 때가 아닌 해당 값 타입 컬렉션에 접근하는 경우에 값 타입 테이블을 조회하는 쿼리를 수행한다.
값 타입을 변경하는 경우에는 삭제 후에 변경된 값으로 신규 생성하여 데이터를 저장해야 한다. 기본적으로 불변 객체를 사용하것이 안전한데, 이 경우 값을 변경할 수 없기 때문에 신규 객체를 생성하도록 해야 한다.
※ 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없어 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. (null X, 중복 X) 만약 따로 PK 를 주는 경우에는 값 타입 테이블이 아닌 엔티티 테이블이 된다.
실무에서는 상황에 따라서 값 타입 컬렉션 대신에 일대다 관계를 고려한다. 일대다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용한다.
엔티티 | 값 타입 |
식별자 O | 식별자 X |
생명 주기 관리 | 생명 주기를 엔티티에 의존 |
값을 공유 | 값을 공유하지 않는 것이 안전하다. 값 복사 사용. 불변객체로 사용하는 것이 안전하다. |
엔티티로 만들어야 할 것을 혼동하여 값 타입으로 만들면 안된다. 식별자를 통해서 값을 추적하고 변경해야 하는 객체에 대해서는 엔티티로 만들도록 한다.
'Tech > JPA' 카테고리의 다른 글
[JPA] JPQL 경로표현식과 fetch join (0) | 2022.06.19 |
---|---|
[JPA] JPQL 기본 문법 (0) | 2022.06.18 |
[JPA] 영속성 전이 (CASCADE) (0) | 2022.05.26 |
[JPA] 프록시와 지연로딩 (0) | 2022.05.23 |
[JPA] 상속관계 매핑 (0) | 2022.05.23 |