값 타입

뚝딱이·2022년 9월 18일
0

JPA

목록 보기
8/11

JPA는 최상위 데이터 타입을 2가지로 분류한다.

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

값 타입은 크게 세가지로 분류할 수 있다.

  1. 기본값 타입
  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String
  1. 임베디드 타입(embedded type, 복합 값 타입)
    예) x,y좌표가 있을 때 이것을 묶어서 사용하고 싶을 때 사용한다.
  2. 컬렉션 값 타입(collection value type)
    예) 자바 컬렉션에 값 타입들을 넣어 사용하는 것

기본 값 타입

String name과 int age같은 것들이다.
생명주기가 엔티티에 의존한다. 따라서 만약 회원을 삭제하면 안에 name과 age 필드도 함께 삭제 된다.
값 타입은 공유하면 안된다.
예를 들어 회원 이름을 변경할 때 다른 회원의 이름이 변경 되면 안된다.

int a = 10;
int b =a;

a = 20;

위와 같은 상황이라고 했을 때 b는 a가 복사되어서 값이 들어가는 것이다. 따라서 둘은 다른 저장공간을 가지고 있다. 그러므로 a와 b는 공유되고 있지 않다.

참고 : 자바의 기본 타입은 절대 공유 되지 않는다.
기본 타입은 항상 값을 복사한다.
Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체지만 변경되지 않는다.

Integer나 String은 클래스이므로 reference를 긁어간다. 따라서 공유 된다.

Integer a = new Integer(10);
Integer b = a;

위의 상황에선 Integer는 클래스이기 때문에 b에는 a값이 복사되는 게 아닌 a의 주소값이 들어가게 된다.

그러면 a가 변경될 경우 b도 변경된다.

Integer a = new Integer(10);
Integer b = a;

a.setValut(20);

위와 같이 a의 값을 설정하는 setValue라는 메소드가 있다고 하자. 그러면 a의 값을 20으로 설정하면 b의 값도 20으로 바뀌게 된다.
같은 인스턴스를 공유하기 때문이다. 하지만 변경이 불가능하므로 공유로 일어날 문제를 사전에 차단한다.

이것이 기본 값으로 개발을 할 수 있는 이유이다.

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

  • 새로운 값 타입을 직접 정의할 수 있다.
  • JPA는 임베디드 타입(embedded type)이라고 한다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
  • int, String과 같은 값 타입이다. 따라서 변경시에 추적이 불가능하다.

위와 같은 회원 엔티티가 있다고 하자.
그렇다면 이 회원 엔티티에서 공통으로 관리할 수 있을 것 같은 컬럼들이 보일 것이다.

startDate와 endDate를 날짜로 묶고, city, street, zipcode를 주소로 묶을 수 있을 것이다. 그리서 날짜를 workPeriod로, 주소는 homeAddress로 묶어보자.
그렇다면 아래와 같은 엔티티가 될 것이다.

위의 그림을 보면 workPeriod의 타입은 Period로, homeAddress의 타입은 Address로 자바가 기본으로 제공하는 타입이 아니다. 우리가 이 타입들을 만들어 볼 것이다.

Member엔티티를 풀면 위와 같이 연결되어있는 것이다.

사용법

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

장점

  • 재사용이 가능하다. 기간이나 주소같은 경우엔 이 시스템 전체에서 재사용이 가능할 것이다.

  • 높은 응집도, 클래스 내에선 응집도가 높다.

  • Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.

  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.

테이블 매핑

실제 테이블 매핑시에는 기간과 주소가 처음과 같이 들어간다. 데이터베이스는 데이터를 잘 보관하는 것이 목적이기 때문이다.

그럼 왜 사용하는 것일까 ?
객체는 데이터 뿐만아니라 메서드 같은 기능도 가지고 있기 때문에 공통된 것들 끼리 묶었을 때 이점이 많다.

예제를 통해 알아보자.

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    private LocalDateTime startDate;
    private LocalDateTime endDate;

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

    public Member() {
    }
}

위와 같이 Member엔티티를 만들면 아래와 같이 모든 필드로 데이터베이스가 만들어지는 것을 볼 수 있다.

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        endDate timestamp,
        startDate timestamp,
        street varchar(255),
        USERNAME varchar(255),
        zipcode varchar(255),
        primary key (MEMBER_ID)
    )

임베디드를 활용해보자.

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    //Period
    @Embedded
    private Period workPeriod;

    //Address
    @Embedded
    private Address homeAddress;

    public Member() {
    }
}
@Embeddable
public class Address {

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

    public Address() {
    }
}
@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public Period() {
    }

@Embeddable과 @Embeded는 둘 중 하나만 써도 되지만 두개 다 쓰는 것을 권장한다.
위와 같이 엔티티를 설계하면 어떤 쿼리가 나가게 될까.

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        endDate timestamp,
        startDate timestamp,
        primary key (MEMBER_ID)
    )

