[JPA] 값 타입
Updated:
들어가며
해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.
JPA에서 사용할 수 있는 값 타입에 대해서 알아본다.
가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있는데 엔티티 타입은 @Entity
로 정의한 객체이고,
값 타입은 integer, String과 같은 단순한 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
기본값 타입
- 자바에서 제공하는 기본 값 타입
- String, int, Integer, Double 등
임베디드 타입(복합 값 타입)
Profile에서 중복되는 address, phoneNumber, email 필드에 대해서
Embedded
객체를 만들어서 하나의 필드로 나타낼 수 있다.
개선 전
@Entity
public class AdminProfile {
@Id
@GeneratedValue
private Long id;
private String address;
private String phoneNumber;
private String email;
}
@Entity
public class UserProfile {
@Id
@GeneratedValue
private Long id;
private String address;
private String phoneNumber;
private String email;
}
PrivateData
라는 Embedded
객체를 생성하여 Profile 객체에서 중복되는 부분을
하나의 임베디드 객체로 변경할 수 있다.
책에서는 임베디드 객체를 사용함에 있어서 객체지향적인 코드와 응집력을 높인다고 되어 있지만 크게 와닿지는 못했다.😢
내 생각으로는 여러 엔티티에서 묶어서 관리해야 하는 데이터 필드라면 임베디드로 만드는 것이 좋아보이지만 단순히 중복되는 필드를 하나의 객체로 묶어버리는 케이스면 나중에 변경이 발생했을 때 더 어려움이 있을 것 같다.
개선
@Entity
public class UserProfile {
@Id
@GeneratedValue
private Long id;
@Embedded
PrivateData privateData;
}
@Entity
public class AdminProfile {
@Id
@GeneratedValue
private Long id;
@Embedded PrivateData privateData;
}
@Embeddable
public class PrivateData {
private String address;
private String phoneNumber;
private String email;
}
만약에 임베디드 객체를 엔티티에서 2개 이상 가져야 하는 경우엔 @AttributeOverride를 통해서 필드 이름을 변경해줘야 문제가 발생하지 않는다.
@Entity
public class UserProfile {
@Id
@GeneratedValue
private Long id;
@Embedded
PrivateData privateData;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "address", column = @Column(name = "office_address")),
@AttributeOverride(name = "phoneNumber", column = @Column(name = "office_phone_number")),
@AttributeOverride(name = "email", column = @Column(name = "office_email"))
})
PrivateData officePrivateData;
}
Hibernate:
create table user_profile (
id bigint not null,
office_address varchar(255),
office_email varchar(255),
office_phone_number varchar(255),
address varchar(255),
email varchar(255),
phone_number varchar(255),
primary key (id)
)
- @Embedded : 값 타입을 사용하는 곳에 표시
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @AttributeOverrides: 여러개의 속성을 변경하기 위해 사용
- @AttributeOverride: 속성 재정의
- 임베디드 타입이 nullable이면 매핑한 컬럼 값은 모두 nullable이 된다.
값 타입 공유 참조
값 타임 공유 참조 문제
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
@Test
void 임베디드_객체_공유시에_문제_테스트() {
AdminProfile firstAdmin = new AdminProfile();
PrivateData privateData = new PrivateData("서울", "000-000-000", "abc@naver.com");
firstAdmin.setPrivateData(privateData);
em.persist(firstAdmin); em.flush();
AdminProfile secondAdmin = new AdminProfile();
PrivateData firstPrivateData = firstAdmin.getPrivateData();
firstPrivateData.setAddress("인천");
secondAdmin.setPrivateData(firstPrivateData);
em.persist(secondAdmin); em.flush();
AdminProfile firstAdminResult = em.find(AdminProfile.class, firstAdmin.getId());
AdminProfile secondAdminResult = em.find(AdminProfile.class, secondAdmin.getId());
System.out.println("firstAdmin address: " + firstAdminResult.getPrivateData().getAddress());
System.out.println("secondAdmin address: " + secondAdminResult.getPrivateData().getAddress());
}
firstAdmin address: 인천
secondAdmin address: 인천
- 해당 방식으로 업데이트 하는 경우 firstAdmin과 secondAdmin에 대해서 UPDATE SQL이 실행된다.
- PrivateData가 firstAdmin에서도 참조하고 있기 때문에 secondAdmin에서 재사용하는 경우 참조하고 있는 객체에 부작용(side effect)가 발생할 수 있다.
값 타입 공유 참조 해결
- 참조 문제를 해결하기 위해 새로운 객체를 생성해서 참조하는 객체가 아닌 새로운 객체를 생성한다.
- 참조 문제를 해결 하기 위해서는 객체를 불변하게 만들어 값을 수정할 수 없게 하므로 부작용을 원천 차단할 수 있다.
- 따라서 값 타입은 될 수 있으면 불변 객체(immutable Object)로 설계해야 한다.
- Setter method를 만들지 않아 불변 객체가 될 수 있게 한다.
- lombok의
@Value
어노테이션을 이용해 final class가 되게 한다.
@Value
@Embeddable
public class PrivateData {
private String address;
private String phoneNumber;
private String email;
}
compile 결과 setter는 생성되지 않고 클래스, 필드에 final이 추가되어 객체가 생성 된다.
@Embeddable
public final class PrivateData {
private final String address;
private final String phoneNumber;
private final String email;
public PrivateData(String address, String phoneNumber, String email) {
this.address = address;
this.phoneNumber = phoneNumber;
this.email = email;
}
...
}
값 타입 컬렉션
- @ElementCollection : 해당 컬렉션은 신규 테이블로 생성된다.
@Data
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@CollectionTable
@ElementCollection
List<String> interestList = new ArrayList<>();
}
Hibernate:
create table user (
id bigint not null,
primary key (id)
)
Hibernate:
create table user_interest_list (
user_id bigint not null,
interest_list varchar(255)
)
- @CollectionTable
- @ElementCollection로 테이블 생성시 원하는 이름과 조인 컬럼을 지정할 수 있다.
- 생략 가능하다.
- 기본값: {엔티티이름}_{컬렉션 속성 이름}, 예를 들어 User 엔티티의 interestList는 user_interest_list 테이블과 매핑한다.
- 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
- 값 타임 컬렉션을 조회할 때 페치 전략은 LAZY가 기본 값이다.
@Target( { METHOD, FIELD })
@Retention(RUNTIME)
public @interface ElementCollection {
...
FetchType fetch() default LAZY;
}
값 타입 컬렉션의 제약사항
- 값 타입 컬렉션의 경우 식별자 값이 없기 때문에 값 변경이 발생했을 때 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다.
- 값 타입 컬렉션의 데이터가 변경되면 JPA에서는 해당 테이블에 값을 전체 DELETE 후 새롭게 INSERT 한다.
@DataJpaTest
class ElementCollectionTest {
@Autowired
private TestEntityManager em;
@Test
void changeElementCollectionTest() {
User user = new User();
List<String> interestList = user.getInterestList();
interestList.add("축구");
interestList.add("노래");
interestList.add("여행");
em.persist(user); em.flush();
interestList.remove("노래");
interestList.add("요리");
em.persist(user); em.flush();
}
}
Hibernate: // user insert
insert
into
user
(id)
values
(?)
Hibernate: // user interest insert (축구, 노래, 여행)
insert
into
user_interest_list
(user_id, interest_list)
values
(?, ?)
생략...
// ElementCollection에 변경이 발생하여 delete query 실행
Hibernate:
delete
from
user_interest_list
where
user_id=?
Hibernate: // user interest insert(축구,여행,요리)
insert
into
user_interest_list
(user_id, interest_list)
values
(?, ?)
...생략
- 따라서 데이터 변경이 발생하는 경우 일대다 관계를 고려하는게 좋다.
- 일대다 관계에서 영속성 전이 + 고아 객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다.
마치며
임베디드 객체를 활용하여 엔티티 타입에서 공통으로 관리해야 하는 필드를 하나의 객체로 만들어서 관리할 수 있게 알아보았다. 하지만 임베디드 객체를 사용할 경우 객체를 재사용하지 않도록 주의해야 하며, 이러한 문제를 해결하기 위해 불변 객체로 만들어서 사용하는 것을 추천한다.
간단하게 1:N 관계를 생성할 수 있는 ElementCollection에 대해서도 알아보았다. ElementCollection을 사용할 때는 변경이 발생하지 않는 history에 대해서 사용하는 경우는 성능상 문제가 없지만 자주 변경이 발생하는 경우는 OneToMany 관계를 이용하는걸 추천한다.
- JPA 리뷰 글 보러가기
Leave a comment