JPA - 값 타입

bp.chys·2020년 6월 8일
0

JPA

목록 보기
8/15

JPA 데이터 타입 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적이 가능하다.
  • 값 타입
    • 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 별도의 식별자(id)가 없고 값만 존재하므로 변경시 추적이 불가하다.

값 타입 분류

기본 값 타입

  • 원시타입(int, boolean..), 래핑 타입(String, Integer..)
  • 생명주기를 엔티티에 의존한다.
    • 회원을 삭제하면 이름, 나이 필드도 함께 삭제된다.
  • 값 타입은 공유하면 안된다.
    • 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안된다. → 불변 객체로 만들어야 함.
    • 그렇기 때문에 자바의 기본 타입은 항상 값을 복사하는 특징이 있다.
    • 래퍼 클래스는 기본 타입이 아니지만 특수한 케이스로 변경할 수 없다.

임베디드 타입

  • 사용자 정의 참조형 타입으로 새로운 값 타입을 직접 정의할 수 있다.(복합 값 타입이라고도 함)
  • @Embeddable : 값 타입을 정의하는 곳에 표시한다.
  • @Embedded : 값 타입을 사용하는 곳에 표시한다.
  • JPA 스펙상 public 기본 생성자가 필수이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티의 생명주기를 의존한다.
  • 재사용높은 응집도가 장점이다.

@AttributeOverride

  • 한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복되기 때문에 속성을 재정의 해줘야 한다.
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    
    @Embedded
    private Address homeCity;
    
    @Embedded
    @AttributeOverrides({  // ** 재정의!!
            @AttributeOverride(name = "city",
                column = @Column("WORK_CITY")),
            @AttributeOverride(name = "street",
                column = @Column("WORK_STREET")),
            @AttributeOverride(name = "zipcode",
                column = @Column("WORK_ZIPCODE"))
    })
    private Address officeCity;

    // getter, setter ...
}

값 타입과 불변 객체

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
  • 아래 예시처럼 의도치않게 member1과 member2의 city가 모두 바뀌어버린다.
  • 이러한 부작용을 막기 위해서 값 타입은 항상 불변해야 한다.
... 
    Address address = new Address("city", "street", "10000");
    
    Member member = new Member();
    member.setUsername("member1");
    member.setHomeAddress(address);
    em.persist(member);
    
    Member member2 = new Member();
    member2.setUsername("member2");
    member2.setHomeAddress(address); // 인스턴스 공유 중
    em.persist(member2);
    
    // 이때 만약 member의 주소를 바꾼다면?
    member.getHomeAddress.setCity("newCity");
    em.persist(member);
...

값 타입 복사

  • 불변 객체인 값 타입을 수정하고 싶을 때는 새로운 인스턴스를 생성해서 갈아끼워야 한다.
  • 불변 객체(immutable object) : 생성 시점 이후 절대 값을 변경할 수 없는 객체

... 
    Address address = new Address("city", "street", "10000");
    
    Member member = new Member();
    member.setUsername("member1");
    member.setHomeAddress(address);
    em.persist(member);
    
    
    Address address2 = new Address(address.getCity(), address.getStreet(), address.getZipcode());
    
    Member member2 = new Member();
    member2.setUsername("member2");
    member2.setHomeAddress(address2); // 값은 같지만 주소값은 다른 새로운 인스턴스 
    em.persist(member2);
    
    member.getHomeCity().setCity("newCity");
...

값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 어노테이션을 사용한다.
  • 데이터베이스는 컬렉션 필드를 같은 테이블에 저장할 수 없기 때문에 별도의 테이블이 필요하다.
  • 값 타입은 엔티티와 다르게 식별자 개념이 없기 때문에 값은 변경하면 추적이 어렵다.
  • 따라서 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. 엄청난 비효율이다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. : null x, 중복 저장 x

// Member.java
...

    @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"))
    private List<Address> addressHistory = new ArrayList<>();
...

값 타입 저장/조회

... 
    Address address = new Address("city", "street", "10000");
    
    Member member = new Member();
    member.setUsername("member1");
    member.setHomeAddress(address);
    
    member.getFavoriteFoods().add("치킨");
    member.getFavoriteFoods().add("피자");
    
    member.getAddressHistory().add(new Address("old1", "street", "10000"));
    
    em.persist(member); 
    
    Member findMember = em.find(Member.class, member.getId());
    // 조회 시 member 테이블 조회만 이뤄진다.
    // ADDRESS, FAVORITE_FOODS은 자동으로 LAZY Fetch가 적용된다.
...
  • 위 예시 코드에서 어떤 쿼리가 수행될까?
  • INSERT INTO FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) VALUES (1, '치킨');
  • INSERT INTO FAVORITE_FOOD (MEMBER_ID, FOOD_NAME) VALUES (1, '피자');
  • INSERT INTO ADDRESS (MEMBER_ID, CITY, STREET, ZIPCODE) VALUES (1, 'old1', 'street', '10000);
  • INSERT INTO MEMBER (USER_NAME, HOME_ADDRESS_ID) VALUES ('member1', '1');
  • 총 4번의 Insert 쿼리가 날라감

값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려한다.
  • 일대다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용한다.
  • 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용한다.
@Entity
public class AddressEntity {
    
    @Id @GeneratedValue
    private Long id;
    
    private Address address;
    
    // getter, setter ..
}
// Member.java
    // @ElementCollection
    // @CollectionTable(name = "ADDRESS",
    //        joinColumns = JoinColumn(name = "MEMBER_ID"))
    // private List<Address> addressHistory = new ArrayList<>();
    
    @OneToMany(fetch = Lazy, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoineColoumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory;
 

결론

값 타입(VO)은 기본키로 식별되는 엔티티와 달리 순수한 값을 표현하고자할 때 사용한다. 값 타입이 공유될 경우 값이 변경되면 부작용이 발생하므로 모든 값 타입은 불변 객체로 만들어야 한다.

하나의 엔티티에 여러개의 값타입을 참조해야할 경우, 값 타입 컬렉션을 사용할 수 있다. 하지만 값타입 컬렉션을 사용할게 될 경우 값의 변경 시 주인 엔티티와 연관된 모든 데이터를 지우고 다시 저장하므로 비효율이 발생하게된다. 따라서 실무에서는 값 타입 컬렉션을 사용해야 하는 경우 엔티티로 만들어서 일대다 관계로 부모엔티티가 생명주기를 관리하게 하는 것이 현명할 것이다.


참고자료

  • 자바 ORM 표준 JPA 프로그래밍, 김영한 저
profile
하루에 한걸음씩, 꾸준히

0개의 댓글