회사에서 JPQL을 통해 테이블 조인 및 검색 조건을 추가하는 쿼리를 작성하고 있었는데, 쿼리 성능 개선을 위하여 inlineView(from 절 subquery)를 작성해야 할 필요가 있었다.
많은 게시글에서 JPA에서 inlineView를 사용하지 못한다는 글을 봐서, 사용할 수 있는 다양한 샘플과 함께 회사 코드에서는 사용이 어려웠던 이유, Spring Data JPA 레포지토리에 이슈를 생성한 것 까지 모두 작성해본다.
JPA에서 inlineView, 사용가능한가?
사용가능하다. 이전에 작성한 글에서 이미 말했듯이, Hibernate 6.1 버전부터 inlineView가 사용가능 하다.
다만, 다양한 케이스를 테스트 하면서 알게 된 점은 데이터를 Entity, Interface Projection, Class Projection, List 형식 모두 상관없이 쿼리를 작성할 수 있지만, PageRequest와 함께 inlineView 쿼리를 작성하게 되면 JPA가 제공하는 count 쿼리만으로는 오류가 발생한다.
이 부분에 대해서는 Spring-Data-JPA 레포지토리에 이슈를 작성하여 문제가 맞는것인지, 해결 가능한 문제인지를 천천히 살펴 본 후 공유할 생각이다.
라이브러리를 유지보수 하시는 개발자께서는 이를 countQuery를 직접 작성하는 방법으로 문제를 해결하시길 권유하셨고, 이슈는 closed 상태가 되었다. 아마도 이 부분에 대해서 따로 지원하실 계획은 없는것으로 보인다.
[Spring JPA][Interface Projection] inline subquery with join - 가능
[Spring JPA][Class Projection] inline subquery with join - 가능
[Spring JPA][Interface Projection] multiple Join inline subquery with join - 가능
[Spring JPA][Interface Projection] multiple Join inline subquery with join and param - 가능
[Spring JPA][Interface Projection] page request without subquery - 가능
[Spring JPA][Interface Projection] page request without join - countQuery 추가 작성 필요
샘플 쿼리를 디버깅하면서 page타입으로 데이터를 수신할 때에만 Spring-Data-JPA의 AbstractJpaQuery 클래스의 createCountQuery에서 호출이 실패하고 오류 로그가 발생하는 것을 알 수 있었다.
아래는 호출에 실패한 count Query와 오류 로그이다.
SELECT count(T) FROM (SELECT T2.id as id, T2.name as name, T2.address as address FROM Member T2 ) T
org.hibernate.query.SemanticException: The derived SqmFrom[id, name, address] can not be used in a context where the expression needs to be expanded to identifying parts, because a derived model part does not have identifying parts. Replace uses of the root with paths instead e.g. `derivedRoot.get("alias1")` or `derivedRoot.alias1`
생성한 카운트 쿼리를 실제로 실행해봐도 정상적으로 동작하지 않는것을 알 수 있었는데, 위의 카운트 쿼리에서 InlineView 쿼리까지는 정상적으로 동작하지만 결과 테이블을 T라는 alias로 접근 가능하도록 했고 이를 바깥 쪽 SELECT 할 때 alias 테이블 이름만 사용했기 때문에 문제가 발생하는 것으로 보인다. 이를 T.id 처럼 alias와 함께 필드 이름을 적용하면 정상적으로 카운트 쿼리가 동작한다.
마무리
Spring-Data-JPA에 기여하는 것은 실패했다. 내 샘플 코드와 함께 문제를 찾고, 이를 통해 비슷한 문제를 겪었던 사람들에게 도움이 되길 바랬는데.. (Spring 기여자가 되고 싶은 마음이 제일 크긴하다) 아쉽지만 다음 기회를 노려야겠다 ㅎㅎ
결국에는 countQuery를 직접 입력하여 inlineView 쿼리문을 작성할 수 있었고 이를 통해 기존 쿼리 응답의 경우822.2ms, 수정된 쿼리의 경우 489.9ms 로 67.83%의 성능 개선을 얻을 수 있었다
회사에서 쿼리작성도중 From절 Subquery가 필요한 상황이 있었는데, 서비스가 Spring Data JPA를 사용하고 있었는데, 아직 자료가 많이 없는 상황이라 도움이 되기를 바라며 쿼리를 작성한 경험을 공유하고자 글을 쓴다.
JPA에서의 From절 Subquery 사용가능 여부 확인
JPA에서 From절 Subquery로 많은 검색을 해봤지만, 쓸만한 내용이 없었고 검색 결과에 가장 많이 보이는 내용은 JPA의 한계라는 내용이었다. 많은 글에서 JPA는 From절 Subquery를 지원하지 않는다는 내용을 볼 수 있었고, 그 중 Hibernate 6.1 버전에서 Subquery에 관한 지원이 추가되었다는 사실을 알게 되었다.
이 사실을 통해 hibernate 문서를 뒤졌고, from절에서 subquery를 사용하는 예제를 찾을 수 있었다.
하지만, 현재 Spring Data JPA 버전에서 6.1버전 이상의 Hibernate를 사용했는지 여부를 확인해야 했기에, 프로젝트 내부에서 버전 정보를 확인했다.
보시다시피 Hibernate6.4.1 버전을 사용하고 있었기에, 충분히 JPA로 쿼리를 생성할 수 있겠다는 결론을 내렸고, From절 Subquery를 만들어 보기로 했다.
From 절 Subquery 작성
테스트를 위해 기존에 JPA 학습을 위해 생성한 레포지토리 내에서 몇가지를 수정하여 테스트를 시작했다. 파일 구성은 아래에 소개한다.
테스트 파일을 작성 할 수도 있겠지만, 있었던 controller를 수정하는 것이 더 빠를것 같아서 Controller를 아래처럼 수정했다.
package com.hyeonwoo.spring.jpa.springjpa;
import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
@Controller
@RequiredArgsConstructor
public class HelloController {
private final MemberService memberService;
@GetMapping("test")
public void test(Model model) {
memberService.testSpringMember();
}
@GetMapping("insert")
public void insert() {
Member a = new Member(1L,"박현우", new Address("Seoul", "Main Street", "12345"));
Member b = new Member(2L,"차은우", new Address("Seoul", "Main Street", "12345"));
Member c = new Member(3L,"장원영", new Address("Seoul", "Main Street", "12345"));
Member d = new Member(4L,"카리나", new Address("Seoul", "Main Street", "12345"));
Member e = new Member(5L,"홍윤기", new Address("Seoul", "Main Street", "12345"));
memberService.insertTestDataSet(a);
memberService.insertTestDataSet(b);
memberService.insertTestDataSet(c);
memberService.insertTestDataSet(d);
memberService.insertTestDataSet(e);
}
}
아래는 Service 클래스이다.
Service 클래스에서는 Spring Data JPA를 통해 쿼리를 생성하는 방법과 Entity Manager를 직접 다루는 방법 두가지를 시도해보았다.
package com.hyeonwoo.spring.jpa.springjpa.service;
import com.hyeonwoo.spring.jpa.springjpa.domain.Address;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import com.hyeonwoo.spring.jpa.springjpa.repository.MemberRepository;
import com.hyeonwoo.spring.jpa.springjpa.repository.SpringMemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final SpringMemberRepository springMemberRepository;
public void testSpringMember() {
List<Member> test = springMemberRepository.findByInlineViewTest("박현우");
log.info(test.toString());
// EXCEPTION
}
public void testMember() {
List<Member> test = memberRepository.findByInlineViewTest("박현우");
log.info(test.toString());
// EXCEPTION
}
@Transactional
public void insertTestDataSet(Member a) {
springMemberRepository.save(a);
}
}
아래는 Spring Data JPA를 통해 서브쿼리를 사용해보는 Repository 예제 코드이다.
Select 조건에 new Member라는 생성자를 통해 데이터를 삽입했는데, 생성자를 사용하지 않을 시 오류가 발생하여 아래의 StackOverflow 포스팅을 참고했다.
package com.hyeonwoo.spring.jpa.springjpa.repository;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface SpringMemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT new Member(T.id as id,T.name as name,T.address as address) FROM (SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T")
List<Member> findByInlineViewTest(@Param("name") String name);
}
EntityManager를 사용하여 쿼리를 생성한 예제이다.
package com.hyeonwoo.spring.jpa.springjpa.repository;
import com.hyeonwoo.spring.jpa.springjpa.domain.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
public List<Member> findByInlineViewTest(String name) {
return em.createQuery("SELECT T.id, T.name, T.address FROM ( SELECT T2.id as id,T2.name as name,T2.address as address FROM Member T2 WHERE T2.id = 1) T WHERE T.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
마지막으로 Entity 클래스이다. 특별한 내용은 없다.
package com.hyeonwoo.spring.jpa.springjpa.domain;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@RequiredArgsConstructor
@AllArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
}
package com.hyeonwoo.spring.jpa.springjpa.domain;
import jakarta.persistence.Embeddable;
import lombok.Getter;
@Embeddable
@Getter
public class Address { // 값 타입은 변경 불가능하게 설계해야 한다.
private String city;
private String street;
private String zipCode;
protected Address(){} // 임베디드 타입은 자바 기본 생성자를 public 또는 protected로 설정해야 한다.
// Setter를 사용하지 않고 생성자에서 값을 모두 초기화 해서 변경 불가능한 클래스로 제공
public Address(String city, String street, String zipCode) {
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
}
위처럼 파일 세팅 후 H2 데이터베이스에 데이터를 임시로 삽입하는 API를 호출했고, 이후 서브쿼리를 통해 데이터를 조회하는 부분에서 디버깅을 통해 데이터가 잘 들어오는지 확인할 수 있었다.
알게된 점
hibernate 6.1버전 이상에서는 JPA를 통해 From 절 Subquery SQL을 작성 할 수 있다.
Subquery 작성 시 alias를 작성하지 않으면 이름을 찾지 못하는 오류가 있는 것 같다. 예제에서도 alias가 필수적으로 붙어있는 이유로 보인다.
Entity 타입으로 데이터를 조회할 수 있었지만, class Projection을 통해서는 데이터를 수신하는데 실패하고 있다. 이 부분은 좀 더 확인해보고, 공유할 만한 내용이 있다면 공유할 생각이다.
이 튜토리얼에서는 Spring Boot 애플리케이션의 로깅 수준을 런타임에 변경하는 방법을 살펴보겠습니다. 많은 것들과 마찬가지로 Spring Boot에는 우리를 위해 구성하는 기본 제공 로깅 기능이 있습니다 . 실행 중인 애플리케이션의 로깅 수준을 조정하는 방법을 살펴보겠습니다.
이를 위해 세 가지 방법을 살펴보겠습니다. Spring Boot Actuator 로거 엔드포인트를 사용하는 방법, Logback 의 자동 스캔 기능을 사용하는 방법 , 마지막으로 Spring Boot Admin 도구를 사용하는 방법입니다.
이 번역본에서는 Spring Boot Actuator를 통해 로깅 레벨을 변경하는 방법만 안내합니다.
Spring boot Actuator
/ loggers Actuator 엔드포인트를 사용하여 로깅 레벨을 표시하고 변경하는 것으로 시작하겠습니다 . / loggers 엔드포인트는 actuator/loggers 에서 사용할 수 있으며 경로의 일부로 이름을 추가하여 특정 로거에 액세스할 수 있습니다.
마지막으로, 실험의 효과를 볼 수 있도록 일련의 로깅 명령문을 포함하는 컨트롤러를 만들어 보겠습니다.
@RestController
@RequestMapping("/log")
public class LoggingController {
private Log log = LogFactory.getLog(LoggingController.class);
@GetMapping
public String log() {
log.trace("This is a TRACE level message");
log.debug("This is a DEBUG level message");
log.info("This is an INFO level message");
log.warn("This is a WARN level message");
log.error("This is an ERROR level message");
return "See the log for details";
}
}
/ loggersEndpoint 사용
애플리케이션을 시작하고 로그 API에 접근해 보겠습니다.
curl http://localhost:8080/log
그럼, 로그를 확인해 볼까요? 로그에는 세 개의 로깅 문장이 있습니다.
2019-09-02 09:51:53.498 INFO 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController : This is an INFO level message
2019-09-02 09:51:53.498 WARN 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController : This is a WARN level message
2019-09-02 09:51:53.498 ERROR 12208 --- [nio-8080-exec-1] c.b.s.b.m.logging.LoggingController : This is an ERROR level message
이제 / loggers Actuator 엔드포인트를 호출하여 com.baeldung.spring.boot.management.logging 패키지 의 로깅 수준을 확인해 보겠습니다.
마지막으로, 로그 API를 다시 실행하여 변경 사항이 실제로 어떻게 적용되는지 확인할 수 있습니다.
curl http://localhost:8080/log
이제 로그를 다시 확인해 보겠습니다.
2019-09-02 09:59:20.283 TRACE 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController : This is a TRACE level message
2019-09-02 09:59:20.283 DEBUG 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController : This is a DEBUG level message
2019-09-02 09:59:20.283 INFO 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController : This is an INFO level message
2019-09-02 09:59:20.283 WARN 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController : This is a WARN level message
2019-09-02 09:59:20.283 ERROR 12208 --- [io-8080-exec-10] c.b.s.b.m.logging.LoggingController : This is an ERROR level message