-> 블로그 이전

[JPA] 값 타입

2022. 7. 22. 15:19Language`/JPA

JPA의 데이터 타입을 큰 카테고리로 나누면 총 2개의 카테고리로 나눌 수 있다

  1. 엔티티 타입
  2. 값 타입

엔티티 타입은 말그대로 @Entity로 정의되는 객체를 의미하고, 값 타입단순히 값으로 사용되는 자바의 기본 타입이나 객체를 의미한다

 

여기서 이 두 타입의 가장 큰 차이점은 "추적의 가능성"이다

→ 엔티티 타입은 "식별자"를 통해서 엔티티를 추적할 수 있지만, 값 타입은 별도의 식별자 개념이 없기 때문에 값이 바뀌어버리면 추적 자체가 불가능하다


@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private Integer age;
}

현재 Member라는 엔티티가 존재하고 DB에는 다음 데이터가 있다고 하자

그리고 식별자(1L)에 대한 Member를 찾은 다음에 정보를 update해보자

Member findMember = em.find(Member.class, 1L);
findMember.updateInfo("memberA-Update", 99);

그러면 여기서 위의 [식별자 = 1 {20, memberA}]란 Member와 [식별자  1 {99, memberA-Update}]라는 Member는 서로 다른 Member일까??

>> 엔티티 타입은 "식별자"로 추적이 가능하고 둘다 동일한 식별자를 가지고 있기 때문에 같은 Member이다

 

그러면 이제 자바의 기본 값 타입을 살펴보자

int value = 100;
System.out.println(Integer.hashCode(value));
value = 200;
System.out.println(Integer.hashCode(value));

과연 두 출력문에서는 value라는 값 타입에 대한 동등성을 찾을 수 있을까?

완전히 다른 hashCode를 가지고 있다. 왜냐하면 값 타입은 단순한 수치 정보이고 값이 바뀌게 되면 완전히 다른 것으로 보기 때문이다

String line = "hello";
System.out.println(line.hashCode());
line = "Spring";
System.out.println(line.hashCode());

String이라는 자바 기본 타입으로 hashCode를 비교해봐도 서로 완전히 다른 hashCode를 가짐을 확인할 수 있다

>> hashCode가 다르다는 의미는 타입의 주소값이 다르다는 의미이므로 결론적으로 값 타입은 추적이 불가능하다는 것을 알 수 있다


값 타입

값 타입은 크게 3가지로 나눌 수 있다

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

 

그리고 여기서 하나 알아야 하는 것은 "기본 값 타입"[equals() & hashCode()]를 따로 구현하지 않아도 비교가 되지만 "임베디드 타입과 같은 복합 값 타입""반드시 equals() & hashCode()를 재정의"해야 한다


▶ 1. 기본값 타입

기본값 타입은 말그대로 [자바 기본 타입 / 래퍼 클래스 / String]을 의미한다

int value1 = 100;
long value2 = 200L;
double value3 = 5.0;

Integer value4 = 100;
Long value5 = 200L;
Double value6 = 5.0;

String line = "Hello Spring World!!";

이러한 기본값 타입들은 값이 변경되면 이전 값에 대한 추적이 완전히 불가능한 상태이다

따라서 식별자 불가능하고 그에 따른 생명주기도 없는 타입이다

 

값 타입"절대 공유하면 안되는 타입"이다. 그리고 애초에 기본값 타입은 공유 자체가 불가능한 타입이다

왜냐하면 기본값 타입은 값에 대한 참조를 그대로 전달해주는게 아니라 값을 "복사"해서 전달해주기 때문에 공유가 애초에 되지 않는다

 


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

임베디드 타입은 새로운 값 타입을 직접 정의해서 사용하는 것을 의미한다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private Integer age;

    // 근무 기간
    private LocalDate startDate;
    private LocalDate endDate;
    
    // 집 주소
    private String city;
    private String street;
    private String zipcode;
}

이러한 Member Entity가 존재하고 내부 필드는 다음과 같이 부를 수 있다

Member Entity는 (이름, 나이. [근무 시작일, 근무 종료일], [주소 도시, 주소 번지, 주소 우편번호])를 가진다

그런데 현실적으로 생각해보면 어느 누가 Member에 대한 설명을 위와 같이 할까라는 생각이 든다

더 보편적인 설명은 다음과 같을 것이다

Member Entity는 (이름, 나이, 근무 기간, 집 주소)를 가진다

이렇게 설명하는게 더 깔끔하고 현실적일 것이다

 

따라서 "근무 기간 & 집 주소"를 임베디드 타입으로 생성해서 적용시켜보자

근무 기간

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Period {
    private LocalDate startDate;
    private LocalDate endDate;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Period period = (Period) o;
        return Objects.equals(getStartDate(), address.getStartDate()) && Objects.equals(getEndDate(), address.getEndDate());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getStartDate(), getEndDate());
    }
}

집주소

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    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(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

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private Integer age;

    // 근무 기간
    @Embedded
    private Period workPeriod;

    // 집 주소
    @Embedded
    private Address address;
}

이와 같이 더 깔끔한 Entity를 정의할 수 있다

그리고 임베디드 값 타입을 사용할 때는 다음 과정을 거쳐야 한다

1. 임베디드 값 타입에는 반드시 @Embeddable을 붙여야 한다
2. 임베디드 값 타입을 "사용하는" Entity에서는 사용할 때 @Embedded를 붙여야 한다
3. 임베디드 타입은 "기본 생성자가 필수"이다

여기서 가장 중요한 사실은 "임베디드 타입도 값 타입"이라는 점이다

>> 따라서 임베디드 타입은 절대 공유되면 안되는 타입이다

 

그리고 임베디드 타입을 "재정의"해서 사용할 때는 @AttributeOverride를 사용하면 된다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    // 집 주소
    
    // 회사 주소
}

현재 Address라는 임베디드 값 타입을 사용하려고 하는데 이를 (집 주소 & 회사 주소)로 나눠서 사용하려고 한다

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    // 집 주소
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "HOME_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "HOME_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "HOME_ZIPCODE"))
    })
    private Address homeAddress;

    // 회사 주소
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
            @AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
            @AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE"))
    })
    private Address companyAddress;
}

이와 같이 하나의 속성에 대해서 @AttributeOverride로 재정의하고 재정의할 속성이 여러개면 @AttributeOverrides내부에 재정의해주면 된다

Hibernate: 
    create table member (
       member_id bigint not null auto_increment,
        age integer,
        COMPANY_CITY varchar(255),
        COMPANY_STREET varchar(255),
        COMPANY_ZIPCODE varchar(255),
        HOME_CITY varchar(255),
        HOME_STREET varchar(255),
        HOME_ZIPCODE varchar(255),
        name varchar(255),
        endDate date,
        startDate date,
        primary key (member_id)
    ) engine=MyISAM

 


값 타입의 공유

임베디드 타입 같은 값 타입은 여러 엔티티에서 공유하면 위험하고 절대 공유되서는 안되는 타입이다

 

다음과 같은 시나리오가 존재한다고 하자

현재 DB상에 MemberA, memberB가 존재하고 둘다 Address라는 임베디드 값 타입을 가지고 있다고 하자
Address address = new Address("city", "street", "12345");
Member memberA = new Member("memberA", 30, address);
Member memberB = new Member("memberB", 50, address);

여기서 memberA가 이사를 해서 Address가 [city-Update, street-Update, 54321]로 바뀌었다고 하자

Address address = new Address("city", "street", "12345");
Member memberA = new Member("memberA", 30, address);
Member memberB = new Member("memberB", 50, address);

memberA.getAddress().setCity("city-Update");
memberA.getAddress().setStreet("street-Update");
memberA.getAddress().setZipcode("city-54321");

em.persist(memberA);
em.persist(memberB);

이러면 과연 DB에는 각 member에 대한 어떤 값들이 저장될까?

>> 변경을 하지 않은 memberB의 주소도 동일하게 변경됨을 확인할 수 있다

사실 당연한 변화이다. 왜냐하면 두 memberA, memberB 모두 동일한 "Address"라는 임베디드 값 타입을 참조하고 있고, 이 값에 대한 변화가 이루어지면 당연히 둘다 변화된 참조를 가지고 DB에 저장이 되기 때문이다

 

따라서 임베디드 값 타입은 "반드시 불변 객체"로 만들어야 한다

불변 객체로 만드는 방법은 굉장히 다양하지만 지금은 "생성자를 통한 생성"을 다룰 것이다
반드시 임베디드 값 타입은 생성자로만 값이 할당되어야 하고 setter와 같은 변경을 가하는 메소드는 만들지 않아야 한다
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

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

    @Override
    public int hashCode() {
        return Objects.hash(getCity(), getStreet(), getZipcode());
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    ...
    ...

    @Embedded
    private Address address;

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

그리고 엔티티레벨에 아예 따로 "의미있는 변경 메소드"를 만들어서 구현해주는 것이 더 바람직한 설계이다

Member memberA = new Member("memberA", 30, new Address("city", "street", "12345"));
Member memberB = new Member("memberB", 50, new Address("city", "street", "12345"));

memberA.updateAddress(new Address("city-Update", "street-Update", "54321"));

em.persist(memberA);
em.persist(memberB);

이제 원하는대로 memberA에 대한 변경만 이루어진 것을 확인할 수 있다

 


▶ 3. 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 추가적인 애노테이션을 붙여주면 된다

  • @ElementCollection
  • @CollectionTable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private Integer age;
    
    @Embedded
    private Address address;

    @ElementCollection
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    private List<Address> addressHistory = new ArrayList<>();
}

1. favoriteFood라는 String 값 타입을 여러개 저장하고 Set 컬렉션에 보관하기

2. Address라는 임베디드 값 타입을 여러개 저장하고 List 컬렉션에 보관하기

관계형 DB에서는 컬럼내부에 컬렉션을 포함할 수 없기 때문에 컬렉션인 값들에 대해서는 "별도의 테이블"이 구축되어서 FK로 조인하는 형태이다

<Member Table>
member_id라는 PK와 더불어서 {name, age, 임베디드 값 Address : [city, street, zipcode]} 컬럼을 가지고 있다

<Member_addressHistory Table>
member 테이블의 PK인 member_id를 식별관계로 맺어서 본인의 PK이자 FK로 사용한다
그리고 임베디드값 타입의 필드인 {city, street, zipcode}를 컬럼으로 가지고 있다
→ 모든 컬럼을 하나의 PK로 활용한다

<Member_favoriteFoods Table>
member 테이블의 PK인 member_id를 식별관계로 맺어서 본인의 PK이자 FK로 사용한다
그리고 String 필드인 {favoriteFoods}를 컬럼으로 가지고 있다
→ 모든 컬럼을 하나의 PK로 활용한다

여기서 @CollectionTable 애노테이션을 활용하면 생성되는 컬렉션 테이블에 대한 추가 정보도 설정해줄 수 있다

거의 @JoinTable과 비슷한 속성들을 가지고 있고, 거의 동일하게 매핑해주면 된다

 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private Integer age;
    
    @Embedded
    private Address address;

    @ElementCollection
    @CollectionTable(
            name = "favorite_foods",
            joinColumns = @JoinColumn(name = "member_id")
    )
    @Column(name = "food_name")
    private Set<String> favoriteFoods = new HashSet<>();

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

@CollectionTable에 설정한대로 [테이블 이름, FK/PK로 가져오는 이름]이 제대로 적용됨을 확인할 수 있다

  • 임베디드 값 타입인 Address는 따로 @Column을 매핑하지 않아서 임베디드 값 타입 내부 필드이름이 그대로 들어갔다. 만약에 변경하고 싶으면 @AttributeOverride로 재정의 해주면 된다
  • favoriteFoods의 경우 @Column으로 컬럼이름 설정해주지 않으면 그대로 컬렉션 필드이름인 favoriteFoods가 들어가게 되고 위에서는 @Column(name = "food_name")으로 설정해주었기 때문에 food_name으로 컬럼이 생성됨을 확인할 수 있다

값 타입 컬렉션 사용 (저장/수정/삭제)

public void registerAddress(Address address){
    this.address = address;
    this.getAddressHistory().add(address);
}

일단 address를 저장할 때 컬렉션에도 저장할 수 있도록 간편한 편의 메소드를 하나 생성해두었다

Member member = new Member("member", 30);
member.registerAddress(new Address("city1", "street1", "zipcode1"));

member.getFavoriteFoods().add("짜장면");
member.getFavoriteFoods().add("탕수육");
member.getFavoriteFoods().add("짬뽕");

member.registerAddress(new Address("city2", "street2", "zipcode2")); // 주소 변경

em.persist(member);

이렇게 최종적으로 member만 persist하면 어떠한 query가 db로 날라가게 되는걸까?

>> 결론적으로 말하자면 총 6개의 insert query가 날라가고 하나씩 살펴보자

 

(1) Member에 대한 insert query

Hibernate: 
    /* insert Type.domain.Member
        */ insert 
        into
            member
            (city, street, zipcode, age, name) 
        values
            (?, ?, ?, ?, ?)

이 insert query는 em.persist(member)에 대한 insert query이다

(2) List<Address> 컬렉션에 존재하는 2개의 Address에 대한 insert query

Hibernate: 
    /* insert collection
        row Type.domain.Member.addressHistory */ insert 
        into
            address_history
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row Type.domain.Member.addressHistory */ insert 
        into
            address_history
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)

(3) Set<String> 컬렉션에 존재하는 3개의 food에 대한 insert query

Hibernate: 
    /* insert collection
        row Type.domain.Member.favoriteFoods */ insert 
        into
            favorite_foods
            (member_id, food_name) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row Type.domain.Member.favoriteFoods */ insert 
        into
            favorite_foods
            (member_id, food_name) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row Type.domain.Member.favoriteFoods */ insert 
        into
            favorite_foods
            (member_id, food_name) 
        values
            (?, ?)

 

근데 나머지 컬렉션의 5개 instance에 대한 insert query는 어떻게 발생된걸까??

사실 값 타입 컬렉션"Cascade + orphanRemoval" 기능을 필수적으로 가지고 있다.

이러한 기능덕분에 값 타입 컬렉션의 내부 instance들은 포함관계의 Entity가 persist될 때 추가적으로 insert되는 것이다


그리고 추가적으로 @ElementCollection으로 지정된 값 타입 컬렉션의 기본 로딩 전략은 "지연 로딩"이다

Member findMember = em.find(Member.class, 1L);
Set<String> favoriteFoods = findMember.getFavoriteFoods();
List<Address> addressHistory = findMember.getAddressHistory();

// favoriteFoods 내부 리스트들 조회
System.out.println("===== favoriteFoods 내부 instance 조회 =====");
for(String s : favoriteFoods){
    System.out.println(s);
}

// addressHistory 내부 리스트들 조회
System.out.println("===== addressHistory 내부 instance 조회 =====");
for(Address a : addressHistory){
    System.out.println(a);
}
Hibernate: 
    select
        member0_.member_id as member_i1_2_0_,
        member0_.city as city2_2_0_,
        member0_.street as street3_2_0_,
        member0_.zipcode as zipcode4_2_0_,
        member0_.age as age5_2_0_,
        member0_.name as name6_2_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
===== favoriteFoods 내부 instance 조회 =====
Hibernate: 
    select
        favoritefo0_.member_id as member_i1_1_0_,
        favoritefo0_.food_name as food_nam2_1_0_ 
    from
        favorite_foods favoritefo0_ 
    where
        favoritefo0_.member_id=?
짬뽕
짜장면
탕수육
===== addressHistory 내부 instance 조회 =====
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_history addresshis0_ 
    where
        addresshis0_.member_id=?
Address{city='city1', street='street1', zipcode='zipcode1'}
Address{city='city2', street='street2', zipcode='zipcode2'}

"지연 로딩"전략을 활용하기 때문에 컬렉션 내부 값에 실제로 접근할 때 select query가 나가게 되는 것을 확인할 수 있다

 

그러면 이제 값 타입 컬렉션에 대한 "수정"을 해보자

Member findMember = em.find(Member.class, 1L); // member(1L) 조회

Set<String> favoriteFoods = findMember.getFavoriteFoods(); // favoriteFoods에 대한 수정 (삭제 -> 추가)
favoriteFoods.remove("탕수육");
favoriteFoods.add("마라탕");

List<Address> addressHistory = findMember.getAddressHistory(); // addressHistory에 대한 수정 (삭제 -> 추가)
addressHistory.remove(new Address("city1", "street1", "zipcode1"));
addressHistory.add(new Address("city3", "street3", "zipcode3"));

(1) Member, favorite_food, address_history에 대한 select query

Hibernate: 
    select
        member0_.member_id as member_i1_2_0_,
        member0_.city as city2_2_0_,
        member0_.street as street3_2_0_,
        member0_.zipcode as zipcode4_2_0_,
        member0_.age as age5_2_0_,
        member0_.name as name6_2_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
        
Hibernate: 
    select
        favoritefo0_.member_id as member_i1_1_0_,
        favoritefo0_.food_name as food_nam2_1_0_ 
    from
        favorite_foods 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_history addresshis0_ 
    where
        addresshis0_.member_id=?

(2) 기본 값 타입 (favorite_food) 컬렉션 수정

Hibernate: 
    /* delete collection row Type.domain.Member.favoriteFoods */ delete 
        from
            favorite_foods 
        where
            member_id=? 
            and food_name=?
            
Hibernate: 
    /* insert collection
        row Type.domain.Member.favoriteFoods */ insert 
        into
            favorite_foods
            (member_id, food_name) 
        values
            (?, ?)

일단 delete query를 통해서 "탕수육" instance를 제거해주고, insert query를 통해서 "마라탕" instance를 넣어준 것을 알 수 있다

 

 

(3) 임베디드 값 타입 (address_history) 컬렉션 수정

Hibernate: 
    /* delete collection Type.domain.Member.addressHistory */ delete 
        from
            address_history 
        where
            member_id=?
            
Hibernate: 
    /* insert collection
        row Type.domain.Member.addressHistory */ insert 
        into
            address_history
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)
            
Hibernate: 
    /* insert collection
        row Type.domain.Member.addressHistory */ insert 
        into
            address_history
            (member_id, city, street, zipcode) 
        values
            (?, ?, ?, ?)

여기서는 delete query를 보면 "member_id = 1"인 모든 instance를 삭제하는 것을 볼 수 있다

이 과정을 자세히 살펴보면 다음과 같다

1. member_id = 1인 "모든 instance"를 삭제

2. 현재 값 타입 컬렉션에 있는 값들을 "다시 전부 저장(insert)"

 

따라서 (1) 과정에서 일단 member_id = 1인 모든 instance를 삭제한다. 그리고나서 List<Address> 값 타입 컬렉션 내부에는 [주소2, 주소3]이 존재하기 때문에 이 2개의 instance를 다시 전부 DB에 insert query로 넣어주는 것이다

 

이렇게 전부 삭제하고 다시 다 넣는 이유는 다음과 같다

엔티티 타입은 식별자가 존재하기 때문에 값을 변경해도 식별자를 통해서 DB에서 쉽게 찾아올 수 있다
하지만 값 타입은 식별자라는 개념이 없고 단순한 값들이기 때문에 이 값이 변경되면 원본 데이터를 추적할 수 없다

따라서 원래 보관된 값들에 대해서 어느것이 삭제되었는지 판별하기 어렵기 때문에 일단 전부 삭제하고 나서 값 타입 컬렉션에 남아있는 instance들을 다시 전부 insert해주는 것이다

 

따라서 이렇게 값 타입 컬렉션에는 성능적 이슈가 발생할 수 있기 때문에 차라리 값 타입 컬렉션을 사용하기 보다는 새로운 엔티티를 만들어서 일대다 관계로 설정하는 것이 더 바람직하다고 볼 수 있다


엔티티 타입 & 값 타입의 특징

엔티티 타입

- 식별자(@Id)가 존재하고 이 식별자를 통해서 엔티티를 구별할 수 있다

- 생명주기가 존재한다 [생성 / 영속 / 소멸]

  • em.persist를 통해서 영속 / em.remove를 통해서 소멸

- 공유가 가능하다

 

값 타입

- 식별자가 없다

- 생명주기를 엔티티에 의존한다

  • 기본적으로 cascade + orphanRemoval 기능이 존재하기 때문에 의존하는 엔티티가 제거되면 같이 제거된다

- 공유가 불가능하고 공유하면 데이터 정합성에 문제가 발생한다

- 값 타입은 VO(Value Object)라고도 한다