SlideShare a Scribd company logo
KSUG 이수홍
Deep dive into
Spring Boot
Autoconfiguration
Why
Autoconfiguration?
스프링부트 이전의 설정
기존 MVC 설정
@Configuration

@EnableWebMvc

public class WebAppConfig extends WebMvcConfigurerAdapter {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler(“/resource/")
.addResourceLocations("/resource/**");

}

@Bean

public ViewResolver internalResourceViewResolver() {

InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/views/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}
…
DB 설정
@EnableTransactionManagement

@Configuration

public class DatabaseConfig {


@Bean

public DataSource dataSource(

@Value("${db.driver}") Class<Driver> driverClass,

@Value("${db.url}") String url,

@Value("${db.user}") String user,

@Value("${db.password}") String password) {

return new SimpleDriverDataSource(

BeanUtils.instantiateClass(driverClass),
url, user, password);

}

}
…
JPA 설정
@Configuration

public class JpaConfig {

@Autowired

private DataSource dataSource;



@Bean

public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean() {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

em.setDataSource(dataSource);

em.setPackagesToScan("com.springcamp.test");

em.setPersistenceProvider(new HibernatePersistenceProvider());

em.setMappingResources("META-INF/orm.xml");

em.setJpaProperties(hibernateProperties());

return em;

}

@Bean

public Properties hibernateProperties() {

Properties properties = new Properties();

properties.put(DIALECT, MySQLDialect.class.getName());

properties.put(HBM2DDL_AUTO, ddlAutoProp);

return properties;

}

….
새로운 프로젝트 마다
반복적인 설정
스프링부트에서 어떻게
설정하는가?
기존 MVC 설정
@Configuration

@EnableWebMvc

public class WebAppConfig extends WebMvcConfigurerAdapter {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler(“/resource/")
.addResourceLocations("/resource/**");

}

@Bean

public ViewResolver internalResourceViewResolver() {

InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/views/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}
}
기존 MVC 설정
@Configuration

@EnableWebMvc

public class WebAppConfig extends WebMvcConfigurerAdapter {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler(“/resource/")
.addResourceLocations("/resource/**");

}

@Bean

public ViewResolver internalResourceViewResolver() {

InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/views/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}
}
기존 MVC 설정
@Configuration

@EnableWebMvc

public class WebAppConfig extends WebMvcConfigurerAdapter {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler(“/resource/")
.addResourceLocations("/resource/**");

}

@Bean

public ViewResolver internalResourceViewResolver() {

InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/views/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}
}
“/resource/"
"/resource/**"
ResourceHandler
기존 MVC 설정
@Configuration

@EnableWebMvc

public class WebAppConfig extends WebMvcConfigurerAdapter {

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler(“/resource/")
.addResourceLocations("/resource/**");

}

@Bean

public ViewResolver internalResourceViewResolver() {

InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();

viewResolver.setPrefix("/WEB-INF/views/");

viewResolver.setSuffix(".jsp");

return viewResolver;

}
}
“/resource/"
"/resource/**"
ResourceHandler
Prefix("/WEB-INF/views/")
Suffix(".jsp")
스프링 부트 MVC 설정
1. 의존성 추가

spring-boot-starter-web
2. 환경변수 추가 

spring.mvc.view.prefix: /WEB-INF/
views

spring.mvc.view.suffix: .jsp
기존 DB 설정
@EnableTransactionManagement

@Configuration

public class DatabaseConfig {


@Bean

public DataSource dataSource(

@Value("${db.driver}") Class<Driver> driverClass,

@Value("${db.url}") String url,

@Value("${db.user}") String user,

@Value("${db.password}") String password) {

return new SimpleDriverDataSource(

BeanUtils.instantiateClass(driverClass),
url, user, password);

}

}
}
기존 DB 설정
@EnableTransactionManagement

@Configuration

public class DatabaseConfig {


@Bean

public DataSource dataSource(

@Value("${db.driver}") Class<Driver> driverClass,

@Value("${db.url}") String url,

@Value("${db.user}") String user,

@Value("${db.password}") String password) {

return new SimpleDriverDataSource(

BeanUtils.instantiateClass(driverClass),
url, user, password);

}

}
}
기존 DB 설정
@EnableTransactionManagement

