Spring Framework/[인프런] Java ORM 표준 프로그래밍 - JPA

Chapter 09. 값 타입

계란💕 2022. 9. 7. 17:25

9.1 기본값 타입 

 

JPA의 데이터 타입 분류

  • 엔티티 타입(entity type)
    • 엔티티 타입이란 @Entity로 정의하는 클래스 객체를 말한다.
    • 데이터가 변해도 식별자로 지속해서 추적 가능하다
    • ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능하다.
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경 시 추적이 불가능하다
    • ex) 숫자 100 => 200 바꾸면 완전히 다른 값으로 대체
      1. 기본값 타입: 자바 기본 타입(int, double) / 래퍼 클래스 / String
        • 생명 주기를 엔티티에 의존한다.
        • ex) 회원 삭제하면 이름, 나이도 함께 삭제
        • 값 타입은 공유하면 안 된다. ex) 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안된다. (부수 효과)
          • cf) 자바의 기본 타입은 절대 공유되지 않는다.
          •  기본 타입은 항상 값을 복사한다.
          • Integer같은 래퍼 클래스나 String같은 특수한 클래스는 공유 가능한 객체이지만 변경할 수 없다.
      2. 임베디드 타입(embedded type, 복합 값 타입): 새로운 클래스를 만들어서 사용하는 경우
      3. 컬렉션 값 타입 (collection value type): List 처럼 뭔가 담을 수 있는 형태

 

 

 

9.2 임베디드 타입 

 

임베디드 타입(Embedded type, 내장 타입, 복합 값 타입)

  • 새로운 값 타입을 직접 정의 가능하다
  • JPA는 임베디드 타입이다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
  • 추적이 안 되고 변경하면 끝이다.

 

 

임베디드 타입 예시

  • 회원 엔티티는 이름, 근무 기간, 집 주소를 가진다.

 

 

임베디드 타입 사용법

  • @Embeddable: 값 타입을 정의하는 곳에 표시한다.
  • @Embedded: 값 타입을 사용하는 곳에 표시한다.
  • @Embeddable, @Embedded 둘 중 하나만 넣어도 되지만 둘 다 넣는 걸 권장한다.
  • 기본 생성자가 필수
  • 임베디드 타입의 장점
    • 재사용성 
    • 높은 응집도
    • Period.isWork() 처럼 해당 값 타입만 사용하는 의미있는 매소드를 만들 수 있다.
    • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명 주기를 의존한다.

 

 

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

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게 (find-grained) 매핑하는게 가능하다.
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다. value type이 많이 튀어나온다.

 

 

임베디드 타입과 연관 관계 

 

 

임베디드 타입과 null

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

 

 

  Ex) 임베디드 타입

  • Member 클래스
    • 여기에서 기간 / 주소 끼리 속성을 묶으려면?
    • 아래처럼 따로 빼버린다.
    • @Embeddable을 붙여서 값 타입임을 보여준다.
<hide/>
package hellojpa;
import java.time.LocalDateTime;
import javax.persistence.Embeddable;

@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public LocalDateTime getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDateTime startDate) {
        this.startDate = startDate;
    }

    public LocalDateTime getEndDate() {
        return endDate;
    }

    public void setEndDate(LocalDateTime endDate) {
        this.endDate = endDate;
    }
}

 

<hide/>
package hellojpa;
import javax.persistence.Embeddable;
@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
    
    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name= "MEMBER_ID")
    private Long id;

    @Column(name= "USERNAME")   // DB에 넣고 싶은 값
    private String userName;    // 객체에 주는 값

    //기간
    @Embedded
    private Period workPeriod;

    // 주소
    @Embedded
    private  Address homeAddress;
}

  Note) 실행 결과 - embed 한 객체들이 모두 Member클래스의 필드로  들어간다.

 

 

  Ex) 같은 엔티티가 중복되면?

<hide/>
@Embedded
private  Address homeAddress;

@Embedded
private  Address WORKAddress;

 

  Note) 실행 결과 - MappingException

  • Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: hellojpa.Member column: city (should be mapped with insert="false" update="false")
    • 이를 해결하기 위해 @AttributeOverride(s)를 사용한다.

 

  • 따라서, 멤버 클래스에 다음과 같이 애너테이션 @AttributeOverrides을 추가한다.
    • 그러면 상단의 homeAddress의 속성 city / street / zipcode는 이름 그대로 들어가고 workAddress는 아래에서 정한대로 column name이 들어간다.
    • @AttributeOverride의 name 안에는 Address에서 정의한 필드명이 그대로 들어가야한다.
<hide/>
@Embedded
private  Address homeAddress;

