나를 기록하다
article thumbnail
반응형

1. 기본값 타입

1) JPA의 데이터 타입 분류

엔티티 타입

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

값 타입

  • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없고 값만 있으므로 변경시 추적 불가
  • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

2) 값 타입 분류

기본값 타입

  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String
  • 예) String name, int age
  • 생명주기를 엔티티에 의존
    • 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 안된다.
    • 회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안된다.(사이드 이펙트, 부수효과)
  • int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다.
  • 기본 타입은 항상 값을 복사한다.
  • Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경되지 않는다.

임베디드 타입(embedded type, 복합 값 타입)

컬렉션 값 타입(collection value type)

 

 

2. 임베디드 타입⭐️

특징

  • 새로운 값 타입을 직접 정의할 수 있음
  • JPA는 임베디드 타입(embedded type)이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입

예시

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

회원(Member) 엔티티

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

WorkPeriod & homeAddress

구성도

값 타입의 Period, Address 생성

 

임베디드 타입 사용법

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

임베디드 타입의 장점

  1. 재사용 가능
  2. 높은 응집도
  3. Period.isWork()처럼 해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있다.
  4. 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티의 생명주기에 의존한다.

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

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

 

실습

- Member

package hellojpa;

import javax.persistence.*;

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

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

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

    public Long getId() {
        return id;
    }

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Period getWorkPeriod() {
        return workPeriod;
    }

    public void setWorkPeriod(Period workPeriod) {
        this.workPeriod = workPeriod;
    }

    public Address getHomeAddress() {
        return homeAddress;
    }

    public void setHomeAddress(Address homeAddress) {
        this.homeAddress = homeAddress;
    }
}

- Period(값 타입)

package hellojpa;

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

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

- Address(값 타입)

package hellojpa;

import javax.persistence.Embeddable;

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

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

 

- JpaMain

try {

            Member member = new Member();
            member.setUsername("hello");
            member.setHomeAddress(new Address("city","street","1000"));
            member.setWorkPeriod(new Period());

            em.persist(member);

            tx.commit();
        }

- 결과

Hibernate: 

    create table Team (
       TEAM_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createdDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        name varchar(255),
        primary key (TEAM_ID)
    )

...

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

H2 데이터베이스 결과

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

임베디드 타입과 연관관계

임베디드 타입과 연관관계 - 값 타입은 엔티티를 가질 수 있다.

@AttributeOverride: 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하면?
  • 컬럼명이 중복된다.
  • @AttributeOverrides(여러개), @AttributeOverride(하나)를 사용해서 컬럼명 속성을 재정의

예시

public class Member {

...

@Embedded
    private Address homeAddress;
    // 주소 Address
    @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 Address workAddress;

...

}
public class JpaMain {

...

try {

            Member member = new Member();
            member.setUsername("hello");
            member.setHomeAddress(new Address("city","street","1000"));
            member.setWorkPeriod(new Period());

            em.persist(member);

            tx.commit();
        }

...

}
Hibernate: 

    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        WORK_CITY varchar(255),
        WORK_STREET varchar(255),
        WORK_ZIPCODE varchar(255),
        endDate timestamp,
        startDate timestamp,
        TEAM_ID bigint,
        primary key (MEMBER_ID)
    )
  • 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null이다.

 

 

3. 값 타입과 불변 객체

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

 

값 타입 공유 참조

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

값 타입 공유 참조의 부작용

 

예시

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

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

    // 기간 Period
    @Embedded
    private Period workPeriod;
    // 주소 Address
    @Embedded
    private Address homeAddress;
...
}
  • JpaMain
try {

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

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

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

            em.persist(member);

            tx.commit();
        }

member1과 member2 모두 같은 address를 받아옴.


  • JpaMain
try {

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

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

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

            member.getHomeAddress().setCity("newCity");
            // 첫번째 member의 주소만 newCity로 바꾸겠다는 생각으로 코딩

            em.persist(member);

            tx.commit();
        }
  • 결과 - update 쿼리가 두번 나감.

