[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 관계를 이용하는걸 추천한다.



Leave a comment