SQL mapper & ORM/JPA

9. 값 타입

wooweee 2023. 10. 6. 12:38
728x90
  • 핵심
    • 임베디드 타입(복합 값 타입)
    • 값 타입 컬렉션

 

1. JPA 데이터 타입 분류

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

 

2. 값 타입의 분류

 

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

 

3. 기본값 타입

  • 생명주기를 엔티티에 의존 - 회원 삭제시 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 안된다  -  cf> 엔티티 타입은 공유해도 된다.

3.1. 자바의 기본 타입은 절대 공유되지 않는다.

  • 기본 타입은 항상 값을 복사한다. (기본형의 특징. 변수에 할당된 메모리주소에 값이 그대로 들어간다.)
  • 래퍼 클래스(Integer), String 같이 특수 클래스는 참조 변수를 사용해서 값이 공유가 되지만 값 변경이 불가하여 공유로 인한 변경 문제가 발생하지 않는다.

 

  • 래퍼 클래스
    • String처럼 메모리에 값을 저장하고 해당 주소를 래퍼클래스 변수에 넣는다.
    • 그래서 참조변수를 공유를 할 수 있다.
    • 하지만 해당 참조변수 내부의 실제 값을 변경할 수 값없기 때문에 값을 변경하게 되면
    • 새로운 메모리 주소에 새 값을 넣고,  새 값이 들어간 메모리 주소를 래퍼 클래스의 변수에 넣는다.
    • 참고로 jvm은 java runtime 시 -128~127까지의 Interger를 위한 값을 미리 생성해놓기 때문에 성능 최적화가 가능하다.
  • String
    • 동일하다. 변경불가한 class 이다. 변경하고 싶으면 stringBuffer 사용하면 된다.

 

package hellojpa.chap9;

public class ValueMain {
    public static void main(String[] args) {
        Integer a = new Integer(100);
        Integer b = a;
        a = 1222;

        System.out.println("a = " + a); //1222
        System.out.println("b = " + b); //100

        int c = 10;
        int d = c;
        c = 100;

        System.out.println("c = " + c); //100
        System.out.println("d = " + d); //10
    }
}

 

4. 임베디드 타입(복합 값 타입)

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

 

4.1. 예시

  • 모델링

객체 입장에서 임베디드 타입을 이용해서 의미가 명확한 엔티티가 되었다.
객체만 늘어났지 table은 바뀐게 없다.

4.2. 임베디드 타입 사용법

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

 

  • 임베디드 타입 사용법
    1. @Embeddable: 값 타입을 정의하는 곳에 표시
    2. @Embedded: 값 타입을 사용하는 곳에 표시
    3. 기본 생성자 필수

 

  • Address
package hellojpa.chap9;

@Embeddable
public class AddressChap9 {
    private String city;
    private String street;
    @Column(name = "ZIPCODE")
    private String zipcode;

    // 해당 type도 기본 생성자가 필수
    public AddressChap9() {}

    // 불변 객체 1 - setter 없애기
    // public void setCity(String city) { this.city = city; }

    // 불변 객체 2 - private
    private void setStreet(String street) { this.street = street; }

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

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

 

  • Period
package hellojpa.chap9;

@Embeddable
public class PeriodChap9 {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    // 해당 type도 기본 생성자가 필수
    public PeriodChap9() {}
}

 

  • Member
package hellojpa.chap9;

@Entity
public class MemberChap9 {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @Embedded // 기간 Period - @Embeddable만 있으면 @Embedded 안넣어도 되는데 넣는 걸 추천
    private PeriodChap9 workPeriod;
    @Embedded // 주소 Address
    private AddressChap9 homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE"))
    })
    
    private AddressChap9 workAddress;
}

 

4.3. 임베디드 타입 장점

  • 재사용
  • 높은 응집도
  • Address.isHome() 처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음

 

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

  • 임베디드 타입으로 엔티티를 가질 수 있다.

 

 

4.6. @AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 임베디드 타입을 사용시 컬럼 명이 중복된다.
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의

 

package hellojpa.chap9;

@Entity
public class MemberChap9 {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @Embedded // 기간 Period - @Embeddable만 있으면 @Embedded 안넣어도 되는데 넣는 걸 추천
    private PeriodChap9 workPeriod;
    @Embedded // 주소 Address
    private AddressChap9 homeAddress;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "WORK_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "WORK_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "WORK_ZIPCODE"))
    })
    
    private AddressChap9 workAddress;
}

 

4.7. 임베디드 타입과 null

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

 

5. 값 타입과 불변 객체

  • 임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험
  • side effect 발생 - 이걸 의도했어라고 변명하면 안되는게 그렇게 써야하는 경우면 엔티티로 따로 뽑아 써야한다.
  • 값타입 참조를 공유하므로 변경시 모든 것이 변경됨 -> 새로 복사본을 만들어서 사용해야함 (깊은 복사)
  • 문제는 개발자가 참조 값을 직접 대입하는 것을 막을 방법이 없다. -> 아예 직접 넣는 방식을 막아버림 (불변 객체)
    1. setter 없애기, private으로 두기
    2. 해시코드, equals 오버라이딩 하기 - 값 타입 비교의 문제점을 막기 위함