@Configuration

public class DatabaseConfig {


@Bean

public DataSource dataSource(

@Value("${db.driver}") Class<Driver> driverClass,

@Value("${db.url}") String url,

@Value("${db.user}") String user,

@Value("${db.password}") String password) {

return new SimpleDriverDataSource(

BeanUtils.instantiateClass(driverClass),
url, user, password);

}

}
}
"${db.driver}"

"${db.url}"

"${db.user}"

"${db.password}"
기존 DB 설정
@EnableTransactionManagement

@Configuration

public class DatabaseConfig {


@Bean

public DataSource dataSource(

@Value("${db.driver}") Class<Driver> driverClass,

@Value("${db.url}") String url,

@Value("${db.user}") String user,

@Value("${db.password}") String password) {

return new SimpleDriverDataSource(

BeanUtils.instantiateClass(driverClass),
url, user, password);

}

}
}
"${db.driver}"

"${db.url}"

"${db.user}"

"${db.password}"
SimpleDriverDataSource
스프링 부트 DB 설정
1. 의존성 추가

spring-boot-starter-jdbc
2. 환경변수 추가

spring.datasource.url: 접속 url 

spring.datasource.driverClassName: 드
라이브spring.datasource.username: sa

spring.datasource.password:
JPA 설정
@Configuration

public class JpaConfig {

@Autowired

private DataSource dataSource;



@Bean

public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean() {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

em.setDataSource(dataSource);

em.setPackagesToScan("com.springcamp.test");

em.setPersistenceProvider(new HibernatePersistenceProvider());

em.setMappingResources("META-INF/orm.xml");

em.setJpaProperties(hibernateProperties());

return em;

}

@Bean

public Properties hibernateProperties() {

Properties properties = new Properties();

properties.put(DIALECT, MySQLDialect.class.getName());

properties.put(HBM2DDL_AUTO, ddlAutoProp);

return properties;

}

//..
}
JPA 설정
@Configuration

public class JpaConfig {

@Autowired

private DataSource dataSource;



@Bean

public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean() {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

em.setDataSource(dataSource);

em.setPackagesToScan("com.springcamp.test");

em.setPersistenceProvider(new HibernatePersistenceProvider());

em.setMappingResources("META-INF/orm.xml");

em.setJpaProperties(hibernateProperties());

return em;

}

@Bean

public Properties hibernateProperties() {

Properties properties = new Properties();

properties.put(DIALECT, MySQLDialect.class.getName());

properties.put(HBM2DDL_AUTO, ddlAutoProp);

return properties;

}

//..
}
JPA 설정
@Configuration

public class JpaConfig {

@Autowired

private DataSource dataSource;



@Bean

public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean() {

LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();

em.setDataSource(dataSource);

em.setPackagesToScan("com.springcamp.test");

em.setPersistenceProvider(new HibernatePersistenceProvider());

em.setMappingResources("META-INF/orm.xml");

em.setJpaProperties(hibernateProperties());

return em;

}

@Bean

public Properties hibernateProperties() {

Properties properties = new Properties();

properties.put(DIALECT, MySQLDialect.class.getName());

properties.put(HBM2DDL_AUTO, ddlAutoProp);

return properties;

}

//..
}
properties.put(DIALECT, MySQLDialect.class.getName());

properties.put(HBM2DDL_AUTO, ddlAutoProp);
스프링 부트 JPA 설정
1. 의존성 추가

spring-boot-starter-data-jpa
2. 환경변수 추가

spring.jpa.show-sql: true

