0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.(값 타입, 엔티티 타입)

힘들면힘을내는쿼카 2023. 2. 27. 14:54
728x90
반응형

[JPA] 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.(값 타입, 엔티티 타입)

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

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

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

www.inflearn.com

 

 

 

JPA의 데이터 타입은 2가지로 분류 됩니다.

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
  • 값 타입
    • int, Integer, String, Long 처럼 단순히 값으로 사용하는 java 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적이 불가

 

값 타입과 불변 객체

값 타입에 대해서 본격적으로 들어가기에 앞서 개념적으로 정리해봅시다…!!

값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 등장한 개념 입니다.


값 타입은 등장 목적에 맞게 단순하고 안전하게 다룰 수 있어야 합니다.

 

값 타입 공유 참조

  • 임베디드 타입을 여러 엔티티에 공유하면 위험
  • side-effect 발생

 

어떤 일이 발생하는지 코드를 작성하여 보여드리겠습니다.

 

User

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address homeAddress;
}

 

Address

@Embeddable
@Setter @Getter
@Access(AccessType.FIELD) // 필드에 직접 접근
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
    private String detailAddress;

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

 

실험

tx.begin();

Address address = new Address("서울시", "광장시장로", "110", "신영상가 3층");

User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address);
em.persist(user1);

User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(address);
em.persist(user2);

// 의도: user1의 detail 주소만 변경
user1.getHomeAddress().setDetailAddress("민혁상가 2층");

tx.commit();

 

결과

Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* update
        com.study.purejpa.User */ update
            User 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=?,
            name=? 
        where
            id=?
Hibernate: 
    /* update
        com.study.purejpa.User */ update
            User 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=?,
            name=? 
        where
            id=?

 


Update SQL이 2개 발생하면서 용준이도 민혁상가2층으로 변경되었습니다.

 

이렇게 값 타입의 실제 인스턴스인 값을 공유하는 것은
매우 매우 매우 위.험⚠️ 합니다.

 

 

그러면 어떻게 해야할까요?
바로 값을 복사(깊은 복사)해서 사용해야 합니다.

 

실험

 tx.begin();

Address address = new Address("서울시", "광장시장로", "110", "신영상가 3층");

User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address);
em.persist(user1);

// 깊은 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode(), address.getDetailAddress());

User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(copyAddress);
em.persist(user2);

// 의도: user1의 detail 주소만 변경
user1.getHomeAddress().setDetailAddress("민혁상가 2층");

tx.commit();

 

결과

/* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* update
        com.study.purejpa.User */ update
            User 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=?,
            name=? 
        where
            id=?

 


의도한대로 민혁이만 민혁상가 2층으로 변경되었습니다!!

 

정리

  • java 기본 타입(primitive type)에 값을 대입하면 값을 복사함
  • 임베디드 타입처럼 직접 정의한 값 타입은 객체 타입
  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없음
  • 객체의 공유 참조는 피할 수 없음
  • 임베디드 타입은 값을 복사(깊은 복사)해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있음
    • 하지만 얇은 복사를 막을수 없기 때문에 컴파일러에서 확인할 수 있는 방법이 없음 🥲

 

기본 타입

int a = 10;
int b = a; // 기본 타입은 값을 복사(깊은 복사)

 

객체 타입

Address a1 = new Address("old");
Address a2 = a1; // 객체 타입은 참조를 전달(얇은 복사)
a2.setCity("New"); // a1의 city도 변경됨

객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다….


그러면 어떻게 하면 될까? 🤔

 

객체 타입을 수정할 수 없게 만들면 된다!




불변 객체

  • 값 타입은 불변 객체(immutable object)로 설계해야함
    • 생성 시점 이후 절대 값을 변경할 수 없는 객체(불변 객체)
    • 재할당은 가능하지만, 한번 할당하면 내부 데이터를 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 됨
    • Integer, String, Booleanjava가 제공하는 대표적인 불변 객체

 

Address
setter 제거

