저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

스프링은 캐시 구현으로 레디스를 지원한다. 스프링 부트를 사용하면 다음의 간단한 설정만 추가하면 된다.


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-cache</artifactId>

</dependency>


이 설정을 추가하면 @EnableCaching 만으로 쉽게 @Cacheable과 관련 애노테이션을 사용해서 레디스를 캐시로 사용할 수 있다.

그런데, 스프링 부트가 제공하는 설정만으로 레디스를 캐시로 사용하면 캐시별로 유효 시간을 줄 수 없다. Ehcache를 캐시로 사용하면 설정 파일을 이용해서 유효 시간을 줄 수 있는데 레디스의 경우 설정 파일도 없다.(http://docs.spring.io/spring-boot/docs/1.4.3.RELEASE/reference/html/common-application-properties.html 참고)


진행중인 프로젝트에 캐시별 유효시간 설정 기능이 필요해서 스프링 부트로 다음과 같은 프로퍼티를 사용해서 캐시별로 유효 시간을 설정할 수 있도록 구현해봤다.


# application.properties

spring.cache.redis.defaultExpireTime=0

spring.cache.redis.expireTime.billDetailData=3600

spring.cache.redis.expireTime.billSummaryInfos=3600


이 설정에서 "spring.cache.redis"는 접두어이다. defaultExpireTime은 전체 캐시에 기본으로 적용할 유효시간을 설정한다. "expirTime.캐시이름"은 캐시 이름별로 캐시 시간을 설정한다. 유효 시간은 초 단위이다.


이 설정을 담기 위한 @ConfigurationProperties 클래스를 프로퍼티 클래스를 다음과 같이 작성했다.


import java.util.HashMap;

import java.util.Map;

import java.util.Map.Entry;


import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "spring.cache.redis")

public class CacheRedisProperties {


    private long defaultExpireTime = 0L;

    private Map<String, Long> expireTime = new HashMap<>();


    private CacheTimeParser parser = new CacheTimeParser();


    public long getDefaultExpireTime() {

        return defaultExpireTime;

    }


    public void setDefaultExpireTime(long defaultExpireTime) {

        this.defaultExpireTime = defaultExpireTime;

    }


    public Map<String, Long> getExpireTime() {

        return expireTime;

    }


    public void setExpireTime(Map<String, Long> expireTime) {

        this.expireTime = expireTime;

    }

}


다음으로 RedisCacheManager에 유효 시간 설정하면 된다. 이를 위한 코드는 다음과 같다.


import java.util.Map.Entry;


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;

import org.springframework.boot.context.properties.EnableConfigurationProperties;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Profile;

import org.springframework.data.redis.cache.RedisCacheManager;


/**

 * org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 클래스 참고

 */

@Configuration

@EnableCaching

@EnableConfigurationProperties(CacheRedisProperties.class)

public class CustomRedisCacheConfiguration {

    private Logger logger = LoggerFactory.getLogger(getClass());


    @Autowired

    private CacheRedisProperties cacheRedisProperties;


    @Bean

    public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {

        return new CacheManagerCustomizer<RedisCacheManager>() {

            @Override

            public void customize(RedisCacheManager cacheManager) {

                cacheManager.setDefaultExpiration(cacheRedisProperties.getDefaultExpireTime());

                cacheManager.setExpires(cacheRedisProperties.getExpireTime());

            }

        };

    }

}


스프링부트가 제공하는 CacheManagerCustomizer를 이용하면 부트가 생성한 CacheManager를 커스터마이징할 수 있다. 이 기능을 사용해서 RedisCacheManager에 캐시 유효 시간을 설정하면 된다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. ㅇㅇ 2017.01.29 20:55 신고  댓글주소  수정/삭제  댓글쓰기

    최범균님이 집필하신 책으로 공부하고 있습니다.

    저에게 많은 도움이 되는 것 같아 감사의 의미로 댓글 남깁니다.

    새해 복 많이 받으시고 건강하세요. ^^

  2. 조승규 2018.02.07 10:44 신고  댓글주소  수정/삭제  댓글쓰기

    많은 도움 되었습니다.
    감사합니다.

내장 톰캣을 사용해서 동작하는 스프링부트 기반 웹어플리케이션을 AJP 프로토콜을 이용해서 아파치 HTTPD 웹 서버와 연동할 일이 생겼다. 연동을 위해 할 내용은 생각보다 간단했다. 다음의 두 가지만 해 주면 된다.

  1. 스프링부트 어플리케이션: 내장 톰캣을 위한 AJP 커넥터 설정
  2. 아파치 웹 서버 : ProxyPass로 ajp 연동 설정