spring.jpa.hibernate.ddl-auto: create-
drop
반복적인 설정 부분 제거
의존성과 환경변수 추가
그리고 소스
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
설정 부분 해결?!
설정 클래스는 다 어디
갔을까?
그 내부를 들여다 보자
Into the 스프링 부트
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
Into the 스프링 부트
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
Into the 스프링 부트
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
스프링 컨테이너를 실행
Into the 스프링 부트
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
스프링 컨테이너를 실행
Into the 스프링 부트
@SpringBootApplication

public class Application {


public static void main(String[] args) {

SpringApplication.run(Application.class, args);

}


}
스프링 컨테이너를 실행
부트 단순함을 위한 어노테이션
Into the @SpringBootApplication
@Configuration

@EnableAutoConfiguration

@ComponentScan

public @interface SpringBootApplication {
…
}
Into the @SpringBootApplication
@Configuration

@EnableAutoConfiguration

@ComponentScan

public @interface SpringBootApplication {
…
}
Into the @SpringBootApplication
@Configuration

@EnableAutoConfiguration

@ComponentScan

public @interface SpringBootApplication {
…
} @EnableAutoConfiguration
스프링부트 마법의 실마리
About @EnableAutoConfiguration
• 특정 기준의 설정클래스를 로드
• @Enable* 모듈화된 설정의 일종
• JavaConfig에서 모듈화된 설정시 사용 ( 스프링 3.1 부터 지원 )
• 기존 XML 커스텀태그(<mvc:*/>,<context:*/> …)와 대응
• 쉽게 자신의 설정 모듈을 작성 가능

( @Import 어노테이션 사용 )
@Enable* 어노테이션이란?
@EnableWebMvc
@EnableAspectJAutoProxy
…
@Configuration
public class ApplicationConfiguration { … }
Into the @EnableAutoConfiguration
@AutoConfigurationPackage

@Import(EnableAutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {

…

}
Into the @EnableAutoConfiguration
@AutoConfigurationPackage

@Import(EnableAutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {

…

}
Into the @EnableAutoConfiguration
@AutoConfigurationPackage

@Import(EnableAutoConfigurationImportSelector.class)

public @interface EnableAutoConfiguration {

…

}
스프링부트 자동 설정 기능담당
Into the
EnableAutoConfigurationImportSelector 1
public class EnableAutoConfigurationImportSelector implements
DeferredImportSelector, … {
@Override

public String[] selectImports(AnnotationMetadata metadata) {
…
}
}
Into the
EnableAutoConfigurationImportSelector 1
public class EnableAutoConfigurationImportSelector implements
DeferredImportSelector, … {
@Override

public String[] selectImports(AnnotationMetadata metadata) {
…
}
}
Into the
EnableAutoConfigurationImportSelector 1
public class EnableAutoConfigurationImportSelector implements
DeferredImportSelector, … {
@Override

public String[] selectImports(AnnotationMetadata metadata) {
…
}
}
설정 클래스(@Configuration)
리스트 받아서 활성화
Into the
EnableAutoConfigurationImportSelector 1
public class EnableAutoConfigurationImportSelector implements
DeferredImportSelector, … {
@Override

public String[] selectImports(AnnotationMetadata metadata) {
…
}
}
설정 클래스(@Configuration)
리스트 받아서 활성화
Into the
EnableAutoConfigurationImportSelector 1
public class EnableAutoConfigurationImportSelector implements
DeferredImportSelector, … {
@Override

public String[] selectImports(AnnotationMetadata metadata) {
…
}
}
설정 클래스(@Configuration)
리스트 받아서 활성화
설정 클래스 리스트 

(패키지명 포함 클래스명)
Into the
EnableAutoConfigurationImportSelector 2
…
protected List<String> getCandidateConfigurations(…) {

return SpringFactoriesLoader.loadFactoryNames(

getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());

}
…
}
Into the
EnableAutoConfigurationImportSelector 2
…
protected List<String> getCandidateConfigurations(…) {

return SpringFactoriesLoader.loadFactoryNames(

getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());

}
…
}
Into the
EnableAutoConfigurationImportSelector 2
…
protected List<String> getCandidateConfigurations(…) {

return SpringFactoriesLoader.loadFactoryNames(

getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());

}
…
}
Classpath의 모든 라이브러리의 

