쿠릉쿠릉 쾅쾅 2022. 3. 31. 05:01
728x90

 

 

기본값 타입

  • JPA 데이터 타입은 2가지로 분류된다.
    • 엔티티 타입
    • 값 타입

 

1. JPA의 데이터 타입 분류

  • 엔티티 타입
  • 값 타입
    •  

🔍 엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자로 지속해서 추적할 수 있다.
    • 예) 회원 엔티티의 나이, 키 값을 변경해도 식별자로 인식할 수 있다. 

🔍 값 타입

  • 값타입은 int / Integer / String처럼 단순히 값으로 사용하는 자바 기본 타입 또는 객체다.
  • 식별자가 없고 값만 있으면 변경시 추적이 불가능하다.
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체된다.
  • 값 타입은 절대 공유가 되어선 안된다.
    • 회원 이름 변경시 다름 회원의 이름도 함께 변경되면 안된다.
  • 값 타입의 생명 주기는 엔티티에 의존한다.
    • 회원이 삭제되면 이름, 나이 필드도 함께 삭제된다.

💡 값 타입 분류

  • 기본 값 타입
    • 자바 기본 타입 (int, double)
    • 래퍼 클래스 (Integer, Long)
    • String
  • 임베디드 타입 (embedded type, 복합 값 타입)
  • 컬렉션 값 타입 (collection value type)

✔ 기본값 타입
기본 타입은 공유가 되지 않는다. 공유되지 않고 값이 복사가 된다. 그래서 기본 값은 사용했을 때 사이드 이펙트가 일어나지 않아서 안전하다.

int a = 5;
int b = 1;
b = a;
a = 10;

System.out.println(a);  // 10
System.out.println(b);  // 5
  • b = a 에서 b의 값을 a 값으로 바꿨다. 그리고 a의 값을 10으로 바꿨다.
  • 출력해보면 a10이다. b5다.
  • b = a 에서 a의 값이 복사되어 b의 값이 된 것이다. a가 공유된 것이 아니다.
  • 그러므로 a 값을 자유롭게 바꿔도 b 값은 변경이 일어나지 않는다.
  • 기본 값은 안전하게 쓸 수 있다.

특수한 클래스(래퍼 클래스, String 클래스)도 공유가 가능하면서 값이 바뀌지 않는다.

String a = "abc";
String b = "ABC";
b = a;
a = "xyz";

System.out.println(a);  // xyz
System.out.println(b);  // abc
  • String 클래스 / 래퍼 클래스는 객체이므로 공유가 가능하다.
  • 하지만 기본 타입(primitive type)처럼 a 값을 바꿔도 b의 값이 바뀌지 않는다.
  • 그래서 안전하게 쓸 수 있다.

 


 

임베디드 타입 (embedded type)

  • 새로운 값 타입을 직접 정의할 수 있다.
  • 내장타입 또는 임베디드 타입 또는 복합 값 타입 로 불린다.
  • 임베디드 타입은 int / String 처럼 값 타입이다. 엔티티가 아니다.
  • 기본 생성자가 필수로 존재해야 한다.
  • 임베디드 타입의 값이 null이면 매핑한 칼럼 값도 모두 null이다.

 

1. 임베디드 타입 사용법

  • @Embeddable : 값 타입을 정의하는 곳에 사용한다.
    • 주로 임베디트 타입의 해당 클래스에 쓰인다.
  • @Embedded : 값 타입을 사용하는 곳에 사용한다.
    • 주로 엔티티 내에서 쓰인다.
  • @Embeddable 애노테이션을 사용할 경우 @Embedded 애노테이션을 생략할 수 있으나 둘 다 같이 명시하는 것을 권장한다.

💡 예시

멤버 엔티티의 속성들이다.

속성들을 분류할 수 있다. startDateendDate 속성은 기간의 의미를 담고있는 Period 임베디드로 생성할 수 있다.
city / street / zipcode 속성은 주소를 의미하는 Adddress 임베디드로 생성할 수 있다.