첫번째 member의 주소만 newCity로 바꾸려 했으나 모든 주소가 newCity로 바뀜 - Side Effect 발생

→ 버그 발생. 첫번째와 두번째 멤버 모두 newCity로 변경(Side Effect 발생)

 

값 타입 복사

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

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

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

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

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

            member.getHomeAddress().setCity("newCity");
            // 첫번째 member의 주소만 newCity로 바꾸겠다는 생각으로 코딩

            em.persist(member);

            tx.commit();
        }

인스턴스를 복사하여 값을 넣음으로써 해결

→ 첫번째 member의 City만 newCity로 변경 완료

객체 타입의 한계

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

 

기본 타입(primitive type)

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

객체 타입

Address a = new Address(“Old”);
Address b = a; //객체 타입은 참조를 전달
b.setCity(“New”)

 

불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야함
  • 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체

예시

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

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public String getZipcode() {
        return zipcode;
    }
}

→ 코드와 같이 setter를 지우고 getter만 남겨둬서 변경할 수 없도록 한다.

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

    public String getCity() {
        return city;
    }

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

    public String getStreet() {
        return street;
    }

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

    public String getZipcode() {
        return zipcode;
    }

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

→ private를 사용하여 내부적으로만 사용할 수 있도록 한다.

불변 객체를 만든 후 값을 변경하고 싶다면?

try {

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

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

            Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
            member.setHomeAddress(newAddress);

            em.persist(member);

            tx.commit();
        }

→ 위와 같이 새로운 객체를 생성하고 변경하고 싶은 값을 넣은 후 완전히 갈아끼워야 한다.

 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.

 

 

 

4. 값 타입의 비교

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

equals() 메소드 재정의하기 전 결과

public class ValueMain {
    public static void main(String[] args) {

        int a = 10;
        int b = 10;

        System.out.println("a == b : " + (a == b));

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

        System.out.println("address1 == address2 : " + (address1 == address2));
        System.out.println("address1 equals address2 : " + (address1.equals(address2)));
    }
}
a == b : true
address1 == address2 : false
address1 equals address2 : false

→ 이유: equals의 기본값은 == 비교이기 때문

equals() 메소드 재정의 과정

  • equals를 재정의할 때는 검색해서 나온 기본 설정값들을 사용. equals()를 구현할 때는 hashCode도 같이 구현해야함.
  • getter 체크! 체크하지 않을 경우 필드에 직접 접근하는데 프록시일 경우 계산이 안된다. getter를 체크해야 프록시일 때도 진짜 객체에 접근 가능하기에 getter를 체크하는 것이 좋다. (대부분의 코드도 이와 같다)
public class ValueMain {
    public static void main(String[] args) {

        int a = 10;
        int b = 10;

        System.out.println("a == b : " + (a == b));

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

        System.out.println("address1 == address2 : " + (address1 == address2));
        System.out.println("address1 equals address2 : " + (address1.equals(address2)));
    }
}
a == b : true
address1 == address2 : false
address1 equals address2 : true

 

5. 값 타입 컬렉션⭐️

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

값 타입 컬렉션(Collection)

 

값 타입 저장 예제

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

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

    @Embedded
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME") // String 값이 하나기에 매핑하게 허용해줌. 예외적으로 가능
    private Set<String> favoriteFoods = new HashSet<>();

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

        ...

}
try {

            Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homecity", "street", "10000"));

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

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

            em.persist(member);

            tx.commit();
        }

h2 데이터베이스 조회 결과

따로 persist하지 않고 member만 persist하니 값 타입 또한 같이 persist 되었다.

이유: 값 타입이기 때문에 모든 lifecycle(생명주기)가 member에 소속.

값 타입들은 별도로 persist할 필요가 없다. 일대다 연관관계에서 CascadeType.ALL + orphanRemoval=true한 것과 유사

 