“META-INF/spring.factories“ 위치의 파일
에서 설정파일 리스트 읽어온다.
Into the
EnableAutoConfigurationImportSelector 2
Into the META-INF/spring.factories
• Key=Value 형태로 설정클래스 리스트 기록
• 스프링부트의 기본 설정 정보는 아래의 라이브러리 안에 포함

o.s.boot:spring-boot-autoconfigure:x.x.x.jar
# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,

…

org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,

org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,

…
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,

org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,
…
중간 정리
• 스프링부트의 설정의 시작

@EnableAutoConfiguration
• 설정리스트의 보관소 

“META-INF/spring.factories”
• 모든 설정클래스를 로딩!?
스프링부트는 지정된 

모든 설정클래스가
스프링컨테이너에 올라
가는가?

당연히 NO!
@Conditional
@Conditional은
어노테이션통한 조건적
으로 Bean을 등록
@Conditional
•스프링 4에서 도입된 어노테이션
•조건부로 Bean을 (스프링컨테이너)에 등록
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
class PropertiesCondition implements Condition {

@Override

public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {

return context.getEnvironment()
.containsProperty("test.hasValue");

}

}
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
class PropertiesCondition implements Condition {

@Override

public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {

return context.getEnvironment()
.containsProperty("test.hasValue");

}

}
@Conditional
@Bean

@Conditional(PropertiesCondition.class)

public CommandLineRunner propertiesConditional() {

return (args) -> {

System.out.println("test.hasValue 환경변수가 있으면 보입니다");

};

}
class PropertiesCondition implements Condition {

@Override

public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {

return context.getEnvironment()
.containsProperty("test.hasValue");

}

}
true이면 Bean이 등록
@Profile
@Profile
@Profile("dev")

@Configuration

class HelloProfileDevConfig {



@Bean

public HelloWorld helloDevWorld() {

return new HelloWorld("dev");

}

}



@Profile("test")

@Configuration

class HelloProfileTestConfig {

@Bean

public HelloWorld helloTestWorld() {

return new HelloWorld("test");

}

}
@Profile
@Profile("dev")

@Configuration

class HelloProfileDevConfig {



@Bean

public HelloWorld helloDevWorld() {

return new HelloWorld("dev");

}

}



@Profile("test")

@Configuration

class HelloProfileTestConfig {

@Bean

public HelloWorld helloTestWorld() {

return new HelloWorld("test");

}

}
@Profile
@Profile("dev")

@Configuration

class HelloProfileDevConfig {



@Bean

public HelloWorld helloDevWorld() {

return new HelloWorld("dev");

}

}



@Profile("test")

@Configuration

class HelloProfileTestConfig {

@Bean

public HelloWorld helloTestWorld() {

return new HelloWorld("test");

}

}
@Profile("dev")
@Profile
@Profile("dev")

@Configuration

class HelloProfileDevConfig {



@Bean

public HelloWorld helloDevWorld() {

return new HelloWorld("dev");

}

}



@Profile("test")

@Configuration

class HelloProfileTestConfig {

@Bean

public HelloWorld helloTestWorld() {

return new HelloWorld("test");

}

}
@Profile("test")
@Profile("dev")
Into @Profile
Into @Profile
@Conditional(ProfileCondition.class)

public @interface Profile {

String[] value();

}
Into @Profile
@Conditional(ProfileCondition.class)

public @interface Profile {

String[] value();

}
Into @Profile
@Conditional(ProfileCondition.class)