@Embeddable
@Getter
@Access(AccessType.FIELD)
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
    private String detailAddress;

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

 

tx.begin();

Address address1 = new Address("서울시", 
        "광장시장로", 
        "110", 
        "신영상가 3층");

User user1 = new User();
user1.setName("민혁이");
user1.setHomeAddress(address1);
em.persist(user1);

/**
 * address1의 값을 변경하고
 * 싶으면 새로운 address2를 만들어야함
 */
Address address2 = new Address(address1.getCity(), 
        address1.getStreet(), 
        address1.getZipcode(), 
        "민혁상가 2층");

User user2 = new User();
user2.setName("용준이");
user2.setHomeAddress(address2);
em.persist(user2);

tx.commit();

 

Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)

 

 

 

 

값 타입은 불변 객체로 작성하자! 👍



값 타입의 비교

값 타입을 비교하려면 어떻게 해야할까요? 🤔

  • 동일성 비교(==)
    • 인스턴스의 참조 값을 비교
  • 동등성 비교(equals())
    • 인스턴스의 값을 비교

 

값 타입은 equals()를 사용해서 동등성 비교를 해야 합니다.
equals()를 적절하게 재정의 해서 사용하면 됩니다.^^
(주로 모든 필드를 사용 합니다.)

// 임베디드 타입을 정의하는 곳에 표시
@Embeddable
@Getter
@Access(AccessType.FIELD)
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
    private String detailAddress;

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

      // 인텔리제이의 도움을 받아 생성(cmd + N)
    @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()) && Objects.equals(getDetailAddress(),
                address.getDetailAddress()
        );
    }

      // 인텔리제이의 도움을 받아 생성(cmd + N)
    @Override
    public int hashCode() {
        return Objects.hash(getCity(), getStreet(), getZipcode(), getDetailAddress());
    }
}

 

 

값 타입 분류

값 타입은 JPA에서 3가지로 분류할 수 있습니다.

  • 기본 값 타입
  • 임베디드 타입(복합 값 타입)
    • JPA에서 정의해서 사용
  • 컬렉션 값 타입
    • JPA에서 정의해서 사용

 

기본 값 타입

기본 값 타입은 프로그래밍시 자주 사용하는
자바 기본 타입(int, double) ,래퍼 클래스(Integer, Long), String 입니다.

 

기본 값 타입은 생명주기를 엔티티에 의존 합니다.
(회원 엔티티를 삭제하면 이름(String), 나이(int)가 함께 삭제)
그리고 값 타입은 공유하면 안됩니다.
(회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됩니다…)

 

참고
javaint, double 같은 기본 타입은 절대 절대 절대로 공유 되지 않습니다.
기본 타입은 항상 값을 복사해서 사용합니다.

int a = 100;
int b = a; // a의 값을 b에 복사

a = 20;

System.out.println(a); // 20
System.out.println(b); // 100

 

Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경이 불가합니다.

Integer a = new Ingteger(10);
Integer b = a; // 공유 가능(클래스라서 참조값(주소값)이 복사됨)

a.setValue(100) // 변경이 불가함(이러한 메소드 존재하지 않음)

 

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

임베디드 타입은 JPA에서 정의해서 사용하는 타입 입니다.
새로운 값(주로 기본 값 타입)을 직접 모아서 정의합니다.

 

임베디드 타입은 엔티티의 값일 뿐 입니다.^^
(임베디드 타입의 값이 null 이면 매핑한 컬럼 모두 null 입니다.)


임베디드 타입도 결국에는 값 타입 입니다.(추적이 불가 합니다.)
JPA 입장에서 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같습니다.

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address address; // 집 주소
}

 

@Embeddable
@Access(AccessType.FIELD)
public class Address {
    private String city;
    private String street;
    private String zipcode;
    private String detailAddress;

      public boolean isDetailAddress(String detailAddress) {
        if(detailAddress.isBlank()) {
            return false;
        }
        return true;
    }
}

 

장점

  • 재사용
  • 높은 응집도
  • 값 타입만 사용하는 의미있는 메소드 생성 가능
  • 임베디드 타입을 소유한 엔티티에 생명주기를 의존

 

