값 타입

식빵·2022년 1월 12일
0

JPA 이론 및 실습

목록 보기
11/17

JPA의 데이터 타입은 크게 엔티티와 값 타입으로 나뉜다.
여태까지는 엔티티를 많이 다뤄봤으니 이제 값 타입을 알아보자.



🍀 엔티티 vs 값 타입

시작하기 앞서 기존 엔티티와 어떻게 다른지 둘을 비교해보고,
상세한 내용들은 목차를 따라가면서 알아가자.


엔티티 타입

  • 식별자(@Id)가 존재한다.
  • 엔티티 각각 생명 주기가 있다.
  • 공유할 수 있다.

값 타입

  • 식별자가 없다.
  • 소속된 엔티티의 생명주기에 의존한다.
  • 공유할 수 있다.
  • 공유 상태에서 변경이 안되도록 불변 객체로 만드는 것이 안전하다.




🍀 값 타입


필요성

지금부터 예제 코드를 통해서 언제 이러한 값 타입을 쓰면 좋은지 알아보자.
조금 억지스러운 예제이지만 이해해주길 바란다.


엔티티 코드

// 기타 엔티티
@Entity
@Setter @Getter
public class Guitar {

    @Id @GeneratedValue
    private Long id;

    private String name; // 기타 이름

    // 기타 메이커(상표)와 관련된 정보들
    private String makerName;       // 메이커(상표) 이름

    private LocalDate establishDate;// 건립일

    private String ceoName;         // CEO 이름
}

위의 엔티티 코드에서 메이커 정보는 DB Table 상에 있는 컬럼이니 당연히 필드가 있기는 해야한다. 하지만 객체의 세계에서는 이런 관련된 정보들을 하나의 클래스로 모아 놓는 게 좋다.
이때 필요한 게 값 타입이다.



값 타입 테스트

아래 예시 코드를 보자.

엔티티 코드

// 기타 엔티티
@Entity
@Setter @Getter
public class Guitar {

    @Id @GeneratedValue
    private Long id;

    private String name; // 기타 이름

    @Embedded
    private Maker maker; // 값타입 사용!
    
    // 유틸리성 메소드 추가...
}

값 타입 클래스 생성

// 악기 메이커 엔티티
// ex: Martin, Yamaha
@Embeddable  // 값 타입 클래스에는 필수 작성
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
public class Maker {

    private String makerName;       // 메이커(상표) 이름

    private LocalDate establishDate;// 건립일

    private String ceoName;         // CEO 이름
}
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)를 하는 이유와 setter 메소드를 생성하지 않는 안하는 이유는 값 타입의 안정성 파트에서 알아볼 것이다.

AUTO DDL QUERY

create table guitar (
    id bigint not null,
    ceo_name varchar(255),
    establish_date date,
    maker_name varchar(255),
    name varchar(255),
    primary key (id)
)
  • 값 타입은 컬렉션으로 사용하지 않는 한 자신만의 테이블을 생성하지 않는다.
  • 그저 자신이 포함된 엔티티의 필드로서 추가만 된다.

값 타입은 JPA에서 사용하려면 @Embedded, @Embeddable 애노테이션을 사용해야 한다.
이런 값타입은 엔티티 persist 시 어떻게 사용할까?

아래 테스트 코드를 보자.


테스트 코드

Guitar guitar = new Guitar();
Maker maker = new Maker("martin", // 상품명
        		LocalDate.of(1833, 9, 9), // 회사 건립일
        		"Christian Frederick Martin"); // 회사 건립자(= CEO )
guitar.setMaker(maker);
guitar.setName("GRAND J-16E");
em.persist(guitar);

테이블 확인



안정성

  • 값 타입은 공유가 가능하다. 객체니까.
  • 공유 상태이기 때문에 어디서나 수정이 가능함
  • 엄청난 부작용이 일어남
  • 이를 막기 위해서 초기에만 값 세팅이 되도록 클래스를 작성해야함
    • 생성자 또는 Builder 를 사용한다.
  • 이런 안정성 유지를 하면서 편의성도 제공하기 위해서 추가적인 작업이 필요할 수 있다.
    • ex) 복제 메소드

아래 코드를 보자.


@Embeddable 
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 1. PROTECTED 접근자 사용
@AllArgsConstructor
@Getter  // 2. Setter 메소드 없음
public class Maker {

    private String makerName;

    private LocalDate establishDate;

    private String ceoName;
        
    // 3. 편의 메소드 추가
    public Maker createCopy() {
        return new Maker(this.makerName, this.establishDate, this.ceoName);
    }
}

1. @NoArgsConstructor(access = AccessLevel.PROTECTED) 설명