public @interface Profile {

String[] value();

}
@Conditional 를 통해 Profile이 구현
@Conditional 예제
@Conditional 확장 1
@ConditionalOnClass
@ConditionalOnProperties
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnResources
@Conditional 확장 1
@ConditionalOnClass
@ConditionalOnProperties
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnResources
스프링 컨테이너의 Bean 존재유무
@Conditional 확장 1
@ConditionalOnClass
@ConditionalOnProperties
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnResources
스프링 컨테이너의 Bean 존재유무
classpath에서 클래스의 존재유무
@Conditional 확장 1
@ConditionalOnClass
@ConditionalOnProperties
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnResources
스프링 컨테이너의 Bean 존재유무
classpath에서 클래스의 존재유무
환경 변수의 유무
@Conditional 확장 1
@ConditionalOnClass
@ConditionalOnProperties
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnResources
스프링 컨테이너의 Bean 존재유무
classpath에서 클래스의 존재유무
환경 변수의 유무 Resources 존재
@Conditional 확장 2
@ConditionalOnJava
@OnJndiCondition
@ConditionalOnWebApplication
@ConditionalOnExpression
@ConditionalOnNotWebApplication
@Conditional 확장 2
@ConditionalOnJava
@OnJndiCondition
@ConditionalOnWebApplication
@ConditionalOnExpression
현재 프로젝트가 웹애플리케이션인지 여부
@ConditionalOnNotWebApplication
@Conditional 확장 2
@ConditionalOnJava
@OnJndiCondition
@ConditionalOnWebApplication
@ConditionalOnExpression
현재 프로젝트가 웹애플리케이션인지 여부
JAVA 버전 종류 선택
@ConditionalOnNotWebApplication
@Conditional 확장 2
@ConditionalOnJava
@OnJndiCondition
@ConditionalOnWebApplication
@ConditionalOnExpression
현재 프로젝트가 웹애플리케이션인지 여부
JAVA 버전 종류 선택
@ConditionalOnNotWebApplication
SPEL의 true false 여부
@Conditional 확장 2
@ConditionalOnJava
@OnJndiCondition
@ConditionalOnWebApplication
@ConditionalOnExpression
현재 프로젝트가 웹애플리케이션인지 여부
JAVA 버전 종류 선택
JNDI lookup 가능 여부
@ConditionalOnNotWebApplication
SPEL의 true false 여부
스프링부트는
@Conditional*을 통한
Autoconfiguration
구현
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
서블릿 인터페이스 있는가?
이미 MVC 설정하는게 있는가?
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
서블릿 인터페이스 있는가?
이미 MVC 설정하는게 있는가?
스프링 시큐리티 라이브러리가 있
는가? 등등
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
서블릿 인터페이스 있는가?
이미 MVC 설정하는게 있는가?
스프링 시큐리티 라이브러리가 있
는가? 등등
DataSource 객체가 스프링 컨테
이너에 존재하는가? 등
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
서블릿 인터페이스 있는가?
이미 MVC 설정하는게 있는가?
스프링 시큐리티 라이브러리가 있
는가? 등등
DataSource 객체가 스프링 컨테
이너에 존재하는가? 등
JPA, 하이버네이트 라이브리
가 있는가? DataSource 설정이 이미되었
는가?
스프링부트 설정
MVC 설정
Security 설정
JDBC 설정
JPA 설정
AOP 설정
서블릿 인터페이스 있는가?
이미 MVC 설정하는게 있는가?
스프링 시큐리티 라이브러리가 있
는가? 등등
DataSource 객체가 스프링 컨테
이너에 존재하는가? 등
JPA, 하이버네이트 라이브리
가 있는가? DataSource 설정이 이미되었
는가?
스프링 AOP, AspectJ 라이브러리
가 존재하는가?
즉 spring.factories 의
설정 클래스는
조건(@Conditional)에
따라 컨테이너에 등록
Autoconfiguration 지원 기술
• Core

Spring (Security, AOP, Session, Cache, DevTools …) Atomikos, …
• Web

Spring (MVC, Websocket, Rest Docs), WS, Jersey, …
• Template Engines

Freemarker, Velocity, Thymeleaf, Mustache, …
• SQL 

JPA, JOOQ, JDBC, H2, HSQLDB, Derby, MySQL, PostreSQL
• NoSQL, Cloud, Social, I/O, Ops, …
커스텀 모듈 만들기
(소스)
감사합니다!

More Related Content

스프링캠프 2016 발표 - Deep dive into spring boot autoconfiguration