위 사진처럼 저렇게 임베디드를 2개로 생성할 수 있다.

📌 MemberV2 엔티티

package hellojpa.domain2;

import javax.persistence.*;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // 기간 Period
    @Embedded
    private Period workPeriod;

    // 주소 Address
    @Embedded
    private Address homeAddress;
}
  • 임베디드 타입에 @Embedded 애노테이션을 붙인다.

📌 Address 임베디드

package hellojpa.domain2;

import javax.persistence.Embeddable;

@Embeddable
@Getter @Setter
public class Address {

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

    protected Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}
  • 임베디드 타입에 기본 생성자는 필수다.
  • 엠베디드 타입 클래스에 @Embeddable 애노테이션을 붙인다.

📌 Period 임베디드 타입

package hellojpa.domain2;

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

@Embeddable
@Getter @Setter
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public Period() {
    }
    public Period(LocalDateTime startDate, LocalDateTime endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public boolean isWork() {
        ...
    }
}
  • 임베디드 타입에 기본 생성자는 필수다.
  • 엠베디드 타입 클래스에 @Embeddable 애노테이션을 붙인다.
  • isWork() 메서드 처럼 임베디드 타입 클래스 내에서 필드값을 통해 의미있는 메서드를 만들 수 있다.

📌 메인 메서드

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;
import hellojpa.domain2.Period;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.time.LocalDateTime;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("hello");
            member.setHomeAddress(new Address("city", "street", "10000"));

            LocalDateTime startDate = LocalDateTime.of(2022, 3, 20, 12, 00, 00);
            LocalDateTime endDate = LocalDateTime.now();

            member.setWorkPeriod(new Period(startDate, endDate));

            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

  • 임베디드 타입을 사용해도 사용안한 것 처럼 정상적으로 테이블이 생성된다.

 

2. 임베디드 타입 장점

  • 재사용성이 높다.
  • 높은 응집도
  • 예를 들어서 Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있다.
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.

 

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

  • 임베디드 타입은 그저 엔티티의 값이다.
  • 임베디드 타입은 사용하기 매핑하는 테이블은 같다.
  • 임베디드를 통해 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다.
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스 수가 더 많다.

 

4. 임베디드 타입과 연관관계

  • 임베디드 타입이 다른 임베디드 타입을 가질 수 있다.
  • 임베디드 타입이 엔티티를 가질 수 있다.
    • 엔티티의 외래키를 가지고 있으면 되기 때문이다.
package hellojpa.domain2;

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

@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    
    // 임베디드가 엔티티 소유
    private Item item;
    
    // @Column 사용
    @Column(name="CODE")
    private Code code;
}

이런식으로 임베디드가 엔티티를 소유할 수 있다.
엔티티 타입 클래스 안에서 @Column도 사용할 수 있다.

 

5. 임베디드 타입 내 제약 생성

package hellojpa.domain;

import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class AddressEmd {

    @Column(length = 10)
    private String city;
    
    @Column(length = 20)
    private String street;
    
    @Column(length = 5)
    private String zipcode;
    
    public void isValid() {
        ....
    }

}
  • @Column 애노테이션을 통해 임베디드 타입의 각 속성마다 제약을 걸수 있다.
  • 검증 조건을 체크하는 isValid() 메서드 처럼 의미있는 메서드를 만들 수 있다.
  • 제약 조건이나 검증 조건이 있을 경우 임베디트로 통합적으로 관리해주는게 좋다. 

 

6. @AttributeOverride : 속성 재정의

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

📌 MemberV2 엔티티

package hellojpa.domain2;