위에서와 같이 모든 필드가 데이터베이스에 쿼리로 나가는 것을 볼 수 있다.
객체는 객체 지향스럽게 설계하고, 데이터베이스는 데이터 베이스의 목적에 맞게 설계가 된것이다.

Member member = new Member();
member.setUsername("hello");
member.setAddress(new Address("city", "street", "zipcode"));
member.setWorkPeriod(new Period());
em.persist(member);

위와 같이 활용할 수 있다.

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

Member는 Address와 PhoneNumber 임베디드 타입을 가질 수 있다. 임베디드 타입은 임베디드 타입을 가질 수 있으므로 Address는 Zipcode를 가지고 있는데 임베디드 타입은 엔티티 또한 가질 수 있다. 따라서 PhoneNumber는 PhoneEntity를 가진다. 이는 foreign key만 가지고 있으면 되기때문에 가능하다.

만약 한 엔티티에서 같은 값 타입을 사용한다면 어떻게 될까. 예를 들어 Member가 타입이 Address인 homeAddress와 workAddress를 가진다면 어떻게 해야할까.

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때
    
    @Embedded
    private Period workPeriod;
    
    @Embedded
    private Address homeAddress;

    @Embedded
    private Address workAddress;

    public Member() {
    }
}

위와 같이 작성하게 되면 오류가 난다. 중복 매핑 되었기 때문이다.

이때 사용할 수 있는 것이 AttributeOverride이다.

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address 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 Address workAddress;

    public Member() {
    }
}

위와 같이 수정해보자. 그렇다면 쿼리는 어떻게 나갈까.

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,
        primary key (MEMBER_ID)
    )

위와 같이 workAddress도 잘 추가된 것을 확인할 수 있다.

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

@Embedded
private Period workPeriod = null;

로 수정하면 wordkPeriod의 모든 값인 startDate와 endDate모두 null값이 들어간다.

값 타입과 불변 객체

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

값 타입 공유 참조

개발할 때 크게 신경쓰지 않는 것이 있다. 바로 값을 복사하는 것이다. 이 이유는 자바에서 단순하고 안전하게 다룰 수 있도록 설계가 되어있기 때문이다.

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유할 수 있다. 때문에 이것을 공유하면 위험하다.

회원 1과 회원 2가 같은 값타입인 주소를 공유할 때, 주소에 있는 city가 NewCity로 변경된다고 해보자. 그러면 회원 1과 회원 2의 테이블이 모두 NewCity로 바뀌게 된다.

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

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

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

member1과 member2가 같은 address를 가지고 있다. 이때 DB는 아래와 같다.

문제 없이 값이 들어간 것을 볼 수 있다.
하지만 이때 member1의 address를 변경하고 싶어서 변경해보면 어떻게 될까.

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

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

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

            member1.getHomeAddress().setCity("newCity");

위의 코드를 실행하면 update 쿼리가 아래와 같이 두번 나가는 것을 확인할 수 있다.

Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            city=?,
            street=?,
            zipcode=?,
            USERNAME=?,
            endDate=?,
            startDate=? 
        where
            MEMBER_ID=?

데이터베이스의 값을 확인해보자.

위와 같이 member1 뿐만 아니라 member2의 address 값도 변경 된 것을 볼 수 있다.
이러한 side effect로 인한 버그는 잡기가 굉장히 어렵다.

따라서 값을 공유하는 것은 위험하므로 값을 복사해서 사용해야한다.

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

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

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

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

            member1.getHomeAddress().setCity("newCity");

위와 같이 address의 값을 복사해서 address1에 넣고, member2가 address1를 사용하면 member1의 address값이 변경되어도 member2의 address값이 변경되지 않는다.

한계

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

따라서 정리하자면 위의 그림과 같이 기본 타입은 값을 복사해서 위험하지 않으나 객체 타입은 참조를 복사해서 전달해 변경시에 위험이 있다.

a와 b모두 같은 인스턴스를 가리키기 때문이다.

불변 객체

따라서 불변객체를 만들어서 사용해야한다.

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

setter를 만들지 않으면 값을 변경할 수 없으므로 이러한 위험이 없어진다. 아예 만들지 말던지 setter를 private으로 만들면 된다.

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

그렇다면 값을 바꾸고 싶을 땐 어떻게 해야할까.

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

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

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

address를 새로 만들어서 member1에 넣으면 된다.

값 타입 비교

값 타입 : 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.

예를 들어

int a = 10;
int b = 10;

일 때 a와 b는 같다.

Address address1 = new Address("서울시");
Address address2 = new Address("서울시");

와 같이 만들어도 address1과 address2는 ==비교를 하면 false가 나온다. address1과 address2의 참조값이 각각 다르기 때문이다.

  • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
  • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)

그냥 equals를 사용하면 안된다. equal는 기본으로 == 비교하게 되어있기 때문이다.

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

위와 같이 오버라이드해서 사용한다.

값 타입 컬렉션

값타입을 컬렉션에 담아서 사용하는 것을 말한다.

객체입장에서 컬렉션을 사용하는 것은 자연스럽지만, DB에선 따로 테이블 내에 컬렉션을 저장할 수 없다.

