쿼리를 통해서 가져온 데이터를 객체로 매핑할때 커스터마이징 하는 방법을 학습 해봤다.
jpa는 orm의 일종의 인터페이스다. 즉, 데이터베이스의 레코드를 자바의 객체화 시켜주는 역할을 한다.
자바에 객체화를 할 때 db데이터와 형식이 다른 경우 어떻게 매핑하는지 알아보겠다.
바로 @Converter를 통해서 데이터를 가져오는 즉시 정보를 변경해서 마치 엔티티에 원래 그런값들이 있었던것처럼 핸들링 해준다.
jpa에서 enum데이터를 가져오는 경우 converter를 사용하게 된다.
@Enumerated(value = EnumType.STRING)
private Gender gender;
위 코드는 DB의 Enum과 자바의 Enum Type 형식이 다르기 때문에 (value = EnumType.STRING)속성을 지정했다. 다른 데이터를 변환하기 위해서는 어떻게 해야할까?
숫자코드를 의미가 있는 임의의 객체로 변경해보자
Book클래스에 판매상태를 나타내는 필드를 추가해준다.
@Convert(converter = BookStatusConverter.class)
private BookStatus status; //판매상태
Dto패키지에 BookStatus클래스를 생성하고
package com.fastcampus.jpa.bookmanager.repository.dto;
import lombok.Data;
@Data
public class BookStatus {
private int code;
private String description;
public BookStatus(int code) {
this.code = code;
this.description = parseDescription(code);
}
private String parseDescription(int code) {
return switch (code) {
case 100 -> "판매종료";
case 200 -> "판매중";
case 300 -> "판매보류";
default -> "미지원";
};
}
public boolean isDisplayed() {
return code == 200;
}
}
domain패키지에 converter패키지를 하나 추가해서
package com.fastcampus.jpa.bookmanager.domain.converter;
import com.fastcampus.jpa.bookmanager.repository.dto.BookStatus;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter(autoApply = true)
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) {
// 객체를 DB로 저장할 때 어떻게 변경할 것이냐 묻는 것이다.
return attribute.getCode();
}
@Override
public BookStatus convertToEntityAttribute(Integer dbData) {
// DB값을 객체로 변경할 때 어떻게 할거냐?
// null이 아니라면 새 객체를 만들어서 반환할것이라고 작성하였다.
return dbData != null ? new BookStatus(dbData) : null;
}
}
data.sql의 쿼리에 status 컬럼도 추가해준다.
insert into book(`id`,`name`,`publisher_id`, `deleted`, `status`) values(1, 'JPA 초격차 패키지', 1, false, 100);
insert into book(`id`,`name`,`publisher_id`, `deleted`, `status`) values(2, 'Srping Security 초격차 패키지', 1, false, 200);
insert into book(`id`,`name`,`publisher_id`, `deleted`, `status`) values(3, 'SpringBoot 올인원 패키지', 1, true, 100);
@Test
void converterTest() {
bookRepository.findAll().forEach(System.out::println);
Book book = new Book();
book.setName("또다른 IT전문서적");
book.setStatus(new BookStatus(200));
bookRepository.save(book);
System.out.println(bookRepository.findRawRecord().values());
bookRepository.findAll().forEach(System.out::println);
}
Book(super=BaseEntity(createdAt=2024-02-01T15:07:55.355759, updatedAt=2024-02-01T15:07:55.355759), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false,
status=BookStatus(code=100, description=판매종료))
Book(super=BaseEntity(createdAt=2024-02-01T15:07:55.358632, updatedAt=2024-02-01T15:07:55.358632), id=2, name=Srping Security 초격차 패키지, category=null, authorId=null, deleted=false,
status=BookStatus(code=200, description=판매중))
Book(super=BaseEntity(createdAt=2024-02-01T15:07:56.072319, updatedAt=2024-02-01T15:07:56.072319), id=4, name=또다른 IT전문서적, category=null, authorId=null, deleted=false,
status=BookStatus(code=200, description=판매중))
나는 DB에 저장할 때는 숫자값만 넣었을 뿐인데 조회를 해보니 이렇게 나온다. 그렇다면 DB에 저장은 어떻게 되어있을까?
@Query(value = "select * from book order by id desc limit 1", nativeQuery = true)
Map<String, Object> findRawRecord();
bookRepository에 위 와같은 native query를 추가해준 뒤, 결과를 보면
[false, 200, null, 2024-02-01 15:07:56.072319, 4, null, 2024-02-01 15:07:56.072319, null, 또다른 IT전문서적]
number타입의 숫자값으로 들어가 있는 것을 알 수 있다.
근데 애초에 Enum이나 임베디드 타입클래스를 활용해서 DB를 설계하면 되지 않느냐고 반문 할 수 있겠지만 다른 시스템이나 레거시 데이터를 연동하는 경우. 특히 예전에는 int값으로 상태를 드러내는 방식을 활용 했기 때문에 converter를 사용 할 줄 알아야 한다.
jpa는 자동으로 영속성 관리를 해줘서 편리하지만 개발자가 생각하지 못한 에외를 일으키기도 한다.
어떤 개발자가 attributeConverter를 개발했는데 그 중 메소드 하나를 사용하지 않아 개발 하지 않았다고 가정해보자.
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) {
return null;
}
그리고 BookService에 아래와 같이 작성하고
@Transactional
public List<Book> getAll() {
List<Book> books = bookRepository.findAll();
books.forEach(System.out::println);
return books;
}
간단한 조회 테스트를 진행하였다.
@Test
void converterErrorTest() {
bookService.getAll();
bookRepository.findAll().forEach(System.out::println);
}
Book(super=BaseEntity(createdAt=2024-02-01T16:03:32.683835, updatedAt=2024-02-01T16:03:32.683835), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false,
status=BookStatus(code=100, description=판매종료))
Book(super=BaseEntity(createdAt=2024-02-01T16:03:32.687960, updatedAt=2024-02-01T16:03:32.687960), id=2, name=Srping Security 초격차 패키지, category=null, authorId=null, deleted=false,
status=BookStatus(code=200, description=판매중))
Hibernate:
update
book
set
author_id=?,
category=?,
deleted=?,
name=?,
publisher_id=?,
status=?,
updated_at=?
where
id=?
Hibernate:
update
book
set
author_id=?,
category=?,
deleted=?,
name=?,
publisher_id=?,
status=?,
updated_at=?
where
id=?
Book(super=BaseEntity(createdAt=2024-02-01T16:03:32.683835, updatedAt=2024-02-01T16:03:33.467189), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false,
status=null)
Book(super=BaseEntity(createdAt=2024-02-01T16:03:32.687960, updatedAt=2024-02-01T16:03:33.489213), id=2, name=Srping Security 초격차 패키지, category=null, authorId=null, deleted=false,
status=null)
JPA에서는 영속성 컨텍스트를 관리하고, 트랜잭션 단위 내에서 엔티티의 상태 변화를 추적한다. session이 완료되는 시점에 영속성 컨텐스트는 해당 엔티티중에서 변경된 값이 있는지 검사를 하고 있다면 데이터를 db에 영속화 하게 된다. Service의 getAll()메소드가 실행되면서 converter가 작동했기 때문에 update구문이 발생한것이고, 영속성 컨텍스트에 로딩되어 있는 값을 findAll()에서 null출력이 되는것이다.
그렇기에 converter를 구현한다면 필요하든 필요하지 않든 모든 로직을 구현하는것이 바람직하다.
converter를 자주 사용하게 될 때에 오토어플라이 옵션을 사용하는것이 좋다. @Converter에 autoapply라는 속성을 제공하고 있다. true로 변경하면 @Convert가 없어도 해당객체 타입이 엔티티에 필드로 선언되어 있으면 converter로 변환되도록 처리된다.
@Converter(autoApply = true) // true로 설정한다면
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
//@Convert(converter = BookStatusConverter.class)//없어도 된다.
private BookStatus status;
편리하긴 하지만 주의해야할 점이 있다. 일반적으로 예시로 든BookStatus처럼 개발자가 생성한 타입에 한해서 활용해야한다는 점이다. 예를 들어 String, Integer컨버터를 만들어서 autoApply를 적용하게 되면 모든 Varchar, number타입의 데이터는 컨버터가 작동 되어 문제점이 발생할 수도있다.