@Embedded
@AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),     // DB 컬럼을 따로 매핑한다.
        @AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE"))
})
private  Address workAddress;

  Note) 실행 결과

  •  create table member을 보면 새로 오버라이드 한 것과 기존에 쓴 것 모두 나온다.

 

 

  Ex) 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null 이다.

<hide/>
@Embedded
private Period workPeriod = null;

 

 

 

9.3 값 타입과 불변 객체

 

값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
  •  부작용(side effect) 발생 가능성 때문이다.

 

 

  Ex) 값 타입 공유 참조 - 부작용

<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
            Address address = new Address("city", "street", "200-000");

            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.getHomeAddress().setCity("newCity");
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과 - 업데이트 쿼리 두 번 실행된다.

 

 

 

값 타입 복사

  • 위의 예제에서 봤듯이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다.
  • 따라서, 값(인스턴스)을 복사해서 사용한다.

 

 

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
  • 객체의 공유 참조는 피할 수 없다.
    • ex) 위 사진에서 a, b는 Address라는 같은 인스턴스를 가리키므로 둘다 "New"로 바뀐다.

 

 

불변 객체(immutable object)

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단 가능
  • 값 타입은 불변 객체(immutable object, 생성 시점 이후로 절대 값을 변경할 수 없는 객체)로 설계해야한다.
  • 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다. 또는 private으로 만든다.
  • ex) Ingeger, String은 불변 객체

 

 

  Ex) 그럼에도 불구하고 바꾸려면?

  • 아래처럼 newAddress를 새로 넣어야 한다.
<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
            Address address = new Address("city", "street", "200-000");
            Member member = new Member();
            member.setUserName("member");
            member.setHomeAddress(address);
            em.persist(member);

            Address newAddress = new Address(address.getCity(), address.getStreet(),
                address.getZipcode());
            member.setHomeAddress(newAddress);
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

 

9.4 값 타입의 비교

 

값 타입의 비교

  • 값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
  • 동일성 비교(identity) 비교: 인스턴스의 참조값을 비교한다. '=='
  • 동등성 비교(equivalence) 비교: 인스턴스의 을 비교, equals() 
  • 값 타입을 equals()을 사용해서 동등성 비교를 해야한다.
  • 값 타입의 equals()를 적절하게 재정의한다. (주로 모든 필드사용) 
    • equals() 를 구현하면 그에 맞게 hashcode()도 꼭 재정의한다.

 

 

  Ex) Address에 다음과 같이 두 메서드를 override한다.

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

@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) ;
}

  Note) 실행 결과

 

 

 

9.5 값 타입 컬렉션 

 

값 타입 컬렉션

  • 값 타입을 컬렉션에 담아 쓰는 걸 말한다. 하나 이상 저장할 때 사용한다.
  • @ElementCollection, @CollectionTable 사용한다.
    • @ElementCollection
    • @CollectionTable 
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
    • 컬렉션은 일대다라서 데이터베이스에는 하나의 테이블에 넣을 수 있는 방법이 없다. (그래서 별도 테이블 필요)
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요한다.
  • 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.
  • 데이터베이스는 보통 value만 넣을 수 있다. 요즘에야 Json 지원하기도 한다.
    • 컬렉션은 "일대다" 개념이다. 
    • member의 주소 이력을 저장할 때, 별도의 테이블을 뽑아서 저장 가능하다.
    • 테이블 FAVORITE_FOOD 의 기본 키: MEMBER_ID + FOOD_NAME
    • 테이블 ADDRESS의 기본 키: MEMBER_ID + CITY + STREET + ZIPCODE 모두 묶어서 PK를 쓴다.
    • ADDRESS, FAVORITE_FOOD 에 Long id를 기본 키로 넣으면? 
      • 값 타입이 아닌 엔티티가 되버린다. 

 

 

  Ex) 값 타입 저장

  • 멤버 
    • 둘다 MEMBER_ID로 조인한다.
<hide/>
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name ="MEMBER_ID"))
@Column(name ="FOOD_NAME")  // 값이 하나이고 내가 정의한 게 아니라서 테이블 이름으로 이렇게 지정 가능 - 예외적
private Set<String> favoriteFoods = new HashSet<>();

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

 

  Note) 실행 결과

  • MEMBER에 주소 3가지 정보가 들어간다.
  • address 테이블에도 멤버ID(외 래키)와 세가지 주소가 들어간다.

 

 

 

 

  Ex) 값 타입 저장

<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 멤버가 먼저 INSERT
  • 주소는 3개 food는 2개 INSERT 된다.
  • 값 타입을 따로 persist()하지 않았는데 member를 persist()하니까 값 타입 컬렉션도 함께 저장된다.
  • 값 타입은 라이프 사이클이 따로 없다. 따라서 생명 주기가 member에 소속된다.