값 타입 클래스는 초기에만 초기화가 되도록 생성자를 쓴다고 하였다.
그런데 @Embeddable 클래스는 기본 생성자가 필수다.
하지만 필수라 하여도 쓸모가 없다. 그러니 외부에서 최대한 못 쓰도록 막아 놓은 것이다.


2. setter 사용 ❌

공유 상태에서 서로 값을 바꾸는 것을 막기 위해서 애초에 setter를 생성하지 않는 것이다.


3. createCopy 편의 메소드 (Optional)

다른 엔티티도 값들을 갖고 있는 값 타입 객체를 원할 수 있다.
그때 사용하기 위한 편의 메소드이다.




비교를 위한 메소드 구현

equal, hashcode 메소드를 구현해주자.

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor @Getter
@EqualsAndHashCode // 롬복은 간단하게 애노테이션 추가!!
public class Maker { ~ }



🍀 값 타입 컬렉션과 제약사항

경고! 미리 말하지만 값 타입 컬렉션 보다는 엔티티를 사용하는 것을 추천한다.

이전에는 엔티티 내에서 값 타입을 사용할 때는 Collection 이 아닌 단일 타입으로 사용했다.
이번에는 Collection 타입으로 값타입을 사용해 보자.


엔티티 코드

// 악기점 엔티티
@Entity
@Getter @Setter
public class MusicStore {

    @Id @GeneratedValue
    @Column(name = "music_store_id")
    private Long id;    // 아이디

    private String name; // 가게 명

    @ElementCollection
    @CollectionTable(name = "instrument_types",
                    joinColumns = @JoinColumn(name = "music_store_id"))
    @Column(name = "inst_type") // Collection의 값이 하나면 @Column 사용 가능
    private Set<String> instTypeList = new HashSet<>(); // 악기 종류 리스트

    @ElementCollection
    @CollectionTable(name = "maker_for_sale",
                    joinColumns = @JoinColumn(name = "music_store_id"))
    private List<Maker> makerList = new ArrayList<>(); // 파는 악기의 메이커 리스트

}

생성되는 테이블의 모습

값 타입 컬렉션 하나당 하나의 테이블과 매핑된다.
그리고 이 테이블의 이름은 @CollectionTable 을 통해서 지정이 가능하다.


테스트 코드

만들어봤으니 이제 테스트를 돌려보자.


값 컬렉션 테스트 코드 - INSERT

MusicStore musicStore = new MusicStore();
musicStore.setName("musicStore");

musicStore.getInstTypeList().add("guitar");
musicStore.getInstTypeList().add("bass");
musicStore.getInstTypeList().add("drum");

musicStore.getMakerList().add(new Maker("maker1", LocalDate.now(), "ceo1"));
musicStore.getMakerList().add(new Maker("maker2", LocalDate.now(), "ceo2"));
musicStore.getMakerList().add(new Maker("maker3", LocalDate.now(), "ceo3"));

em.persist(musicStore);

Table 확인



값 컬렉션 테스트 코드 - SELECT

MusicStore musicStore = em.find(MusicStore.class, 1L);


Set<String> instTypeList = musicStore.getInstTypeList();

for (String type : instTypeList) {
    System.out.println("type = " + type);
}

// @ElementCollection 은 기본이 fetch = Fetch.LAZY 이다!
List<Maker> makerList = musicStore.getMakerList();

System.out.println("111111111111111111111");

for (Maker maker : makerList) {
    System.out.println("maker = " + maker.getMakerName());
}

System.out.println("2222222222222222222222");

콘솔 출력 확인



값 컬렉션 테스트 코드 - DELETE

List<Maker> makerList = musicStore.getMakerList();
makerList.remove(1); // 3개중 1개만 삭제 

참고. 값 타입 컬렉션은 이전에 배운 Cascade, 고아객체의 기능을 포함하고 있다.


콘솔 출력 확인

  • 3개 전체를 다 지우고 남는 2개를 다시 insert 한다 -> 값 타입 컬렉션 한계

  • 수정은 단순히 remove + add 를 번갈아서 사용하는 것이므로 PASS!




제약사항 정리

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



값 타입 컬렉션 대안

  • 값 타입 컬렉션대신에 일대다 관계를 고려하자
  • 일대다 관계를 위한 엔티티를 만들고, 이 엔티티에 값 타입을 사용한다.
  • 영속성 전이 + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용한다.



값 타입 컬렉션은 언제 쓰면 좋을까?

추적할 필요도 없고 값이 바뀌어도 update 칠 필요가 없을 때 값 타입 컬렉션을 쓰자.
정말 단순할 때 사용한다.




🍀 참고

자바 ORM 표준 JPA 프로그래밍
인프런 JPA 관련 로드맵

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글