참고
잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다고 한다! ㅎㅎ

 

 

그런데 하나의 엔티티에서 같은 임베디드 타입을 사용하면?🤔

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address homeAddress;
    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address workAddress;
}

 

@AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의 하면 됩니다.

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @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")
            ),
            @AttributeOverride(
                    name = "detailAddress", 
                    column = @Column(name = "work_detailAddress")
            ),
    })
    private Address workAddress;
}

 

 

컬렉션 값 타입

컬렉션 값 타입이란 값 타입을 컬렉션에 담아서 사용하는 것을 의미 합니다.

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

 

예제

 

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address homeAddress;

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

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

 

Hibernate: 

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

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

    create table User (
       id bigint not null,
        city varchar(255),
        detailAddress varchar(255),
        street varchar(255),
        zipcode varchar(255),
        name varchar(255),
        primary key (id)
    )

FAVORITE_FOODADDRESS 테이블이 생성되는 create SQL을 보면 PK가 없는 것을 확인할 수 있습니다.
이처럼 값 타입 컬렉션에는 몇 가지 제약사항이 존재합니다.

 

값 타입 컬렉션 제약사항

  • 값 타입은 엔티티와 다르게 식별자(PK)가 없음
    • 값이 변경되면 추적이 어려움
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함
    • null 허용 🙅‍♂️, 중복 저장 🙅‍♂️

 

저장 예제

tx.begin();

User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));

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

user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));

em.persist(user1);

tx.commit();

 

결과

Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.addressHistory */ insert 
        into
            ADDRESS
            (user_id, city, detailAddress, street, zipcode) 
        values
            (?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.addressHistory */ insert 
        into
            ADDRESS
            (user_id, city, detailAddress, street, zipcode) 
        values
            (?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)

 


자세히 보면 em.persist(user1)만 했는데도
자동으로FAVORITE_FOOD, ADDRESS 테이블에 데이터가 들어갔습니다.


생명주기(life-cycle)가 User에 소속되었기 때문입니다.

 

 

조회할 때는 어떨까요? 🤔
조회 예제

tx.begin();

User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));

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

user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));

em.persist(user1);

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

// 조회
User findUser = em.find(User.class, user1.getId());

tx.commit();

 

결과

Hibernate: 
    select
        user0_.id as id1_2_0_,
        user0_.city as city2_2_0_,
        user0_.detailAddress as detailad3_2_0_,
        user0_.street as street4_2_0_,
        user0_.zipcode as zipcode5_2_0_,
        user0_.name as name6_2_0_ 
    from
        User user0_ 
    where
        user0_.id=?

엇! user만 조회하는것을 확인할 수 있습니다.
값 타입 컬렉션은 지연 로딩을 사용하는 것 같은데요?

 

 

조회 지연로딩 예제

// 조회
User findUser = em.find(User.class, user1.getId());

// 값 타입 컬렉션 조회(지연로딩)
List<Address> addressHistory = findUser.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("city="+address.getCity());
}

// 값 타입 컬렉션 조회(지연로딩)
Set<String> favoriteFoods = findUser.getFavoriteFoods();

for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood="+favoriteFood);
}

 

결과

Hibernate: 
    select
        addresshis0_.user_id as user_id1_0_0_,
        addresshis0_.city as city2_0_0_,
        addresshis0_.detailAddress as detailad3_0_0_,
        addresshis0_.street as street4_0_0_,
        addresshis0_.zipcode as zipcode5_0_0_ 
    from
        ADDRESS addresshis0_ 
    where
        addresshis0_.user_id=?
city=과천시
city=안산시
Hibernate: 
    select
        favoritefo0_.user_id as user_id1_1_0_,
        favoritefo0_.FOOD_NAME as food_nam2_1_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.user_id=?
favoriteFood=족발
favoriteFood=치킨
favoriteFood=피자

값 타입 컬렉션은 지연로딩 전략을 사용하는 것을 확인할 수 있습니다!!!

 

 

