@MappedSuperclass

  • 상속 관계x

  • 엔티티x, 테이블과 매핑x

  • 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공

    부모 클래스에서 수정하면 자식 클래스에서도 적용

    이렇게 적용하면 자식 클래스에서 적용이 됩니다.

  • 조회, 검색 불가(em.find(BaseEntity)불가)

  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할

    부모 클래스에 적으면 매핑 정보가 모인다.

  • 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지
    정한 클래스만 상속 가능

BaseEntity

package com.example.jpa.hellojpa;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@Setter
@ToString
// 매핑 정보만 맞는 부모클래스
@MappedSuperclass
public class BaseEntity {
    @Column(name = "insert_member")
    private String createBy;
    private LocalDateTime createDate;
    @Column(name = "update_member")
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}
@Entity(name = "member_ex")
@Table(name = "MBR")
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@SequenceGenerator(name = "member_seq_generator")
public class Member extends BaseEntity {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @Column(name = "user_name", nullable = false)
    private String userName;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;
}

extends로 BaseEntity를 상속받아준다.
그리고 실행해보면

부모 클래스에서 바꾸었던 insert_member가 자식 클래스에도 적용되서 바뀌는 것을 볼 수 있습니다.


프록시

프록시는 "대신하다"라는 의미를 가지고 있는 단어인데 동작을 대신해주는 가짜 객체의 개념이라고 생각하면 됩니다. 하이버네이트는 지연 로딩을 구현하기 위해 프록시를 사용합니다. 지연 로딩을 하려면 연관된 엔티티의 실제 데이터가 필요할 때 까지 조회를 미뤄야 합니다. 그렇다고 해당 엔티티를 연관관계로 가지고 있는 엔티티의 필드에 null 값을 넣어 둘 수는 없겠죠?

하이버네이트는 지연 로딩을 사용하는 연관관계 자리에 프록시 객체를 주입하여 실제 객체가 들어있는 것처럼 동작하도록 합니다. 덕분에 우리는 연관관계 자리에 프록시 객체가 들어있든 실제 객체가 들어있든 신경쓰지 않고 사용할 수 있습니다. 참고로 프록시 객체는 지연 로딩을 사용하는 것 외에도 em.getReference를 호출하여 프록시를 호출할 수도 있습니다.

기초

  • em.find() : 데이터베이스를 통해서 실제 엔티티 객체 조회
  • em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회

    Id값만 가지고 있는 가짜가 반환이 된다.
  • 실제 클래스를 상속 받아서 만들어짐

    프록시가 실제 객체처럼 동작할 수 있는 이유

  • 실제 클래스와 겉 모양이 같다.
  • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)

특징

  • 프록시 객체는 실제 객체의 참조(target)를 보관

  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

  • 프록시 객체는 처음 사용할 때 한 번만 초기화

  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능

  • 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면문제 발생
    (하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)

프록시 객체의 초기화

최초 지연 로딩 시점에는 당연히 참조 값이 없습니다. 때문에 실제 객체의 메서드를 호출할 필요가 있을 때 데이터베이스를 조회해서 참조 값을 채우게 되는데요, 이를 프록시 객체를 초기화한다고 합니다. 앞서 말씀드렸듯이 실제 객체의 메서드를 호출할 필요가 있을 때 select 쿼리를 실행하여 실제 객체를 데이터베이스에서 조회해오고, 참조 값을 저장하게 됩니다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;

    @JoinColumn(name = "team_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;

    public Member(String name, Team team) {
        this.name = name;
        this.team = team;
    }
    ...
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;

    public Team(String name) {
        this.name = name;
    }
    ...
}

Member의 연관관계 Team은 지연 로딩으로 설정되어 있기 때문에, Member를 조회해오게 되면 team 필드 자리에는 프록시가 들어가 있습니다. 이 때 프록시 Team의 getName 메서드를 호출하게 되면 select 쿼리가 실행되고 프록시가 초기화됩니다.

Team team = member.getTeam();
System.out.println(team.getName()); // 이 시점에 프록시 초기화!