import javax.persistence.*;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
   
    // 회사 주소 Address
    @Embedded
    private Address workAddress;

    // 집주소 Address
    @Embedded
    private Address homeAddress;
}
  • 이렇게 workAddresshomeAddress 처럼 집 주소와 회사 주소를 모두 포함하고 싶을 때 문제가 있다.
  • 바로 집 주소와 회사 주소의 임베디드 타입이 같다는 것이다. 그러면 칼럼명이 중복된다.
  • 그래서 Repeated column in mapping for entity 예외가 발생한다.
  • 이럴 떄 @AttributeOverride / @AttributeOverrides 애노테이션을 사용하여 속성 재정의를 해야한다.

@AttributeOverrides 사용하여 해결하기

 @AttributeOverride : 하나의 칼럼의 속성을 재정의할 경우 사용
 @AttributeOverrides : 여러 칼럼의 속성을 재정의할 경우 사용

package hellojpa.domain2;

import javax.persistence.*;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
   
    // 회사 주소 Address
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column = @Column(name = "w_city")),
        @AttributeOverride(name = "street", column = @Column(name = "w_street")),
        @AttributeOverride(name = "zipcode", column = @Column(name = "w_zipcode"))
    })
    private Address workAddress;

    // 집주소 Address
    @Embedded
    private Address homeAddress;
}
  • 속성을 재정의하여 컬렴 명 중복을 피할 수 있다.

 


 

값 타입과 불변 객체

  • 값 타입은 복잡한 객체 세상을 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

 

1. 값 타입 공유 참조

  • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
  • 주소의 city 값을 NewCity로 바꿔버리면 공유하고 있는 회원1, 회원2의 주소의 city값이 모두 바뀌는 부작용(Side Effect)이 발생한다.

🔍 공유 객체의 값 변경 전

📌 MemberV2 엔티티

package hellojpa.domain2;

import javax.persistence.*;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    // 집 주소 Address
    @Embedded
    private Address homeAddress;
}

📌 메인 메서드

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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", "10000");

            MemberV2 member1 = new MemberV2();
            member1.setUsername("member1");
            member1.setHomeAddress(address);

            MemberV2 member2 = new MemberV2();
            member2.setUsername("member2");
            member2.setHomeAddress(address);

            em.persist(member1);
            em.persist(member2);

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

  • member1.setHomeAddress(address)member2.setHomeAddress(address) 를 통해 member1member2는 서로 같은 주소 타입 객체를 공유하고 있다.

🔍 공유 객체의 값 변경 후

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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", "10000");

            MemberV2 member1 = new MemberV2();
            member1.setUsername("member1");
            member1.setHomeAddress(address);

            MemberV2 member2 = new MemberV2();
            member2.setUsername("member2");
            member2.setHomeAddress(address);

            // member1의 주소 타입의 도시 값 수정
            member1.getHomeAddress().setCity("NewCity");

            em.persist(member1);
            em.persist(member2);

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

  • member1의 주소 타입의 city을 바꿨더니 member2의 주소 타입의 city도 같이 변경되었다.
  • 이처럼 공유 객체를 사용하면 Side Effect가 발생할 우려가 있기 때문에 공유 객체를 사용하면 안된다.

 

2. 값 타입 복사

  • 공유 객체를 사용하지 말고 값을 복사해서 사용해야 한다.

📌 메인 메서드

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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 m1Address = new Address("city", "street", "10000");
            
            // 값 복사
            Address m2Address = new Address(
                    m1Address.getCity(),
                    m1Address.getStreet(),
                    m1Address.getZipcode());

            MemberV2 member1 = new MemberV2();
            member1.setUsername("member1");
            member1.setHomeAddress(m1Address);

            MemberV2 member2 = new MemberV2();
            member2.setUsername("member2");
            member2.setHomeAddress(m2Address);

            // member1의 주소 타입의 도시 값 수정
            member1.getHomeAddress().setCity("NewCity");

            em.persist(member1);
            em.persist(member2);

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

  • 값을 공유하지 않고 복사했기 때문에 member1의 도시 값을 바꿔도 member2는 영향을 받지 않았다.

 

