(JPA,SpringData) Named Query 사용하기

|

JPA를 사용하다보면 쿼리메소드만으로는 감당이 안되는 부분이 많아 @Query를 이용하여 아래와 같이 늘어놓기 시작합니다.

@Query("select p from Post p where p.id > :id")
Post findPostByPk(@Param("id") Long id);

쿼리문이 짧을때는 상관없는데 쿼리문이 길어지고, 또 많아지면 그때부터는 관리가 안되기 시작하는데 가급적 JPA의 장점을 살리면서 Native를 쓰지 않고 버티기 위해 아래와 같이 설정할 수 있습니다.

쿼리문 xml로 빼기 (export query string to orm.xml)

Post라는 Entity를 조회하기 위한 쿼리를 만들어보겠습니다. 일단 여러개의 xml resource를 사용하기 위해 아래와 같이 설정했습니다.

# application.yml
spring.jpa.orm:
  path: queries
  queries:
  - ${spring.jpa.orm.path}/post.xml
  - ${spring.jpa.orm.path}/user.xml
  ...
@Configuration
public class JpaConfig extends HibernateJpaAutoConfiguration {
    @Data
    @Component
    @ConfigurationProperties("spring.jpa.orm")
    public class OrmProps {
        private String[] queries;
    }

    @Autowired private OrmProps ormProps;

    public JpaConfig(DataSource dataSource, JpaProperties jpaProperties, ObjectProvider<JtaTransactionManager> jtaTransactionManager, ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
        super(dataSource, jpaProperties, jtaTransactionManager, transactionManagerCustomizers);
    }

    @Bean
    @Override
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder factoryBuilder)
    {
        final LocalContainerEntityManagerFactoryBean ret = super.entityManagerFactory(factoryBuilder);
        ret.setMappingResources(ormProps.getQueries());
        return ret;
    }
}

쿼리문만 따로 모으고 싶어서 아래와 같이 배치하였습니다.

query files under resource folder

@Query어노테이션에 있던 쿼리문은 xml하위에 아래와 같이 정의합니다.

<named-query name="Post.findPostByPk">
    <query><![CDATA[ select p from Post p where p.id > :id ]]></query>
</named-query>

그리고 dao 소스에서 @Query어노테이션을 제거해주면 끝.

Post findPostByPk(@Param("id") Long id);

결과를 Map으로 받기

Entity의 전체 결과를 받아올수도 있지만 일부만 필요할 수 도 있습니다.

<named-query name="Post.findPostByPk">
    <query><![CDATA[ select p.id, p.message, p.user from Post p where p.id > :id ]]></query>
</named-query>

위 결과를 Post객체로 받을 경우 리턴값이 Object[]이기 때문에 파싱 오류가 발생합니다. (아래 경우 p.id가 Long타입인데 저걸 Post객체로 변환하려 했기때문에 생기는 오류입니다.)

[11-06 14:44:48.269] ERROR [http-nio-8080-exec-9] [o.a.j.l.DirectJDKLog.log]  181 - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.Object[]] to type [io.github.jistol.geosns.jpa.entry.Post] for value '{65, 1234555666}'; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.Long] to type [io.github.jistol.geosns.jpa.entry.Post]] with root cause
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.Long] to type [io.github.jistol.geosns.jpa.entry.Post]
	at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324)
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206)
	at org.springframework.core.convert.support.ArrayToObjectConverter.convert(ArrayToObjectConverter.java:66)
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:37)
	...

named-native-queryresult-class, result-map-class등을 설정 할 수 있지만 JPA를 쓰면서 native쿼리 쓰려면 MyBatis를 쓰는게 더 낫다고 생각하기 때문에 다른 방법으로 해결하겠습니다.

  1. Object[] 를 원하는 객체로 Application단에서 직접 바꾸기.(설명 생략)
  2. Map으로 변환하여 결과 받기

Hibernate Select clause문서를 참고하면 HQL에서 어떻게 select한 값을 반환해주는지 잘 설명이 되어있습니다. (다행히도 JPQL에서도 동일하게 동작하는것 같습니다.) 그 중 Map으로 반환받기 위해서는 아래와 같이 설정 가능합니다.