단, 이 때 프록시가 실제 객체를 참조하게 되는 것이지 프록시가 실제 객체로 바뀌지는 않는다는 점을 주의하셔야합니다. 참고로 프록시를 초기화하지 못하는 경우도 있습니다. 프록시의 초기화는 영속성 컨텍스트의 도움을 받습니다. 때문에 영속성 컨텍스트의 관리를 받지 못하는 상황, 즉 준영속 상태의 프록시를 초기화 한다거나 OSIV 옵션이 꺼져 있는 상황에(기본값으로는 켜져 있습니다.) 트랜잭션 바깥에서 프록시를 초기화 하려 하는 경우 LazyInitializationException을 만나게 되실 수 있습니다. 때문에 프록시를 초기화할 때는 반드시 프록시가 영속 상태여야 한다는 점에 주의해주세요!

프록시 확인

  • 프록시 인스턴스의 초기화 여부 확인

    PersistenceUnitUtil.isLoaded(Object entity)

  • 프록시 클래스 확인 방법

    entity.getClass().getName() 출력(..javasist.. or
    HibernateProxy…)

  • 프록시 강제 초기화

    org.hibernate.Hibernate.initialize(entity);

  • 참고: JPA 표준은 강제 초기화 없음

    강제 호출: member.getName()


지연 로딩 & 즉시로딩

지연 로딩

지연 로딩(Lazy)를 사용해서 프록시로 조회

즉시 로딩

즉시 로딩(EAGER)은 조회시 같이 조회
예를 들어, Member 조회시 항상 Team도 조회

JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회

프록시와 즉시로딩 주의

  • 실무에서는 가급적 지연 로딩만 사용
  • 즉시 로딩을 적용하면 에상하지 못한 SQL이 발생
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne은 기본이 즉시 로딩 → LAZY로 설정해야 함
  • @OneToMany, @ManyToMany는 기본이 지연 로딩

영속성 전이 : CASCADE

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용

    예 : 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장

@Entity
@Getter
@Setter
@ToString
public class Parent {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();
    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
}
@Entity
@Getter
@Setter
@ToString
public class Child {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}
@Slf4j
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager entityManager = emf.createEntityManager();

        EntityTransaction tx = entityManager.getTransaction();
        tx.begin();

        try {
            Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addChild(child1);
            parent.addChild(child2);

            entityManager.persist(parent);
            entityManager.persist(child1);
            entityManager.persist(child2);

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

이렇게 하면 일일히 저장을 해줘야 하는데 불편하다. 이럴 때 사용하는 것이 CASCADE입니다.

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();

Parent 클레스에서 childer에 cascade를 적용하고

위에서는

    entityManager.persist(parent);
    entityManager.persist(child1);
    entityManager.persist(child2);

persist를 3번 해줬는데

    entityManager.persist(parent);

한번만 해줘도 됩니다.

CASCADE 주의

  • 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
  • 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐

종류

  • ALL : 모두 적용

    • 라이프사이클을 전부 맞춰야한다면 all 사용
    • CascadeType.PERSIST 와 CascadeType.REMOVE의 기능을 모두 수행 해주는 옵션입니다.
  • PERSIST : 영속

    저장할 때만 맞출거고 따로 둘거면 persist

언제 사용해야할까?

  • 게시판이라던지 첨부파일경로 같은 경우
  • 단일 엔티티에 완전 종속적일 때

    라이프사이클이 같기 때문에

  • 단일 소유자

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티

고아 객체 제거

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

여기 컬렉션에서 빠진 애는 삭제가 된다.

고아 객체 - 주의

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭자하는 기능

  • 참조하는 곳이 하나일 때 사용해야 함

  • 특정 엔티티가 개인 소유일 때 사용

  • @OneToOne, @OneToMany만 가능

  • 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 cascadeType.REMOVE처럼 동작을 한다.

    영속성 전이 + 고아 객체, 생명주기

  • cascadeType.ALL + orphanRemovel=true

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거

  • 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음

  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용


JPA의 데이터 타입 분류

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

값 타입 분류

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

기본 값 타입

  • 생명 주기를 엔티티에 의존

    회원을 삭제하면 이름, 나이 필드도 함께 삭제

  • 값 타입은 공유하면 X

    회원 이름 변경시 다른 회원의 이름도 함께 변경되면 안됨

참고: 자바의 기본 타입은 절대 공유 안됨

  • int, double 같은 기본 타입(primitive type)은 절대 공유x
  • 기본 타입은 항상 값을 복사함
  • Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경x

임베디드 타입

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

임베디드 타입 사용법

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

임베디드 타입의 장점

  • 재사용

  • 높은 응집도

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

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

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

  • 임베디드 타입은 엔티티의 값일 뿐이다.

  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.

  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능

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

@Entity(name = "member_ex")
@Table(name = "MBR")
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@SequenceGenerator(name = "member_seq_generator")
public class Member extends BaseEntity {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @Column(name = "user_name", nullable = false)
    private String userName;

    // 기간
    @Embedded
    private Period workPeriod;

    // 주소
    @Embedded
    private Address homeAddress;
}
@Embeddable
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}
@Embeddable
@ToString
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;
}

