09강. 값 타입


🚨 주의

  • 값 타입 컬렉션 가능하면 사용하지 말기
  • 값 타입은 모두 불변으로 만들기


1. 기본값 타입

JPA의 데이터 타입 분류


타입 상세
엔티티 타입 - @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값 타입 - int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체



값 타입 분류


분류 상세
기본값 타입 • 자바 기본 타입(int, double)
• 래퍼 클래스(Integer, Long)
• String
임베디드 타입 (embedded type, 복합 값 타입)  
컬렉션 값 타입 (collection value type)  
  • 기본값 타입 : 자바에서 기본적으로 제공하는 타입
    ex. String name, int age
    • ⭐️ 생명주기를 엔티티에 의존 ex. 회원을 삭제하면 이름, 나이 필드도 함께 삭제됨
    • 값 타입은 공유하면 X ex. 회원 이름 변경시 다른 회원의 이름도 함께 변경되므로


    🚨 자바의 기본 타입(primitive type)은 절대 공유X

    • int, double 같은 기본 타입(primitive type)은 절대 공유 X
    • 기본 타입은 항상 값을 복사함
    • Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경X
  • 임베디드 타입, 컬렉션 값 타입 : JPA에서 정의해서 사용해야 함

    ex. 좌표를 (x, y) 묶어서 사용하고 싶을 때 : position class 만들어서 사용



2. 임베디드 타입 (복합 값 타입) ⭐️

임베디드 타입


  • 새로운 값 타입을 직접 정의할 수 있음
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • ⭐️ int, String과 같은 값 타입
    • 엔티티가 아님. 추적 안되고 변경하면 끝

ex. 회원 엔티티 : 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가짐

유사한 것끼리 묶어보면

  • 시간 : 근무 시작일, 근무 종료일 -> Period Type

  • 주소 : 주소 도시, 주소 번지, 주소 우편번호 -> Address Type

=> Period, Address가 임베디드 타입이 됨



임베디드 타입 사용법


  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수



임베디드 타입의 장점


  • 재사용
  • 높은 응집도
  • 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음

    ex. Period의 startDate, endDate를 이용해서 현재 일하고 있는지의 여부를 판단하는 Period.isWork() 메소드 생성

  • 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존함
    • 임베디드 타입도 값 타입 !!



임베디드 타입과 테이블 매핑


  • Embedded Type에 @Embeddable, Embedded Type을 포함하는 쪽에 @Embedded
  • 임베디드 타입을 쓰거나 쓰지 않거나 DB 테이블은 똑같음. 매핑만 해주면 됨

ex. 임베디드 타입 사용 X


@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private String city;
    private String street;
    private String zipcode;

}

실행 결과 (commit)

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        endDate timestamp,
        startDate timestamp,
        street varchar(255),
        USERNAME varchar(255),
        zipcode varchar(255),
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )

임베디드 타입을 사용하려면

// Member.java
private LocalDateTime startDate;
private LocalDateTime endDate;

private String city;
private String street;
private String zipcode;

// Period.java
package hellojpa;

import javax.persistence.Embeddable;
import java.time.LocalDateTime;

@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

		// 기본 생성자 필수
		public Period() {}

    // getter, setter 생략

}

// Address.java
package hellojpa;

import javax.persistence.Embeddable;

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    // 기본 생성자 필수
    public Address() { }

    // getter, setter 생략

}

// Member.java
@Embedded
private Period workPeriod;

@Embedded
private Address homeAddress;

똑같이 Member table을 생성

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )
  • Embedded Type 안에 다른 메소드 만들어서 사용 가능

  • 이용 예제

    
      @Entity
      public class Member {
        
          @Id @GeneratedValue
          @Column(name = "MEMBER_ID")
          private Long id;
        
          // 기본 생성자 필수
          public Member() {}
        
          @Column(name = "USERNAME")
          private String username;
        
          // Period
          @Embedded
          private Period workPeriod;
        
          // Address
          @Embedded
          private Address homeAddress;
        
          // getter, setter 생략
        
      }
    
    
      @Embeddable
      public class Address {
        
          private String city;
          private String street;
          private String zipcode;
        
          // 기본 생성자 필수
          public Address() {}
        
          public Address(String city, String street, String zipcode) {
              this.city = city;
              this.street = street;
              this.zipcode = zipcode;
          }
        
          // getter, setter 생략
    
      }
    
    
      @Embeddable
      public class Period {
        
          private LocalDateTime startDate;
          private LocalDateTime endDate;
        
          public Period() {}
        
          public Period(LocalDateTime startDate, LocalDateTime endDate) {
              this.startDate = startDate;
              this.endDate = endDate;
          }
        
          // getter, setter 생략
        
      }
    
      // JpaMain
    
      Member member = new Member();
      member.setUsername("hello");
      member.setHomeAddress(new Address("city", "street", "zipcode"));
      member.setWorkPeriod(new Period());
        
      em.persist(member);
        
      tx.commit();
    
    

    실행 결과

      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) 
              values
                  (?, ?, ?, ?, ?, ?, ?)
    

    DB