<named-query name="Post.findPostByPk">
    <query><![CDATA[ select new map(p.id, p.message, p.user) from Post p where p.id > :id ]]></query>
</named-query>

그리고 dao 소스에서도 Return객체를 Map으로 바꿔주면 정상동작합니다.

Map<String, Object> findPostByPk(@Param("id") Long id);

참고

Hibernate Select clause Spring Data JPA - 4.3.3 Using JPA NamedQueries

(SpringBoot) application.yml에서 placeholder 기능 동작안할때

|

application.yml 파일을 아래와 같이 만들고 실행했습니다.

app.url: http://localhost:${server.port}/
app.domain1: ${app.url}/domain/1
@Value("${app.domain1}") private String domain1;

public void print() {
  System.out.println(domain1);
}

소스에서 위와 같이 참조하면 http://localhost:8080/domain/1로 출력 되기를 기대했으나 ${app.url}/domain/1 로 출력됩니다.

문제는 app.url에 설정한 server.port값을 application.yml 파일에 명시하지 않아 생긴 문제로 SpringBoot의 Placeholder가 파싱할때 참조 값이 없어 app.url값을 파싱하지 못하면서 다른 placeholder 설정들도 모두 일반 text로 인식해버리는 문제입니다. 아래와 같이 명시해주면 정상동작합니다.

server.port: 8080
app.url: http://localhost:${server.port}/
app.domain1: ${app.url}/domain/1

참고

Part IV. Spring Boot features - Placeholders in properties

(SpringBoot) bootRun 실행중 멈춤 현상

|

gradle bootRun을 통해 SpringBoot를 실행하던 도중 아래와 같이 로그가 찍히고 멈춰서 더이상 동작하지 않는 현상이 발생하였습니다.

오전 11:27:32: Executing external task 'bootRun -Dspring.profiles.active=local'...
:compileJava UP-TO-DATE
:processResources
:classes
:findMainClass
Connected to the target VM, address: '127.0.0.1:61333', transport: 'socket'
:bootRun
11:27:33.296 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Included patterns for restart : []
11:27:33.300 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Excluded patterns for restart : [/spring-boot-starter/target/classes/, /spring-boot-autoconfigure/target/classes/, /spring-boot-starter-[\w-]+/, /spring-boot/target/classes/, /spring-boot-actuator/target/classes/, /spring-boot-devtools/target/classes/]
11:27:33.301 [main] DEBUG org.springframework.boot.devtools.restart.ChangeableUrls - Matching URLs for reloading : [file:/Users/jistol/IdeaProjects/github/geo-sns/src/main/resources/, file:/Users/jistol/IdeaProjects/github/geo-sns/build/classes/java/main/, file:/Users/jistol/IdeaProjects/github/geo-sns/build/resources/main/]

기다려도 오류도 안나고 아무런 메시지 없이 멈춰 있어서 디버깅 해본 결과 application.yml 파일을 잘못 설정했을때 위와 같이 멈춰버립니다.

# 예시
base.url : localhost

# ERROR : base.url을 사용하기 위해서는 ${base.url}로 표기해야합니다.
call-url : {base.url}/call

application.yml파일을 파싱하지 못하여 내부적으로 오류가 나지만 따로 찍어주진 않고 SpringBoot를 deploy하지 못한채 끝나버립니다.

docker-compose를 이용한 Nginx + Tomcat 클러스터링 샘플

|

Nginx와 Tomcat을 이용하여 클러스터 환경을 구축/테스트 진행하였는데 서버를 각각 3대나 띄우려니 여간 귀찮을수가 없더군요.
docker-compose를 이용하여 가장 심플하고 최소한의 설정만으로 한방에 띄우는 방법 및 샘플을 포스팅합니다.
샘플 소스는 jistol/docker-compose-nginx-tomcat-clustering-sample에서 다운로드 가능합니다.

기본구조

2대의 Tomcat 컨테이너를 올리고 앞단에 Nginx로 reverse proxy 합니다.
2대의 Tomcat은 가장 기본적인 클러스터링 설정을 사용하며 multicast 방식에 의해 세션 공유를 합니다.

서버 구성도

server structure

샘플 폴더 구조

sample file list

