Web & Server/Spring & Spring Boot

Spring Boot WAR 로 배포하기 (How to deploy with *.war file with Spring Boot)

Simplify - Jonghun 2019. 6. 17. 10:26

Spiring Boot 는 기본적으로 jar 배포형태를 가지고 있습니다. 그 말은 그 자체로서 java 실행 프로그램이다 라는 의미로 해석될 수 있다고 생각합니다. tomcat 같은 웹서버도, db 도 다 내장으로 가질 수 있는 형태이기 때문에, '독립적인 프로그램이다' 라는 의미를 가져간 것이라고 보입니다. 잘 알려진 것 처럼 war 는 웹 프로젝트, 즉 tomcat 과 같은 웹 서버 위에서 돌아가는 프로젝트라고 보시면 될 것 같습니다. 

 

지금까지 프로젝트, 운영을 해오던 환경에서는, 혹은 웹 프로젝트인데 기존 환경에 익숙한 경우에, 그 구조를 변경하는 것을 꺼려하는 것이 일반적입니다. (명령어 한줄 조차도 말이죠) 그렇기 때문에 기존 프로젝트에서 신규 프로젝트로 변경하려면 war로 변경/배포해야 하는 경우가 생깁니다. (제 경우에는 docker와의 조합에서 유리함을 가져가려고 그렇게 했습니다) 여기서는 기존 jar를 war로 변경 해 보고, 이때 발생한 문제 해결했던 것 까지 공유합니다. (여기서는 기존 방식대로 Maven 을 사용하는 경우 기준으로 설명합니다. Gradle이어도 비슷하리라 추측됩니다)

 

다음의 세 가지 과정으로 진행됩니다. 

  1. SpringBootServletInitializer 상속받는 것으로 변경
  2. Embed servlet 을 'provided' 로 변경
  3. war로 빌드

SpringBootServletInitializer 상속받는 것으로 변경

 

Spring Boot 프로젝트를 생성하고 나면, main method를 가진 하나의 클래스가 자동 생성됩니다. 아래와 같은 @SpringBootApplication annotation을 갖는 클래스가 그것인데, 이 안에서는 SpringApplication 클래스의 run 함수를 호출하게 되어 있습니다. 

package com.simplify.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class SpringBootSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootSampleApplication.class, args);
	}
}

이제 코드를 수정하여 다음과 같이 만들어 둡니다. 

package com.simplify.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class SpringBootSampleApplication extends SpringBootServletInitializer{

	public static void main(String[] args) {
		SpringApplication.run(SpringBootSampleApplication.class, args);
	}

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
		return builder.sources(SpringBootSampleApplication.class);
	}
	
}

달라진 것은 SpringBootServletInitializer 를 상속받고, configure 함수를 override합니다. 

 

 

Embed servlet 을 'provided' 로 변경

 

 

이제 pom.xml 을 열어 아래 소스를 추가합니다. 

		<!-- marked the embedded servlet container as provided -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>

제가 보기에, 이 tomcat 은 기본적으로 추가되어 있으나(임의로 pom.xml 에 추가하지 않아도), scope 태그를 바로잡기 위해서 적어주는 것으로 보입니다. 

 

war로 빌드

 

그리고 아래 처럼 packaging 태그를 war로 수정합니다. (안적혀 있다면 추가합니다)

<packaging>war</packaging>

 

 

여기까지 진행하고 나서 Maven build ... 을 눌러서 clean package 로 빌드하면 정상적으로 빌드됩니다. 그런데, 앞선 포스트에서 처럼 저는 jdbc 에 추가적으로 log4jdbc 를 추가하여 조금 더 보기좋게 log 부분을 수정한 내용이 있습니다. 여기부터 문제가 발생합니다.

 

처음에 구동하면 db에 접속하지 않는 일반적인 사항은 잘 동작하지만, db에서 데이터를 가져오는 등의 작업을 하는 것들은 다음과 같은 에러가 발생합니다. (검색 용이성 때문에 로그 내용을 거의 다 첨부합니다)