스프링부트 내장 톰캣 설정


내장 톰캣 설정에 AJP 커넥터를 추가한다.


@Configuration

public class ContainerConfig {

    @Bean

    public EmbeddedServletContainerCustomizer containerCustomizer() {

        return container -> {

            TomcatEmbeddedServletContainerFactory tomcat

                    (TomcatEmbeddedServletContainerFactory) container;


            Connector ajpConnector = new Connector("AJP/1.3");

            ajpConnector.setProtocol("AJP/1.3");

            ajpConnector.setPort(9090);

            ajpConnector.setSecure(false);

            ajpConnector.setAllowTrace(false);

            ajpConnector.setScheme("http");

            tomcat.addAdditionalTomcatConnectors(ajpConnector);

        };

    }

}


아파치 설정


아파치에 톰캣 관련 설정을 추가한다.


ProxyPass "/contextPath" "ajp://localhost:9090/contextPath"


그리고 아파치 서버를 재시작하면 끝이다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 임예준 2016.06.14 10:29 신고  댓글주소  수정/삭제  댓글쓰기

    이걸 몰라서 boot 내장 톰캣을 못쓰고,
    static 과 java 리소스를 httpd 와 외부 톰캣에 각각 배포 해야 되나 싶었는데 말입니다.
    ProxyPass를 사용하면 JK를 안써도 되는거죠?

스프링 부트는 application.properties 파일을 이용해서 설정을 제공한다. 이 파일에는 부트가 제공하는 프로퍼티뿐만 아니라 커스텀 프로퍼티를 추가할 수 있다. 커스텀 프로퍼티를 사용하는 몇 가지 방법이 있는데 그 중에서 설정 프로퍼티 클래스를 사용하면 관련 설정을 한 클래스에서 관리할 수 있어 편리하다.


설정 프로퍼티 클래스를 사용하는 방법은 간단하다. 먼저, 설정 프로퍼티 클래스로 사용할 클래스를 작성한다. 이 클래스는 다음과 같이 작성한다.

  • @ConfigurationProperties 애노테이션을 클래스에 적용한다. application.properties 파일에서 사용할 접두어를 지정한다.
  • application.properties 파일에 설정한 프로퍼티 값을 전달받을 setter를 추가한다.
  • 프로퍼티 값을 참조할 때 사용할 get 메서드를 추가한다.
다음은 설정 프로퍼티 클래스의 작성 예이다.


@ConfigurationProperties(prefix = "eval.security")

public class SecuritySetting {

    private String authcookie;

    private String authcookieSalt;


    public String getAuthcookie() {

        return authcookie;

    }


    public void setAuthcookie(String authcookie) {

        this.authcookie = authcookie;

    }


    public String getAuthcookieSalt() {

        return authcookieSalt;

    }


    public void setAuthcookieSalt(String authcookieSalt) {

        this.authcookieSalt = authcookieSalt;

    }


}


application.properties 파일의 프로퍼티의 설정 프로퍼티 클래스의 프로퍼티는 다음과 같이 매칭된다.


* application.properties 프로퍼티 이름 = prefix + "." + setter 프로퍼티 이름


예를 들어, 위 코드에서 prefix는 "eval.security"이므로 setAuthcookie()에 해당하는 프로퍼티는 "eval.security.authcookie"가 된다. 만약 setter의 프로퍼티 이름 중간에 대문자가 포함되어 있다면 다양한 매핑을 지원한다. 예를 들어, 위 코드에서 authcookieSalt가 중간에 대문자를 포함하고 있는데 이 경우 다음과 같은 프로퍼티로부터 값을 가져올 수 있다.

  • eval.security.authcookieSalt
  • eval.security.authcookie-salt
  • eval.security.authcookie_salt
  • EVAL_SECURITY_AUTHCOOKIE_SALT

@ConfigurationProperties를 적용한 클래스를 만들었다면, 다음 할 일은 빈으로 등록하는 것이다. 스프링 빈으로 등록하는 방법은 간단하다. 다음의 두 가지 방식 중 하나를 사용하면 된다.

  • @EnableConfigurationProperties을 이용해서 지정하기
  • 설정 프로퍼티 클래스에 @Configuration 적용하기 (또는 설정 프로퍼티 클래스를 @Bean으로 등록하기)