3. 객체 타입의 한계

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

 

4. 불변객체

  • 불변 객체는 생성 시점 이후 절대 값을 변경할 수 없는 객체다.
  • 값 타입의 모든 객체를 모두 불변 객체로 만들어 부작용(=Side Effect)을 막아야한다.
  • 만약에 불변 객체의 값을 변경하고 싶다면 생성자롤 다시 만들어서 새로운 불변객체를 생성해야 한다.

🔍 불변 객체 만드는 방법

  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않거나 private로 만들면 된다.
  • Integer, String은 자바가 제공하는 대표적인 불변 객체다.

 


 

값 타입의 비교

  • 값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
  • 값 타입은 equals() 메서드를 사용해서 동등성 비교를 해야한다.
  • 실무에서 값 타입을 비교하는 경우가 별로 없지만 비교할 경우가 생기면 equlas() hashCode()를 재정의해서 사용해야 한다.

 

1. 동일성(identity) 비교

  • 인스턴스의 참조 값을 비교한다.
  • == 사용

 

2. 동등성(equivalence) 비교

  • 인스턴스의 값 비교
  • equals() 사용
    • equals() 메서드를 재정의 해야한다.
    • hashCode() 메서드도 같이 재정의 한다.

📌 Address 임베디드 타입

package hellojpa.domain2;

import javax.persistence.Embeddable;
import java.util.Objects;

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

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

}
  • equals() 메서드와 hashCode() 메서드를 오버라이딩할 때 필드 값(city, street, zipcode)을 그대로 가져와서 쓸 수도 있지만 만약에 프록시 객체일 경우 필드값을 getXXX로 가져와야한다.

  • equals()hashCode()를 재정의할 때 저기에 체크박스를 눌러서 getter로 값을 가져와서 비교하는 것이 제일 안전하다.
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    AddressEmd addressEmd = (AddressEmd) o;
    return Objects.equals(getCity(), addressEmd.getCity())
            && Objects.equals(getStreet(), addressEmd.getStreet())
            && Objects.equals(getZipcode(), addressEmd.getZipcode());
}

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

 


 

값 타입 컬렉션

  • 결론 : 값 타입은 실무에서 권장하지 않는다.
  • 컬렉션을 값 타입으로 사용하는 것을 값 타입 컬렉션이라고 한다.
  • 관계형 데이터베이스는 기본적으로 컬렉션을 지원하지 않는다.
    • 컬렉션은 일대다 개념이기 때문에 데이터베이스에 컬렉션과 엔티티를 같은 테이블에 저장할 수 없다. 
  • 그래서 컬렉션을 일대다 개념으로 풀어내야 한다.
    • 컬렉션을 별도의 테이블로 만들어야 한다.
    • 컬렉션을 별도의 테이블로 만들 때 엔티티의 id값컬렉션의 모든 요소를 묶어서 하나의 pk값으로 만들어야 한다.
    • 참고로 컬렉션은 값 타입이기 때문에 pk 값을 id 값으로 식별자를 넣게 되면 값 타입이 아니라 엔티티가 되어 버린다.
  • 값 타입은 하나 이상 저장할 때 사용할 수 있다.
  • @ElementCollection / @CollectionTable  애노테이션을 사용한다.
  • 값 타입 컬렉션은 영속성 전이(Casecade) + 고아 객체 제거 기능을 필수로 가지고 있다.

 

1. 값 타입 컬렉션 사용

🔍 값 타입 컬렉션 생성

package hellojpa.domain2;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    @Embedded
    private Address homeAddress;

    @ElementCollection  // 값 타입 컬렉션
    @CollectionTable(name = "FAVORITE_FOOD",                     // 테이블명 지정
            joinColumns= @JoinColumn(name = "MEMBER_ID") )       // 외래 키 지정
    @Column(name = "FOOD_NAME")  // 컬렉션 내부 값 타입이 String 이므로 @Column 을 사용할 수 있다.
    private Set<String> favoriteFoods = new HashSet<>();

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

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

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