임베디드 타입과 테이블 매핑


  • 임베디드 타입은 엔티티의 값일 뿐
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같음
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능

    ex. 좌표값을 클래스로 -> 이 클래스를 이용해서 다양한 메서드를 만들어서 사용할 수 있음

  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음



임베디드 타입과 연관관계


  • 임베디드 타입은 임베디드 타입을 포함할 수 있음
  • 임베디드 타입은 엔티티를 포함할 수 있음
    • 임베디드 타입에서 엔티티의 FK만 들고 있으면 됨



@AttributeOverride: 속성 재정의


  • 한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복됨

    -> @AttributeOverrides, @AttributeOverride를 사용해서 컬럼명 속성을 재정의

ex. Member에 homeAddress, workAddress

@AttributeOverride 하지 않으면


@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    // 기본 생성자 필수
    public Member() {}

    @Column(name = "USERNAME")
    private String username;

    // Period
    @Embedded
    private Period workPeriod;

    // Address
    @Embedded
    private Address homeAddress;

    @Embedded
    private Address workAddress;

    // getter, setter (생략)

}
  • 이 상태로 실행하면 에러 발생 : Repeated column in mapping for entity

    -> @AttributeOverrides, @AttributeOverride를 이용

      @Entity
      public class Member {
        
          @Id @GeneratedValue
          @Column(name = "MEMBER_ID")
          private Long id;
        
          // 기본 생성자 필수
          public Member() {}
        
          @Column(name = "USERNAME")
          private String username;
        
          // Period
          @Embedded
          private Period workPeriod;
        
          // Address
          @Embedded
          private Address homeAddress;
        
          @Embedded
          @AttributeOverrides({
                  @AttributeOverride(name="city",
                      column=@Column(name="WORK_CITY")), // DB Column을 따로 매핑
                  @AttributeOverride(name="street",
                      column=@Column(name="WORK_STREET")),
                  @AttributeOverride(name="zipcode",
                          column=@Column(name="WORK_ZIPCODE"))
          })
          private Address workAddress;
        
          // getter, setter
        
      }
    

    실행 결과

      Hibernate: 
            
          create table Member (
             MEMBER_ID bigint not null,
              city varchar(255),
              street varchar(255),
              zipcode varchar(255),
              USERNAME varchar(255),
              WORK_CITY varchar(255),
              WORK_STREET varchar(255),
              WORK_ZIPCODE varchar(255),
              endDate timestamp,
              startDate timestamp,
              TEAM_ID bigint,
              primary key (MEMBER_ID)
          )
    



임베디드 타입과 null


  • 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null



3. 값 타입과 불변 객체

값 타입 공유 참조


  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 수 있어 위험함

ex.

  • 회원1과 회원2 모두 주소의 city 값이 OldCity
  • 그런데 이때 city 값을 NewCity로 바꾸면 회원1, 회원의 값이 모두 NewCity로 바뀜

ex.


@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    // 기본 생성자 필수
    public Member() {}

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

    // getter, setter 생략
    
}
// JpaMain

Address address = new Address("city", "street", "10000");

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);

tx.commit();

Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?, ?)

이때, member1의 City 값만 바꾸려고 했는데

// JpaMain

Address address = new Address("city", "street", "10000");

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);

// member1의 City 값만 바꾸려고 이렇게 코딩했는데..
member1.getHomeAddress().setCity("newCity");

tx.commit();

실행결과 : Update 쿼리 두 번 나가서 member1, member2의 city가 모두 바뀜

Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?