그런데 말 입니다..?
값 타입 컬렉션에 변경 사항이 발생하면? 🤔

수정 예제

tx.begin();

User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));

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

user1.getAddressHistory().add(new Address("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));

em.persist(user1);

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

// 조회
User findUser = em.find(User.class, user1.getId());

/**
 * 변경사항이 생기면?
 * 값 타입은 불변객체이기 때문에
 * 새로운 인스턴스를 넣어야 합니다.
 */
// 민혁상가 1층 -> 민혁상가 5층
findUser.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 5층"));

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

// Address에 equals, hashCode 재정의 되어있어야함.
// 안산시 -> 시흥시
findUser.getAddressHistory().remove(new Address("안산시", "중앙로", "2448", "민혁상가3 3층"));
findUser.getAddressHistory().add(new Address("시흥시", "중앙로", "2448", "민혁상가3 3층"));

tx.commit();

 

결과

Hibernate: 
    /* update
        com.study.purejpa.User */ update
            User 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=?,
            name=? 
        where
            id=?

// ADDRESS를 통으로 삭제하고 시흥시와 과천시 Address를 삽입
Hibernate: 
    /* delete collection com.study.purejpa.User.addressHistory */ delete 
        from
            ADDRESS 
        where
            user_id=?
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.addressHistory */ insert 
        into
            ADDRESS
            (user_id, city, detailAddress, street, zipcode) 
        values
            (?, ?, ?, ?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.addressHistory */ insert 
        into
            ADDRESS
            (user_id, city, detailAddress, street, zipcode) 
        values
            (?, ?, ?, ?, ?)
//================================================================//

Hibernate: 
    /* delete collection row com.study.purejpa.User.favoriteFoods */ delete 
        from
            FAVORITE_FOOD 
        where
            user_id=? 
            and FOOD_NAME=?
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)

 

 

여기서 신기한건 FAVORITE_FOOD 테이블의 치킨 -> 한식은 delete SQL이후 insert SQL이 발생했습니다.
그런데 ADDRESS 테이블은 ADDRESS 전체 데이터를 삭제하고 insert SQL 2개가 발생했습니다.


그 이유는 주인 엔티티와 연관된 모든 데이터를 삭제하고,

값 타입 컬렉션에 있는 현재 값을 모두 다시 저장해야 하기 때문입니다.

 

복잡하지요…?

 

값 타입 컬렉션 사용하지 마세요! 🙅‍♂️

 

✅ 값 타입 컬렉션 대신에 일대다(1:N) 관계를 사용하는 것을 권장드립니다.

 

AddressEntity

@Entity
@Getter
@Table(name = "address")
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;
    private Address address;
}

 

User

@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @Embedded // 임베디드 타입 사용하는 곳에 표시
    private Address homeAddress;

    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD",
            joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();
	
    // 일대다 관계 사용(컬렉션 값 타입 X)
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "user_id")
    private List<AddressEntity> addressHistory = new ArrayList<>();
}

 

값 타입 컬렉션 대신 일대다 관계 사용

tx.begin();

User user1 = new User();
user1.setName("홍길동");
user1.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 1층"));

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

user1.getAddressHistory().add(new AddressEntity("과천시", "정부청사로", "1550", "민혁상가2 2층"));
user1.getAddressHistory().add(new AddressEntity("안산시", "중앙로", "2448", "민혁상가3 3층"));

em.persist(user1);

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

// 조회
User findUser = em.find(User.class, user1.getId());

/**
 * 변경사항이 생기면?
 * 값 타입은 불변객체이기 때문에
 * 새로운 인스턴스를 넣어야 합니다.
 */
// 민혁상가 1층 -> 민혁상가 5층
findUser.setHomeAddress(new Address("서울시", "민혁로", "105", "민혁상가 5층"));

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

// 민혁상가3 3층 -> 뉴진스의 하입보이
AddressEntity addressEntity = findUser.getAddressHistory().get(1);
addressEntity.setAddress(new Address("서울시", "홍대로", "88", "뉴진스의 하입보이"));