Hibernate: 
    
    create table MemberV2 (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )
  • 값 타입 컬렉션의 테이블을 별도로 생성해야 한다.

🔍 저장 예제

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));
            
            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("피자");
            member.getFavoriteFoods().add("족발");

            member.getAddressesHistory().add(new Address("old1", "street", "10000"));
            member.getAddressesHistory().add(new Address("old2", "street", "10000"));

            em.persist(member);

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
Hibernate: 
    /* insert hellojpa.domain2.MemberV2
        */ insert 
        into
            MemberV2
            (city, street, zipcode, TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (MEMBER_ID, FOOD_NAME) 
        values
            (?, ?)

  • 컬렉션도 값 타입이기 때문에 컬렉션의 생명 주기는 엔티티에 의존하게 된다.
  • 따라서 컬렉션을 따로 영속화 시키지 않아도 엔티티가 영속화 될 때 같이 영속화 된다.
  • 그리고 만약에 컬렉션 값을 변경하고 싶다면 엔티티를 통해서 값을 변경해야 한다.

🔍 조회 예제

엔티티 조회하기

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Set;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));

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

            member.getAddressesHistory().add(new Address("old1", "street", "10000"));
            member.getAddressesHistory().add(new Address("old2", "street", "10000"));

            em.persist(member);

            em.flush();
            em.clear();

            System.out.println("===========START=========");

            MemberV2 findMember = em.find(MemberV2.class, member.getId());

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
===========START=========
Hibernate: 
    select
        memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
        memberv2x0_.city as city2_8_0_,
        memberv2x0_.street as street3_8_0_,
        memberv2x0_.zipcode as zipcode4_8_0_,
        memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
        memberv2x0_.USERNAME as USERNAME5_8_0_,
        team1_.TEAM_ID as TEAM_ID1_12_1_,
        team1_.name as name2_12_1_ 
    from
        MemberV2 memberv2x0_ 
    left outer join
        Team team1_ 
            on memberv2x0_.TEAM_ID=team1_.TEAM_ID 
    where
        memberv2x0_.MEMBER_ID=?
  • 컬렉션은 기본값이 지연로딩이다.
  • 엔티티만 조회했을 때는 컬렉션이 조회되지 않는다.

컬렉션 값 조회하기

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
import java.util.Set;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));

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

            member.getAddressesHistory().add(new Address("old1", "street", "10000"));
            member.getAddressesHistory().add(new Address("old2", "street", "10000"));

            em.persist(member);

            em.flush();
            em.clear();

            System.out.println("===========START=========");

            MemberV2 findMember = em.find(MemberV2.class, member.getId());

            List<Address> addressesHistory = findMember.getAddressesHistory();
            for (Address address : addressesHistory) {
                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) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
Hibernate: 
    select
        memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
        memberv2x0_.city as city2_8_0_,
        memberv2x0_.street as street3_8_0_,
        memberv2x0_.zipcode as zipcode4_8_0_,
        memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
        memberv2x0_.USERNAME as USERNAME5_8_0_,
        team1_.TEAM_ID as TEAM_ID1_12_1_,
        team1_.name as name2_12_1_ 
    from
        MemberV2 memberv2x0_ 
    left outer join
        Team team1_ 
            on memberv2x0_.TEAM_ID=team1_.TEAM_ID 
    where
        memberv2x0_.MEMBER_ID=?
Hibernate: 
    select
        addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
        addressesh0_.city as city2_0_0_,
        addressesh0_.street as street3_0_0_,
        addressesh0_.zipcode as zipcode4_0_0_ 
    from
        ADDRESS addressesh0_ 
    where
        addressesh0_.MEMBER_ID=?