<hide/>
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (city, street, zipcode, USERNAME, MEMBER_ID) 
        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
            (?, ?, ?, ?)
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
            (?, ?)

 

 

 

  Ex) 값 타입 조회 - LAZY (디폴트)

<hide/>
package hellojpa;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 멤버 테이블만 가져오고 끝난다. => 컬렉션들은  "지연 로딩"
  • Address는 멤버에 소속된 값 타입이라서 같이 들어온다.

 

 

  • 멤버 외에 다른 테이블도 가져오도록 하려면? 
    • addressHistory 추가
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            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();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 이전 예제에 컬렉션 addressHistory 리스트를 추가했다.
  • history 테이블 가져와서 주소 정보 찍는다.
  • food 테이블 가져와서 food 정보를 찍는다.
  • @ElementCollection의 fetch type이 LAZY로 기본 설정되어 있기 때문이다.
<hide/>
================ START =================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_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_2_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자

 

 

 

  Ex) 값 타입 수정의 잘못된 예시

  • 값 타입은 불변이어야한다.
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());
            findMember.getHomeAddress().setCity("newCity");
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과

  • 그런데 값 타입은 부작용(side effect)이 생길 수 있어서 이런 식으로 수정하면 절대 안된다.
<hide/>
================ START =================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=? 
        where
            MEMBER_ID=?

 

 

 

  Ex) 값 타입 수정의 올바른 예시

  • Address 자체를 통으로 갈아 끼워야한다.
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());

//            findMember.getHomeAddress().setCity("newCity");
            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 값 타입은 추적도 안되니까 완전히 교체를 해줘야한다.
<hide/>
================ START =================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=? 
        where
            MEMBER_ID=?

 

 

 

  Ex) 값 타입 컬렉션 업데이트

<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());

//            findMember.getHomeAddress().setCity("newCity");
            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));

            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

  Note) 실행 결과

  • 컬렉션의 값만 변경해도 실제 데이터베이스 쿼리가 날아가고 JPA가 데이터를 바꿔준다.
  • MEMBER_ID, FOOD_NAME 찾아서 DELETE 한 다음에 데이터를 새로 집어 넣는다.
<hide/>
================ START =================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=? 
        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
            (?, ?)

 

 

 

값 타입 컬렉션의 제약 사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 그래서 값을 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블을 모든 컬럼을 묶어서 기본 키를 구성해야한다 => null 입력 X, 중복 저장
  • ex) addressHistory를 지우는 경우, 멤버 아이디와 관련한  정보를 DB에서 모두 지운다.
    • 그 다음 컬렉션에 최종으로 남은 것들에 대해서만 INSERT 실행한다.

 

 

  Ex) 주소 바꾸기 - 값 타입 컬렉션의 제약 사항

  • 컬렉션은 대상을 찾을 때,  기본적으로 equals()를 쓰기 때문에 괄호 안에 완전히 같은 값을 넣어준다.
    • 전에 오버라이드한 equals(), hashcode() 를 사용
<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new Address("old1", "street", "100-000"));
             member.getAddressHistory().add(new Address("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());

//            findMember.getHomeAddress().setCity("newCity");
            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));

            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");

            findMember.getAddressHistory().remove(new Address("old1", "street", "100-000"));
            findMember.getAddressHistory().add(new Address("newCity1", "street", "100-000"));
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과

  • 순서는 JPA가 알아서 고른다.
  • addressHistory를 DELETE 하는데 ADDRESS 테이블을 통으로 지운다. 멤버 아이디 기준으로
    • 그러고나서 address  관련 INSERT가 두 번 날아간다.
    • old1을 remove() 하고나면 old2, newCity1은 남아 있을 것이다. 이 두 개를 테이블을 완전히 갈아끼운 것이다.
    • 왜 INSERT 가 두 번 날아갈까?