💡 만약 값을 공유하는 것이 의도였다면 -> 값 타입이 아닌 엔티티를 사용해야 함



값 타입 복사


  • 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험
  • 대신 값(인스턴스)를 복사해서 사용

    ex. 회원1의 address 값을 getter로 가져와서 회원2의 address를 새로 만들어줌

// JpaMain

Address address1 = new Address("city", "street", "10000");

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address1);
em.persist(member1);

Address copyAddress = new Address(address1.getCity(), address1.getStreet(), address1.getZipcode());

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);

member1.getHomeAddress().setCity("newCity");

tx.commit();

실행 결과 : member1만 업데이트

Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?



객체 타입의 한계


그런데,, 복사 안하고 쓰는 것을 막을 수 없다

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있음
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive type)이 아니라 객체 타입
  • 자바 기본 타입에 값을 대입하면 값을 복사
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
  • 객체의 공유 참조는 피할 수 없음


💡 Java 기본 타입 vs. 객체 타입

  • 기본 타입(primitive type)

     int a = 10;
     int b = a;//기본 타입은 값을 복사
     b = 4;
    
  • 객체 타입

     Address a = new Address(Old);
     Address b = a; // 객체 타입은 참조를 전달
     b. setCity(New) // a, b가 같은 Address 인스턴스를 가리킴 -> a, b 모두 값이 바뀜
    



불변 객체


  • 값 타입을 불변 객체(immutable object)로 설계해서 부작용 가능성을 차단

💡 불변 객체

  • 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성 방법 : 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체


ex. 기존 Address에서 setter만 삭제(getter만 만듦)하거나 setter는 private으로 만들기


@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    // 기본 생성자 필수
    public Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // getter 생략

}

수정하려면 Address 객체를 새로 만들어서 넣어줘야 함

// JpaMain

Address address1 = new Address("city", "street", "10000");

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address1);
em.persist(member1);

//member1.getHomeAddress().setCity("newCity"); // setter를 만들지 않아서 setCity으로 수정하는 것 자체가 불가능
// 수정하려면 새로 만들어서 address 자체를 수정해주어야 함
Address newAddress = new Address("NewCity", address1.getStreet(), address1.getZipcode());
member1.setHomeAddress(newAddress);

tx.commit();



4. 값 타입의 비교

값 타입의 비교


  • 값 타입: 인스턴스가 달라도 값이 같으면 같은 것으로 봐야 함

      // Java primitive type
      int a = 10;
      int b = 10;
      // a == b -> true
        
      // Java 객체 타입
      Address a = new Address(서울시)
      Address b = new Address(서울시)
      // a == b -> false
    
  • a.equals(b)를 사용해서 동등성 비교

    -> 값 타입의 equals() 메소드를 적절하게 재정의(override)(주로 모든 필드 사용)

    🚨 주의

    • equals를 바로 사용하면 안되고 override를 해주고 사용해야함
    • equals 구현하면 반드시 hashCode도 override

    💡 동일성 비교 vs. 동등성 비교

    비교 상세
    동일성(identity) 비교 인스턴스의 참조 값을 비교, == 사용
    동등성(equivalence) 비교 인스턴스의 값을 비교, equals() 사용
    
      @Embeddable
      public class Address {
        
          private String city;
          private String street;
          private String zipcode;
        
          // 기본 생성자 필수
          public Address() {}
        
          public Address(String city, String street, String zipcode) {
              this.city = city;
              this.street = street;
              this.zipcode = zipcode;
          }
        
          // getter 생략
        
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              Address address = (Address) o;
              return Objects.equals(city, address.city) &&
                      Objects.equals(street, address.street) &&
                      Objects.equals(zipcode, address.zipcode);
          }
        
          @Override
          public int hashCode() {
              return Objects.hash(city, street, zipcode);
          }
      }
    
    
      public class ValueMain {
        
          public static void main(String[] args) {
        
              int a = 10;
              int b = 10;
        
              System.out.println("a == b : " + (a == b));
        
              Address address1 = new Address("city", "street", "10000");
              Address address2 = new Address("city", "street", "10000");
        
              System.out.println("address1 == address2 : " + (address1 == address2));
              System.out.println("address1 equals address2 : " + (address1.equals(address2)));
        
          }
      }
    
      a == b : true
      address1 == address2 : false
      address1 equals address2 : true
    