address = old1
address = old2
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_5_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_5_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자
3월 31, 2022 5:33:33 오후 org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
INFO: HHH10001008: Cleaning up connection pool [jdbc:h2:tcp://localhost/~/test]

Process finished with exit code 0
  • 컬렉션을 조회할 때 컬렉션 테이블을 조회한다.

🔍 수정 예제

package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));

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

            member.getAddressesHistory().add(new Address("old1", "street", "10000"));
            member.getAddressesHistory().add(new Address("old2", "street", "10000"));

            em.persist(member);

            em.flush();
            em.clear();

            System.out.println("===========START=========");

            MemberV2 findMember = em.find(MemberV2.class, member.getId());
            
            // 값 타입 컬레션 수정하기
            findMember.getAddressesHistory().remove(new Address("old1", "street", "10000"));  // equals(), hashCode() 재정의 된 상태이어야 한다.
            findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
===========START=========
Hibernate: 
    select
        memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
        memberv2x0_.city as city2_8_0_,
        memberv2x0_.street as street3_8_0_,
        memberv2x0_.zipcode as zipcode4_8_0_,
        memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
        memberv2x0_.USERNAME as USERNAME5_8_0_,
        team1_.TEAM_ID as TEAM_ID1_12_1_,
        team1_.name as name2_12_1_ 
    from
        MemberV2 memberv2x0_ 
    left outer join
        Team team1_ 
            on memberv2x0_.TEAM_ID=team1_.TEAM_ID 
    where
        memberv2x0_.MEMBER_ID=?
Hibernate: 
    select
        addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
        addressesh0_.city as city2_0_0_,
        addressesh0_.street as street3_0_0_,
        addressesh0_.zipcode as zipcode4_0_0_ 
    from
        ADDRESS addressesh0_ 
    where
        addressesh0_.MEMBER_ID=?
Hibernate: 
    /* delete collection hellojpa.domain2.MemberV2.addressesHistory */ delete 
        from
            ADDRESS 
        where
            MEMBER_ID=?
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
  • 값 컬렉션 1개를 추가 했는데 Insert 구문이 2번 나간다.
  • 왜냐하면 값 타입 컬렉션에서 변경 사항이 발생하면, 주인 ㅇ엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하기 때문이다.
  • 이런 이유로 실무에선 값 타입 컬렉션을 사용하면 안된다.

 

<참고>

더보기
package hellojpa;

import hellojpa.domain2.Address;
import hellojpa.domain2.MemberV2;

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 {

            MemberV2 member = new MemberV2();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homeCity", "street", "10000"));

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

            member.getAddressesHistory().add(new Address("old1", "street", "10000"));
            member.getAddressesHistory().add(new Address("old2", "street", "10000"));

            em.persist(member);

            em.flush();
            em.clear();

            System.out.println("===========START=========");

            MemberV2 findMember = em.find(MemberV2.class, member.getId());

            /* homeCity → newCity 값을 바꾸는 방법 */
//            findMember.getHomeAddress().setCity("newCity");  // 값 타입은 불변 객체이기 때문에 이렇게 값을 수정하면 안된다.
/            Address a = findMember.getHomeAddress();
            findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));  // 새로운 값 타입으로 교체

            /* 치킨 → 한식 */
            // 값 타입 컬렉션을 수정할 때는 삭제하고 다시 추가해야한다.
            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("치킨");

            findMember.getAddressesHistory().remove(new Address("old1", "street", "10000"));  // equals(), hashCode() 재정의 된 상태이어야 한다.
            findMember.getAddressesHistory().add(new Address("newCity1", "street", "10000"));

            tx.commit();
        } catch (Exception e) {
            System.out.println("e.getMessage=" +e.getMessage());
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
===========START=========
Hibernate: 
    select
        memberv2x0_.MEMBER_ID as MEMBER_I1_8_0_,
        memberv2x0_.city as city2_8_0_,
        memberv2x0_.street as street3_8_0_,
        memberv2x0_.zipcode as zipcode4_8_0_,
        memberv2x0_.TEAM_ID as TEAM_ID6_8_0_,
        memberv2x0_.USERNAME as USERNAME5_8_0_,
        team1_.TEAM_ID as TEAM_ID1_12_1_,
        team1_.name as name2_12_1_ 
    from
        MemberV2 memberv2x0_ 
    left outer join
        Team team1_ 
            on memberv2x0_.TEAM_ID=team1_.TEAM_ID 
    where
        memberv2x0_.MEMBER_ID=?
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_5_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_5_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
Hibernate: 
    select
        addressesh0_.MEMBER_ID as MEMBER_I1_0_0_,
        addressesh0_.city as city2_0_0_,
        addressesh0_.street as street3_0_0_,
        addressesh0_.zipcode as zipcode4_0_0_ 
    from
        ADDRESS addressesh0_ 
    where
        addressesh0_.MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.domain2.MemberV2 */ update
            MemberV2 
        set
            city=?,
            street=?,
            zipcode=?,
            TEAM_ID=?,
            USERNAME=? 
        where
            MEMBER_ID=?
Hibernate: 
    /* delete collection hellojpa.domain2.MemberV2.addressesHistory */ delete 
        from
            ADDRESS 
        where
            MEMBER_ID=?
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.domain2.MemberV2.addressesHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)

 

 