package hellojpa.chap9;

@Embeddable
public class AddressChap9 {
    private String city;
    private String street;
    @Column(name = "ZIPCODE")
    private String zipcode;

    // 해당 type도 기본 생성자가 필수
    public AddressChap9() {}

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

    // 불변 객체 1 - setter 없애기
    // public void setCity(String city) { this.city = city; }
    // 불변 객체 2 - private
    private void setStreet(String street) { this.street = street; }

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

 

package hellojpa.chap9;

public class JpaChap9Share {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            AddressChap9 shareAddress = new AddressChap9("city", "street", "10000");

            MemberChap9 member = new MemberChap9();
            member.setUsername("member1");
            member.setHomeAddress(shareAddress);
            em.persist(member);

            MemberChap9 member2 = new MemberChap9();
            member2.setUsername("member2");
            member2.setHomeAddress(shareAddress);
            em.persist(member2);

            // 공유 참조의 위험성
            // member.getHomeAddress().setCity("newCity"); // 해당 참조를 사용하는 모든 멤버의 값이 변경되는 버그 발생

            // 값 타입 복사
            AddressChap9 copyAddress = new AddressChap9(shareAddress.getCity(), shareAddress.getStreet(), shareAddress.getZipcode());
            MemberChap9 member3 = new MemberChap9();
            member3.setUsername("member3");
            member3.setHomeAddress(copyAddress);
            em.persist(member3);

            // 영향 없음
            // member.getHomeAddress().setCity("newCity"); // member3은 영향을 받지 않는다.

            // 문제는 개발자들이 직접 값을 변경하게 막을 수 있는 방법이 없다. 참조주소를 사용하는 것이기 때문 -> 불변객체로 수정자체를 막음
            // member.getHomeAddress().setCity("newCity"); // setter를 없애거나 private으로 둔다. 외부에서 사용을 막음


            // 대신 불변 객체 값을 변경하고 싶을 때는 new Address()를 새로만들어서 통으로 넘겨야한다.
            AddressChap9 newAddress = new AddressChap9("NewCity", shareAddress.getStreet(), shareAddress.getZipcode());
            member.setHomeAddress(newAddress);


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

 

 

6. 값 타입 컬렉션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요

 

 

6.1. 값 타입 컬렉션 사용

  • 저장
  • 조회 - 지연로딩 전략 사용 (@ElementCollection에서 설정 가능)
  • 수정 -> 문제가 많기 때문에 엔티티 승계 작업 필요
  • 값 타입 컬렉션은 영속성 전이(all), 고아 객체 제거 기능을 필수로 가진다.

 

6.2. 값 타입 등록, 조회, 수정-문제점

 

  • @ElementColletion : 값 타입 collection 명시 및 지연로딩 및 즉시로딩 설정 가능

  • @CollectionTable
    • name : collection table의 이름
    • joinColumn : 설계상 "collection : member = n : 1" 이므로 @JoinColumn 존재

    • <String>처럼 예외적으로 하나의 컬럼만 존재할 경우 @Column으로 컬럼명 변경 허용

  • 등록
    • db상에서는 collection을 담는 기능이 없기 때문에 collection을 table로 분리해서 저장

  • 조회
    • default인 지연로딩 수행
    • 임베디드타입은 그런 것이 필요없는데 객체 입장에서 분리된 것이지 db상에서는 동일 테이블에 존재하는 컬럼이기 때문
  • 수정
    • 임베디드때처럼 list의 요소를 찾아서 제거하고 변경요소를 넣는 코드를 작성해도 아래와 같은 문제 발생
      Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert
    • 성능에 매우 안좋을 듯하다. 결론은 사용하지 말 것

 

  • JpaChap9Collection
package hellojpa.chap9;

public class JpaChap9Collection {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            AddressChap9 address = new AddressChap9("city", "street", "10000");

            MemberChap9Collection member = new MemberChap9Collection();
            member.setUsername("member1");
            member.setHomeAddress(address);

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

            member.getAddressHistory().add(new AddressChap9("old1", "street", "10000"));
            member.getAddressHistory().add(new AddressChap9("old2", "street", "10000"));
            em.persist(member);

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

            System.out.println("=== START ===");
            // 값 타입 Collection은 지연 로딩
            // 임베디드는 지연로딩할 필요가 없음 - 실제 db에서는 동일 table의 값이 뭉태기로 있는 것이기 때문
            MemberChap9Collection findMember = em.find(MemberChap9Collection.class, member.getId());

            List<AddressChap9> addressHistory = findMember.getAddressHistory();

            for (AddressChap9 a : addressHistory) System.out.println("address = " + a.getCity());

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

            System.out.println("=== 값 타입 컬렉션 수정 ===");
            MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());