따라서 Member에 favoriteFoods 테이블을 별도로 뽑아 일대 다 관계로 매핑해야한다.

테이블을 뽑을 때 따로 식별자를 pk로 지정하지 않는 이유는, 값 타입이기 때문이다. 식별자로 추정이 가능해지면 그것은 값 타입이 아닌 엔티티 타입이 되어버린다.

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;//객체는 username db엔 name이라고 쓰고 싶을 때

    @Embedded
    private Address homeAddress;

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

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

    public Member() {
    }
}

위와 같이 작성하면 된다.
쿼리를 확인해보자.

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255),
        USERNAME varchar(255),
        primary key (MEMBER_ID)
    )
Hibernate: 
    
    create table FAVORITE_FOOD (
       MEMBER_ID bigint not null,
        FOOD_NAME varchar(255)
    )
Hibernate: 
    
    create table ADDRESS (
       MEMBER_ID bigint not null,
        city varchar(255),
        street varchar(255),
        zipcode varchar(255)
    )

Member 테이블 외에도 FAVORITE_FOOD와 ADDRESS 테이블이 생성된 것을 확인할 수 있다.

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

값 타입 저장

그렇다면 컬렉션에 값을 넣어보자


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

위와 같이 값을 넣으면 아래와 같이 저장된다.

기대했던 대로 잘 동작하는것을 확인할 수 있다.

값 타입 컬렉션을 따로 persist하지 않고 member만 persist해도 각각의 테이블에 값이 들어간다. 값 타입이기 때문에 라이프 사이클(생명 주기)이 member에 의존한다.

값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

값 타입 조회

그럼 Member를 조회해보자.

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

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

Member를 조회하면 과연 FAVORITE_FOOD와 ADDRESS까지 모두 조회될까?
실행 결과는 아래와 같다.

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만 조회하는 것을 알 수 있다. 따라서 컬렉션은 모두 지연로딩하는 것이다.

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

List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
	System.out.println("address = " + address.getCity());
}

그럼 getAddressHistory를 통해 Address 테이블을 건드리면 ? 그제서야 쿼리가 나가는 것을 확인할 수 있다.

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=?

따라서 컬렉션은 지연로딩 전략을 사용한다.

값 타입 수정

조회한 findMember의 address에 city를 수정해보자. 어떻게 하면 될까

findMember.getHomeAddress().setCity("newCity");

와 같이 하면 된다고 생각할 수 있다. 하지만 앞에서도 이야기 했듯이 임베디드 타입은 불변해야하므로 setter를 없애거나, private으로 제약을 걸어야한다. 따라서 위의 코드는 사용불가하다.

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

위와 같이 새로운 인스턴스를 만들어서 넣어야한다.

그럼 컬렉션은 어떻게 업데이트하는지 알아보자.

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

위의 코드와 같이 변경하려는 항목을 삭제 후 추가해야한다. 그럼 아래와 같이 delete 쿼리한번 insert 쿼리한번으로 수정이 이루어지는 것을 볼 수 있다.

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

그럼 임베디드 타입으로 된 address를 업데이트 해보자.

findMember.getAddressHistory().remove(new Address("old1", "street","10000"));
findMember.getAddressHistory().add(new Address("newCity1","street","10000"));

컬렉션은 remove할 대상을 찾을 때 기본적으로 equals를 사용한다. 이때 equals는 기본적으로 == 비교를 하기 때문에 address에 equals를 오버라이드해야한다. equals가 구현되어있지 않으면 위의 코드 처럼 remove에 똑같은 값을 가진 인스턴스를 넣어도, 다른 인스턴스이기 때문에 remove하지 못한다.
remove한 다음엔 새로운 인스턴스를 생성해 add해주면 된다.

그런데 위의 코드를 실행했을 경우 쿼리를 한번 살펴보자.

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
        

address 테이블을 모두 삭제 하고 insert 쿼리가 두번 나간다. 우리는 분명히 add를 한번하고 특정 주소를 remove했는데 결과는 우리가 의도한대로 되었으나, 과정이 우리가 의도한 바와는 다르게 진행된것이다.

제약 사항

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

대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
  • EX) AddressEntity
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "MEMBER_ID")
    private List<AddressEntity> addressEntity = new ArrayList<>();

Member에선 AddressEntity로 리스트를 만들고 OneToMany로 매핑한다.

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

    @Id @GeneratedValue
    private Long id;

    private Address address;
}

그리고 AddressEntity를 만들어 id를 만들고 address를 한번 감싸면 된다.

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

와 같이 사용하자. 이렇게 되면 식별자가 있기 때문에 찾아서 수정이 가능하다.

값 타입 컬렉션은 언제 사용하는가 ? 진짜 단순할 때. select 박스에 치킨과 피자를 멀티로 선택할 수 있을 때, 이렇게 추정할 필요도 없고 값이 바뀌어도 update할 필요가 없을 때 사용한다.

정리

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

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


출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편

profile
백엔드 개발자 지망생

0개의 댓글