tomcat     | 06-17 02:08:12.503  INFO [http-nio-8080-exec-3] com.zaxxer.hikari.HikariDataSource       | HikariPool-1 - Starting...
tomcat     | 06-17 02:08:12.506 ERROR [http-nio-8080-exec-3] o.s.b.w.servlet.support.ErrorPageFilter  | Forwarding to error page from request [/openapi/query] due to exception [nested exception is org.apache.ibatis.exceptions.PersistenceException:
tomcat     | ### Error querying database.  Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     | ### The error may exist in file [/usr/local/tomcat/webapps/SpringBootSampleWeb/WEB-INF/classes/mybatis/mapper/TestDB.xml]
tomcat     | ### The error may involve com.simplify.sample.db.mapper.TestMapper.getAll
tomcat     | ### The error occurred while executing a query
tomcat     | ### Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB]
tomcat     | org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
tomcat     | ### Error querying database.  Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     | ### The error may exist in file [/usr/local/tomcat/webapps/SpringBootSampleWeb/WEB-INF/classes/mybatis/mapper/TestDB.xml]
tomcat     | ### The error may involve com.simplify.sample.db.mapper.TestMapper.getAll
tomcat     | ### The error occurred while executing a query
tomcat     | ### Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     |    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)
tomcat     |    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
tomcat     |    at com.sun.proxy.$Proxy66.selectList(Unknown Source)
tomcat     |    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
tomcat     |    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:137)
tomcat     |    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:75)
tomcat     |    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
tomcat     |    at com.sun.proxy.$Proxy67.getAll(Unknown Source)
tomcat     |    at com.simplify.sample.db.service.TestService.getAll(TestService.java:18)
tomcat     |    at com.simplify.sample.db.controller.TestController.query(TestController.java:21)
tomcat     |    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
tomcat     |    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
tomcat     |    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
tomcat     |    at java.lang.reflect.Method.invoke(Method.java:498)
tomcat     |    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:209)
tomcat     |    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:136)
tomcat     |    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
tomcat     |    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:891)
tomcat     |    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
tomcat     |    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
tomcat     |    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
tomcat     |    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
tomcat     |    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
tomcat     |    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:866)
tomcat     |    at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
tomcat     |    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
tomcat     |    at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:209)
tomcat     |    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
tomcat     |    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:357)
tomcat     |    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:270)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
tomcat     |    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
tomcat     |    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
tomcat     |    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:130)
tomcat     |    at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66)
tomcat     |    at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:105)
tomcat     |    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
tomcat     |    at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:123)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
tomcat     |    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
tomcat     |    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
tomcat     |    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
tomcat     |    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
tomcat     |    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
tomcat     |    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:137)
tomcat     |    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
tomcat     |    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:660)
tomcat     |    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
tomcat     |    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
tomcat     |    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:798)
tomcat     |    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
tomcat     |    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:808)
tomcat     |    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
tomcat     |    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
tomcat     |    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
tomcat     |    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
tomcat     |    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
tomcat     |    at java.lang.Thread.run(Thread.java:748)
tomcat     | Caused by: org.apache.ibatis.exceptions.PersistenceException:
tomcat     | ### Error querying database.  Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     | ### The error may exist in file [/usr/local/tomcat/webapps/SpringBootSampleWeb/WEB-INF/classes/mybatis/mapper/TestDB.xml]
tomcat     | ### The error may involve com.simplify.sample.db.mapper.TestMapper.getAll
tomcat     | ### The error occurred while executing a query
tomcat     | ### Cause: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     |    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
tomcat     |    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
tomcat     |    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
tomcat     |    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
tomcat     |    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
tomcat     |    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
tomcat     |    at java.lang.reflect.Method.invoke(Method.java:498)
tomcat     |    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
tomcat     |    ... 76 common frames omitted
tomcat     | Caused by: java.lang.RuntimeException: Driver net.sf.log4jdbc.sql.jdbcapi.DriverSpy claims to not accept jdbcUrl, jdbc:log4jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB
tomcat     |    at com.zaxxer.hikari.util.DriverDataSource.<init>(DriverDataSource.java:106)
tomcat     |    at com.zaxxer.hikari.pool.PoolBase.initializeDataSource(PoolBase.java:332)
tomcat     |    at com.zaxxer.hikari.pool.PoolBase.<init>(PoolBase.java:107)
tomcat     |    at com.zaxxer.hikari.pool.HikariPool.<init>(HikariPool.java:108)
tomcat     |    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112)
tomcat     |    at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151)
tomcat     |    at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115)
tomcat     |    at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78)
tomcat     |    at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82)
tomcat     |    at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:68)
tomcat     |    at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:336)
tomcat     |    at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:84)
tomcat     |    at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
tomcat     |    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
tomcat     |    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
tomcat     |    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
tomcat     |    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
tomcat     |    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
tomcat     |    ... 82 common frames omitted

 위에 보면 DriverSpy 클래스가 claims not to accept jdbcUrl 이라고 되어 있습니다.url이 달라졌나 소스를 보니 Driver를 로드하게 되는데 내장 톰캣에는 내장된 Driver가 아무래도 일반 tomcat 8 에는 없는 것 같습니다. 

 