값 타입 조회 예제 → 값 타입 컬렉션도 지연로딩 전략을 사용한다.

try {

            Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homecity", "street", "10000"));

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

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

            em.persist(member);

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

            System.out.println("=============== S T A R T ===============");
            Member findMember = em.find(Member.class, member.getId());

            tx.commit();
        }
=============== S T A R T ===============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

→ member만 가져온다 → 컬렉션들은 지연로딩이라는 뜻! but 멤버에 소속된 homeAddress는 같이 불러와짐.

try {

            Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homecity", "street", "10000"));

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

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

            em.persist(member);

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

            System.out.println("=============== S T A R T ===============");
            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();
        }
=============== S T A R T ===============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.zipcode as zipcode4_0_0_ 
    from
        ADDRESS addresshis0_ 
    where
        addresshis0_.MEMBER_ID=?
address = old1
address = old2
Hibernate: 
    select
        favoritefo0_.MEMBER_ID as MEMBER_I1_4_0_,
        favoritefo0_.FOOD_NAME as FOOD_NAM2_4_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.MEMBER_ID=?
favoriteFood = 족발
favoriteFood = 치킨
favoriteFood = 피자

@ElementCollection의 fetch를 찍어보면 기본값이 LAZY로 설정되어 있기 때문

 

값 타입 수정 예제 - setter를 private으로 설정한 homeAddress 수정

try {

            Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homecity", "street", "10000"));

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

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

            em.persist(member);

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

            System.out.println("=============== S T A R T ===============");
            Member findMember = em.find(Member.class, member.getId());

            // homeCity -> newCity
//            findMember.getHomeAddress().setCity("newCity");
            // 값 타입은 immutable 해야하기 때문에 setCity로 변경 X

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

            tx.commit();
        }
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=? 
        where
            MEMBER_ID=?

 

값 타입 수정 예제 - 컬렉션 수정 1

    ...

// 치킨 -> 한식으로 바꾸고 싶어 -> String 자체가 값 타입 -> 통째로 갈아껴야 함. update 불가
            findMember.getFavoriteFoods().remove("치킨");
            findMember.getFavoriteFoods().add("한식");

    ...
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
            (?, ?)

 

값 타입 수정 예제 - 컬렉션 수정 2

// 기본적으로 컬렉션은 대부분 equals를 사용. 완전히 똑같은 값을 넣으면 지워준다.
            // equals와 hashCode가 제대로 구현되어 있어야 함.
            findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
            findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
=============== S T A R T ===============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?
Hibernate: 
    select
        addresshis0_.MEMBER_ID as MEMBER_I1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.street as street3_0_0_,
        addresshis0_.zipcode as zipcode4_0_0_ 
    from
        ADDRESS addresshis0_ 
    where
        addresshis0_.MEMBER_ID=?
Hibernate: 
    /* delete collection hellojpa.Member.addressHistory */ delete 
        from
            ADDRESS 
        where
            MEMBER_ID=?
Hibernate: 
    /* insert collection
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row hellojpa.Member.addressHistory */ insert 
        into
            ADDRESS
            (MEMBER_ID, city, street, zipcode) 
        values
            (?, ?, ?, ?)

  • 참고: 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
  • insert를 2번하는 이유
    remove에서 하나를 지움. old2와 newCity1이 남아있음. old2와 newCity1은 기대한대로 값이 들어가있음. 결론적으론 원하는대로 동작… 이해하려면 값 타입 컬렉션의 제약사항을 확인!!

 

값 타입 컬렉션의 제약사항

  1. 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  2. 값은 변경하면 추적이 어렵다.
  3. 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
    1. 예시) addressHistory를 변경하면 DB에 쿼리를 날릴 때 address에 내 member_id와 관련된 값들을 다 지운다. 그 다음에 최종 컬렉션에 남은 데이터들을 insert 한다. → 사용하면 안됨!!
  4. 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X

 

