springboot-autoconfig 프로젝트를 생성하였습니다.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
스프링 부트에서 다음 라이브러리를 선택했다.
Lombok
, Spring Web
, H2 Database
, JDBC API
JdbcTemplate
을 사용해서 회원 데이터를 DB에 저장하고 조회하는 간단한 기능을 만들어보겠습니다.
Member
package hello.member;
import lombok.Data;
@Data
public class Member {
private String memberId;
private String name;
public Member() {
}
public Member(String memberId, String name) {
this.memberId = memberId;
this.name = name;
}
}
DbConfig
package hello.config;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.transaction.TransactionManager;
import javax.sql.DataSource;
@Slf4j
@Configuration
public class DbConfig {
@Bean
public DataSource dataSource(){
log.info("datasource 빈 등록");
HikariDataSource dataSource = new HikariDataSource();
// H2 데이터 베이스 사용
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setJdbcUrl("jdbc:h2:mem:test");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
public TransactionManager transactionManager() {
log.info("transactionManager 빈 등록");
return new JdbcTransactionManager(dataSource());
}
@Bean
public JdbcTemplate jdbcTemplate() {
log.info("jdbcTemplate 빈 등록");
return new JdbcTemplate(dataSource());
}
}
MemberRepository
package hello.member;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class MemberRepository {
private JdbcTemplate template;
public MemberRepository(JdbcTemplate template) {
this.template = template;
}
public void initTable(){
template.execute("create table member(member_id varchar primary key,name varchar )");
}
public void save(Member member) {
template.update("insert into member(member_id, name) values(?,?)",
member.getMemberId(),
member.getName());
}
public Member find(String memberId) {
return template.queryForObject("select member_id, name from member where member_id=?"
, BeanPropertyRowMapper.newInstance(Member.class), memberId);
}
public List<Member> findAll() {
return template.query("select member_id, name from member"
, BeanPropertyRowMapper.newInstance(Member.class));
}
}
initTable
: 보통 리포지토리에 테이블을 생성하는 스크립트를 두지는 않는다. 여기서는 예제를 단순화 하기 위해 이곳에 사용했다.MemberRepositoryTest
package hello.member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Transactional
@Test
void memberTest(){
Member member = new Member("idKwonyongho", "memberKwonyongho");
memberRepository.initTable();
memberRepository.save(member);
Member findMember = memberRepository.find(member.getMemberId());
assertThat(findMember.getMemberId()).isEqualTo(member.getMemberId());
assertThat(findMember.getName()).isEqualTo(member.getName());
}
}
JdbcTemplate
, DataSource
,TransactionManager
가 모두 사용되었다.DbConfigTest
package hello.config;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionManager;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@SpringBootTest
public class DbConfigTest {
@Autowired
DataSource dataSource;
@Autowired
TransactionManager transactionManager;
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void checkBean(){
log.info("dataSource = {}", dataSource);
log.info("transactionManager = {}", transactionManager);
log.info("jdbcTemplate = {}", jdbcTemplate);
assertThat(dataSource).isNotNull();
assertThat(transactionManager).isNotNull();
assertThat(jdbcTemplate).isNotNull();
}
}
JdbcTemplate
, DataSource
, TransactionManager
스프링 컨테이너 등록 확인
이번에는 DBConfig에서 빈 등록을 제거해보자 (2가지 방법)
@Configuration
을 주석처리: 이렇게 하면 해당 설정 파일 자체를 스프링이 읽어들이지 않는다. (컴포넌트 스캔의 대상이 아니다.)@Bean
주석처리: @Bean
이 없으면 스프링 빈으로 등록하지 않는다.DbConfig
에 @Configuration
를 주석처리했다.
JdbcTemplate
, DataSource
, TransactionManager
가 분명히 스프링 빈으로 등록되지 않았다는 것이다.spring-boot-autoconfigure
라는 프로젝트 안에서 수 많은 자동 구성을 제공한다.JdbcTemplate
을 설정하고 빈으로 등록해주는 자동 구성을 확인해보자.
JdbcTemplateAutoConfiguration
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ DatabaseInitializationDependencyConfigurer.class,
JdbcTemplateConfiguration.class,
NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {
}
- @AutoConfiguration
: 자동 구성을 사용하려면 이 애노테이션을 등록해야 한다.
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalXxx
시리즈가 있다. 자동 구성의 핵심이므로 뒤에서 자세히 알아본다.JdbcTemplate
은 DataSource
, JdbcTemplate
라는 클래스가 있어야 동작할 수 있다.@Import
: 스프링에서 자바 설정을 추가할 때 사용한다.JdbcTemplateConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {
@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
JdbcProperties.Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int)
template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
}
@Configuration
: 자바 설정 파일로 사용된다.
@ConditionalOnMissingBean(JdbcOperations.class)
JdbcOperations
빈이 없을 때 동작한다.JdbcTemplate
의 부모 인터페이스가 바로 JdbcOperations
이다.JdbcTemplate
이 빈으로 등록되어 있지 않은 경우에만 동작한다.JdbcTemplate
과 자동 구성이 등록하는 JdbcTemplate
이 중복 등록되는 문제가 발생할 수 있다.JdbcTemplate
이 몇가지 설정을 거쳐서 빈으로 등록되는 것을 확인할 수 있다
즉 자동 구성 기능들이 Bean으로 등록해 주지 않아도 자동으로 Bean에 등록해주는 것이다.
그래서 개발자가 직접 빈을 등록하지 않아도 JdbcTemplate
, DataSource
, TransactionManager
가 스프링 빈으로 등록된 것이다.
스프링 부트가 제공하는 자동 구성
https://docs.spring.io/spring-boot/docs/current/reference/html/auto-configuration-classes.html
Auto Configuration 용어
자동 설정
자동 구성
스프링 부트가 제공하는 자동 구성 기능을 이해하려면 다음 두 가지 개념을 이해해야 한다.
@Conditional
(설정): 특정 조건에 맞을 때 설정이 동작하도록 한다.@AutoConfiguration
(자동 구성): 자동 구성이 어떻게 동작하는지 내부 원리 이해자동 구성에 대해서 자세히 알아보기 위해 간단한 예제
Memory
package memory;
public class Memory {
private long used;
private long max;
public Memory(long used, long max) {
this.used = used;
this.max = max;
}
public long getUsed() {
return used;
}
public long getMax() {
return max;
}
@Override
public String toString() {
return "Memory{" +
"used=" + used +
", max=" + max +
'}';
}
}
used
: 사용중인 메모리max
: 최대 메모리used
가 max
를 넘게 되면 메모리 부족 오류가 발생한다.MemoryFinder
package memory;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MemoryFinder {
public Memory get() {
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();
long used = total - free;
return new Memory(used, max);
}
@PostConstruct
public void init() {
log.info("init memoryFinder");
}
}
max
JVM이 사용할 수 있는 최대 메모리, 이 수치를 넘어가면 OOM이 발생한다.total
JVM이 확보한 전체 메모리(JVM은 처음부터 max 까지 다 확보하지 않고 필요할 때 마다 조금씩 확보한다.)free
total
중에 사용하지 않은 메모리(JVM이 확보한 전체 메모리 중에 사용하지 않은 것)used
JVM이 사용중인 메모리이다. (used
= total
- free
)MemoryController
package memory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
public class MemoryController {
private final MemoryFinder memoryFinder;
@GetMapping("/memory")
public Memory system() {
Memory memory = memoryFinder.get();
log.info("memory={}", memory);
return memory;
}
}
MemoryConfig
package hello.config;
import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
실행
간단하게 메모리 사용량을 실시간으로 확인할 수 있다.
@Conditional
이다.Condition.interface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
-matches()
메서드가 true
를 반환하면 조건에 만족해서 동작하고, false
를 반환하면 동작하지 않는다.
ConditionContext
: 스프링 컨테이너, 환경 정보등을 담고 있다.AnnotatedTypeMetadata
: 애노테이션 메타 정보를 담고 있다.MemoryCondition
package memory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
@Slf4j
public class MemoryCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String memory = context.getEnvironment().getProperty("memory");
log.info("memory={}",memory);
// memory값이 on이면 true를 반환
return "on".equals(memory);
}
}
MemoryConfig 추가
@Configuration
@Conditional(MemoryCondition.class) //추가
public class MemoryConfig {}
@Conditional(MemoryCondition.class)
MemoryConfig
의 적용 여부는 @Conditional
에 지정한 MemoryCondition
의 조건에 따라MemoryCondition
의 matches()
를 실행해보고 그 결과가 true
이면 MemoryConfig
는 정상memoryController
, memoryFinder
가 빈으로 등록된다.false
면 빈은 등록되지 않는다.실행
memory=on
을 설정하지 않았기 때문에 동작하지 않는다.memory=on으로 바꾸기
실행
Condition
인터페이스를 직접 구현해서 MemoryCondition
이라는 구현체를 만들었다.
스프링은 이미 대부분의 구현체를 만들어 놨다. 사용해보자
MemoryConfig - 수정
@ConditionalOnProperty(name = "memory", havingValue = "on") // 수정
public class MemoryConfig {}
@ConditionalOnProperty(name = "memory", havingValue = "on")
를 추가하자memory=on
이라는 조건에 맞으면 동작하고, 그렇지 않으면 동작하지 않는다.@ConditionalOnProperty
도 내부에서는 @Conditional
을 사용한다. 그안에도 Condition
인터페이스를 구현한 OnPropertyCondition
를 가지고 있다.@ConditionalOnXxx
등등등
ConditionalOnXxx 공식 메뉴얼
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations
@Conditional
: 특정 조건에 맞을 때 설정이 동작하도록 한다.
@AutoConfiguration
: 자동 구성이 어떻게 동작하는지 내부 원리 이해
위에서 만든 메모리 조회 기능을 라이브러리로 만들어서 사용해보겠다.
memory-v1 프로젝트를 새로 생성해서 해당 메모리 조회 기능을 추가했다.
MemoryFinderTest
package memory;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
public class MemoryFinderTest {
@Test
void get() {
MemoryFinder memoryFinder = new MemoryFinder();
Memory memory = memoryFinder.get();
assertThat(memory).isNotNull();
}
}
테스트 결과만 확인해보겠다.
빌드하기
gradlew clean build
압축 풀기
jar -xvf memory-v1.jar
memory-v1.jar
는 스스로 동작하지는 못하고 다른 곳에 포함되어서 동작하는 라이브러리이다. 이제 이
라이브러리를 다른 곳에서 사용해보자
project-v1 프로젝트를 새로 생성했다. 이곳에서 memory-v1.jar 라이브러리를 사용해보겠다.
build.gradle
dependencies {
implementation files('libs/memory-v1.jar') //추가
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
라이브러리를 스프링 빈으로 등록해서 동작하도록 만들어보자.
MemoryConfig
package hello.config;
import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryConfig {
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
}
실행
라이브러리가 잘 동작 하는 것을 확인 할 수 있다.
정리
프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리되도록 해보자.
memory-v1
프로젝트를 복사해서memory-v2
를 만들었습니다.
MemoryAutoConfig
package memory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryAutoConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
자동 구성 대상 지정
src/main/resources/META-INF/spring/
에
org.springframework.boot.autoconfigure.AutoConfiguration.imports
생성
memory.MemoryAutoConfig
org.springframework.boot.autoconfigure.AutoConfiguration.imports
의 정보를 읽어서 자동 구성으로 사용한다. 따라서 내부에 있는MemoryAutoConfig
가 자동으로 실행된다.빌드해서 build/libs/memory-v2.jar
를 생성했습니다.
project-v1
와 비슷한project-v2
생성
memory-v2.jar
라이브러리를project-v2
에 적용해보자.
build.gradle
dependencies {
implementation files('libs/memory-v2.jar') // 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
라이브러리 추가
project-v2/libs
폴더를 생성memory-v2
프로젝트에서 빌드한 memory-v2.jar
를 이곳에 복사project-v2/build.gradle
에 memory-v2.jar
를 추가memory=on으로 바꾸기
결과
메모리 조회 라이브러리가 잘 동작하는 것을 확인할 수 있다.
@ConditionalOnXxx
덕분에 라이브러리 설정을 유연하게 제공할 수 있다.스프링 부트는 다음 경로에 있는 파일을 읽어서 스프링 부트 자동 구성으로 사용한다.
resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
동작 순서
1. @SpringBootApplication
2. @EnableAutoConfiguration
3. @Import(AutoConfigurationImportSelector.class)
@SpringBootApplication
public class AutoConfigApplication {
public static void main(String[] args) {
SpringApplication.run(AutoConfigApplication.class, args);
}
}
@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =
AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
@EnableAutoConfiguration
이다. 이름 그대로 자동 구성을 활성화 하는 기능을 제공한다.@EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {…}
@Import
는 주로 스프링 설정 정보(@Configuration
)를 포함할 때 사용한다.AutoConfigurationImportSelector
를 열어보면 @Configuration
이 아니다.@Import
에 설정 정보를 추가 하는 방법은 2가지
1. 정적 방법
@Import(클래스)
이것은 정적이다. 코드에 대상이 딱 박혀 있다. 설정으로 사용할 대상을 동적으로 변경할 수 없다.@Configuration
@Import({AConfig.class, BConfig.class})
public class AppConfig {...}
2. 동적 방법
@Import(ImportSelector)
코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다.public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
//...
}
ImportSelector 예제를 만들어보자.
HelloBean
package hello.selector;
public class HelloBean {
}
HelloConfig
package hello.selector;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloConfig {
@Bean
public HelloBean helloBean() {
return new HelloBean();
}
}
HelloImportSelector
package hello.selector;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class HelloImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"hello.selector.HelloConfig"};
}
}
ImportSelector
인터페이스를 구현했다.hello.selector.HelloConfig
설정 정보를 반환한다.ImportSelectorTest
package hello.selector;
import jdk.dynalink.linker.support.Guards;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
public class ImportSelectorTest {
@Test
void staticConfig(){
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(StaticConfig.class);
HelloBean bean = appContext.getBean(HelloBean.class);
assertThat(bean).isNotNull();
}
@Test
void selectorConfig(){
// HelloImportSelector에 return값 인식
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(SelectImports.class);
HelloBean bean = appContext.getBean(HelloBean.class);
assertThat(bean).isNotNull();
}
@Configuration
@Import(HelloConfig.class)
public static class StaticConfig{
}
@Configuration
@Import(HelloImportSelector.class)
public static class SelectImports{
}
}
selectorConfig()
selectorConfig()
는 SelectorConfig
를 초기 설정 정보로 사용SelectorConfig
는 ImportSelector
의 구현체인HelloImportSelector
를 사용"hello.selector.HelloConfig"
라는 문자를 반환hello.selector.HelloConfig
이 설정 정보로 사용@EnableAutoConfiguration 동작 방식
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {…}
AutoConfigurationImportSelector
는 ImportSelector
의 구현체이다. 따라서 설정 정보를 동적으로 선택할 수 있다.META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
경로의 파일을 확인한다.memory.MemoryAutoConfig
를 설정해줘서 자동으로 Bean에 등록된 것이다.