결국 어떤 방법을 써봐도 안되서, 다음과 같이 해결하였습니다. 

#spring.datasource.driverClassName=org.mariadb.jdbc.Driver
#spring.datasource.url=jdbc:mariadb://jonghiphop.asuscomm.com:63306/TestDB

spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:mysql://jonghiphop.asuscomm.com:63306/TestDB

우선, 위와 같이 jdbc:log4jdbc:mariadb 부분을 jdbc:log4jdbc:mysql 로 변경합니다. 이는 다음과 같이 mysql driver를 강제 로드해서 사용하기 위함입니다. (mysql과 mariadb 는 유사한 형태를 가지고 있습니다. mariadb가 mysql 기반이라는..?)

 

그리고 pom.xml 에 가서 다음과 같이 driver를 추가합니다. 

		<dependency>
		    <groupId>mysql</groupId>
		    <artifactId>mysql-connector-java</artifactId>
		</dependency>

이렇게 하고 나면 정상적으로 접속되고 로그도 아래처럼 잘 나옵니다. 

tomcat     | 06-17 02:22:32.178  INFO [http-nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       | HikariPool-1 - Starting...
tomcat     | 06-17 02:22:32.680  INFO [http-nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       | HikariPool-1 - Start completed.
tomcat     | 06-17 02:22:32.693 DEBUG [http-nio-8080-exec-1] c.s.sample.db.mapper.TestMapper.getAll   | ==>  Preparing: SELECT * FROM Test
tomcat     | 06-17 02:22:32.737 DEBUG [http-nio-8080-exec-1] c.s.sample.db.mapper.TestMapper.getAll   | ==> Parameters:
tomcat     | 06-17 02:22:32.740 DEBUG [http-nio-8080-exec-1] jdbc.sqltiming                           |  com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44)
tomcat     | 1. SELECT * FROM Test
tomcat     |  {executed in 1 msec}
tomcat     | 06-17 02:22:32.783  INFO [http-nio-8080-exec-1] jdbc.resultsettable                      |
tomcat     | |---|------|
tomcat     | |id |name  |
tomcat     | |---|------|
tomcat     | |1  |Test1 |
tomcat     | |2  |Test2 |
tomcat     | |3  |Test3 |
tomcat     | |---|------|
tomcat     |
tomcat     | 06-17 02:22:32.784 DEBUG [http-nio-8080-exec-1] c.s.sample.db.mapper.TestMapper.getAll   | <==      Total: 3

 

 

 

참고 : https://www.mkyong.com/spring-boot/spring-boot-deploy-war-file-to-tomcat/