스프링 부트와 JPA에서 엠베디드 값 타입(Embedded Value Type)을 사용할 때, DTO 객체에도 따로 값 타입 클래스를 만들어 사용하는 것은 일반적인 접근 방법입니다. 이를 통해 엔티티와 DTO 사이의 의존성을 줄이고, API의 요청/응답 구조를 명확하게 유지할 수 있습니다.

DTO는 엔티티와 API 간의 데이터 교환을 위한 객체이며, 엔티티에서 필요한 정보를 선택적으로 포함하고 구조를 재조정하여 API의 요청/응답 구조에 적합하게 설계됩니다. 값 타입은 주로 엔티티의 속성으로 사용되는 객체이므로, 엔티티와 마찬가지로 DTO에서도 값 타입 객체를 사용하는 것이 바람직합니다.

예를 들어, Member 엔티티에서 Address 값을 타입으로 사용한다고 가정해봅시다. 이 경우 MemberDto라는 DTO를 만들 때, MemberDto 내부에 AddressDto 클래스를 만들어서 사용하는 것이 좋습니다. AddressDto는 Address 값을 타입으로 갖는 DTO로써, 필요한 필드와 구조를 가지고 있습니다.

public class MemberDto {
    private Long id;
    private String userName;
    private AddressDto homeAddress;
    // ...

    public AddressDto getHomeAddress() {
        return homeAddress;
    }

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

    // ...
}

public class AddressDto {
    private String city;
    private String street;
    private String zipcode;
    // ...

    // Getters and Setters
    // ...
}

이렇게 값을 타입으로 사용하는 클래스인 Address에 대한 DTO인 AddressDto를 별도로 정의하여 사용하면, 엔티티와 DTO의 의존성을 분리할 수 있고, API의 요청/응답 구조를 명확하게 유지할 수 있습니다. 또한, 필요에 따라 DTO 클래스에 변환 로직을 추가하여 엔티티와 DTO 간의 데이터 변환을 수행할 수도 있습니다.

이렇게 DTO에도 별도의 값 타입 클래스를 만들어 사용함으로써, 엔티티와 DTO의 구조적인 차이를 유지하고, 유지보수성과 확장성을 높일 수 있습니다.


@AttributeOverride : 속성 재정의

  • 한 엔티티에서 같은 값 타입을 사용하면?
  • 컬럼 명이 중복됨
  • @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의

@MappedSuperclass를 통해 상속 받은 경우나 @Embedded를 통해 다른 객체를 필드에 선언한 경우 해당 엔티티에서는 다른 컬럼명을 사용하고 싶을 때가 있다. 이러한 경우에 사용할 수 있는 어노테이션이다.

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

엠베디드 타입과 null

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

값 타입과 불변 객체

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

값 타입 공유 참조

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

  Address address = new Address("city", "street", "10000");
            
            Member member = new Member();
            member.setUserName("member1");
            member.setHomeAddress(address);
            entityManager.persist(member);
            
            Member member2 = new Member();
            member2.setUserName("member2");
            member2.setHomeAddress(address);
            entityManager.persist(member2);
            
            member.getHomeAddress().setCity("newCity");

부작용이 발생하면 하나를 바꿔주려고 했는데 전체가 바뀌는 부작용이 있을 수 있다. member1과 member2 모두 newCity로 바뀌는 것이다.

값 타입 복사

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

