Zayson
A to Zayson!
Zayson
전체 방문자
오늘
어제
  • 분류 전체보기 (132)
    • Computer Science (20)
      • Network (4)
      • DB (12)
      • OS (4)
    • Algorithm (32)
      • 완전탐색(Brute-Force) (3)
      • 그리디(Greedy) (6)
      • 투포인터(Two-Pointer) (1)
      • 그래프(Graph) (5)
      • BFS & DFS (9)
      • 구현, 시뮬레이션(Implementation) (5)
      • 다이나믹 프로그래밍(DP) (3)
    • Backend (51)
      • Spring Boot (19)
      • JPA (16)
      • Kafka (2)
      • Java (13)
      • Kotlin (1)
    • DevOps (1)
      • Jenkins (5)
      • Oracle Cloud Infrastructure (1)
      • Kubernetes & Docker (1)
    • Trouble Shooting (3)
      • JPA (1)
      • Spring Boot (2)
    • 회고 (5)
      • 엔빵 프로젝트 포스트 로드맵 (1)
      • 2022년 (4)
    • Kafka (7)
      • Kafka (5)
      • Kafka Connect (2)
    • 기술 서적 (6)
      • 데이터 중심 애플리케이션 설계 (3)
      • 개발자가 반드시 정복해야할 객체 지향과 디자인 패턴 (2)
      • 가상 면접 사례로 배우는 대규모 시스템 설계 기초 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • Java
  • JPA
  • 구현
  • 관계형 데이터베이스 실전 입문
  • 백준
  • 프로그래머스
  • 나 혼자 스프링부트!
  • SpringBoot
  • kafka
  • CS
  • spring boot
  • Backend
  • 엔빵프로젝트
  • Computer science
  • 그리디
  • 라이브스터디
  • 완전탐색
  • dfs
  • Kafka Connect
  • BFS

최근 글

티스토리

hELLO · Designed By 정상우.
Zayson

A to Zayson!

값 타입
Backend/JPA

값 타입

2022. 7. 26. 12:05
  • 엔티티 타입
    • @Entity
    • 데이터가 변해도 식별자를 이용해 추적하는 것이 가능하다. 즉 컬럼의 데이터가 변경되더라도 @Id로 데이터를 추적하는 것이 가능하다.
  • 값 타입
    • int, Integer, String 같이 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있기 때문에 변경되더라도 추적이 불가능하다.

 

값 타입 분류

  1. 기본값 타입 : 자바 기본 타입 (프리미티브 타입), 래퍼 클래스, String
  2. 임베디드 타입 (Embeddet Type, 복합 값 타입) : 커스텀 클래스 같은 형태
    • ex) Position (x, y), Address (city, street, zipcode)
  3. 컬렉션 값 타입 : 기본 값타입 , 임베디드 타입을 컬렉션에 넣은 형태

 

기본값 타입

  • 생명주기를 엔티티에 의존한다.
    • 회원 엔티티를 삭제하는 순간 가지고 있는 나이, 이름 같은 값 타입도 함께 삭제된다.
  • 값 타입은 공유하면 안된다.
    • 회원 이름 변경 시 다른 회원의 이름도 변경되면 안된다.
  • 자바의 프리미티브 타입은 값을 복사해서 사용하기 때문에 공유되지 않는다.
int a = 5; 
int b = a;

a = 10; 

// 결과 a : 10 , b : 5 -> 값 자체를 복사해서 사용하기 때문에 공유하지 않는다.
  • 래퍼클래스, String 클래스 같은 경우는 참조 값을 이용해 공유하는 것은 가능하지만 변경하는 것이 불가능하다.

 

임베디드 타입

  • 새로운 값 타입을 직접 정의하는 것이 가능하다.
  • 주로 기본 값 타입을 모아서 만든다.
  • 공통되는 속성을 하나의 클래스로 추출하고 값 타입으로 생성해 사용한다.
  • 값 타입으로 추출한 클래스에는 @Embeddable을 붙여서 값 타입임을 명시해준다.
  • 값 타입을 이용하는 엔티티에서는 @Embedded를 이용해 주입해준다. (@Embeddable을 값 타입 클래스에 붙여놓은 경우 생략이 가능하지만 작성하는 것이 권장된다.)
  • 값 타입으로 추출한 클래스는 반드시 기본 생성자를 가져야한다.
// As-Is Member
// 근무 기간을 하나의 값 타입으로 추출
private LocalDateTime startDate;
private LocalDateTime endDate;

