모듈화한 Spring Boot 1.4 애플리케이션 예제
기존에 작성하던 프로젝트를 Spring Boot 1.3.x
에서 Spring Boot 1.4.1
로 업그레이드 하던 중, DB 커넥션을 맺지 못하는 현상이 생겼다.
해결을 위해 예제 애플리케이션을 만드는 과정을 기록한다.
목표
- 서브 모듈로 나눠 레이어별로 개발 및 테스트 환경 구축.
- Spring Boot의 자동 설정을 사용.
- Spring Data JPA / Hibernate / MySQL
- 어노테이션 기반 설정
- 내부 로직 공통 모듈화, 인터페이스 부분만 다른 웹 사이트와 JSON API 서버.
All-In-One(v.1)
조건을 간단히 하기 위해, 하나의 모듈과 패키지에 인터페이스 없이 웹 애플리케이션을 만들었다.
최소한의 설정으로도 DB 커넥션을 생성할 수 있음을 확인.
커밋 트리에서는 일부 중간 단계를 생략했지만, 기본적인 설정으로도 DB 커넥션을 설정할 수 있다. 코드는 springboot14v1에서 확인할 수 있다.
Maven 설정
parent 모듈 설정으로 JDK의 버전 1.8
과 Spring Boot의 버전 1.4.1.RELEASE
를 생략. 사용한 라이브러리는 spring-boot-dependencies
모듈에서 지정한 버전을 사용한다.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>kr.lul.pages</groupId>
<artifactId>spring-boot14</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>spring-boot14-v1</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
애플리케이션 설정
최소 설정으로도 DB 커넥션은 생성할 수 있는데, 예를 들어 spring.datasource.driver-class-name
이 없어도 커넥션을 맺을 수 있다.
그런데 spring.datasource.tomcat.validation-interval
은 작동이 이상해, 설정하더라도 5초 단위로 spring.datasource.tomcat.validation-query
가 실행된다. 원인은 불명. 확인은 MySQL의 general-query.log
파일로 할 수 있다(tail -f general-query.log
).
# src/main/resources/application.yml
spring:
datasource:
driver-class-name: 'com.mysql.jdbc.Driver'
url: jdbc:mysql://localhost/springboot14v1
username: test
password: testuser
tomcat:
test-while-idle: true
validation-query: 'SELECT 1'
jpa:
open-in-view: false
generate-ddl: true
show-sql: true
패키지 분리(v2)
애플리케이션의 로직 레이어에 맞게 패키지를 분리한다.
코드는 springboot14v2에서 확인.
kr.lul.pages.spring.boot14.v2 ├── business │ ├── dto │ │ └── FooDto.java │ └── service │ ├── FooService.java │ └── FooServiceImpl.java ├── domain │ └── Foo.java ├── jpa │ ├── converter │ │ └── InstantBigintConverter.java │ ├── entity │ │ └── FooEntity.java │ └── repository │ └── FooRepository.java └── rest ├── Spring14Runner.java ├── controller │ ├── FooController.java │ └── FooControllerImpl.java └── resp ├── FooListResp.java └── FooResp.java
내장 DB를 사용한 테스트 문제
@DataJpaTest
어노테이션을 사용하면 H2 같은 내장 DB를 사용해 레포지토리를 테스트할 수 있다.
하지만 H2 의존성을 추가한 후 테스트를 실행하면 아래와 같은 에러가 발생한다. 에러메시지는 H2에 커넥션을 맺을 URL 정보를 찾을 수 없다는 것이지만, 다른 이유로 보인다.
package kr.lul.pages.spring.boot14.v2.jpa.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import kr.lul.pages.spring.boot14.v2.jpa.entity.FooEntity;
import kr.lul.pages.spring.boot14.v2.rest.Spring14Runner;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Spring14Runner.class)
@DataJpaTest
public class FooRepositoryTest {
@Autowired
private FooRepository fooRepository;
@Before
public void setUp() throws Exception {
assertThat(this.fooRepository).isNotNull();
}
@Test
public void testSave() throws Exception {
// Given
FooEntity expected = new FooEntity();
Instant before = Instant.now();
// When
FooEntity actual = this.fooRepository.save(expected);
// Then
assertThat(actual).isNotNull();
assertThat(actual.getId()).isGreaterThan(0);
assertThat(actual.getCreate()).isGreaterThanOrEqualTo(before);
}
}
추가한 H2 의존성 :
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
에러 메시지 :
java.lang.IllegalStateException: Failed to load ApplicationContext at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:83) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:189) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:131) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:230) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:228) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:287) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:289) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:247) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12] at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12] at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191) [spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) [.cp/:na] at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) [.cp/:na] at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) [.cp/:na] at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678) [.cp/:na] at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) [.cp/:na] at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192) [.cp/:na] Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Cannot determine embedded database for tests. If you want an embedded database please put a supported one on the classpath. at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:189) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1148) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1051) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:372) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1128) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1023) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1076) ~[spring-context-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:851) ~[spring-context-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:541) ~[spring-context-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:761) ~[spring-boot-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:371) ~[spring-boot-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:315) ~[spring-boot-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:111) ~[spring-boot-test-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:98) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:116) ~[spring-test-4.3.3.RELEASE.jar:4.3.3.RELEASE] ... 25 common frames omitted Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Cannot determine embedded database for tests. If you want an embedded database please put a supported one on the classpath. at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1583) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:545) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:207) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1128) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1056) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] ... 52 common frames omitted Caused by: java.lang.IllegalStateException: Cannot determine embedded database for tests. If you want an embedded database please put a supported one on the classpath. at org.springframework.util.Assert.state(Assert.java:392) ~[spring-core-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.boot.test.autoconfigure.orm.jpa.TestDatabaseAutoConfiguration$EmbeddedDataSourceFactory.getEmbeddedDatabase(TestDatabaseAutoConfiguration.java:189) ~[spring-boot-test-autoconfigure-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.boot.test.autoconfigure.orm.jpa.TestDatabaseAutoConfiguration$EmbeddedDataSourceFactoryBean.afterPropertiesSet(TestDatabaseAutoConfiguration.java:154) ~[spring-boot-test-autoconfigure-1.4.1.RELEASE.jar:1.4.1.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1642) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1579) ~[spring-beans-4.3.3.RELEASE.jar:4.3.3.RELEASE] ... 63 common frames omitted
모듈 분리(v3)
각 레이어를 따로 다룰 수 있도록 모듈로 분리한다. 모듈로 나누면 모듈(레이어)별로 다른 테스트 설정을 사용 할 수 있다. 예를 들면 JPA 모듈은 필요에 따라 임베디드 DB를 사용할 수도 있고, 실재 DB에 붙어 사용할 수도 있다.
코드 : springboot14/v3
모듈 구성 :
spring-boot14-v3 # 전체 프로젝트 관리 모듈 ├── spring-boot14-v3-business # 비지니스 로직(서비스) 모듈 │ └──kr/lul/pages/spring/boot14/v3 │ └── business │ ├── dto │ │ └── FooDto.java │ └── service │ ├── FooService.java │ └── FooServiceImpl.java ├── spring-boot14-v3-domain # 도메인 오브젝트 모듈 │ └── kr/lul/pages/spring/boot14/v3 │ └── domain │ └── Foo.java ├── spring-boot14-v3-jpa # DB 매핑(JPA) 모듈 │ └── kr/lul/pages/spring/boot14/v3 │ └── jpa │ ├── converter │ │ └── InstantBigintConverter.java │ ├── entity │ │ └── FooEntity.java │ └── repository │ └── FooRepository.java └── spring-boot14-v3-rest └── kr/lul/pages/spring/boot14/v3 └── rest ├── Spring14Runner.java ├── controller │ ├── FooController.java │ └── FooControllerImpl.java └── resp ├── FooListResp.java └── FooResp.java
kr.lul.pages.spring.boot14.v3
를 프로젝트의 기본 패키지로 하고, 그 밑에 각 모듈용 패키지를 둔다.
이 구성에서는 의존성이
domain <- jpa <- business <- rest
의 직선적인 형식을 가지지만, business
모듈을 공유해 HTML의 웹 애플리케이션을 추가로 개발하는 경우도 생각해볼 수 있다.
JPA 모듈 테스트
JUnit 테스트를 @org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
로 설정하면, 실재 DB가 아닌 테스트용 메모리 DB를 사용할 수 있다.
메인 JDBC Driver인 MySQL 의존성을 지우고, Apache Derby를 테스트 스코프로 추가한다.
<!-- Maven -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<scope>test</scope>
</dependency>
spring-boot-autoconfigure
모듈에 있는 org.springframework.boot.autoconfigure.jdbc.EmbeddedDatabaseConnection
의 코드를 보면 테스트용 메모리 DB로 H2도 사용할수 있는 것으로 보이지만, 테스트용 JDBC 모듈로 H2(h2database
)를 사용하면 DataSource
인스턴스를 생성하지 못하면서 위의 에러가 발생한다.
로그 설정 문제
src/main/resources/application.yml
에서 root 로거의 레벨을 error
로 설정하고 config/application.yml
에서 warn
으로 설정을 덮어쓰면 덮어쓴 값이 무시된다.