값 타입 컬렉션 대안

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

 

예시

  • Member
@Entity
public class Member {

        ...

//    @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<>();

        ...
}
  • AddressEntity
package hellojpa;

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

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

    @Id
    @GeneratedValue
    private Long id;

    private Address address;

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

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

    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;
    }
}
  • JpaMain
try {

            Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("homecity", "street", "10000"));

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

            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("=============== S T A R T ===============");
            Member findMember = em.find(Member.class, member.getId());
  • 결과
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=?

...

=============== S T A R T ===============
Hibernate: 
    select
        member0_.MEMBER_ID as MEMBER_I1_6_0_,
        member0_.city as city2_6_0_,
        member0_.street as street3_6_0_,
        member0_.zipcode as zipcode4_6_0_,
        member0_.USERNAME as USERNAME5_6_0_ 
    from
        Member member0_ 
    where
        member0_.MEMBER_ID=?

→ 값 타입을 엔티티로 승급한다고 표현. 실무에서 이 방법을 많이 사용한다.

 

값 타입 컬렉션은 언제 사용하는가?

  • 진짜 단순할 때
  • 예시
    • 셀렉트 박스에 치킨과 피자가 멀티로 셀렉트 할 수 있음.
    • 내가 좋아하는 메뉴가 뭐냐? 치킨, 피자 선택
    • 이와 같이 단순하고 추적할 필요 없으며 업데이트할 필요도 없을 때
  • 값을 변경하지 않는다고 해도 주소 이력과 같은 것들은 다 엔티티.
  • 예시 → 주소가 다 사라져도 이력은 남겨야할 수도 있음

 

정리

엔티티 타입의 특징

  1. 식별자 O
  2. 생명 주기 관리
  3. 공유할 수 있다

 

값 타입의 특징

  1. 식별자 X
  2. 생명 주기를 엔티티에 의존
  3. 공유하지 않는 것이 안전(복사해서 사용)
  4. 불변 객체로 만드는 것이 안전하다.

 

꼭 기억할 내용

  1. 값 타입은 정말 값 타입이라 판단될 때만 사용한다.
  2. 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다.
  3. 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.

 

 

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

값 타입 매핑 예제

  • Address
...

@Embeddable
public class Address {

    // 값 타입의 장점 2 Validation Rule을 공통으로 적용시킬 수 있다>
    @Column(length = 10)
    private String city;
    @Column(length = 20)
    private String street;
    @Column(length = 5)
    private String zipcode;

    // 값 타입의 장점 1) 의미있는 메소드를 만들 수 있다.
    public String fullAddress() {
        return getCity() + " " + getStreet() + " " + getZipcode();
    }

   ...

    @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());
    }
}
  • Member
...

@Entity
public class Member extends BaseEntity {

        ...

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();
        // 관례상 new ArrayList<>(); 선언

        ...

    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;
    }
}
  • Delivery
...

@Entity
public class Delivery extends BaseEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Embedded
    private Address address;
    private DeliveryStatus status;

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

        ...

    public Address getAddress() {
        return address;
    }

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

        ...

}
  • JpaMain
...

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 {

            Book book = new Book();
            book.setName("JPA");
            book.setAuthor("김영한");

            em.persist(book);

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

Hibernate: 

    create table Delivery (
       id bigint not null,
        INSERT_MEMBER varchar(255),
        createdDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        city varchar(10),
        street varchar(20),
        zipcode varchar(5),
        status integer,
        primary key (id)
    )

Hibernate: 

    create table Member (
       MEMBER_ID bigint not null,
        INSERT_MEMBER varchar(255),
        createdDate timestamp,
        UPDATE_MEMBER varchar(255),
        lastModifiedDate timestamp,
        city varchar(10),
        street varchar(20),
        zipcode varchar(5),
        name varchar(255),
        primary key (MEMBER_ID)
    )

        ...
반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...