// 주소 정보를 하나의 값 타입으로 추출
private String city;
private String street;
private String zipcode;

// To-Be Member
@Embedded
private Period period;

@Embedded
private Address address;
@Embeddable
public class Address {
    // 주소 정보
    private String city;
    private String street;
    private String zipcode;
}

@Embeddable
public class Period {
    // 근무 기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

 

  • 모든 값 타입은 소유한 엔티티에 생명주기를 의존한다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 동일하다.
  • 응집도가 높은 컬럼을 가지고 해당 컬럼들을 이용해 의미있는 메서드를 만듬으로 써 재사용하는 것이 가능하다.
  • 값 타입은 내부에 다른 엔티티 값을 가질 수 있다.
@Embeddable
public class Period {
    // 근무 기간
    private LocalDateTime startDate;
    private LocalDateTime endDate;

        private Member member; // 엔티티 값을 가질 수 있다. 
}

 

  • 한 엔티티 내에서 중복되는 값 타입을 가질 수 있는 경우가 있다. 이 때 컬럼 명이 중복될 수 있다.
    • @AttributeOverrides, @AttributeOveride를 이용한다.
// 주소 정보 값 타입
    @Embedded
    private Address 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 homeAddress;

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

 

값 타입과 불변 객체

  • 값 타입을 여러 엔티티에서 공유할 수 있지만, 다른 엔티티들이 하나의 값 타입 참조를 가지는 경우 하나의 값이 변경되면 모두 변경되게 된다.
Address address = new Address("seoul", "road", "12345");

// 동일한 주소를 갖는 경우
Member member = new Member();
member.setUsername("zayson");
member.setAddress(address);
em.persist(member);

Member member1 = new Member();
member1.setUsername("zayson2");
member1.setAddress(address);  // Address에 대해 동일한 참조를 가지고 있다.
em.persist(member1);

member1.getAddress().setCity("Bundang"); // update 쿼리가 2번 날아간다.

Member findMember = em.find(Member.class, member1.getId());
findMember.getAddress().setCity("Busan"); // 이 경우도 영속성 컨텍스트가 현재 관리하고 있기 때문에 동일하게 두개의 값이 변경된다

 

  • 값 타입의 실제 인스턴스 값을 공유하는 것 보다 인스턴스 값을 복사해서 사용하는 것이 해결책이 될 수 있다.
Address address = new Address("seoul", "road", "12345");
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

 

  • 직접 정의한 값 타입은 기본 타입이 아니라 객체 타입이기 때문에 참조 값을 직접 대입하는 것을 막을 수 없다.
member1.setAddress(member.getAddress()); 
Address copyAddress = address;

 

  • 값 타입은 생성 시점 이후에 값을 변경할 수 없도록 불변 객체로 설계해야한다.
  • 생성자로만 값을 생성하고, Setter를 만들지 않는다.
  • 값 타입을 변경하고 싶은 경우 새로 생성해서 값을 교체한다.
// 값 타입을 변경하고 싶은 경우
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member.setAddress(newAddress);

 

  • 프리미티브 타입의 경우 값을 비교하는 것이 가능하지만 임베디드 된 값 타입의 경우 데이터는 동일하다고 해도 참조가 다르기 때문에 다른 값이다.
  • 모든 값 타입은 값의 동등성 비교가 가능해야 하기 때문에 .equals()와 hashCode()를 오버라이딩 해 동등성 비교를 구현할 수 있다.
@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()를 사용한다.

 

값 타입 컬렉션

  • 값 타입 컬렉션의 경우 별도의 테이블로 관리해야한다. (1:N 테이블로 표현된다.)
  • Set, List와 같이 기본적인 자료구조를 이용할 수 있다.
  • 값 타입 컬렉션 매핑
    • @ElementCollection, @CollectionTable을 이용해 매핑한다.
@ElementCollection // 값 타입 컬렉션을 나타낸다.
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
     @JoinColumn(name = "FODD_NAME"))  // 값 타입 컬렉션의 테이블 명 지정
private Set<String> favoriteFoods = new HashSet<>();

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

 

  • 값 타입 컬렉션 데이터 세팅
    • persist(member)를 한 번만 호출해도 관련된 값 타입 컬렉션 값들이 테이블에 각각 삽입된다.
    • Favorite_Food insert 쿼리 3번 호출, AddressHistory 쿼리 2번 호출
    • 값 타입 컬렉션의 경우 엔티티의 라이프 사이클이 맞춰진다. 값 타입, 값 타입 컬렉션은 자신의 라이프 사이클을 갖고 있지 않고 엔티티에 의존된다.
    • Cascade + 고아 객체 제거 기능을 필수로 가진다.
