개요

회사에서 쿼리작성도중 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 포스팅을 참고했다.

https://stackoverflow.com/questions/67500203/failed-to-convert-from-type-java-lang-object-to-type-org-springframework-d

 

Failed to convert from type [java.lang.Object[]] to type [@org.springframework.data.jpa.repository.Query com.data.models.Users]

I am trying to limit my query to only select specific columns from a table which am going to use, but when I write a simple select query I end up with an error Resolved [org.springframework.core.c...

stackoverflow.com

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을 통해서는 데이터를 수신하는데 실패하고 있다. 이 부분은 좀 더 확인해보고, 공유할 만한 내용이 있다면 공유할 생각이다.

 

https://in.relation.to/2022/06/14/orm-61-final/

 

Hibernate 6.1 Final - In Relation To

Hibernate ORM 6.1 was released last week with a few new features[1] and bugfixes. The new features include:

in.relation.to

 

https://docs.jboss.org/hibernate/orm/6.1/userguide/html_single/Hibernate_User_Guide.html

 

Hibernate ORM 6.1.7.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

 

'Spring' 카테고리의 다른 글

Spring Boot에서 Runtime Logging Level 변경하기  (1) 2024.09.03
Spring Boot 서버 포트 변경하기  (1) 2023.10.03

개요

이 튜토리얼에서는 Spring Boot 애플리케이션의 로깅 수준을 런타임에 변경하는 방법을 살펴보겠습니다. 많은 것들과 마찬가지로 Spring Boot에는 우리를 위해 구성하는 기본 제공 로깅 기능이 있습니다 . 실행 중인 애플리케이션의 로깅 수준을 조정하는 방법을 살펴보겠습니다.

이를 위해 세 가지 방법을 살펴보겠습니다. Spring Boot Actuator 로거 엔드포인트를 사용하는 방법, Logback 의 자동 스캔 기능을 사용하는 방법 , 마지막으로 Spring Boot Admin 도구를 사용하는 방법입니다.

이 번역본에서는 Spring Boot Actuator를 통해 로깅 레벨을 변경하는 방법만 안내합니다.

Spring boot Actuator

/ loggers Actuator 엔드포인트를 사용하여 로깅 레벨을 표시하고 변경하는 것으로 시작하겠습니다 . / loggers 엔드포인트는 actuator/loggers 에서 사용할 수 있으며 경로의 일부로 이름을 추가하여 특정 로거에 액세스할 수 있습니다.

예를 들어, URL http://localhost:8080/actuator/loggers/root 를 사용하여 루트 로거에 접근할 수 있습니다.

설정

Spring Boot Actuator를 사용하여 애플리케이션을 설정하는 것부터 시작해 보겠습니다.

먼저, pom.xml 파일 에 Spring Boot Actuator Maven 종속성을 추가해야 합니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <version>3.1.5</version>
</dependency>

gradle을 통해 종속성 추가

implementation 'org.springframework.boot:spring-boot-starter-actuator:3.1.5'

대부분의 엔드포인트는 기본적으로 비활성화되어 있으므로 application.properties 파일 에서 / loggers 엔드포인트 도 활성화해야 합니다 .

management.endpoints.web.exposure.include=loggers
management.endpoint.loggers.enabled=true

마지막으로, 실험의 효과를 볼 수 있도록 일련의 로깅 명령문을 포함하는 컨트롤러를 만들어 보겠습니다.

@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 패키지 의 로깅 수준을 확인해 보겠습니다.

curl http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  {"configuredLevel":null,"effectiveLevel":"INFO"}

로깅 수준을 변경하려면 / loggers 엔드포인트 에 POST 요청을 발행할 수 있습니다.

curl -i -X POST -H 'Content-Type: application/json' -d '{"configuredLevel": "TRACE"}'
  http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  HTTP/1.1 204
  Date: Mon, 02 Sep 2019 13:56:52 GMT

로깅 수준을 다시 확인하면 TRACE 로 설정되어 있어야 합니다.

curl http://localhost:8080/actuator/loggers/com.baeldung.spring.boot.management.logging
  {"configuredLevel":"TRACE","effectiveLevel":"TRACE"}

마지막으로, 로그 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

Reference

https://www.baeldung.com/spring-boot-changing-log-level-at-runtime

개요

스프링 부트 프로젝트 생성 후 main 실행 시 아래의 로그와 함께 실행에 실패했다.

아래의 로그에서 알 수 있듯이, 8080 포트가 이미 사용중이므로 실행에 실패한것을 알 수 있다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::               (v2.7.16)

2023-10-03 22:35:24.447  INFO 24480 --- [           main] c.h.springboot.SpringbootApplication     : Starting SpringbootApplication using Java 11.0.17 on DESKTOP-5TJ7OBF with PID 24480 (D:\GitRepository_HW\springboot-2023\out\production\classes started by HyeonWoo in D:\GitRepository_HW\springboot-2023)
2023-10-03 22:35:24.451  INFO 24480 --- [           main] c.h.springboot.SpringbootApplication     : No active profile set, falling back to 1 default profile: "default"
2023-10-03 22:35:25.689  INFO 24480 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-10-03 22:35:25.701  INFO 24480 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-10-03 22:35:25.702  INFO 24480 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.80]
2023-10-03 22:35:25.836  INFO 24480 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-10-03 22:35:25.836  INFO 24480 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1303 ms
2023-10-03 22:35:26.071  INFO 24480 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2023-10-03 22:35:26.178  WARN 24480 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates, check your Thymeleaf configuration, or set spring.thymeleaf.check-template-location=false)
2023-10-03 22:35:26.220  WARN 24480 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.PortInUseException: Port 8080 is already in use
2023-10-03 22:35:26.223  INFO 24480 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2023-10-03 22:35:26.235  INFO 24480 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-10-03 22:35:26.251 ERROR 24480 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.


Process finished with exit code 1

또한 localhost 접속 시 서버를 실행하지 않았음에도 아래와 같이 It works! 라는 문구를 볼 수 있었다.

 

 

원인파악

예상했던대로, 8080포트를 사용할 것으로 예상되는 프로그램이 실행중인것을 작업관리자에서 확인할 수 있었다.

이 프로그램 httpd는 아파치 하이퍼텍스트 전송 프로토콜 (HTTP) 서버 프로그램이다.

아파치 설치시에 서비스 또는 시작 프로그램에 등록된 것으로 보인다.

여러가지 방법을 통해 이를 해결할 수 있겠지만, 나는 두가지의 경우만 확인해봤다.

1. SpringBoot의 기본 서버 포트를 변경한다.

2. 아파치 httpd를 서비스 또는 시작 프로그램에서 제거하기

 

오류 해결

1. SpringBoot의 기본 서버 포트를 변경한다.

이 경우 application.properties 파일에 새로운 옵션을 추가한다. server.port = [원하는 포트] 를 입력하여 쉽게 해결할 수 있다.

 

 

2. 아파치 httpd를 서비스 또는 시작 프로그램에서 제거하기.

역시나 service 목록에 아파치가 등록되어 있는것을 확인했다. 

Windows + R 버튼을  눌러 실행 창을 띄운 후 services.msc를 입력하여 서비스 창을 띄운 후 Apache2.4를 중지하면 된다.

컴퓨터를 새로 킬 때마다 실행되는 것을 막으려면 오른쪽 마우스로 Apache2.4를 클릭 후 속성에서 시작유형을 사용 안 함으로 변경하자.

 

Reference

https://zetawiki.com/wiki/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8_%EC%84%9C%EB%B2%84_%ED%8F%AC%ED%8A%B8_%EB%B3%80%EA%B2%BD

+ Recent posts