<hide/>
================ START =================
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_2_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_2_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.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: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=? 
        where
            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: 
    /* 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
            (?, ?)

 

 

 

 

  cf) addressHistory 에 다음 애너테이션 OrderColumn을 넣는 것도 위험하다.

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

 

 

 

값 타입 컬렉션 대안

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

 

 

  Ex) 대안 - 값 타입을 엔티티로 승급

 

  • Address는 값 타입, AddressEntity는 엔티티
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {

    @Id @GeneratedValue
    private Long id;
    private Address address;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Address getAddress() {
        return address;
    }
    public void setAddress(Address address) {
        this.address = address;
    }
}

 

  • 다음과 같이 멤버의 addressHistory를 바꾼다.
<hide/>
// 기존 코드
//    @ElementCollection
//    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name ="MEMBER_ID"))
//    private List<Address> addressHistory = new ArrayList<>();

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

 

<hide/>
package hellojpa;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try{
             Member member = new Member();
             member.setUserName("member1");
             member.setHomeAddress(new Address("homeCity", "street", "100-000"));
             member.getFavoriteFoods().add("치킨");
             member.getFavoriteFoods().add("족발");
             member.getFavoriteFoods().add("피자");
             member.getAddressHistory().add(new AddressEntity("old1", "street", "100-000"));
             member.getAddressHistory().add(new AddressEntity("old2", "street", "100-000"));
             em.persist(member);
             em.flush();
             em.clear();

            // DB에 데이터 있는 상태에서 조회한다.
            System.out.println("================ START =================");
            Member findMember = em.find(Member.class, member.getId());

//            findMember.getHomeAddress().setCity("newCity");
//            Address a = findMember.getHomeAddress();
//            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
//
//            findMember.getFavoriteFoods().remove("치킨");
//            findMember.getFavoriteFoods().add("한식");

//            findMember.getAddressHistory().remove(new Address("old1", "street", "100-000"));
//            findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "100-000"));
            tx.commit();

        }catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }finally {
            em.close();
        }
        emf.close();    // 팩토리를 나중에 닫는다.
    }
}

 

  Note) 실행 결과

  • 일대다 단방향 매핑은 update 쿼리가 나간다. (다른 테이블에 FK 가 있기 때문)
  • 값 타입으로 쓸 때보다 훨씬 유용하다.
<hide/>
    /* 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_4_0_,
        member0_.city as city2_4_0_,
        member0_.street as street3_4_0_,
        member0_.zipcode as zipcode4_4_0_,
        member0_.USERNAME as USERNAME5_4_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
  • 데이터베이스를 보면 ADDRESS에 ID라는 개념이 생겼다.
  • 자체적인 ID가 있다는 것은  값 타입이 아니라 엔티티라는 뜻이다. 수정 가능 
    • => 값 타입을 엔티티로 승급

 

 

엔티티와 값 타입 비교

  • 엔티티 타입의 특징: 식별자 있음 / 생명 주기 관리 / 공유
  • 값 타입의 특징: 식별자 없음 / 생명 주기를 엔티티에 의존 / 공유하지 않는게 안전 (또는 복사해서 사용) / 불변 객체로 만드는 게 안전하다.

 

 

 

9.6 실전 예제 6 - 값 타입 매핑

값 타입 매핑

 

  cf) 

  • equals(), hashcode()를 재정의하는 경우에 필드에 직접 접근하면 프록시일 때 문제가 생긴다.
  • getter()로 접근해야 프록시인 경우에 문제가 안 생긴다.

 

 

  Ex)

  • jpa-shop
  • Member
<hide/>
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Member extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private  String name;
//    private String city;
//    private String street;
//    private String zipcode;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

    public Long getId() {return id;
    }

    public void setId(Long id) {this.id = id;
    }

    public String getName() {return name;
    }

    public void setName(String name) {this.name = name;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public List<Order> getOrders() {
        return orders;
    }

    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }
}

 

  • Address
<hide/>
package jpabook.jpashop.domain;
import java.util.Objects;
import javax.persistence.Embeddable;

@Embeddable
public class Address {

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

    public String getCity() {
        return city;
    }

    private void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    private void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    private void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    @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(getCity(), address.getCity()) && Objects.equals(
            getStreet(), address.getStreet()) && Objects.equals(getZipcode(),
            address.getZipcode());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCity(), getStreet(), getZipcode());
    }
}

 

  • Delivery
<hide/>
package jpabook.jpashop.domain;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;

@Entity
public class Delivery extends BaseEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address address;

    private DeliveryStatus status;

    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public DeliveryStatus getStatus() {
        return status;
    }

    public void setStatus(DeliveryStatus status) {
        this.status = status;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }
}

 

  Note) 실행 결과

  • 주소 관련된 필드 3개가 공통으로 들어간다.
  • Address 클래스에 fullAddress() 와 같은 메서드를 추가하거나 @Column(length= "제한 길이")를 넣어서 쓸 수도 있다.
<hide/>

create table Delivery (
   id bigint not null,
    createdBy varchar(255),
    createdDate timestamp,
    lastModifiedBy varchar(255),
    lastModifiedDate timestamp,
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    status integer,
    primary key (id)
)

====중간 생략 =====

create table Member (
   MEMBER_ID bigint not null,
    createdBy varchar(255),
    createdDate timestamp,
    lastModifiedBy varchar(255),
    lastModifiedDate timestamp,
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    name varchar(255),
    primary key (MEMBER_ID)
)

 

 

 

출처 - https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com