            /* 잘못된 방식. 참조 공유기 때문에 setter 사용하면 안됨 */
            // findMember2.getHomeAddress().setCity("newCity")
            /* 올바른 방식*/
            AddressChap9 homeAddress = findMember2.getHomeAddress();
            findMember2.setHomeAddress(new AddressChap9("newCity", homeAddress.getStreet(), homeAddress.getZipcode()));

            // 치킨 -> 한식
            findMember2.getFavoriteFoods().remove("치킨");
            findMember2.getFavoriteFoods().add("한식");


            // 주소변경 old1 -> changeOld1
            // 문제점 : Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert
            // 결론 : 쓰지 말고 아예 다른 방식으로 수행 할 것
            findMember2.getAddressHistory().remove(new AddressChap9("old1", "street", "10000"));
            findMember2.getAddressHistory().add(new AddressChap9("changeOld1", "street", "10000"));

            // 다른 방식 : 엔티티로 값을 승계 할 것

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

  • Member
package hellojpa.chap9;

@Entity
public class MemberChap9Collection {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @Embedded // 기간 Period
    private PeriodChap9 workPeriod;
    @Embedded // 주소 Address
    private AddressChap9 homeAddress;

    /* 값타입 Collection
     * db상에서는 collection을 담는 기능이 없기 때문에 collection을 table로 분리해서 저장
     * @ElementColletion : 값 타입 collection 명시
     * @CollectionTable
     *  name : collection table의 이름
     *  joinColumn : 설계상 "collection : member = n : 1" 이므로 @JoinColumn 존재
     * 예외적으로 하나의 컬럼만 존재할 경우 @Column으로 컬럼명 변경 허용
    *
    */
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection // fetch = lazy default로 설정되어 있다.
    @CollectionTable(name = "ADDRESS_HISTORY", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<AddressChap9> addressHistory = new ArrayList<>();
}

 

지연로딩 수정 문제점

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

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

 

6.3. 수정 문제점 해결 - 엔티티 승계

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

  • AddressEntity
package hellojpa.chap9;

@Entity
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;
    @Embedded
    private AddressChap9 address;

    public AddressEntity() {}

    public AddressEntity(String city, String street, String zipcode) {
        this.address = new AddressChap9(city, street, zipcode);
    }
}

 

  • MemberChap9Collection
package hellojpa.chap9;

@Entity
public class MemberChap9Collection {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private PeriodChap9 workPeriod;
    @Embedded
    private AddressChap9 homeAddress;

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

    /*
    @ElementCollection // fetch = lazy default로 설정되어 있다.
    @CollectionTable(name = "ADDRESS_HISTORY", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<AddressChap9> addressHistory = new ArrayList<>();
    */

    // 엔티티로 승계 - pk를 가지게 된다.
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressHistory = new ArrayList<>();
}

 

  • JpaChap9Collection
package hellojpa.chap9;


public class JpaChap9Collection {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager(); // database의 connection 하나 받았다고 생각하셈
        EntityTransaction tx = em.getTransaction(); // jpa는 tx가 필수
        tx.begin(); // tx 시작

        try {
            AddressChap9 address = new AddressChap9("city", "street", "10000");

            MemberChap9Collection member = new MemberChap9Collection();
            member.setUsername("member1");
            member.setHomeAddress(address);

            /* 값타입 컬렉션
            member.getAddressHistory().add(new AddressChap9("old1", "street", "10000"));
            member.getAddressHistory().add(new AddressChap9("old2", "street", "10000"));
            */
            /* 값타입 컬렉션 엔티티 승계 */
            member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
            member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
            em.persist(member);

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

            /* 값 타입 컬렉션 수정 문제점
            System.out.println("=== 값 타입 컬렉션 수정 ===");
            MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());

            // 2. 주소변경 old1 -> changeOld1
            // 문제점 : Collection 자체에 변경이 있음을 인지 -> 모든 데이터 delete -> 변경사항과 동일한 값 다시 insert
            // 결론 : 쓰지 말고 아예 다른 방식으로 수행 할 것
            findMember2.getAddressHistory().remove(new AddressChap9("old1", "street", "10000"));
            findMember2.getAddressHistory().add(new AddressChap9("changeOld1", "street", "10000"));
            */
            
            // 다른 방식 : 엔티티로 값을 승계 할 것
            System.out.println("=== 값 타입 컬렉션 엔티티 승계 수정 ===");
            MemberChap9Collection findMember2 = em.find(MemberChap9Collection.class, member.getId());

            findMember2.getAddressHistory().remove(0); // 현재 pk까지 들어가서 new()로 찾아서 제거할 수 없다. 다른 방식을 찾아야 한다.
            findMember2.getAddressHistory().add(new AddressEntity("changeOld1", "street", "10000"));
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

7. 정리

  • 엔티티 타입의 특징
    • 식별자O
    • 생명 주기 관리
    • 공유
  • 값 타입의 특징
    • 식별자X
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전(복사해서 사용)
    • 불변 객체로 만드는 것이 안전