먼저 @EnableConfigurationProperties을 사용하는 방법은 다음과 같다. @EnableConfigurationProperties을 설정 클래스에 추가하고 @EnableConfigurationProperties의 값으로 @ConfigurationProperties를 적용한 클래스를 지정하면 된다. 이 경우 @EnableConfigurationProperties는 해당 클래스를 빈으로 등록하고 프로퍼티 값을 할당한다.


@SpringBootApplication

@EnableConfigurationProperties(SecuritySetting.class)

public class Application { ... }


두 번째 방법은 설정 프로퍼티 클래스를 빈으로 등록하는 것이다. 스프링 부트는 컴포넌트 스캔을 하므로 설정 프로퍼티 클래스에 @Configuration을 붙이면 자동으로 빈으로 등록된다. 스프링 부트는 해당 빈이 @ConfigurationProperties를 적용한 경우 프로퍼티 값을 할당한다.


@ConfigurationProperties 적용 클래스를 빈으로 등록했다면 이제 설정 정보가 필요한 곳에서 해당 빈을 주입받아 사용하면 된다. 예를 들면 다음과 같이 자동 주입 받아 필요한 정보를 사용하면 된다.


@Configuration

public class SecurityConfig {

    @Autowired

    private SecuritySetting securitySetting;


    @Bean

    public Encryptor encryptor() {

        Encryptor encryptor = new Encryptor();

        encryptor.setSalt(securitySetting.getAuthcookieSalt());

        ...

    }


@ConfigurationProperties를 사용할 때의 장점은 다음과 같다.

  • 필요한 외부 설정을 접두어(prefix)로 묶을 수 있고 중첩 설정을 지원한다. (예, YAML을 사용하면 계층 구조로 묶을 수 있다.)
  • 기본 값을 쉽게 지정할 수 있다. 설정 프로퍼티 클래스의 필드에 기본 값을 주면 된다.
  • int, double 등 String 이외의 타입을 설정 프로퍼티 클래스의 프로퍼티에 사용할 수 있다. 설정 파일의 문자열을 설정 프로퍼티 클래스의 프로퍼티 타입으로 스프링이 알아서 변환해준다.



Posted by 최범균 madvirus

댓글을 달아 주세요

회사에서 입사해서 3년 반 정도 전에 그 당시 신입들 교육 목적으로 만들었던 평가 시스템이 있다. 한 번 쓰고 버릴 거라 생각해서 급한 불을 끄는 심정으로 만들었었다. 그런데 바로 버릴 것 같았던 이 시스템을 3년이나 쓰게 되었고 이번에도 쓰게 되어서, 기술도 익힐 겸 공부도 할 겸 새롭게 구현해 보았다. 몇 가지를 새로 적용해봤는데, 그 중 중요한 것은 다음과 같다.

