[JPA] 값 타입
- [ Backend ]/Spring DB, JPA
- 2022. 1. 15.
JPA에는 엔티티 타입(@Entity로 정의하는 객체)과 값 타입(자바 객체나 primitive type)이 존재한다. 엔티티 타입은 데이터가 변해도 식별자로 추적할 수 있다(엔티티의 값을 변경한다고 해도 식별자를 이용해서 인식할 수 있다). 반대로 값 타입은 단순히 값으로서 사용하는 것으로서 jpa에서 추적하지 않는다. 아래에서는 여러가지 종류의 값 타입에 대해서 알아보겠다.
1. 기본값 타입
- Primitive type
- 래퍼 클래스
- String
기본값 타입은 생명주기를 엔티티에 의존한다. 값 타입을 엔티티끼리 공유하게 되면 문제가 발생할 수 있으므로 같은 참조값을 가지면 안된다. (물론 자바의 primitive type같은 경우는 당연히 reference를 공유할 수 조차 없어 안전하다.) 래퍼클래스같은 경우에도 변경이 불가능하지만 공유는 가능하다.
2. 임베디드 타입(내장 타입)
주로 기본값 타입을 모아서 새롭게 정의한 값 타입을 말한다. 다만 primitive type과 같은 값타입이기 때문에 공유와 변경이 불가능하다. 예를 들어 사람의 키, 몸무게, 발 사이즈라는 int형 기본 타입들을 묶어서 신체 정보라는 새로운 임베디드 타입을 만들 수 있다. 물론 자바에서는 새로운 클래스를 만드는 방법 말고는 불가능하지만, JPA에서는 @Enbeddable과 @Embedded라는 어노테이션을 이용하여 새로운 임베디드 타입을 만들 수 있다.
@Entity
public class Member {
....
private LocalDateTime startDate;
private LocalDateTime endDate;
...
}
원래 Member클래스에 출근시간과 퇴근시간 컬럼이 존재한다고 해보자. 지금까지는 위와 같이 작성하고 관리했을 것이다.
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Entity
public class Member {
...
@Embedded
private Period workPeriod
...
}
@Embeddable 어노테이션을 임베디드 타입 클래스에 달아주고, 원래 엔티티에 임베디드 타입 변수를 선언하고 @Embedded어노테이션을 설정해주면 원래와 같이 동일하게 테이블을 설정하면서도 관련된 필드들을 모아서 객체화시켜 다룰 수 있다.
다만 코드를 변경한다고 해도 이것은 객체에서 기능적인, 혹은 설계상의 이득을 보기 위해서 사용하는 것이며, 테이블에 변화를 불러오진 않는다. 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 동일하다.
<추가>
임베디드 타입 안에 필드로 엔티티가 추가될 수는 있다. 하지만 임베디드 타입을 엔티티에서 사용할 때 여러개의 동일한 임베디드 타입이 필드로 선언되면, 컬럼명의 중복이 이루어지기 때문에 에러가 발생한다. 이를 해결하기 위해서는 @AttributeOverrides를 이용하여 컬럼명 속성을 재정의해주어야 한다.
3. 불변 객체
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하게 되면, 한 엔티티에서 값을 변경했을 때 다른 엔티티의 값도 변경될 수 있기 때문에 안전하지 않다.
Address address = new Address("oldCity", "street", "10000");
Member member1 = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member.setUsername("member2");
member.setHomeAddress(address);
em.persist(member2);
member1.getHomeAddress().setCity("newCity");
위 코드를 보자. 2개의 Member객체를 생성하고 각각에 address를 설정해준 이후, member1의 oldCity정보를 바꿀 목적으로 마지막 코드를 실행하였다. 하지만 이 코드의 실행은 member1과 member2 두 개의 city명이 newCity로 바뀌는 결과를 초래하게 된다.
이렇게 값 타입의 공유는 예측하지 못한 부작용을 발생시킬 수 있다. 대신 값을 복사해서 사용해야 한다.
Address address = new Address("oldCity", "street", "10000");
Member member1 = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member1);
//새로운 address객체를 생성하여 저장해주어야 함
Address address2 = new Address(address.getCity(), address.getStreet(), address.getZipCode());
Member member2 = new Member();
member.setUsername("member2");
member.setHomeAddress(address);
em.persist(member2);
member1.getHomeAddress().setCity("newCity");
이렇게 코드를 작성하면 member2의 address와는 다른 객체의 정보를 변경하기 때문에 공유 참조로 인한 side effect가 발생하지 않는다.
그런데 이러한 방법은 직접 정의한 값 타입은 객체 타입이기 때문에 실수로라도 대입하게 되면 값을 복사하는 것이 아니라 참조를 복사하게 된다. 따라서 개발자의 실수에 따라 에러를 발생시킬 확률이 매우 높다.
//기본타입과 데이터타입의 차이
int a = 10;
int b = a; //primitive type은 값을 복사한다.
Object a = new Object("A");
Object b = a; //reference type(데이터 타입)은 참조를 전달한다.
b.setName("B"); //a의 name이 원치 않게도 "A"에서 "B"로 변경된다.
그렇기 때문에 객체 타입을 변경할 수 없게 만들어야 실수로 인한 부작용을 차단할 수 있다. 불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체로, 생성자나 변수를 final변수로 설정하거나 setter메소드를 private로 선언 or 아예 없애는 방법 등이 있다.
+ (String이나 래퍼 클래스는 대표적인 Java의 불변 객체이다.)
4. 값 타입 비교
데이터 타입에서 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 표시해주어야 한다. 그런데 당연하게도 reference type객체의 경우 ==비교를 이용하면 안의 값이 같아도 참조값의 비교가 이루어지기 때문에 다를 것이다.
동일성 비교(==) : 인스턴스의 참조값을 비교
동등성 비교(.equals()) : 인스턴스의 값을 비교
데이터 타입은 ==비교가 아니라 equals()함수를 오버라이드해서 동등성 비교를 실행해주면 된다. (equals()의 기본꼴이 ==비교이다)
5. 값 타입 컬렉션
말 그대로 값 타입을 자바 컬렉션에 담아서 사용할 수 있다. 단순히 기본 값들을 모아서 저장해서 설계상의 이득을 보기 위함이며 테이블에는 변화가 없는 임베디드 타입과 다르게 값 타입들을 컬렉션으로 만들어 DB에 별도의 테이블을 추가하고 매핑한다.
@Entity
public class Member {
...
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") //String 기본타입 컬렉션이므로 컬럼명 직접지정
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(
name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID")) // Address는 임베디드 타입이므로 지정X
private List<Address> addressHistory = new ArrayList<>();
...
}
@ElementCollection을 통해서 해당 객체가 컬렉션 객체임을 jpa한테 알려주고, @CollectionTable을 통해서 테이블의 정보를 입력해주면 된다.
위 코드는 Member엔티티에 값 타입 컬렉션을 추가한 예시이다.
먼저 favoriteFoods라는 String타입의 HashSet을 만들어주고, 그것이 모인 테이블을 "FAVORITE_FOOD"라고 이름짓고, FOOD_NAME이라는 컬럼을 만들고 MEMBER_ID라는 컬럼과 JOIN해주도록 하였다.
아래에서는 addressHistory라는 Address타입 리스트를 만들었는데, Address는 이전에 우리가 임베디드 타입으로 설정해준 객체이므로 컬럼명을 직접 지정해줄 필요는 없다. @JoinColumn을 통해서 JOIN하는 과정만 넣어주었다.
이렇게 하면 컬렉션 타입을 위한 테이블이 생성되고 엔티티와 JOIN된다.
데이터베이스를 조회해보면 확실하게 알 수 있다.
비록 별도의 테이블을 추가했지만 엔티티가 아니므로 생명주기가 컬렉션 매핑된 클래스에 종속적이다. 따라서 값 타입 컬렉션은 영속성 전이(cascade = CascadeType.ALL)와 고아객체 제거(orphanRemoval = TRUE)옵션 모두의 기능을 가진다고 할 수 있다.
컬렉션 값 타입의 수정을 위해서는 앞서 설명한 참조 공유에 의한 side effect를 방지하기 위해서 무조건 새로운 객체를 만들어서 넣어주도록 하자.
컬렉션 타입은 여러 값을 컬렉션에 담아 관리할 수 있다는 장점이 있다. 다만 고유 pk가 존재하는 엔티티와 다르게 테이블에 값들만 저장하고 이들을 묶어서 pk로 사용하는, 식별자가 존재하지 않는 특성상 값 타입 컬렉션은 값을 변경하면 추적이 어렵다. 따라서 값 타입 컬렉션에 변경사항이 발생하면 값 타입 컬렉션에 있는 현재 값들을 모두 다시 저장해주어야 하는 번거로움이 있다.
따라서 값 타입 컬렉션은 값의 업데이트를 할 일이 없는 아주 단순한 케이스에만 사용하고, 그 외에는 일대다 관계를 사용한 엔티티를 이용하자.
@Entity
public class Member {
...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
}
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
}
이런 방법을 흔히 값 타입의 엔티티 승급이라고 얘기하는데, 컬렉션 타입과 달리 엔티티라는 점 때문에 식별자가 존재하기 때문에 값의 수정이 자유로워진다.
'[ Backend ] > Spring DB, JPA' 카테고리의 다른 글
[Spring JPA] 변경감지 vs 병합 (0) | 2022.02.02 |
---|---|
[JPA] 객체지향 쿼리 언어(JPQL) 이해하기 (0) | 2022.01.17 |
[JPA] 프록시, 연관관계 관리 - 즉시/지연로딩, 영속성 전이, 고아 객체 (0) | 2022.01.13 |
[JPA] 연관관계 매핑(3) - 상속관계 매핑 (0) | 2022.01.11 |
[JPA] 연관관계 매핑(2) - 다양한 연관관계 (0) | 2022.01.06 |