tx.commit();

 

결과

Hibernate: 
    /* insert com.study.purejpa.User
        */ insert 
        into
            User
            (city, detailAddress, street, zipcode, name, id) 
        values
            (?, ?, ?, ?, ?, ?)
Hibernate: 
    /* insert com.study.purejpa.AddressEntity
        */ insert 
        into
            address
            (city, detailAddress, street, zipcode, id) 
        values
            (?, ?, ?, ?, ?)
Hibernate: 
    /* insert com.study.purejpa.AddressEntity
        */ insert 
        into
            address
            (city, detailAddress, street, zipcode, id) 
        values
            (?, ?, ?, ?, ?)

// 일대다(1:N) 연관관계 주인을 일(1)에 설정해서 발생
Hibernate: 
    /* create one-to-many row com.study.purejpa.User.addressHistory */ update
        address 
    set
        user_id=? 
    where
        id=?

// 일대다(1:N) 연관관계 주인을 일(1)에 설정해서 발생
Hibernate: 
    /* create one-to-many row com.study.purejpa.User.addressHistory */ update
        address 
    set
        user_id=? 
    where
        id=?
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)
Hibernate: 
    select
        user0_.id as id1_2_0_,
        user0_.city as city2_2_0_,
        user0_.detailAddress as detailad3_2_0_,
        user0_.street as street4_2_0_,
        user0_.zipcode as zipcode5_2_0_,
        user0_.name as name6_2_0_ 
    from
        User user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        favoritefo0_.user_id as user_id1_1_0_,
        favoritefo0_.FOOD_NAME as food_nam2_1_0_ 
    from
        FAVORITE_FOOD favoritefo0_ 
    where
        favoritefo0_.user_id=?
Hibernate: 
    select
        addresshis0_.user_id as user_id6_0_0_,
        addresshis0_.id as id1_0_0_,
        addresshis0_.id as id1_0_1_,
        addresshis0_.city as city2_0_1_,
        addresshis0_.detailAddress as detailad3_0_1_,
        addresshis0_.street as street4_0_1_,
        addresshis0_.zipcode as zipcode5_0_1_ 
    from
        address addresshis0_ 
    where
        addresshis0_.user_id=?

// 민혁상가 1층 -> 민혁상가 5층
Hibernate: 
    /* update
        com.study.purejpa.User */ update
            User 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=?,
            name=? 
        where
            id=?

// 민혁상가3 3층 -> 뉴진스의 하입보이
Hibernate: 
    /* update
        com.study.purejpa.AddressEntity */ update
            address 
        set
            city=?,
            detailAddress=?,
            street=?,
            zipcode=? 
        where
            id=?

// 치킨 -> 한식
Hibernate: 
    /* delete collection row com.study.purejpa.User.favoriteFoods */ delete 
        from
            FAVORITE_FOOD 
        where
            user_id=? 
            and FOOD_NAME=?
Hibernate: 
    /* insert collection
        row com.study.purejpa.User.favoriteFoods */ insert 
        into
            FAVORITE_FOOD
            (user_id, FOOD_NAME) 
        values
            (?, ?)

 

 

 

정리

엔티티 타입의 특징

  • 식별자 🙆‍♂️
  • 생명 주기 관리
  • 공유 🙆‍♂️

 

 

값 타입의 특징

  • 식별자 🙅‍♂️
  • 생명 주기를 엔티티에 의존
  • 공유하지 않는 것이 안전 👷🏼‍♂️
    • 복사(깊은 복사)해서 사용
  • 불변 객체로 만드는 것이 안전 👷🏼‍♂️

 

값 타입은 정말 값 타입이라 판단될 때만
사용하세요!



 

 

엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 생성하면 안됩니다.

식별자가 반드시 필요하고, 지속적으로 값을 추적하고 변경해야 한다면..........

 

그것은 엔티티 타입 입니다.!!!!😁

 

 

 

 

 

728x90
반응형