member.getFavoriteFoods().add("chicken");
member.getFavoriteFoods().add("pizza");
member.getFavoriteFoods().add("rice");

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

 

  • 값 타입 컬렉션 조회
    • 엔티티 조회 시에 관련된 값 타입 컬렉션은 함께 조회 쿼리가 날아가지 않는다. 즉, 값 타입 컬렉션은 지연 로딩이다.
    • 값 타입 컬렉션을 지정하는 @ElementCollection에 fetchType의 default가 LAZY로 되어있다.
    • 1:1로 매핑되어 있는 (엔티티, 테이블에 포함되어 있는) 값 타입은 조회 쿼리가 함께 나간다.

for (Address address : findMember.getAddressHistory()) {
        System.out.println("address = " + address.getCity());  // 값 타입 컬렉션은 지연로딩
}

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

 

 

  • 값 타입의 경우 immutable해야 하므로 값 자체를 새로 생성해서 갈아 끼워준다.
// 좋지 않음
findMember.getHomeAddress().setCity("new");

// 새로운 인스턴스로 갈아치워야한다.
Address oldAddress = findMember.getHomeAddress(); findMember.setHomeAddress(new Address("new", oldAddress.getStreet(), oldAddress.getZipcode()));

 

 

  • 값 타입 컬렉션도 변경할 값 지운 후 새롭게 삽입 (Delete And Insert)
// 값 타입 컬렉션 수정 
findMember.getFavoriteFoods().remove("chicken");
findMember.getFavoriteFoods().add("korean");

 

  • 값 타입 컬렉션을 수정하는 경우 (커스텀 클래스) 엔티티와 관련된 값 타입 컬렉션이 모두 삭제되고 새로 수정할 값과 기존에 남아있는 값이 다시 삽입된다. (Delete All → 기존 삭제되지 않은 데이터, 변경할 데이터 새로 Insert)
  • 값 타입의 경우 자신의 식별자가 따로 존재하지 않고 연관 엔티티가 라이프 사이클을 관리하기 때문에 중간에 값이 변경되더라도 해당 값 타입의 변경을 추적하는 것이 어렵다.

 

  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어 하나의 기본키로 구성해야한다. (Not Null, 중복 저장 X)
  • 실무에서는 값 타입 컬렉션 대신 1:N 관계를 고려한다.
// As is
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
        @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

// To Be (값 타입 컬렉션을 엔티티로 승격 후 OneToMany 매핑)
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
        @Id
        @GeneratedValue
    private Long id;

    private Address address;
}

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)  // 값 타입은 (Cascade + orphanRemoval을 가진 형식)
private List<AddressEntity> addressHistory = new ArrayList<>();

 

  • 값 타입 컬렉션이 엔티티로 변경되면서 식별자가 생겼고, 이로 인해 값이 변경되더라도 값의 변경을 추적하는 것이 가능하다. (엔티티 승격)

 

값 타입 VS 엔티티

엔티티  값 타입
식별자 O 식별자 X
자체적인 라이프 사이클 라이프 사이클을 엔티티에 의존
값을 공유 값을 공유하지 않는 것이 안전
  • 식별자를 통해 값의 변경을 추적해야 한다면 값 타입을 사용하지 않고 엔티티로 승격해서 사용한다.
  • 엔티티와 값 타입을 혼동해 엔티티 → 값 타입으로 만들면 안된다.

 

📄 References

김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard

반응형
저작자표시 비영리 변경금지 (새창열림)

'Backend > JPA' 카테고리의 다른 글

페치 조인 (Fetch Join)  (0) 2022.08.03
JPQL 기본  (0) 2022.07.29
영속성 전이 (Cascade), 고아 객체  (0) 2022.07.21
즉시 로딩과 지연 로딩  (0) 2022.07.21
프록시  (0) 2022.07.20
    'Backend/JPA' 카테고리의 다른 글
    • 페치 조인 (Fetch Join)
    • JPQL 기본
    • 영속성 전이 (Cascade), 고아 객체
    • 즉시 로딩과 지연 로딩
    Zayson
    Zayson
    공부한 내용을 정리하는 공간

    티스토리툴바