2. 값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 식별자가 없기에 값을 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모두 칼럼을 묶어서 기본 키를 구성해야 한다.
    • null이면안된다.
    • 중복이면 안된다.

 

3. 값 타입 컬렉션 대안

  • 실무에서는 값 타입 컬렉션을 엔티티로 만들어 일대다 관계로 사용하는 것이 좋다.
    • 엔티티로 만들 때 영속성 전이 + 고아 객체 제거 기능을 사용해서 값 타입 컬렉션 처럼 사용할 수 있다.

📌 AddressEntity 엔티티

package hellojpa.domain2;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

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

    @Id @GeneratedValue
    private Long id;

    private Address address;

    public AddressEntity() {
    }

    public AddressEntity(String city, String street, String zipcode) {
        this.address = new Address(city, street, zipcode);
    }
}
  • 값 컬렉션을 엔티티로 만들어서 사용한다.
  • 엔티티이므로 기본 생성자를 필수로 정의해야 한다.

📌 MemberV2

package hellojpa.domain2;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static javax.persistence.CascadeType.*;

@Entity
@Getter @Setter
public class MemberV2 {

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

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

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @Embedded
    private Address homeAddress;

    // 일대다 단방향 관계로 풀어낸다.
    @OneToMany(cascade = ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressesHistory = new ArrayList<>();
}

 

  • 참고로 일대다 단방향 관계일 경우 update 쿼리문이 생성된다.

 

4. 정리

  • 값 타입 컬렉션을 쓰기 보다는 엔티티로 만들어서 사용할 것을 권장한다.
  • 엔티티 타입 특징
    • 식별자를 가진다.
    • 생명 주기를 관리한다.
    • 공유할 수 있다.
  • 값 타입의 특징
    • 식별자가 없다
    • 생명 주기를 엔티티에 의존한다.
    • 공유하지 않는 것이 안전하다. (값을 복사해서 사용할 것)
    • 불변 객체로 만드는 것이 안전하다.
  • 값 타입은 정말 값 타입이라 판단될 때만 사용한다.
    • 정말 단순한 경우에만 사용
    • 예) 좋아하는 음식 메뉴를 다중 선택으로 고르는 것 처럼 심플한 자료들
  • 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 값 타입이 아닌 엔티티로 만들어야 한다.

 

 

 

 

 

 


👀 참고 자료

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

 

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

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

www.inflearn.com

 

https://catsbi.oopy.io/e1315057-ba06-426a-ab07-9b68af0fcae8

 

값 타입(1/2)

기본값 타입

catsbi.oopy.io

 

 

728x90