Nginx와 Tomcat의 설정은 각 폴더별로 구분하고 Docker Build시 copy하도록 설정해두었습니다.

docker-compose.yml 설정

docker명령어로 일일히 다 올리기 귀찮기 때문에 docker-compose를 이용하여 한방에 올립니다.
docker-compose에 관한 자세한 사항은 docker compose doc을 참고하세요.

# 일단 버전은 3을 사용합니다. 덕분에 extends기능이 없어졌더군요 ;(
version: '3'

# 각 서비스 컨테이너를 정의합니다. ( nginx * 1 + tomcat * 2 )
services:
    # tomcat 1번 서버입니다. 같은 설정을 2번에서도 사용하기 때문에 &was로 명명하고 tomcat2에서 참조합니다.
    # tomcat1 서비스의 모든 관련파일은 ./tomcat1 폴더에서 가져옵니다. 
    tomcat1: &was
        # tomcat 기동시 java option값을 추가하기 위해 아래 설정을 추가했습니다. 
        environment:
            - JAVA_OPTS=-Dspring.profiles.active=docker -Dfile.encoding=euc-kr
        build: 
            context: .
            # Dockerfile을 실행시 conf/server.xml과 webapps에 파일 배포를 위해 argument를 추가합니다.
            # 아래 값은 DockerfileTomcat에서 사용됩니다.
            args:
                conf: tomcat1/conf
                warpath: tomcat1/webapps/ROOT.war
            # tomcat 서버 이미지 빌드를 위한 Dockerfile을 별도로 지정해줍니다.
            dockerfile: ./DockerfileTomcat
        # 
        # Docker 컨테이너에 붙지 않고 각 tomcat서버의 로그를 따로 확인하기 위해 외부 저장소와 연결합니다.
        volumes:
            - ./tomcat1/logs/:/usr/local/tomcat/logs/
    
    # tomcat2번 서버입니다. tomcat1 서비스에서 설정한 내용을 그대로 사용하고 달라지는 설정에 대해서는 아래와 같이 직접 입력해줍니다.
    tomcat2:
        <<: *was
        build: 
            context: .
            args:
                conf: tomcat2/conf
                warpath: tomcat2/webapps/ROOT.war
            dockerfile: ./DockerfileTomcat
        volumes:
            - ./tomcat2/logs/:/usr/local/tomcat/logs/
    # nginx 설정입니다.
    nginx:
        build:
            context: .
            # nginx 서버 이미지 빌드를 위한 Dockerfile을 별도로 지정해줍니다.
            dockerfile: ./DockerfileNginx
            # Dockerfile실행시 conf/nginx.conf파일이 복사 될 수 있도록 argument를 추가합니다.
            # 아래 값은 DockerfileNginx에서 사용됩니다.
            args:
                conf: nginx/conf
        # 외부에서 직접 8080포트로 붙어야 하기 때문에 컨테이너 포트를 외부로 열어줍니다.
        ports: 
            - "8080:8080"

위에서 설정한 설정에서 참조값들을 모두 적용한 문서를 보고 싶을 때는 아래와 같은 명령어로 실행할 수 있습니다.

$ docker-compose config

Dockerfile 설정

위에서 docker-compose 실행시 각 컨테이너가 Dockerfile을 실행하도록 설정하였습니다.
다음과 같이 nginx / tomcat 용 Dockerfile을 생성합니다.

DockerfileNginx

FROM nginx:latest
MAINTAINER jistol <pptwenty@gmail.com>

ARG conf

COPY $conf/nginx.conf /etc/nginx/nginx.conf

WORKDIR /usr/local/tomcat/bin
CMD ["nginx", "-g", "daemon off;"]

ARG conf값은 docker-compose.yml에 설정되어 있습니다.

DockerfileTomcat

FROM tomcat:latest
MAINTAINER jistol <pptwenty@gmail.com>

ARG conf
ARG warpath

