Spring-Legacy to Spring-Boot 마이그레이션(Feat. 외장/내장-Tomcat/JSP/Multiple-Datasource(With-JNDI)/External-Lib)
목차
- 개요
- Spring Boot 마이그레이션 결과 조건
- JSP 적용 문제
- Multiple-DataSource 문제
- Tomcat / JNDI DataSource 문제
- External Library(JAR) 문제
- Tomcat/ExecutableJAR 동시 배포 가능의 문제
- 쉘 스크립트 작성
- 완성본
- 참고문헌
개요
드디어 회사 솔루션 최소 Java 버전이 6에서 7로 변경됐다(8 쓰고 싶은데...). 현재 관리하는 Spring 웹 프로젝트에 Boot를 적용한다. 최소 지원 Java 버전이 7로 올라감에 따라 기존 Spring Legacy 기반의 프로젝트를 Spring Boot 기반의 프로젝트로 마이그레이션을 진행한다. Spring Boot는 Boot의 다양한 기능을 활용하여 어플리케이션을 개발할 수 있는 장점이 존재하지만, 주요 기대효과는 배포 및 설치의 간결함이다. Spring Boot의 경우, 내장 Tomcat을 이용하여 WAS를 가동할 수 있다. 해당 기능을 통해 설치 간결함을 제공할 수 있다.
Spring Boot 마이그레이션 결과 조건
당연한 조건이지만, 최종 조건은 프로그램의 API 기능의 변동없는 정상적인 가동이다. 해당 조건을 아래와 같이 명시할 수 있다.
[JSP 템플릿의 사용]
Spring Boot는 타임리프, 프리마커, JSP를 지원한다. 그러나, JSP의 사용을 권고하지 않는다. 그런데 뷰 템플릿을 바꾸기에는 프로젝트의 위험도가 올라간다.
어떻게 해결할 것인가?
[Multiple-DataSource]
Spring Boot의 경우, application.yaml(properties)를 통해 DataSource를 설정할 수 있다.
그런데 2개 이상의 DataSource를 지정할 때 어떻게 할까?
[외장 Tomcat에서의 정상 작동]
솔루션이다보니, 사이트에서 사용하는 WAS Container를 사용할 때가 빈번하다...
한 번의 빌드를 통해 동시에 내장/외장 Tomcat(WAS Container)를 지원하면 그야말로 베스트이다.
어떻게 할 수 있을까?
[외장 Tomcat에서의 JNDI DataSource 사용]
외장 Tomcat(WAS Container)를 사용할 때 해당 Container의 DBCP를 사용해야 하는 경우가 다반사이다. 다음과 같이 구성이 필요하다.
1) Multiple-DataSource, 2) 외장 Tomcat, 3) 외장 Tomcat JNDI DataSource를 사용한다.
[ExecutableJAR/WAR 외의 Classpath 지정을 통해 라이브러리 변경]
Spring Boot는 빌드된 JAR/WAR 안에 종속 라이브러리가 존재한다.
만약 사용하는 Library가 변경됐을 경우 재 빌드가 필요하다.
라이브러리의 변경만 필요하는 경우가 종종 존재한다.
즉 External Library를 사용할 수 있어야 한다.
어떻게 해결할 것인가?
[쉘 스크립트 작성]
개발자 입장에서는 프로그램의 가동/상태 확인/정지 등을 쉽게 할 수 있다.
그러나 설치 인원 또는 고객은 얘기가 다르다. 고객의 경우, 프로그램을 가동함에 있어 지식 습득이 필요하면 절대 안 된다.
가동/상태/정지를 쉽게 할 수 있도록 별도의 쉘 스크립트를 제공한다.
[기타 사항]
본문에서는 다루지 않지만 다음의 사항을 고민하여 마이그레이션을 진행하였다.
- web.xml 마이그레이션(web.xml -> ServletContextInitializer)
- pom.xml 마이그레이션(Spring Boot 디펜던시 및 빌드 설정)
- ClassPath를 통한 리소스 획득 -> Spring ResourceLoader를 통한 리소스 획득
- Custom-CORSFilter -> Spring-CORSFIlter(Boot에서 정상적으로 CORS 필터링이 되지 않아 Spring-CORSFilter 변경)
- Spring Legacy 테스트 -> Spring Boot 테스트
- WebConfig.class -> application.yaml(리소스, 뷰 리졸버, AOP 등의 일부 Servlet Web ApplicationContext 계층의 설정들을 application.yaml로 이동)
- AppConfig.class -> application.yaml(AOP, MyBatis 등의 서비스 계층의 "환경" 관련 설정들이 application.yaml로 이동)
- (그러나 MyBatis의 경우, 복잡한 설정과 DBMS 별 설정 분기들이 존재하여 MyBatis Spring AutoConfiguration은 flase 설정을 하였다.
- (서비스 계층에서 사용하는 즉, 서비스 계층 로직을 위한 빈 Config는 그대로 보존)
- (web.xml이 사라지는 과정은 해당 링크를 확인한다.)
JSP 적용 문제
Spring Boot는 타임리프, 프리마커, JSP를 지원한다. 그러나, JSP의 사용을 권고하지 않는다. 왜 권고하지 않을까? 자세한 사항은 다음의 스프링 사이트에서 확인할 수 있다. 간략하게 정리하면 다음과 같다.
- Undertow WAS는 JSP를 지원하지 않는다(ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 아낰ㅋㅋㅋ).
- Executable JAR 패키징하여 원활하게 사용하지 못한다. WAR 패키징을 통해 해소할 수 있다. 그러나 src/main/resources/META-INF/resources/WEB-INF/jsp 디렉토리에 JSP를 욱여넣으면 된다한다.
- 오류 핸들링 관련 사항들이 오버라이딩 되지 않는다(error.jsp를 만들었을 때 Spring 오류 관련 기본 View의 기능을 사용하지 못한다.).
그래도 우리는 Boot에서 JSP를 사용해야 한다. 다음과 같이 진행한다.
아래와 같이 JSP 관련 디펜던시 설정이 필요하다.
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--JSP에서 JSTL를 사용할 경우 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
jasper 설정은 무엇이고 왜 필요할까?
jasper는 톰켓의 JSP 엔진이다. jasper는 JSP 파일을 파싱하여 서블릿 코드로 변환하는 작업을 진행하며, JSP 파일의 변경을 감지하여 변환 작업을 수행한다.
Spring boot에서 톰켓 관련 디펜던시를 알아서 설정해주는데 위의 항목을 제외하였다. 아래와 같이 타고 타고 올라가면 jasper 관련 디펜던시가 없는 것을 확인할 수 있다.
spring-boot-starter-web => spring-boot-starter-tomcat => tomcat-embed-core |
Multiple-DataSource 문제
Spring Boot에서는 application.yaml(properties)를 이용하여 DataSource를 설정할 수 있다. 그런데.. 2개 이상은?
application.yaml의 spring.datasource. ...을 이용하여 설정하지 못한다. 즉, 우리는 별도의 DataSourceConfig.class 또는 xml 파일을 생성해야 한다. 그리고 Spring Boot의 auto-configuration을 제거해야 한다. 다음과 같다(Transaction 자동 설정도 제거해버렸다. 별도 설정을 진행해야 한다.).
[application.class]
@SpringBootApplication( exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
[DataSourceConfig.class]
@Primary
@Bean(name="***DataSource")
@ConfigurationProperties(prefix="com.example.product.***.datasource.hikari")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name="****DataSource")
@ConfigurationProperties(prefix="com.example.product.****.datasource.hikari")
public DataSource ****DataSource() {
return DataSourceBuilder.create().build();
}
[application.yaml]
com:
example:
product:
***:
datasource:
hikari:
driver-class-name: com.tmax.tibero.jdbc.TbDriver
jdbc-url: jdbc:tibero:thin:@10.10.10.174:8629:tibero
username: user
password: pass
minimum-idle: 50
maximum-pool-size: 200
connection-test-query: select 1 from dual
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
****:
datasource:
hikari:
driver-class-name: com.tmax.tibero.jdbc.TbDriver
jdbc-url: jdbc:tibero:thin:@10.10.10.174:8629:tibero
username: user
password: pass
minimum-idle: 50
maximum-pool-size: 50
connection-test-query: select 1 from dual
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
Spring Boot는 DataSourceBuilder를 통해 편리하게 DataSource를 설정할 수 있다. 해당 클래스의 레퍼런스를 확인하면 다운 캐스팅 또는 @ConfigurationProperties를 통해 상세 설정을 할 수 있다. 캐스팅을 통해 값들을 설정하는 것보다는 properties 값들을 통해 해소하는 것이 좀 더 바람직하다. 그러면 2개의 DataSource Bean을 생성할 수 있으며 Primary을 통해 디폴트(우선순위) 빈을 설정한다.
Tomcat / JNDI DataSource 문제
별도의 외장 WAS 컨테이너에 WAR를 배포하는 경우가 존재한다(매우). 필자가 관리하는 제품은 솔루션이다. 솔루션이다 보니 사이트에 적용/설치가 필요하기 때문에 사이트에서 사용하는 WAS 컨테이너를 사용해야 하는 경우가 많이 존재한다. 그래서 Spring Boot의 내장 톰켓을 사용하는 경우도 존재하지만 외장 WAS 컨테이너를 사용하는 경우도 존재한다.
아래와 같이 설정한다.
[pom.xml]
<project>
...
<packaging>war</packaging>
...
</project>
이렇게 설정하여도 다음과 같은 명령어를 실행할 수 있다.
java -jar application.war |
그런데, 문제가 발생하였다. WAS 컨테이너의 DBCP를 사용해야 한다. 즉, JNDI를 이용하여 WAS에서 관리하는 DataSource를 호출해야 한다.
다음과 같이 설정할 수 있으며, 이때, Java Config가 빛을 바란다. 그저 빛빛빛.
[DataSourceConfig.class]
@Value("${com.example.product.datasource.using-jndi}")
private boolean isJndiDataSource;
@Primary
@Bean(name="***DataSource")
@ConfigurationProperties(prefix="com.example.product.***.datasource.hikari")
public DataSource dataSource() {
if ( isJndiDataSource ) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
return lookup.getDataSource( "java:/comp/env/jdbc/***" );
} else {
return DataSourceBuilder.create().build();
}
}
@Bean(name="****DataSource")
@ConfigurationProperties(prefix="com.example.product.****.datasource.hikari")
public DataSource ****DataSource() {
if ( isJndiDataSource ) {
JndiDataSourceLookup lookup = new JndiDataSourceLookup();
return lookup.getDataSource( "java:/comp/env/jdbc/****" );
} else {
return DataSourceBuilder.create().build();
}
}
[application.yaml](위의 Multiple-DataSource에 아래 설정을 추가하였다.)
com:
example:
product:
datasource:
using-jndi: true
using-jndi 1개의 설정을 통해 WAS 컨테이너의 DBCP를 쓸지 내장 DBCP를 쓸지 결정할 수 있다. 이런 분기문으로 빈 설정을 할 수 있는 것이 JavaConfig의 제일 큰 장점 같다.
External Library(JAR) 문제
솔직히, 사용하는 디펜던시(JAR)가 변경된다고 External Lib를 구성할 필요는 없다. 근데 우리는 다음과 같은 참 별로인 경우가 존재하였다.
- 해당 JAR는 재 빌드 불가(코드 수정 후 다시 빌드 못함, 해당 JAR만 사용해야 함)
- 해당 JAR는 File의 절대 경로를 파싱하여 같은 경로에 있는 HMAC 파일을 통해 무결성 검증을 실시한다.
JAR의 절대경로를 갖고 와 해당 경로에 있는 HMAC파일을 File 객체로 파싱 할 때 다음의 문제점이 생긴다. JAR 안에 있는 것에 대해 File 객체로 만들지 못한다. 당연하다... JAR안에 있는데 파일 시스템에서 어떻게 제대로 읽을 수 있겠나...
1번 조건 때문에 코드 수정은 못 한다. 결국 해당 라이브러리는 밖으로 빼야 한다.
클래스 로더가 외부에 있는 디렉토리를 읽게 하기 위해 클래스 패스 설정 다음과 같이 해봤다.
1) ClassPath를 MANIFEST에 적용해보자.
현재 META-INF의 MANIFEST는 다음과 같다.
위의 노란색 항목이 클래스패스로 설정된 것 같다. 그러면 내가 원하는 클래스 패스를 추가해야한다.
메이븐 빌드 플러그인에서 클래스 패스를 설정할 수 있다.
그런데... 설정에서 ClassPath 설정하는 것을 넣어봤자 제대로 적용이 되지 않는다. Spring Boot 빌드 플러그인이 모든 설정을 무시하는 것 같다.
(아래와 같이 MANIFEST의 Class-Path를 설정하고자 하였다.)
2) java 커맨드 -cp
안 된다. ㅋㅋㅋㅋㅋㅋㅋㅋ 걍 안돼
3) Spring의 PropertiesLauncher를 사용한다.
된다.
Spring Boot에서 WAR/JAR를 읽기 위해 JarLauncher/WarLauncher/PropertiesLauncher를 사용한다. 자세한 사항은 별도의 스터디가 필요할 것 같지만, 일단 사용자 클래스 패스를 설정하기 위해서는 PropertiesLauncher를 사용해야 한다. 그런데 WAR 빌드인데 PropertiesLauncher를 사용했는데 잘 되지 않았다. 몇몇 삽질을 해보니 PropertiesLauncher는 JAR 기준이라 클래스패스를 재설정해야 한다.
다음과 같이 할 수 있다(리눅스의 경우, cp의 구분자를 ":"으로 지정한다.).
java -cp extlib/***.jar;$spring-boot-app.war -Dloader.path=WEB-INF/classes,WEB-INF org.springframework.boot.loader.PropertiesLauncher |
된다...........ㅠㅠ 내 하루...
extlib/***.jar 때문에...
Tomcat/ExecutableJAR 동시 배포 가능의 문제
Spring Boot Maven 빌드에서 WAR 빌드하는 경우, 둘 다 배포 가능하다.
Maven에서 Spring Boot 플러그인으로 빌드된 WAR는 WAS 컨테이너에 잘 배포된다.
Maven에서 Spring Boot 플러그인으로 빌드된 WAR는 java -jar app.war를 통해 잘 실행된다.
그리고 extlib 의존성 문제는 신경 안 써도 된다.
클래스 로더의 클래스 패스 룩업 때문에 위의 -cp 부분에 extlib/***.jar를 $spring-boot-app.war보다 먼저 지정하였다. 그래서 extlib도 걍 lib에 집어넣어 같이 빌드해버렸다.
한번 빌드로 외장/내장 톰캣 가동이 가능하다.
쉘 스크립트 작성
음... 고객한테 위의 스크립트를 입력하여 어플리케이션 가동하라고는 못 하겠닼ㅋㅋㅋㅋㅋㅋ
각설하고 start/stat/stop은 아래와 같다.
아 맞다.
stat/stop을 정상적으로 진행하기 위해 application.pid가 필요하다.
다음과 같이 application.class를 변경하면 spring boot application이 가동할 때 application.pid 파일을 생성한다.
[application.class]
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication( Application.class );
springApplication.addListeners( new ApplicationPidFileWriter() );
springApplication.run( args );
}
[start.sh]
#!/bin/sh
app_pid=`cat ./application.pid`
if [ -z "$app_pid" ]
then
java -cp extlib/***.jar:spring-boot-app.war -Dloader.path=WEB-INF/classes,WEB-INF org.springframework.boot.loader.PropertiesLauncher > /dev/null &
echo "Start ***App Server..."
else
echo "Application(***App) is already running."
fi
[stat.sh]
#!/bin/sh
app_pid=`cat ./application.pid`
if [ -z "$app_pid" ]
then
echo "Application(***App) is stopped"
else
pstree -aAp $app_pid
ps -ef | grep $app_pid | grep -v grep
ss -anlp | grep $app_pid
echo "Application(***App) is running"
fi
[stop.sh]
#!/bin/sh
app_pid=`cat ./application.pid`
opt=$1
if [ -z "$app_pid" ]
then
echo "Application(***App) is stopped or pid is invalid."
else
if [[ $opt = 'immediately' ]]
then
kill -9 $app_pid
echo "Shutdown Application(***App) immediately"
else
kill -15 $app_pid
echo "Shutdown Application(***App)"
fi
fi
완성본
- 위의 모든 디렉토리는 변경이 필요한 설정 값 및 리소스 파일들이다(메이븐 기준, src/main/resources 이하 변경이 되는 파일들이다.).
- 위의 구성을 통해 Spring-Boot 어플리케이션을 배포한다.
음... Spring-Boot의 장점이 배포 및 설치의 간결함이었는데... 그랬는데... 간결한가;;? 음... 흠터레스팅.....ㅋㅋㅋㅋㅋㅋㅋ
참고문헌
docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-jsp-limitations
www.baeldung.com/spring-xml-vs-java-config
www.baeldung.com/spring-boot-dispatcherservlet-web-xml
www.baeldung.com/java-web-app-without-web-xml
stackoverflow.com/questions/42154614/springboot-embedded-tomcat-and-tomcat-embed-jasper
www.baeldung.com/spring-boot-main-class
stackoverflow.com/questions/40499548/how-to-configure-additional-classpath-in-springboot
stackoverflow.com/questions/33956179/can-i-start-a-spring-boot-war-with-propertieslauncher
'개발관련 > 삽질' 카테고리의 다른 글
hashicorp사의 Vault(볼트) - Tutorial-Dev(With-커맨드) - 2 (0) | 2021.07.01 |
---|---|
hashicorp사의 Vault(볼트) - 개요 - 1 (0) | 2021.06.29 |
Servlet/Spring에서 web.xml은 어떻게 사라졌을까? (0) | 2021.04.28 |
Apache Commons-DBCP / Hikari-DBCP 정리 (0) | 2021.04.05 |
안전한 비동기 처리 전략(Feat. Spring) (0) | 2021.04.02 |