[JPA] Native Query

keymu·2025년 1월 2일
1

Native Query

  • 특정 DB에서 동작
  • @Query에 nativeQuery = true 속성 추가
// Native Query 사용
    @Query(value = "select * from book", nativeQuery = true)
    List<Book> findAllCustom1();
  • 특정 DB에 의존 시, JPA의 장점에서 벗어남
  • DB 벤더를 바꾸거나, 이종 DB로 마이그레이션 하는 게 현업에서 흔한 일 아님

Native Query 사용 이유

1. 성능 이슈 해결

// Native Query로 Update
    @Transactional  // UPDATE, INSERT, DELETE 수행하는 @Query 수행 시
    @Modifying  //  UPDATE, INSERT,DELETE 수행하는 @Query 임을 알림
    @Query(value = "update book set category = 'IT 전문서'", nativeQuery = true)
    int updateCategories();
    // DML 의 경우 리턴타입이 void, int, long 일수 있다.
    // int 나 long 리턴하게 되면 affected row 를 받게 된다.

2. JPA 기본 제공하는 기능이 아닐 시

// JPA 에선 지원하지 않는 쿼리
    @Query(value="show tables", nativeQuery = true)
    List<String> showTables();

Converter

  • JPA에서 Entity의 필드와 데이터베이스 칼럼 사이의 변환을 담당
  • 예시:
    - enum: Converter 사용 / 정의한 순서대로 들어가는 Ordinal, 문자열 둘 중 하나를 쓰는데 가급적 문자열을 쓰도록 권장됨(유지보수를 위해)
    - enum Ordinal일 경우: OrdinalEnumValueConverter 동작

기본 사용법

  • 기본적으로 JPA는 다음과 같은 단순 타입 변환을 자동으로 해준다
@Entity
public class Book {
    private String name;        // VARCHAR로 변환
    private LocalDateTime createdAt;  // TIMESTAMP로 변환
    private Boolean isActive;   // BOOLEAN/BIT로 변환
}
  • 하지만 복잡한 객체를 DB에 저장할 때는 @Converter가 필요하다
@Entity
public class Book {
    @Convert(converter = BookStatusConverter.class)
    private BookStatus status;  // 단순 Integer가 아닌 커스텀 객체로 변환
}

Converter 구현

@Converter
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
    // BookStatus -> DB 칼럼값으로 변환
    @Override
    public Integer convertToDatabaseColumn(BookStatus bookStatus) {
        return bookStatus.getCode();
    }

    // DB column 값 -> BookStatus로 변환
    @Override
    public BookStatus convertToEntityAttribute(Integer s) {
        return s != null ? new BookStatus(s) : null;
    }
}

BookStatus 클래스

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

주의할 점

  • @Transactional과 함께 사용할 때 주의가 필요하다. Converter에서 잘못된 값을 반환하면 영속성 컨텍스트의 변경 감지로 인해 데이터가 유실될 수 있다.
  • Converter는 DB에 매우 가깝게 동작하므로 디버깅이 어렵다. 따라서 아래와 같은 점을 고려해야 한다:
  • null 처리를 확실히 한다
  • 예외 발생 가능성을 최소화한다
  • 변환 로직을 단순하게 유지한다

Converter의 장점

  • 도메인 로직 캡슐화: 단순 정수가 아닌 의미있는 객체로 상태를 표현할 수 있다
  • 데이터 일관성: 상태값 변환 로직을 한 곳에서 관리할 수 있다
  • 객체지향적 설계: DB는 단순하게 유지하면서 애플리케이션에서는 풍부한 객체를 사용할 수 있다

Embedded

  • 코드 반복 -> Embed 쓰기
  • 가독성을 높여줌

임베디드 타입 정의 및 사용

@Data
@AllArgsConstructor
@NoArgsConstructor
@Embeddable     // Embed 할 수 있는 클래스임을 선언
public class Address {
    private String city;        // 도시
    private String district;    // 구/군
    @Column(name = "address_detail")
    private String detail;      // 상세주소
    private String zipCode;     // 우편번호
}
@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "home_city")),
    @AttributeOverride(name = "district", column = @Column(name = "home_distirct")),
    @AttributeOverride(name = "detail", column = @Column(name = "home_address_detail")),
    @AttributeOverride(name = "zipCode", column = @Column(name = "home_zip_code")),
})
private Address homeAddress;  // 집주소

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "city", column = @Column(name = "company_city")),
    @AttributeOverride(name = "district", column = @Column(name = "company_distirct")),
    @AttributeOverride(name = "detail", column = @Column(name = "company_address_detail")),
    @AttributeOverride(name = "zipCode", column = @Column(name = "company_zip_code")),
})
private Address companyAddress;  // 회사 주소

임베디드 타입 활용

@Test
void embeddedTest1() {
    // 기본 저장 테스트
    User user = new User();
    user.setName("절미");
    user.setHomeAddress(new Address("서울시", "강남구", "강남대로 888", "08865"));
    user.setCompanyAddress(new Address("문선시", "문선구", "문선로 777", "12345"));
    userRepository.save(user);

    // 저장된 데이터 확인
    userRepository.findAll().forEach(System.out::println);
    userHistoryRepository.findAll().forEach(System.out::println);
}

@Test
void embeddedTest2() {
    // 1. 정상적인 주소 데이터
    User user1 = new User();
    user1.setName("steve");
    user1.setHomeAddress(new Address("서울시", "강남구", "강남대로 777 골드타워", "08765"));
    user1.setCompanyAddress(new Address("서울시", "성동구", "성수1로 333 우리빌딩", "04455"));
    userRepository.save(user1);

    // 2. null 주소 데이터
    User user2 = new User();
    user2.setName("joshua");
    user2.setHomeAddress(null);
    user2.setCompanyAddress(null);
    userRepository.save(user2);

    // 3. empty 주소 데이터
    User user3 = new User();
    user3.setName("jordan");
    user3.setHomeAddress(new Address());
    user3.setCompanyAddress(new Address());
    userRepository.save(user3);

    // 저장된 데이터 확인
    userRepository.findAll().forEach(System.out::println);
    userHistoryRepository.findAll().forEach(System.out::println);
    
    // DB에 실제 저장된 로우 데이터 확인
    System.out.println("🤪".repeat(20));
    userRepository.findAllRowRecord().forEach(a -> System.out.println(a.entrySet()));
}
        // Embedded된 Address 추가
        userHistory.setHomeAddress(user.getHomeAddress());
        userHistory.setCompanyAddress(user.getCompanyAddress());

        userHistoryRepository.save(userHistory);
    }
}
  • @Embeddable과 @Embedded를 사용하여 값 타입을 정의하고 사용
  • @AttributeOverrides로 같은 임베디드 타입을 여러번 사용할 때 컬럼명 충돌 방지
  • Address가 null이거나 empty인 경우 모든 필드가 null로 저장됨

BUT

  • annotation이 지저분해짐
  • @AttributeOverrieds 대신 또 다른 객체를 선언하는게 나을지, 각자의 판단이 필요
profile
Junior Backend Developer

0개의 댓글