🚨 equals, hashCode Override 할 때, 반드시 getter 사용

  • getter 사용하지 않으면 proxy일 때 접근이 안됨



5. 값 타입 컬렉션 ⭐️

값 타입 컬렉션


  • 값 타입을 컬렉션에 담아서 쓰는 것
  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없음 -> 컬렉션을 저장하기 위한 별도의 테이블이 필요
    • 데이터베이스는 다 value로 값만 넣음

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    // 기본 생성자 필수
    public Member() {}

    @Column(name = "USERNAME")
    private String username;

    // Period
    @Embedded
    private Period workPeriod;

    // Address
    @Embedded
    private Address homeAddress;

    // collections를 값으로 가짐
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns =
        @JoinColumn(name = "MEMBER_ID")) // 매핑되는 테이블 지정, join되는 컬럼 지정(외래키 지정)
    @Column(name = "FOOD_NAME") // Member table에 들어갈 컬럼명 지정
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns =
        @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<>();

    // getter, setter 생략

}
Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )

Hibernate: 
    
    create table FAVORITE_FOOD (
       MEMBER_ID bigint not null,
        FOOD_NAME varchar(255)
    )

Hibernate: 
    
    create table ADDRESS (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )



값 타입 컬렉션 사용


  • 값 타입 저장 예제

      // JpaMain
    
      Member member = new Member();
      member.setUsername("member1");
      member.setHomeAddress(new Address("homeCity ", "street", "zipcode"));
        
      member.getFavoriteFoods().add("치킨");
      member.getFavoriteFoods().add("족발");
      member.getFavoriteFoods().add("피자");
        
      member.getAddressHistory().add(new Address("old1", "street", "zipcode"));
      member.getAddressHistory().add(new Address("old2", "street", "zipcode"));
        
      em.persist(member);
        
      tx.commit();
    
    

    실행 결과 : member만 persist 했는데, 값 타입 컬렉션도 자동으로 같이 persist

      Hibernate: 
          /* insert hellojpa.Member
              */ insert 
              into
                  Member
                  (city, street, zipcode, USERNAME, endDate, startDate, MEMBER_ID) 
              values
                  (?, ?, ?, ?, ?, ?, ?)
      Hibernate: // addressHistory에 insert 2회
          /* insert collection
              row hellojpa.Member.addressHistory */ insert 
              into
                  ADDRESS
                  (MEMBER_ID, city, street, zipcode) 
              values
                  (?, ?, ?, ?)
      Hibernate: 
          /* insert collection
              row hellojpa.Member.addressHistory */ insert 
              into
                  ADDRESS
                  (MEMBER_ID, city, street, zipcode) 
              values
                  (?, ?, ?, ?)
      Hibernate: // favoriteFoods에 insert 2회
          /* insert collection
              row hellojpa.Member.favoriteFoods */ insert 
              into
                  FAVORITE_FOOD
                  (MEMBER_ID, FOOD_NAME) 
              values
                  (?, ?)
      Hibernate: 
          /* insert collection
              row hellojpa.Member.favoriteFoods */ insert 
              into
                  FAVORITE_FOOD
                  (MEMBER_ID, FOOD_NAME) 
              values
                  (?, ?)
      Hibernate: 
          /* insert collection
              row hellojpa.Member.favoriteFoods */ insert 
              into
                  FAVORITE_FOOD
                  (MEMBER_ID, FOOD_NAME) 
              values
                  (?, ?)
    

  • 값 타입 조회 예제

      // JpaMain
    
      // 생략 : 위에 insert한 테이블과 동일
        
      em.flush();
      em.clear(); // DB에는 넣고 깔끔하게 다시 시작
        
      Member findMember = em.find(Member.class, member.getId());
        
      tx.commit();
    
    

    실행 결과 : member만 조회됨. 지연로딩 때문

    Embedded Type인 homeAddress는 조회됨

      Hibernate: 
          select
              member0_.MEMBER_ID as MEMBER_I1_6_0_,
              member0_.city as city2_6_0_, // homeAddress는 조회됨
              member0_.street as street3_6_0_,
              member0_.zipcode as zipcode4_6_0_,
              member0_.USERNAME as USERNAME5_6_0_,
              member0_.endDate as endDate6_6_0_,
              member0_.startDate as startDat7_6_0_ 
          from
              Member member0_ 
          where
              member0_.MEMBER_ID=?
    
    • 값 타입 컬렉션도 지연 로딩 전략 사용
    • 값 타입 컬렉션도 불러오려면 따로 조회해야 함

        // JpaMain
      
        Member findMember = em.find(Member.class, member.getId());
              
        List<Address> addressHistory = findMember.getAddressHistory();
        for (Address address : addressHistory) {
            System.out.println("address = " + address.getCity());
        }
              
        Set<String> favoriteFoods = findMember.getFavoriteFoods();
        for (String favoriteFood : favoriteFoods) {
            System.out.println("favoriteFood = " + favoriteFood);
        }
              
        tx.commit();
      
      

      실행 결과

      
        Hibernate: 
            select
                member0_.MEMBER_ID as MEMBER_I1_6_0_,
                member0_.city as city2_6_0_,
                member0_.street as street3_6_0_,
                member0_.zipcode as zipcode4_6_0_,
                member0_.USERNAME as USERNAME5_6_0_,
                member0_.endDate as endDate6_6_0_,
                member0_.startDate as startDat7_6_0_ 
            from
                Member member0_ 
            where
                member0_.MEMBER_ID=?
        Hibernate: 
            select
                addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
                addresshis0_.city as city2_0_0_,
                addresshis0_.street as street3_0_0_,
                addresshis0_.zipcode as zipcode4_0_0_ 
            from
                ADDRESS addresshis0_ 
            where
                addresshis0_.MEMBER_ID=?
        address = old1
        address = old2
        Hibernate: 
            select
                favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
                favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_ 
            from
                FAVORITE_FOOD favoritefo0_ 
            where
                favoritefo0_.MEMBER_ID=?
        favoriteFood = 족발
        favoriteFood = 치킨
        favoriteFood = 피자
      
  • ⭐️ 값 타입 수정 예제

      // JpaMain
    
      Member findMember = em.find(Member.class, member.getId());
        
      // 값 타입 수정
      // homeCity -> newCity
      // findMember.getHomeAddress().setCity("newCity"); // 안됨!! 값타입은 immutable 해야 함
      // 새로 Address를 만들어서 넣어주기
      Address a = findMember.getHomeAddress();
      findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
        
      // 값 타입 컬렉션 수정
      // 치킨 -> 한식 : remove 하고 새로 add 해줘야 함
      findMember.getFavoriteFoods().remove("치킨");
      findMember.getFavoriteFoods().add("한식");
        
      tx.commit();
    
    

    실행 결과

    
      Hibernate: 
          select
              member0_.MEMBER_ID as MEMBER_I1_6_0_,
              member0_.city as city2_6_0_,
              member0_.street as street3_6_0_,
              member0_.zipcode as zipcode4_6_0_,
              member0_.USERNAME as USERNAME5_6_0_,
              member0_.endDate as endDate6_6_0_,
              member0_.startDate as startDat7_6_0_ 
          from
              Member member0_ 
          where
              member0_.MEMBER_ID=?
      Hibernate: 
          select
              favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
              favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_ 
          from
              FAVORITE_FOOD favoritefo0_ 
          where
              favoritefo0_.MEMBER_ID=?
      Hibernate: 
          /* update
              hellojpa.Member */ update
                  Member 
              set
                  city=?,
                  street=?,
                  zipcode=?,
                  USERNAME=?,
                  endDate=?,
                  startDate=? 
              where
                  MEMBER_ID=?
      Hibernate: 
          /* delete collection row hellojpa.Member.favoriteFoods */ delete 
              from
                  FAVORITE_FOOD 
              where
                  MEMBER_ID=? 
                  and FOOD_NAME=?
      Hibernate: 
          /* insert collection
              row hellojpa.Member.favoriteFoods */ insert 
              into
                  FAVORITE_FOOD
                  (MEMBER_ID, FOOD_NAME) 
              values
                  (?, ?)
    

    예제. 값 타입 컬렉션 수정

      // JpaMain
    
      Member findMember = em.find(Member.class, member.getId());
        
      // 만약 Address의 city 중 old1만 newCity으로 바꾸고, old2는 유지하고 싶다면
      // 마찬가지로 지우고 다시 넣어줘야 함
      // remove는 equals로 작동 -> equals, hashCode 제대로 override 해두지 않으면 꼬임!!
      findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
      findMember.getAddressHistory().add(new Address("newCity", "street", "10000"));
        
      tx.commit();
    
    

    실행 결과 : member_id에 해당하는 address를 모두 삭제하고, 수정하는 값, 유지하는 값 모두 다시 insert

      Hibernate: 
          select
              member0_.MEMBER_ID as MEMBER_I1_6_0_,
              member0_.city as city2_6_0_,
              member0_.street as street3_6_0_,
              member0_.zipcode as zipcode4_6_0_,
              member0_.USERNAME as USERNAME5_6_0_,
              member0_.endDate as endDate6_6_0_,
              member0_.startDate as startDat7_6_0_ 
          from
              Member member0_ 
          where
              member0_.MEMBER_ID=?
      Hibernate: 
          select
              addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
              addresshis0_.city as city2_0_0_,
              addresshis0_.street as street3_0_0_,
              addresshis0_.zipcode as zipcode4_0_0_ 
          from
              ADDRESS addresshis0_ 
          where
              addresshis0_.MEMBER_ID=?
      Hibernate: 
          /* delete collection hellojpa.Member.addressHistory */ delete 
              from
                  ADDRESS 
              where
                  MEMBER_ID=?
      Hibernate: 
          /* insert collection
              row hellojpa.Member.addressHistory */ insert 
              into
                  ADDRESS
                  (MEMBER_ID, city, street, zipcode) 
              values
                  (?, ?, ?, ?)
      Hibernate: 
          /* insert collection
              row hellojpa.Member.addressHistory */ insert 
              into
                  ADDRESS
                  (MEMBER_ID, city, street, zipcode) 
              values
                  (?, ?, ?, ?)
      Hibernate: 
          /* insert collection
              row hellojpa.Member.addressHistory */ insert 
              into
                  ADDRESS
                  (MEMBER_ID, city, street, zipcode) 
              values
                  (?, ?, ?, ?)
    
  • 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다



