2020. 6. 29. 17:30ㆍSpring/Spring Boot Tutorial
웹 애플리케이션을 배포할 때, 서버 환경별로 값을 달리해야 하는 값들이 있을 수 있습니다.
예를들면, 로컬에서 테스트하는 database 접속 정보와 실 서버에서 이용하는 database 접속 정보가 다를 수 있을 것이고.
그 외에도 특정 폴더 경로의 절대경로의 경우에도. 리눅스 기반 서버와 Windows 서버의 폴더구조가 다르기에 값을 달리 작성해야하는 필요가 생깁니다.
[Spring Boot] 프로퍼티 파일(yml) 여러개 설정하기 포스팅에서는 긴 프로퍼티 파일을 성질에 따라 쪼개어, 2개 이상의 프로퍼티 파일을 읽어들이는 방법을 알아보았다면,
이번 시간에는 서버 환경별로 활성화 시킬 프로퍼티 파일을 선택하여 배포할 수 있도록 하는 방법을 배워볼 것입니다.
1. 프로파일 폴더 구조
프로파일 별로 프로퍼티 파일을 분리하지 않은 기존의 resources 폴더 구조입니다.
src/main/resources
폴더 내에 application.yml, demo.yml 프로퍼티 파일이 들어있습니다.
설정하는 프로파일에 따라 읽어들일 프로퍼티를 달리 하기 위해 폴더를 생성해봅시다.
resources 폴더 안에 profile 폴더를 만들고, 만들고자 하는 프로파일명을 딴 폴더 이름을 추가합니다.
저는 로컬(local), 운영(prod) 프로파일을 만들고자 하기에, profile 폴더 하위에 local, prod 폴더를 생성하였습니다.
그리고, 각 프로퍼티 파일들을 local, prod 폴더에 복사한 후, 기존의 파일은 삭제해 줍니다.
그러면 위와 같은 폴더구조가 형성됩니다.
각 프로파일 폴더 내에 들어있는 yml 파일은 각 빌드 환경에 맞는 설정으로 변경해 주면 됩니다.
2. 프로파일별 프로퍼티 설정
2-1) local
spring: application: name: demo profiles: active: local ... (생략) ...
application.yml 中 일부
로컬환경의 spring.profiles.active
값은 local입니다.
demo: api: /api/v1 url: 'http://localhost:${server.port}' version: '@project.version@'
demo.yml
2-2) prod
spring: application: name: demo profiles: active: prod ... (생략) ...
application.yml 中 일부
운영환경의 spring.profiles.active
값은 prod입니다.
demo: api: /api/v1 url: https://demo-old.jiniworld.me version: '@project.version@'
demo.yml
운영환경에서는 도메인 주소를 url로 설정했습니다.
@Bean public OpenAPI openAPI(@Value("${demo.version}") String appVersion, @Value("${demo.url}") String url, @Value("${spring.profiles.active}") String active) { Info info = new Info().title("Demo API - " + active).version(appVersion) .description("Spring Boot를 이용한 Demo 웹 애플리케이션 API입니다.") .termsOfService("http://swagger.io/terms/") .contact(new Contact().name("jini").url("https://blog.jiniworld.me/").email("jini@jiniworld.me")) .license(new License().name("Apache License Version 2.0").url("http://www.apache.org/licenses/LICENSE-2.0")); List<Server> servers = Arrays.asList(new Server().url(url).description("demo (" + active +")")); return new OpenAPI() .components(new Components()) .info(info) .servers(servers); }
demo.yml 에 설정했던 demo.url
을 가져와서 OpenAPI 3 api의 서버를 설정하도록 했습니다.
※ OpenAPI 3 에 대해 알고 싶다면 이전 포스팅인 [Spring Boot Tutorial] 13. OpenAPI 3.0를 이용한 REST API 문서 만들기 with Swagger 글을 참고해주세요.
3. pom.xml 설정
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <springdoc.openapi.version>1.4.1</springdoc.openapi.version> </properties> <profiles> <profile> <id>local</id> <properties> <env>local</env> <maven.test.skip>true</maven.test.skip> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>prod</id> <properties> <env>prod</env> </properties> </profile> </profiles>
<project>
태그 하위(<dependencies>
태그와 동등한 레벨)에 <profile>
태그를 추가합니다.
프로파일 설정을 별도로 하지 않을 경우 기본적으로 활성화 시킬 프로파일에 activeByDefault 를 true 설정을 합니다.
활성화된 프로파일은 <properties>
내에 있는 태그들을 프로퍼티로 가질 수 있습니다.
프로파일 내의 properties은 해당 프로파일이 활성화 되었을 경우에면 properties로 설정됩니다.
(공통적으로 이용할 프로퍼티는 ln 1~6 안에 설정하고, 프로파일 별로 설정할 프로퍼티는 ln 11~14, ln 21~23과 같이 profile 내에 설정합니다.)
우리는 <profile>
내에 정의한 프로퍼티인 <env>
를 이용하여 build시 특정 프로파일을 포함시키거나 제외시킬 것입니다.
환경별로 빌드를 달리하기 위해 build 정보를 변경합니다.
<project>
태그 하위(<dependencies>
태그와 동등한 레벨)에 위치한 <build>
태그를 수정해 봅시다.
<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <excludes> <exclude>profile/**/**</exclude> </excludes> </resource> <resource> <directory>src/main/resources/profile/${env}</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <finalName>demo</finalName> </configuration> </plugin> </plugins> </build>
resources 내의 정적 파일 중 profile 하위 파일은 먼저 제외(exclude) 시키고,
활성화된 프로파일의 id가 들어있는 ${env} 폴더를 포함시킵니다.
4. 테스트
4-1) local
local 프로파일을 active 해봅시다.
로그에 local 프로파일이 active 되었다고 출력되었습니다.
Swagger 화면입니다. local 프로파일에 설정했던 spring.profile.active
와 demo.url
값이 잘 출력되는 것을 확인할 수 있습니다.
mvnw 명령어로 local 프로파일로 패키징 합니다.
D:\jini_box\java\workspace-java3\demo>mvnw clean package -P local Found "D:\jini_box\java\workspace-java3\demo\.mvn\wrapper\maven-wrapper.jar" [INFO] Scanning for projects... [INFO] [INFO] -------------------------< me.jiniworld:demo >-------------------------- [INFO] Building demo 1.0.11 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ demo --- [INFO] Deleting D:\jini_box\java\workspace-java3\demo\target [INFO] [INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ demo --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 18 resources [INFO] Copying 2 resources [INFO] [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ demo --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 35 source files to D:\jini_box\java\workspace-java3\demo\target\classes [INFO] [INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ demo --- [INFO] Not copying test resources [INFO] [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ demo --- [INFO] Not compiling test sources [INFO] [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ demo --- [INFO] Tests are skipped. [INFO] [INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ demo --- [INFO] Building jar: D:\jini_box\java\workspace-java3\demo\target\demo-1.0.11.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.1.8.RELEASE:repackage (repackage) @ demo --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 9.512 s [INFO] Finished at: 2020-06-29T17:15:55+09:00 [INFO] ------------------------------------------------------------------------
4-2) prod
local 프로파일을 active 해봅시다.
로그에 prod 프로파일이 active 되었다고 출력되었습니다.
Swagger 화면입니다. prod 프로파일에 설정했던 spring.profile.active
와 demo.url
값이 잘 출력되는 것을 확인할 수 있습니다.
mvnw 명령어로 prod 프로파일로 패키징 합니다.
D:\jini_box\java\workspace-java3\demo>mvnw clean package -P prod Found "D:\jini_box\java\workspace-java3\demo\.mvn\wrapper\maven-wrapper.jar" [INFO] Scanning for projects... [INFO] [INFO] -------------------------< me.jiniworld:demo >-------------------------- [INFO] Building demo 1.0.11 [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ demo --- [INFO] Deleting D:\jini_box\java\workspace-java3\demo\target [INFO] [INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ demo --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Copying 18 resources [INFO] Copying 2 resources [INFO] [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ demo --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 35 source files to D:\jini_box\java\workspace-java3\demo\target\classes [INFO] [INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ demo --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] skip non existing resourceDirectory D:\jini_box\java\workspace-java3\demo\src\test\resources [INFO] [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ demo --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to D:\jini_box\java\workspace-java3\demo\target\test-classes [INFO] [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ demo --- [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running me.jiniworld.demo.DemoApplicationTests 17:16:59.523 [main] DEBUG org.springframework.test.context.junit4.SpringJUnit4ClassRunner - SpringJUnit4ClassRunner constructor called with [class me.jiniworld.demo.DemoApplicationTests] 17:16:59.531 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate] 17:16:59.543 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)] 17:16:59.574 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [me.jiniworld.demo.DemoApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] 17:16:59.599 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [me.jiniworld.demo.DemoApplicationTests], using SpringBootContextLoader 17:16:59.606 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [me.jiniworld.demo.DemoApplicationTests]: class path resource [me/jiniworld/demo/DemoApplicationTests-context.xml] does not exist 17:16:59.607 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [me.jiniworld.demo.DemoApplicationTests]: class path resource [me/jiniworld/demo/DemoApplicationTestsContext.groovy] does not exist 17:16:59.607 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [me.jiniworld.demo.DemoApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}. 17:16:59.608 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [me.jiniworld.demo.DemoApplicationTests]: DemoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration. 17:16:59.696 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.802 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [D:\jini_box\java\workspace-java3\demo\target\classes\me\jiniworld\demo\DemoApplication.class] 17:16:59.804 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration me.jiniworld.demo.DemoApplication for test class me.jiniworld.demo.DemoApplicationTests 17:16:59.925 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [me.jiniworld.demo.DemoApplicationTests]: using defaults. 17:16:59.926 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] 17:16:59.970 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@2d3379b4, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@30c15d8b, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@5e0e82ae, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@6771beb3, org.springframework.test.context.support.DirtiesContextTestExecutionListener@51399530, org.springframework.test.context.transaction.TransactionalTestExecutionListener@6b2ea799, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@411f53a0, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@2b71e916, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@36fc695d, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@28701274, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@13c9d689, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@3754a4bf] 17:16:59.973 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.974 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.976 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.976 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.977 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.977 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.987 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@815b41f testClass = DemoApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@5542c4ed testClass = DemoApplicationTests, locations = '{}', classes = '{class me.jiniworld.demo.DemoApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@481a15ff, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@545997b1, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@24b1d79b, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@8b87145], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null]. 17:16:59.989 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [me.jiniworld.demo.DemoApplicationTests] 17:16:59.989 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [me.jiniworld.demo.DemoApplicationTests] 17:17:00.035 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true, server.port=-1} _ _| |___ _____ ___ | . | -_| | . | |___|___|_|_|_|___| 2020-06-29 17:17:00.699 - INFO [ main] me.jiniworld.demo.DemoApplicationTests : Starting DemoApplicationTests on jini with PID 6612 (started by jini in D:\jini_box\java\workspace-java3\demo) 2020-06-29 17:17:00.700 - INFO [ main] me.jiniworld.demo.DemoApplicationTests : The following profiles are active: prod 2020-06-29 17:17:00.961 -DEBUG [ main] o.s.w.c.s.GenericWebApplicationContext : Refreshing org.springframework.web.context.support.GenericWebApplicationContext@14fa86ae 2020-06-29 17:17:01.897 - INFO [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode. 2020-06-29 17:17:01.997 - INFO [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 87ms. Found 3 repository interfaces. 2020-06-29 17:17:02.015 - WARN [ main] o.m.s.mapper.ClassPathMapperScanner : Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored. 2020-06-29 17:17:02.016 - WARN [ main] o.m.s.mapper.ClassPathMapperScanner : Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored. 2020-06-29 17:17:02.913 - INFO [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$7f0c0d77] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2020-06-29 17:17:02.957 - INFO [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.hateoas.config.HateoasConfiguration' of type [org.springframework.hateoas.config.HateoasConfiguration$$EnhancerBySpringCGLIB$$fe8c5aa9] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2020-06-29 17:17:03.238 - INFO [ main] com.zaxxer.hikari.HikariDataSource : pool-jiniworld - Starting... 2020-06-29 17:17:04.401 - INFO [ main] com.zaxxer.hikari.HikariDataSource : pool-jiniworld - Start completed. 2020-06-29 17:17:04.491 - INFO [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [ name: default ...] 2020-06-29 17:17:04.626 - INFO [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.3.11.Final} 2020-06-29 17:17:04.629 - INFO [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found 2020-06-29 17:17:04.908 - INFO [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final} 2020-06-29 17:17:05.122 - INFO [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect ... (생략) ... 2020-06-29 17:17:10.521 - INFO [ Thread-4] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' 2020-06-29 17:17:10.523 - INFO [ Thread-4] com.zaxxer.hikari.HikariDataSource : pool-jiniworld - Shutdown initiated... 2020-06-29 17:17:10.547 - INFO [ Thread-4] com.zaxxer.hikari.HikariDataSource : pool-jiniworld - Shutdown completed. [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ demo --- [INFO] Building jar: D:\jini_box\java\workspace-java3\demo\target\demo-1.0.11.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.1.8.RELEASE:repackage (repackage) @ demo --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 23.574 s [INFO] Finished at: 2020-06-29T17:17:12+09:00 [INFO] ------------------------------------------------------------------------
운영 환경의 경우 maven.test 를 스킵하지 않기 때문에 TEST 과정도 거칩니다.
※ GitHub에서 demo 프로젝트를 다운받아 볼 수 있습니다.