  • 이벤트 소싱(Event Sourcing)과 CQRS
  • 스프링 부트(Spring boot)
  • 앵귤러JS(AngularJS)
  • 부트스트랩(Bootstrap)
이벤트 소싱(Event Sourcing) 적용

시스템 개편을 한 주된 이유는 이벤트 소싱을 작게나마 적용해보고 싶기 때문이었다. DDD와 CQRS 등을 학습하는 과정에서 이벤트 소싱을 만나게 되었는데, 평소 SQL을 사랑하지 않았던 나로서는 이벤트 소싱과 도메인의 만남이 매우 환상적으로 느껴졌다. (이벤트소싱 자체에 대한 내용이 궁금하다면 http://docs.geteventstore.com/introduction/event-sourcing-basics/ 글을 읽어보자.)

이벤트 소싱은 모든 상태 변경을 이벤트로 보관하고, 모든 이벤트를 차례대로 적용함으로써 최신 상태를 구할 수 있도록 만들어준다. 이런 이벤트 소싱을 도메인에 적용해보니, 보다 객체 지향적인 방법으로 도메인 코드를 구현할 수 있음을 알게 되었다.

그간 JPA와 같은 ORM 프레임워크를 사용해서 도메인 객체의 내부 상태와 DB 테이블 간의 매핑을 처리했었는데, 이는 자유롭게 도메인 코드를 만들지 못하고 테이블 구조에 억지로 맞춰 도메인 코드를 만들도록 유도했다. 예를 들어, 필자의 경우 Map, List, Set, Immutable 객체 등을 자유롭게 쓰고 싶어도, 테이블 구조/쿼리 성능/ORM 한계로 인해 사실상 도메인 코드가 테이블 설계에 영향을 받았다.

이벤트 소싱을 이용해서 도메인 코드를 구현하면, 도메인의 상태 변경 정보를 담은 이벤트 정보만 저장소에 보관하게 된다. 즉, 이벤트와 저장소 사이의 매핑만 처리하면 되기 때문에(예, 이벤트를 HBase에 보관하거나, 이벤트를 RDBMS 테이블에 보관하는 등), 도메인 자체의 코드는 ORM이나 SQL과 같은 데이터 매핑으로부터 자유로워진다. 원하는 클래스를 이용해서 객체 군(Aggregate)을 마음대로 구성할 수 있고, 기능 위주로 객체를 구현하는데 더 집중할 수 있도록 만들어준다. 이는 나에게 굉장한 해방감을 주었다.

이벤트 소싱을 적용하기 위해 Axon Framework를 사용했기 때문에 Axon Framework에 제한된 방식으로 도메인 코드를 구현해야 했지만, JPA나 테이블 구조에 따른 제한에 비하면 아무것도 아니다.

CQRS

이벤트 소싱을 이용해서 도메인을 구현하면, 객체 군 단위로 조회를 하게 된다. 이는 여러 객체 군을 동시에 조회하는 기능을 구현할 때 RDBMS 만큼의 조회 속도를 제공하지 못한다. 예를 들어, 테이블을 사용하면 "select * from PersonalEval e, PerfItem pi, PerfEval ev where ..."와 같이 필요한 데이터만 선택적으로 조회할 수 있는데 반해 이벤트 소싱의 경우는 한 타입의 도메인 객체 군에 해당하는 모든 이벤트 데이터를 로딩해야 하기 때문이다.

이런 문제를 해결하기 위해 CQRS(Command Query Responsiblity Segregation)는 필수다. 상태 변경과 관련된 기능을 제공하는 도메인 모델과 상태 조회를 위한 전용 모델을 구분해서 조회 성능을 높여주어야 한다. 평가 시스템의 경우 '평가 시즌' 도메인 객체는 '평가자 매핑'을 관리하는데, 이 평가자 매핑은 '피평가자-1차 평가자-2차평가자-동료평가자들'로 구성되어 있다.그런데, 1차 평가자 입장에서는 내가 평가해야 할 피평가자 목록이 필요하므로, 조회 시점에서는 '1차 평가자-피평가자목록'이 필요하다. 비슷하게 '2차 평가자-피평가자목록', '동료평가자-피평가자목록'도 조회 시점에 필요하다. 이렇게 조회 시점에 필요한 조회 모델을 별도로 만들어서 조회 성능 문제를 없앴다.

'평가자-피평가자' 매핑 조회 전용 데이터 모델은 도메인 객체가 생성한 이벤트를 이용해서 생성했다. 즉, 도메인 기능 실행 과정에서 발생한 이벤트를 수신해서 그 이벤트로부터 조회 모델의 데이터를 변경하는 방식을 사용했다. 조회 전용 모델을 보관하기 위해 DB를 사용할 수도 있지만, 이 프로젝트의 경우에는 데이터 양이 크지 않아 그냥 메모리에 상태 정보를 보관했다. 어플리케이션이 처음 구동될 때 관련 이벤트를 읽어와 메모리에 최종 상태의 조회 전용 데이터를 생성한 뒤, 이벤트가 발생할 때 마다 메모리의 상태를 수정하는 방식을 사용했다.

낯설고 복잡하게 느껴지지만, 일단 감 잡으면 장점

처음 이벤트 소싱을 공부하고 적용하는 과정은 낯설었다. 만약 SQL만 고집하는 개발자였다면, 심리적으로 저항항하게 될 뿐만 아니라 객체 중심 코드를 만드는데 힘겨워할 것 같다. 또한, CQRS를 처음 만나면 복잡하게 느껴진다. 물론, 최근에는 조회를 위해 데이터를 캐시에 담아두는 곳이 많기 때문에 뷰 전용 모델을 만드는 것에 대한 거부감이 이번보다 덜 하지만, 상태를 변경할 때 사용하는 모델과 조회할 때 사용하는 모델이 다르기 때문에 복잡하게 느껴진다.

하지만, 도메인 모델이 조금만 커져도, 상태 변경을 위한 SQL은 복잡해지는 반면에 객체 기반 코드는 상대적으로 단순해졌다. 새로운 기능을 추가한다거나 로직이 변경되더라도 ERD를 변경할 필요 없이 도메인 코드만 수정하면 되므로 변경에 유연하게 대처할 수 있게 된다. 또한, 조회 전용 모델도 얼마든지 원하는 방식으로 만들어낼 수 있기 때문에 조회를 위한 모델의 자유도도 높아졌다.

스프링 부트(Spring Boot)


작년부터 뜨고 있는 스프링 부트를 적용했다. 사용한 이유는 다음과 같다.

  • 비교적 단순해진 의존 설정
  • 임베디드 톰캣을 이용해서 실행 가능한 war를 배포해서 실행 과정을 단순화
위 두 가지는 확실히 장점이었다. 기본적인 설정을 모두 스프링 부트가 알아서 먹어주기 때문에, 필요한 설정들만 추가해 주면 되었다. 실행 가능한 war는 배포에서의 장점이었다. war를 배포하고 java 명령어를 이용해서 실행해주기만 하면 끝났다. 톰캣의 catalina.sh과 같은 쉘 파일을 만들어주긴 했지만 war를 바로 실행할 수 있기 때문에 배포와 실행이 매우 간단해졌다.

물론, 단점도 있다. 스프링 부트가 너무나 많은 걸 내부적으로 설정해버리기 때문에, 기본 설정에서 벗어나기 위해 부트의 내부를 이해해야 하는 경우도 있었다. 부트에 대한 이해도가 떨어지면, 처음에 설정과 관련해서 삽질할 가능성이 높아진다.

뷰 구현 기술로는 JSP를 사용했는데, 이유는 다른 구현 뷰 구현 기술이 익숙하지 않았기 때문이다. 다음에 임베디드 서버를 이용해서 실행 가능한 war를 만들 때에는 타임리프(thymeleaf)를 사용해보는 것도 고려하고 있다.


앵귤러JS와 부트스트랩


부트스트랩은 "사랑"이다. 디자인 감각이 없는 나에게 부트스트랩은 없어서는 안 될 존재이다. 이번 시스템의 경우도 부트스트랩을 사용한 덕에 디자인이 깔끔해졌다는 평을 들을 수 있었다. 직접 CSS를 만지작거렸다면 아마 무엇을 상상해도 그 이하의 디자인이 나왔을것이다.


프론트 기술이 약한 나에게 앵귤러JS도 '사랑'이다. 앵귤러JS 2 버전이 올해 나올 예정이지만 아직 알파 버전이기 때문에 앵귤러 JS 1을 사용했다. 앵귤러 JS가 없었다면 jQuery와 자바 복잡한 스크립트 코드를 만드느라 멘붕을 수도 없이 겪었을지 모른다. 앵귤러 JS 덕에 폼에 입력한 값을 쉽게 처리할 수 있었고, 서버와의 연동도 간단하게 할 할 수 있었다.


REST API를 연동하는 부분에서는 $q를 사용하면서 Promise에 대해 약간의 감을 잡을 수 있었다.


이 외에 앵귤러 JS의 다음 기능들을 사용했다.

  • 폼 값 검증
  • angular-elastic : textarea의 높이를 입력 값에 따라 자동으로 조절해주는 모듈듈
  • angular-ui-bootstrap : 부트스트랩의 모달 다이얼로그를 앵귤러 컨트롤러로 처리하기 위해 사용
예제 코드

딱히 코드가 아름답진 않지만, 학습 과정에서 작성한 코드를 https://github.com/madvirus/evaluation 에서 확인할 수 있다.


Posted by 최범균 madvirus

댓글을 달아 주세요

최근에 특정 개발 방식을 시도할 겸, 몇 가지 구현 기술도 익힐 겸해서 회사에서 필요한 어플리케이션을 만들어보고 있다. 스프링 부트와 자바, 스칼라, JPA, AngularJS 등을 사용하고 있고, 뷰 템플릿으로는 JSP를 이용했다.


배포의 단순함을 위해 스프링 부트의 실행가능한 war를 만든 뒤, 로컬PC와 운영될 서버에 올려서 기능 테스트를 진행했다. 테스트를 진행하는 과정에서 다음과 같은 증상을 발견했다.

  • 리눅스 서버에서 war를 실행: 동일한 JSP 뷰를 실행할 때, 약 4초 간격으로 응답 속도가 느려짐. 예를 들어, 새로고침을 짧은 간격으로 실행하면, 30~40ms 걸리던 응답 시간이 약 4초마다 300~400ms가 걸림
  • 윈도우 개발 PC에서 war를 실행: 증상 없음
  • 맥 개발 PC에서 war를 실행: 리눅스 서버와 동일 증상 발생
  • JSP를 사용하지 않는 JSON 응답의 경우 비슷한 증상 없음
  • mvn spring-boot:run으로 실행할 때에는 맥 개발 PC에서 이런 증상이 발생하지 않음
한 JSP 코드에서 <jsp:include>를 이용해서 다른 JSP를 include 할 경우, 응답 속도가 더 느려졌다. 몇 가지 의심되는 것이 있어서 차례대로 확인해 나가다보니 결과적으로 응답 속도를 느리게 만드는 용의자를 찾았다. 그 용의자는 톰캣 JSP 엔진 Jasper의 JSP 변경 여부 검사 옵션이었다.

톰캣의 JSP 엔진인 Jasper는 기본적으로 다음과 같이 설정되어 있다.
  • 개발모드: JSP의 변경 여부를 확인할지 여부 지정. 기본 값은 true.
  • 변경확인간격: 개발모드가 true인 경우, 지정한 간격이 지날 때 마다 실행하는 JSP의 변경을 확인. 기본 값은 4초.
4초! 그렇다. 4초다. 앞서 이상 증상에서 발견된 그 약 4초라는 시간이 설정에 있는 것이다. 원인은 잘 모르지만, 의심되는 것은 다음과 같다.
  • 실행 가능한 war로 묶인 경우, 리눅스 기반 OS의 JDK 사용시 war 파일에 포함된 파일의 변경 여부를 확인하는 과정에서 실행 속도가 느려짐
자바8로만 해봤기 때문에 자바7이나 자바6에서 동일 증상이 발생하는지 여부는 모르지만, 어쨋든 war 파일에 포함된 파일을 탐색하는 과정에서 속도가 느려지는 것 같다. 왜 그런지 알고 싶지만, 여기까지 파고 있을 시간이 없어서 다음 과정을 진행했다.

실행 가능 war의 경우 JSP 변경 여부를 확인하는 과정은 별 의미가 없기 때문에, 임베디드 톰캣이 JSP 변경 여부를 확인하지 않도록 스프링 설정을 변경했다. 이때 사용한 것이 TomcatContextCustomizer이다. 아래 코드는 Jasper의 개발모드를 false로 설정하기 위해 사용한 스프링 설정의 일부이다.

import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;

@Configuration
public class ContainerConfig {

    @Bean
    public ServerProperties serverProperties() {
        return new ServerProperties();
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer(){
        return new TomcatContainerCustomizer(tomcatJspServletConfig());
    }

    @Bean
    public TomcatJspServletConfig tomcatJspServletConfig() {
        return new TomcatJspServletConfig();
    }

    static class TomcatContainerCustomizer implements EmbeddedServletContainerCustomizer {

        private TomcatJspServletConfig tomcatJspServletConfig;

        public TomcatContainerCustomizer(TomcatJspServletConfig tomcatJspServletConfig) {
            this.tomcatJspServletConfig = tomcatJspServletConfig;
        }

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            ...
            if (container instanceof TomcatEmbeddedServletContainerFactory) {
                TomcatEmbeddedServletContainerFactory tomcatContainer = 
                      (TomcatEmbeddedServletContainerFactory) container;
                tomcatContainer.addContextCustomizers(tomcatJspServletConfig);
            }
        }

    }

    public static class TomcatJspServletConfig implements TomcatContextCustomizer {

        private ContainerJasperSetting jasperSetting;

        public TomcatJspServletConfig(ContainerJasperSetting jasperSetting) {
            this.jasperSetting = jasperSetting;
        }

        @Override
        public void customize(Context context) {
            // TomcatEmbeddedServletContainerFactory가 설정한 JspServlet의 이름이 "jsp"
            Wrapper jsp = (Wrapper)context.findChild("jsp");
            // JspServlet의 개발모드를 비활성화
            // 실제로는 application.properties 파일에 설정할 수 있는 프로퍼티 추가해서 구현
            jsp.addInitParameter("development", "false");
        }
    }

}

위 코드에서는 개발모드를 비활성화했지만, 실제 코드에서는 application.properties 파일에 "jsp.jasper.development" 프로퍼티의 값에 따라서 개발모드를 활성화/비활성화하도록 구현했다.

위와 같이 Jasper 엔진의 개발모드를 비활성화한 뒤에 실행가능한 war를 다시 실행해보니, 4초 간격으로 응답 시간이 느려지는 증상이 사라졌다.만세다!


Posted by 최범균 madvirus

댓글을 달아 주세요