RUN rm -rf /usr/local/tomcat/webapps/*
COPY $conf/* /usr/local/tomcat/conf/
COPY $warpath /usr/local/tomcat/webapps/ROOT.war

WORKDIR /usr/local/tomcat/bin
CMD ["catalina.sh", "run"]

ARG conf, ARG warpath값은 docker-compose.yml에 설정되어 있습니다.

Tomcat 설정

server.xml파일에 아래와 같이 설정합니다.

<!-- server.xml -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>

자세한 설정은 Tomcat 8 세션 클러스터링 하기에 포스팅한 내용을 참고하세요.

Nginx 설정

nginx.conf파일에 reverse proxy를 위한 설정을 합니다.

http {
    
    ...
    
    # proxy 설정할 서버목록을 만듭니다.
    # host명은 docker 컨테이너의 service 이름과 동일하게 맞추어 줍니다. 
    upstream was-list {
        server tomcat1:8080;
        server tomcat2:8080;
    }
    
    ...
    
    server {
    
        # nginx 서버가 8080을 listen하도록 설정합니다.
        listen       8080;
        server_name  localhost;

        # 8080포트로 들어오는 모든 요청을 위에 설정한 'was-list'그룹으로 보냅니다.
        location / {
            proxy_pass http://was-list;
        }
    }
    
    ...
    
}

실행

다음과 같이 실행합니다.

$ docker-compose up -d

-d 옵션을 사용해야 실행후 각 컨테이너 콘솔 화면에서 detach됩니다.

기본적으로 docker-compose 실행시 이미지를 기존 빌드된 것으로 캐쉬하기 때문에 설정파일이나 배포파일이 바뀔 경우 아래와 같이 실행하여 새로 이미지를 만들도록 합니다.

$ docker-compose up -d --build

실행시 아래와 같이 이미지 빌드 로그와 함께 각 컨테이너가 실행되는 것을 볼 수 있습니다.

$ docker-compose up -d --build
Building nginx
Step 1/6 : FROM nginx:latest
 ---> da5939581ac8
Step 2/6 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 2d9af4961368
Step 3/6 : ARG conf
 ---> Using cache
 ---> ead97bc0569d
Step 4/6 : COPY $conf/nginx.conf /etc/nginx/nginx.conf
 ---> Using cache
 ---> 5ab748ec2a17
Step 5/6 : WORKDIR /usr/local/tomcat/bin
 ---> Using cache
 ---> 3eabdd2a3dd5
Step 6/6 : CMD nginx -g daemon off;
 ---> Using cache
 ---> 7f4f2405e032
Successfully built 7f4f2405e032
Successfully tagged tomcatdocker1_nginx:latest
Building tomcat2
Step 1/9 : FROM tomcat:latest
 ---> 0fbedce2f08c
Step 2/9 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 13adee263b5d
Step 3/9 : ARG conf
 ---> Using cache
 ---> 57a78bc9e8ce
Step 4/9 : ARG warpath
 ---> Using cache
 ---> 638fca357d24
Step 5/9 : RUN rm -rf /usr/local/tomcat/webapps/*
 ---> Using cache
 ---> 928ef1b94bb2
Step 6/9 : COPY $conf/* /usr/local/tomcat/conf/
 ---> Using cache
 ---> 30f8faae0012
Step 7/9 : COPY $warpath /usr/local/tomcat/webapps/ROOT.war
 ---> 3bd3ddeff2d6
Removing intermediate container 9c0b330ce6f6
Step 8/9 : WORKDIR /usr/local/tomcat/bin
 ---> dc773d206d87
Removing intermediate container 693e8b125384
Step 9/9 : CMD catalina.sh run
 ---> Running in c430115e5460
 ---> 3879504509c0
Removing intermediate container c430115e5460
Successfully built 3879504509c0
Successfully tagged tomcatdocker1_tomcat2:latest
Building tomcat1
Step 1/9 : FROM tomcat:latest
 ---> 0fbedce2f08c
Step 2/9 : MAINTAINER jistol <pptwenty@gmail.com>
 ---> Using cache
 ---> 13adee263b5d
Step 3/9 : ARG conf
 ---> Using cache
 ---> 57a78bc9e8ce
Step 4/9 : ARG warpath
 ---> Using cache
 ---> 638fca357d24
Step 5/9 : RUN rm -rf /usr/local/tomcat/webapps/*
 ---> Using cache
 ---> 26dea2133cee
Step 6/9 : COPY $conf/* /usr/local/tomcat/conf/
 ---> Using cache
 ---> c546b169bf25
Step 7/9 : COPY $warpath /usr/local/tomcat/webapps/ROOT.war
 ---> 4f1edfc42b8d
Removing intermediate container f02a70122c3a
Step 8/9 : WORKDIR /usr/local/tomcat/bin
 ---> 50683e072451
Removing intermediate container e2ca57d27775
Step 9/9 : CMD catalina.sh run
 ---> Running in 8da01a9227c2
 ---> 1e050e465174
Removing intermediate container 8da01a9227c2
Successfully built 1e050e465174
Successfully tagged tomcatdocker1_tomcat1:latest
Creating tomcatdocker1_nginx_1 ... 
Creating tomcatdocker1_tomcat1_1 ... 
Creating tomcatdocker1_tomcat2_1 ... 
Creating tomcatdocker1_nginx_1
Creating tomcatdocker1_tomcat1_1
Creating tomcatdocker1_tomcat1_1 ... done

docker 프로세스를 확인해보면 다음과 같이 3개의 컨테이너가 올라간 것을 확인 할 수 있습니다.

$ docker ps -a
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS                     PORTS                                                NAMES
1e2240de4a77        tomcatdocker1_tomcat1    "catalina.sh run"        4 minutes ago       Up 4 minutes               8080/tcp                                             tomcatdocker1_tomcat1_1
9706f7d0f85a        tomcatdocker1_tomcat2    "catalina.sh run"        4 minutes ago       Up 4 minutes               8080/tcp                                             tomcatdocker1_tomcat2_1
20aa7abec733        tomcatdocker1_nginx      "nginx -g 'daemon ..."   4 minutes ago       Up 4 minutes               80/tcp, 0.0.0.0:8080->8080/tcp                       tomcatdocker1_nginx_1

중지

아래 명령어를 통해 중지할 수 있습니다.

$ docker-compose down

Tomcat 8 Session Manager

|

세션을 어떻게 복제/관리 할 지를 결정합니다. Clustering에 사용되는 Manager는 DeltaManager, BackupManager, PersistentManager이며
그 중 in-memory 방식은 DeltaManager, BackupManager를 사용해야합니다.

StandardManager

tomcat에서 기본적으로 사용하는 Manager로 메모리에 세션을 가지고 있다가 tomcat이 중지될때 SESSIONS.ser라는 파일에 세션을 저장하고 재기동시 해당 파일의 내용을 메모리에 올리고 파일을 지웁니다.
conf/context.xml의 pathname에 지정된 이름을 사용하며 파일을 생성하지 않으려면 pathname=""로 설정하면 됩니다.

tomcat에 별도의 설정을 하지 않았을 경우 해당 Manager를 사용하게 됩니다.

DeltaManager

모든 노드에 동일한 세션을 복제합니다. 정보가 변경될때마다 복제하기 때문에 노드 개수가 많을 수록 네트워크 트래픽이 높아지고 메모리 소모가 심해집니다.

BackupManager

Primary Node와 Backup Node로 분리되어 모든 노드에 복제하지 않고 단 Backup Node에만 복제합니다. 하나의 노드에만 복제하기 때문에 DeltaManager의 단점을 커버할 수 있고 failover도 지원한다고 합니다.
동작 방식은 아래 예를 참고하세요.

  • tomcat을 3대, 앞단 loadbalancer를 둔 상태로 session1이 접근합니다.
  • session1이 tomcat1로 접속
  • tomcat1은 정보저장(primary node)후 tomcat2에 정보전달(backup node)
  • session1이 tomcat3으로 접속
  • tomcat3은 session1의 정보가 없으므로 tomcat2에 정보 요청
  • tomcat2는 tomcat3에게 정보 전달

PersistentManager

DB나 파일시스템을 이용하여 세션을 저장합니다. IO문제가 생기기 떄문에 실시간성이 떨어집니다.

각 Manager의 상세 옵션은 Apache Tomcat 8 Configuration Reference - The Cluster object에서 확인하세요.

참고

Apache Tomcat 8 Configuration Reference - The Cluster object
Tomcat Clustering Series Part 4 : Session Replication using Backup Manager
Tomcat Session StandardManager