위의 방법은 부작용이 있으니 값 타입 복사를 해야 합니다.

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

            Member member = new Member();
            member.setUserName("member1");
            member.setHomeAddress(address);
            entityManager.persist(member);

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

            Member member2 = new Member();
            member2.setUserName("member2");
            member2.setHomeAddress(copyAddress);
            entityManager.persist(member2);

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

이렇게 하면 첫 번째만 바뀌고 member2는 바뀌지 않는다.

객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.

  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.

  • 자바 기본 타입에 값을 대입하면 값을 복사한다.

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

  • 객체의 공유 참조는 피할 수 없다.

불변 객체

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

불변 객체에 값을 바꾸고 싶으면 새로 만들어야 한다.

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

            Member member = new Member();
            member.setUserName("member1");
            member.setHomeAddress(address);
            entityManager.persist(member);

            // 불변에서 값을 바꾸러면 다시 만들어줘야 한다.
            Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
            member.setHomeAddress(newAddress);

값 타입의 비교

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

  • 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals() 사용
  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함
  • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)
Address address = new Address("city", "street", "10000");
        Address address2 = new Address("city", "street", "10000");
        
  System.out.println("address equals address2 : " + (address.equals(address2)));      

현재는 false가 나올 것이다.

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

equals를 해줘야 true가 나옵니다.

값 타입 컬렉션

db에는 컬렉션을 저장할 수 없습니다. 따라서 jpa의 값 타입 컬렉션은 @ElementCollection과 @CollectionTable 어노테이션을 통해 구현할 수 있습니다.

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

개념

값 타입을 컬렉션에 담아 사용하는 것을 의미합니다.DB에서는 따로 컬렉션을 저장할 수 없으므로, 컬렉션에 해당하는 테이블을 하나 추가하여 컬렉션을 구현합니다. 이를 위해 @ElementCollection과 @CollectionTable 어노테이션을 사용합니다.

특징

① 값 타입 컬렉션은 값 타입과 마찬가지로, 따로 생명주기를 가지지 않고 엔티티와 같은 생명주기를 갖습니다. 일대다 관계에서 CASCADE = ALL, orphanREmoval = TRUE를 설정해준 것과 같습니다. 아래의 예를 통해 이해해보겠습니다.

@Entity(name = "member_ex")
@Table(name = "MBR")
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@SequenceGenerator(name = "member_seq_generator")
public class Member extends BaseEntity {

    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    @Column(name = "user_name", nullable = false)
    private String userName;

    // 기간
    @Embedded
    private Period workPeriod;

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

② 지연로딩전략을 사용합니다.

try {
    Member member = new Member();
    member.setUsername("member1");

    member.getAddressList().add(new Address("city1", "street1", "1"));
    member.getAddressList().add(new Address("city2", "street2", "2"));

    em.persist(member);

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

    System.out.println("================== START ================");
    Member foundMember = em.find(Member.class, member.getId());
    System.out.println("================== 지연로딩 ================");
    List<Address> addressList = foundMember.getAddressList();
    for (Address address : addressList) {
        System.out.println("address.getCity() = " + address.getCity());
    }

    tx.commit();

member를 find할 때 바로 값 타입 컬렉션을 꺼내오는 것이 아니라, 필요한 순간에 지연로딩 됩니다. 아래의 결과를 보고 이해할 수 있습니다.

값 타입 컬렉션의 수정

remove 후 add 하는 방식
값 타입 컬렉션은 call by reference 이므로 값만 단순히 수정해줄 수 없습니다. 따라서 remove후 add하는 방식으로 값 타입을 수정할 수 있지만 독특한 부분이 있습니다. update로 원하는 부분만 수정해주는 것이아니라, delete로 모두 삭제하고 insert 쿼리가 나가기 때문입니다.

값 타입은 엔티티와 다르게 식별자 개념이 없습니다. 값은 변경하면 추적이 어렵습니다. JPA에서는 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장하게 됩니다.

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

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

            entityManager.persist(member);

            entityManager.flush();
            entityManager.clear();

            System.out.println("=====================================");
            Member findMember = entityManager.find(Member.class, member.getId());

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

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

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

값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티오 다르게 식별자 개념이 없다.

  • 값은 변경하면 추적이 어렵다.

  • 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함

    null 입력x, 중복 저장x

값 타입 컬렉션 대안

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

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

profile
발전하기 위한 공부

0개의 댓글