값 타입 컬렉션의 제약사항


  • 값 타입은 엔티티와 다르게 식별자 개념이 없으므로 값은 변경하면 추적이 어려움
  • ⭐️ 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장
    • 따라서 쓰지 말자 ^^
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함 : null 입력X, 중복 저장X



값 타입 컬렉션 대안


  • 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용

ex. AddressEntity


@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    // 기본 생성자 필수
    public Member() {}

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns =
    @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    // 값 타입이 아닌 Entity로 매핑
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();

    // getter, setter 생략

}

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    // 기본 생성자 필수
    public Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }

    // getter, setter 생략

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) &&
                Objects.equals(street, address.street) &&
                Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}

@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id @GeneratedValue
    private Long id;

    private Address address;

    // 생성자
    public AddressEntity(String city, String street, String zipcode) {
        this.address = new Address(city, street, zipcode);
    }

    public AddressEntity(Address address) { this.address = address; }

    // getter, setter 생략

}
// JpaMain

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity ", "street", "zipcode"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().add(new AddressEntity("old1", "street", "zipcode"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "zipcode"));

em.persist(member);

em.flush();
em.clear(); // DB에는 넣고 깔끔하게 다시 시작

System.out.println("=========== START ==========");
Member findMember = em.find(Member.class, member.getId());

tx.commit();

실행 결과

Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (city, street, zipcode, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.AddressEntity
        */ insert 
        into
            ADDRESS
            (city, street, zipcode, id) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.AddressEntity
        */ insert 
        into
            ADDRESS
            (city, street, zipcode, id) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* create one-to-many row hellojpa.Member.addressHistory */ update
        ADDRESS 
    set
        MEMBER_ID=? 
    where
        id=?
Hibernate: 
    /* create one-to-many row hellojpa.Member.addressHistory */ update
        ADDRESS 
    set
        MEMBER_ID=? 
    where
        id=?
Hibernate: 
    /* insert collection
        row hellojpa.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.Member.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
=========== START ==========
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

  • Address에 ID가 있음 : 값 타입이 아니라 자체로 Entity
  • MEMBER_ID라는 FK로 연결



Leave a comment