자동 구성(Auto Configuration)
예제 만들기
스프링 부트가 제공하는 자동 구성(Auto Configuration)을 이해하기 위해 간단한 예제를 만들어보자.
JdbcTemplate을 사용해서 회원 데이터를 DB에 저장하고 조회하는 간단한 기능이다
Member
@Data
public class Member {
private String memberId;
private String name;
public Member() {
}
public Member(String memberId, String name) {
this.memberId = memberId;
this.name = name;
}
}
memberId , name 필드가 있는 간단한 회원 객체이다.
기본 생성자, memberId, name 을 포함하는 생성자 이렇게 2개의 생성자를 만들었다
DbConfig
@Slf4j
@Configuration
public class DbConfig {
@Bean
public DataSource dataSource() {
log.info("dataSource 빈 등록");
HikariDataSource dataSource = new HikariDataSource();
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());
}
}
JdbcTemplate을 사용해서 회원 데이터를 DB에 보관하고 관리하는 기능이다.
DataSource , TransactionManager , JdbcTemplate을 스프링 빈으로 직접 등록한다.
빈 등록이 실제 호출되는지 확인하기 위해 로그를 남겨두었다.
dataSource.setJdbcUrl("jdbc:h2:mem:test");
DB는 별도의 외부 DB가 아니라 JVM 내부에서 동작하는 메모리 DB를 사용한다
JdbcTransactionManager 는 DataSourceTransactionManager 와 같은 것으로 생각하면 된다.
여기에 예외 변환 기능이 보강되었다.
-> 원래는 아래 코드 처럼 build.gradle에 dependency 추가하고
application.properties(또는 application.yml)에 db 정보 설정하면 알아서 빈 등록된다.[ 자동구성 ]
위의 코드는 자동 구성 설정이 어떤 기능을 하는지 알려주기 위해 수동으로 등록하는 예시이다.
// JDBC 스타터 (HikariCP, JdbcTemplate 포함)
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// H2 데이터베이스
implementation 'com.h2database:h2'
MemberRepository
@Repository
public class MemberRepository {
public final JdbcTemplate jdbcTemplate;
public MemberRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 예제니까 테이블생성을 여기서 진행
public void initTable() {
jdbcTemplate.execute("create table member(member_id varchar primary key, name varchar)");
}
public void save(Member member) {
jdbcTemplate.update("insert into member(member_id,name) values (?,?)",
member.getMemberId(),
member.getName());
}
public Member find(String memberId) {
return jdbcTemplate.queryForObject("select member_id,name from member where member_id=?",
BeanPropertyRowMapper.newInstance(Member.class),
memberId);
}
public List<Member> findAll() {
return jdbcTemplate.query("select member_id, name from member",
BeanPropertyRowMapper.newInstance(Member.class));
}
}
JdbcTemplate 을 사용해서 회원을 관리하는 리포지토리이다.
DbConfig 에서 JdbcTemplate 을 빈으로 등록했기 때문에 바로 주입받아서 사용할 수 있다.
initTable : 보통 리포지토리에 테이블을 생성하는 스크립트를 두지는 않는다.
여기서는 예제를 단순화 하기 위해 이곳에 사용했다.
MemberRepositoryTest
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Transactional
@Test
void memberTest() {
Member member = new Member("idA", "memberA");
memberRepository.initTable();
memberRepository.save(member);
Member findMember = memberRepository.find(member.getMemberId());
Assertions.assertThat(findMember.getMemberId()).isEqualTo(member.getMemberId());
Assertions.assertThat(findMember.getName()).isEqualTo(member.getName());
}
}
@Transactional 을 사용해서 트랜잭션 기능을 적용했다.
참고로 @Transactional 을 사용하려면 TransactionManager가 스프링 빈으로 등록되어 있 어야 한다.
테이블을 생성하고, 회원 데이터를 저장한 다음 다시 조회해서, 기존 데이터와 같은지 간단히 검증한다.
회원 데이터를 DB에 보관하고 관리하기 위해
앞서 빈으로 등록한 JdbcTemplate , DataSource , TransactionManager 가 모두 사용되었다.
그런데 생각해보면 DB에 데이터를 보관하고 관리하기 위해
이런 객체 들을 항상 스프링 빈으로 등록해야 하는 번거로움이 있다.
만약 DB를 사용하는 다른 프로젝트를 진행한다면 이러한 객 들을 또 스프링 빈으로 등록해야 할 것이다.
자동 구성 확인
DbConfigTest
@Slf4j
@SpringBootTest
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();
}
}
해당 빈들을 DbConfig 설정을 통해 스프링 컨테이너에 등록했기 때문에, null 이면 안된다.
사실 @Autowired 는 의존관계 주입에 실패하면 오류가 발생하도록 기본 설정되어 있다.
이해를 돕기 위해 이렇게 코드를 작성했다. 테스트는 정상이고 모두 의존관계 주입이 정상 처리된 것을 확인할 수 있다.
출력 결과를 보면 빈이 정상 등록된 것을 확인할 수 있다.
빈 등록 제거
JdbcTemplate , DataSource , TransactionManager 빈은 모두 DbConfig를 통해서 스프링 컨테이너에 빈 으로 등록되었다.
이번에는 DbConfig 에서 해당 빈들을 등록하지 않고 제거해보자.
DbConfig 에서 빈 등록을 제거하는 방법은 2가지 이다.
@Configuration 을 주석처리: 이렇게 하면 해당 설정 파일 자체를 스프링이 읽어들이지 않는다.
(컴포넌트 스캔의 대상이 아니다.)
@Bean 주석처리: @Bean 이 없으면 스프링 빈으로 등록하지 않는다.
@Configuration 을 주석처리를 하고 다시 테스트를 돌려보면
우리가 등록한 JdbcTemplate , DataSource , TransactionManager 가 분명히 스프링 빈으로 등록되지 않았다는 것이다.
그런데 테스트는 정상 통과하고 심지어 출력결과에
JdbcTemplate , DataSource , TransactionManager 빈들이 존재하는 것을 확인할 수 있다.
어떻게 된 것 일까? 사실 이 빈들은 모두 스프링 부트가 자동으로 등록해 준 것이다.
dataSource = HikariDataSource (null)로 출력되는 이유는
DataSource가 제대로 설정되지 않았거나 HikariDataSource가 아직 초기화되지 않았기 때문입니다.
일반적으로, HikariDataSource는 생성 직후에는 기본적으로 빈 값(null)으로 초기화된 상태일 수 있습니다.
스프링 부트의 자동 구성
스프링 부트는 자동 구성(Auto Configuration)이라는 기능을 제공하는데,
일반적으로 자주 사용하는 수 많은 빈들을 자동으로 등록해주는 기능이다.
앞서 우리가 살펴보았던 JdbcTemplate , DataSource , TransactionManager 모두 스프링 부트가 자동 구성을 제공해서
자동으로 스프링 빈으로 등록된다.
이러한 자동 구성 덕분에 개발자는 반복적이고 복잡한 빈 등록과 설정을 최소화 하고
애플리케이션 개발을 빠르게 시작 할 수 있다.
자동 구성 살짝 알아보기
스프링 부트는 spring-boot-autoconfigure 라는 프로젝트 안에서 수 많은 자동 구성을 제공한다.
JdbcTemplate 을 설정하고 빈으로 등록해주는 자동 구성을 확인해보자.
spring-boot-starter-*를 사용하게되면
해당 라이브러리 안에는 spring-boot-starter가 포함되어있고 그 안에는 autoconfigure 라이브러리가 포함되어있다.
autoconfigure 라이브러리를 살펴보면 그 안에
많은 자동설정 클래스들이 존재하는것을 확인할 수 있다.
@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 {
public JdbcTemplateAutoConfiguration() {
}
}
여기서 모든 것을 깊이있게 이해하지 않아도 된다. 대략 어떻게 동작하는지 감을 잡을 수 있는 정도면 충분하다.
@AutoConfiguration : 자동 구성을 사용하려면 이 애노테이션을 등록해야 한다.
자동 구성도 내부에 @Configuration이 있어서 빈을 등록하는 자바 설정 파일로 사용할 수 있다.
after = DataSourceAutoConfiguration.class 자동 구성이 실행되는 순서를 지정할 수 있다.
JdbcTemplate 은 DataSource 가 필요하기 때문에
DataSource 를 자동으로 등록해주는 DataSourceAutoConfiguration 다음에 실행 하도록 설정되어 있다
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) IF문과 유사한 기능을 제공한다.
이런 클래스가 있는 경우에만 설정이 동작한다.
만약 없으면 여기 있는 설정들이 모두 무효화 되고, 빈도 등록되지 않는다.
@ConditionalXxx 시리즈가 있다. 자동 구성의 핵심이므로 뒤에서 자세히 알아본다.
JdbcTemplate 은 DataSource , JdbcTemplate 라는 클래스가 있어야 동작할 수 있다.
@Import : 스프링에서 자바 설정을 추가할 때 사용한다.
@Import 의 대상이 되는 JdbcTemplateConfiguration 를 추가로 확인해보자.
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({JdbcOperations.class})
class JdbcTemplateConfiguration {
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이 중복 등록되는 문제가 발생할 수 있다.
보통 개발자가 직접 빈을 등록하면 개발자가 등록한 빈을 사용하고, 자동 구성은 동작하지 않는다.
[ @ConditionalOnMissingBean 때문에 ]
JdbcTemplate 이 몇가지 설정을 거쳐서 빈으로 등록되는 것을 확인할 수 있다.
자동 등록 설정
다음과 같은 자동 구성 기능들이 다음 빈들을 등록해준다
JdbcTemplateAutoConfiguration : JdbcTemplate
DataSourceAutoConfiguration : DataSource
DataSourceTransactionManagerAutoConfiguration : TransactionManager
그래서 개발자가 직접 빈을 등록하지 않아도
JdbcTemplate , DataSource , TransactionManager 가 스프링 빈으로 등록된 것이다.
스프링 부트가 제공하는 자동 구성(AutoConfiguration)
https://docs.spring.io/spring-boot/docs/current/reference/html/auto-configuration- classes.html
스프링 부트는 수 많은 자동 구성을 제공하고 spring-boot-autoconfigure 에 자동 구성을 모아둔다.
스프링 부트 프로젝트를 사용하면 spring-boot-autoconfigure 라이브러리는 기본적으로 사용된다.
Auto Configuration - 용어, 자동 설정? 자동 구성?
Auto Configuration은 주로 다음 두 용어로 번역되어 사용된다.
- 자동 설정
- 자동 구성
자동 설정
Configuration이라는 단어가 컴퓨터 용어에서는 환경 설정, 설정이라는 뜻으로 자주 사용된다.
Auto Configuration은 크게 보면 빈들을 자동으로 등록해서 스프링이 동작하는 환경을 자동으로 설정해주기 때문에
자동 설정이라는 용어도 맞다.
자동 구성
Configuration 이라는 단어는 구성, 배치라는 뜻도 있다.
예를 들어서 컴퓨터라고 하면 CPU, 메모리등을 배치해야 컴퓨터가 동작한다.
이렇게 배치하는 것을 구성이라 한다.
스프링도 스프링 실행에 필요한 빈들을 적절하게 배치해야 한다.
자동 구성은 스프링 실행에 필요한 빈들을 자동으로 배치해주는 것이다.
자동 설정, 자동 구성 두 용어 모두 맞는 말이다.
자동 설정은 넓게 사용되는 의미이고,
자동 구성은 실행에 필요한 컴포넌트 조각을 자동으로 배치한다는 더 좁은 의미에 가깝다.
Auto Configuration은 자동 구성이라는 단어를 주로 사용하고, 문맥에 따라서 자동 설정이라는 단어도 사용하겠다.
Configuration이 단독으로 사용될 때는 설정이라는 단어를 사용하겠다.
스프링 부트가 제공하는 자동 구성 기능을 이해하려면 다음 두 가지 개념을 이해해야 한다.
@Conditional : 특정 조건에 맞을 때 설정이 동작하도록 한다.
@AutoConfiguration : 자동 구성이 어떻게 동작하는지 내부 원리 이해
자동 구성 직접 만들기 - 기반 예제
자동 구성에 대해서 자세히 알아보기 위해 간단한 예제를 만들어보자.
실시간으로 자바 메모리 사용량을 웹으로 확인하는 예제이다.
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
@Slf4j
public class MemoryFinder {
public Memory get() {
long max = Runtime.getRuntime().maxMemory();// JVM이 사용할 수 있는 최대 메모리
long total = Runtime.getRuntime().totalMemory();// JVM이 확보한 전체 메모리
long free = Runtime.getRuntime().freeMemory(); // JVM이 확보한 전체 메모리중에 사용하지 않은것 ( total중에 사용하지않은 메모리)
long used = total - free; // JVM이 사용중인 메모리
return new Memory(used, max);
}
@PostConstruct //빈으로 등록 되고 난뒤 로그를 남기기 위함
public void init() { // 모든빈이 초기화되고 어플리케이션이 준비가 완료된 상태를 원한다면 @EventListener(ApplicationReadyEvent.class)를 사용
log.info("init memoryFinder");
}
}
JVM에서 메모리 정보를 실시간으로 조회하는 기능이다.
max 는 JVM이 사용할 수 있는 최대 메모리, 이 수치를 넘어가면 OOM이 발생한다.
total 은 JVM이 확보한 전체 메모리(JVM은 처음부터 max 까지 다 확보하지 않고 필요할 때 마다 조금 씩 확보한다.)
free 는 total 중에 사용하지 않은 메모리(JVM이 확보한 전체 메모리 중에 사용하지 않은 것)
used 는 JVM이 사용중인 메모리이다. ( used = total - free )
모든빈이 초기화되고 어플리케이션이 준비가 완료된 상태를 원한다면 @PostContruct 대신
@EventListener(ApplicationReadyEvent.class)를 사용한다.
MemoryController
@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;
}
}
메모리 정보를 조회하는 컨트롤러이다.
앞서 만든 memoryFinder 를 주입 받아 사용한다
MemoryConfig
@Configuration
public class MemoryConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
현재 Hello와 Memory를 멀티모듈처럼 각각 구성했다.
hello와 memory를 같은 선상에 디렉토리를 만들어서
AutoConfigApplication은 자동컴포넌트스캔할때 자신의 위치 이하를 스캔하니까
memoryController,memoryFinder는 자동빈등록이 안된다
그래서 MemoryConfig를 통해 수동빈 등록했다.
간단하게 메모리 사용량을 실시간으로 확인할 수 있다
패키지 위치
패키지를 이렇게 나눈 이유는, memory 라는 완전히 별도의 모듈이 있고,
hello에서 memory의 기능을 불러다 사용한다고 이해하면 된다.
@Conditional
앞서 만든 메모리 조회 기능을 항상 사용하는 것이 아니라 특정 조건일 때만 해당 기능이 활성화 되도록 해 보자.
예를 들어서 개발 서버에서 확인 용도로만 해당 기능을 사용하고, 운영 서버에서는 해당 기능을 사용하지않는 것이다.
여기서 핵심은 소스코드를 고치지 않고 이런 것이 가능해야 한다는 점이다.
프로젝트를 빌드해서 나온 빌드 파일을 개발 서버에도 배포하고, 같은 파일을 운영서버에도 배포해야 한다
같은 소스 코드인데 특정 상황일 때만 특정 빈들을 등록해서 사용하도록 도와주는 기능이 바로 @Conditional이다.
참고로 이 기능은 스프링 부트 자동 구성에서 자주 사용한다.
지금부터 @Conditional 에 대해서 자세히 알아보자.
이름 그대로 특정 조건을 만족하는가 하지 않는가를 구별하는 기능이다.
이 기능을 사용하려면 먼저 Condition 인터페이스를 구현해야 한다. 그전에 잠깐 Condition 인터페이스를 살펴 보자
package org.springframework.context.annotation;
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
matches() 메서드가 true를 반환하면 조건에 만족해서 동작하고, false를 반환하면 동작하지 않는다.
ConditionContext : 스프링 컨테이너, 환경 정보등을 담고 있다.
AnnotatedTypeMetadata : 애노테이션 메타 정보를 담고 있다.
Condition 인터페이스를 구현해서
다음과 같이 자바 시스템 속성이 memory=on 이라고 되어 있을 때만 메모리 기능이 동작하도록 만들어보자.
[시스템 속성 ,환경 정보, 환경 설정 ]
-D[속성이름]=[속성값]
#VM Options
#java -Dmemory=on -jar project.jar
MemoryCondition
@Slf4j
public class MemoryCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// -Dmemory=on 으로 시스템 프로퍼티가 들어오면 아래로 값이 들어간다.
String memory = context.getEnvironment().getProperty("memory");
log.info("memory={}", memory);
return "on".equals(memory);
}
}
환경 정보에 memory=on 이라고 되어 있는 경우에만 true 를 반환한다
MemoryConfig - 수정
@Configuration
@Conditional(MemoryCondition.class)
public class MemoryConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
@Conditional(MemoryCondition.class)
이제 MemoryConfig 의 적용 여부는 @Conditional 에 지정한 MemoryCondition 의 조건에 따라 달라진다.
MemoryCondition 의 matches() 를 실행해보고 그 결과가 true이면 MemoryConfig 는 정상 동작한다.
따라서 memoryController , memoryFinder 가 빈으로 등록된다.
MemoryCondition 의 실행결과가 false 이면 MemoryConfig 는 무효화 된다.
그래서 memoryController , memoryFinder 빈은 등록되지 않는다.
memory=on 조건을 주고 실행해보자.
VM 옵션을 추가하는 경우 -Dmemory=on 를 사용해야 한다
스프링이 로딩되는 과정은 복잡해서 MemoryCondition 이 여러번 호출될 수 있다.
이 부분은 크게 중요하지 않으니 무시하자.
참고
스프링은 외부 설정을 추상화해서 Environment로 통합했다.
그래서 다음과 같은 다양한 외부 환경 설정을 Environment 하나로 읽어들일 수 있다.
여기에 대한 더 자세한 내용은 뒤에서 다룬다
<VM Options>
java -Dmemory=on -jar project.jar
-Dmemory=on
<Program arguments>
-- 가 있으면 스프링이 환경 정보로 사용
java -jar project.jar --memory=on
--memory=on
<application.properties>
application.properties에 있으면 환경 정보로 사용
memory=on
@Conditional - 다양한 기능
지금까지 Condition 인터페이스를 직접 구현해서 MemoryCondition 이라는 구현체를 만들었다.
스프링은 이미 필요한 대부분의 구현체를 만들어두었다. 이번에는 스프링이 제공하는 편리한 기능을 사용해보자.
MemoryConfig - 수정
@Configuration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
@ConditionalOnProperty(name = "memory", havingValue = "on")
환경 정보가 memory=on 이라는 조건에 맞으면 동작하고, 그렇지 않으면 동작하지 않는다.
우리가 앞서 만든 기능과 동일하다.
@ConditionalOnProperty
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional({OnPropertyCondition.class})
public @interface ConditionalOnProperty {
String[] value() default {};
String prefix() default "";
String[] name() default {};
String havingValue() default "";
boolean matchIfMissing() default false;
}
@ConditionalOnProperty 도 우리가 만든 것과 동일하게 내부에는 @Conditional을 사용한다.
그리고 그 안에 Condition 인터페이스를 구현한 OnPropertyCondition 를 가지고 있다
@ConditionalOnXxx
스프링은 @Conditional 과 관련해서 개발자가 편리하게 사용할 수 있도록 수 많은 @ConditionalOnXxx 를 제공한다.
대표적인 몇가지를 알아보자.
@ConditionalOnClass , @ConditionalOnMissingClass
클래스가 있는 경우 동작한다. 나머지는 그 반대
@ConditionalOnBean , @ConditionalOnMissingBean
빈이 등록되어 있는 경우 동작한다. 나머지는 그 반대
@ConditionalOnProperty
환경 정보가 있는 경우 동작한다.
@ConditionalOnResource
리소스가 있는 경우 동작한다.
@ConditionalOnWebApplication , @ConditionalOnNotWebApplication
웹 애플리케이션인 경우 동작한다.
@ConditionalOnExpression
SpEL 표현식에 만족하는 경우 동작한다.
ConditionalOnXxx 공식 메뉴얼
이름이 직관적이어서 바로 이해가 될 것이다.
@ConditionalOnXxx 는 주로 스프링 부트 자동 구성에 사용된다.
다음 자동 구성 클래스들을 열어서 소스 코드를 확인해보면
@ConditionalOnXxx 가 아주 많이 사용되는 것을 확인 할 수 있다.
참고
@Conditional 자체는 스프링 부트가 아니라 스프링 프레임워크의 기능이다.
스프링 부트는 이 기능을 확장해서 @ConditionalOnXxx 를 제공한다.
스프링 부트가 제공하는 자동 구성 기능을 이해하려면 다음 개념을 이해해야 한다.
@Conditional : 특정 조건에 맞을 때 설정이 동작하도록 한다.
@AutoConfiguration : 자동 구성이 어떻게 동작하는지 내부 원리 이해
지금까지 @Conditional 에 대해서 알아보았으니, 지금부터는 @AutoConfiguration 을 알아보자
순수 라이브러리 만들기
@AutoConfiguration을 이해하기 위해서는 그 전에 먼저 라이브러리가 어떻게 사용되는지 이해하는 것이 필요하다.
여러분이 만든 실시간 자바 Memory 조회 기능이 좋다고 소문이 나서, 여러 프로젝트에서 사용하고 싶어한다.
이 기능을 여러곳에서 사용할 수 있도록 라이브러리로 만들어보자.
참고로 라이브러리를 만들 때는 스프링 부트 플러그인 기능을 사용하지 않고 진행한다.
현재 build.gradle
plugins {
id 'java'
}
group = 'memory'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.0.2'
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.2'
}
test {
useJUnitPlatform()
}
스프링 부트 플러그인을 사용하게 되면 앞서 설명한 실행 가능한 Jar 구조를 기본으로 만든다.
[ 스프링부트는 어플리케이션이 실행되기 위한 모든 라이브러리를 jar안에 포함시키고 메인메소드 하나로 어플리케이션이 동작될수 있게 패키징한다. -> 톰캣과 같은 라이브러리도 포함되고, projectApplication 같은 메인 메소드가 있는 클래스도 포함되고, META-INF/MANIFEST.MF 파일에 어떤 메인 메소드를 실행시켜야하는지도 포함하므로 스프링부트가 적용된 jar 파일은 실행이 가능하다.]
여기서는 실행 가능한 Jar가 아니라, 다른곳에 포함되어서 사용할 순수 라이브러리 Jar를 만드는 것이 목적이므로
스프링 부트 플러그인을 사용하지 않았다.
스프링 컨트롤러가 필요하므로 spring-boot-starter-web 라이브러리를 선택했다.
스프링 부트 플러그인을 사용하지 않아서 버전을 직접 명시했다.
앞서 개발한 것과 같은 실시간 메모리 조회 기능을 추가하자
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 +
'}';
}
}
MemoryFinder
@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");
}
}
MemoryController
@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;
}
}
MemoryFinderTest
class MemoryFinderTest {
@Test
void get() {
MemoryFinder memoryFinder = new MemoryFinder();
Memory memory = memoryFinder.get();
System.out.println("memory = " + memory);
assertThat(memory).isNotNull();
}
}
간단한 테스트를 통해서 데이터가 조회 되는지 정도만 간단히 검증
빌드하기
./gradlew clean build
해당 명령어로 빌드
build/libs/memory-v1.jar
빌드된 파일 경로 [ 빌드 결과 ]
jar -xvf memory-v1.jar
빌드된 파일 압축 풀어서 확인해보기
memory-v1.jar 는 스스로 동작하지는 못하고 다른 곳에 포함되어서 동작하는 라이브러리이다.
이제 이 라이브러리를 다른 곳에서 사용해보자
순수 라이브러리 사용하기1
다른 프로젝트[ project-v1 ]를 하나 더 실행시키고 테스트용 컨트롤러를 추가한다.
HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
순수 라이브러리 사용하기2
앞서 만든 memory-v1.jar 라이브러리를 project-v1 에 적용해보자
project-v1/libs 폴더를 생성하자.
memory-v1 프로젝트에서 빌드한 memory-v1.jar 를 이곳에 복사하자.
project-v1/build.gradle 에 memory-v1.jar 를 추가하자.
dependencies {
....
implementation files('libs/memory-v1.jar') // 라이브러리 의존성 추가
}
라이브러리를 jar 파일로 직접 가지고 있으면 files 로 지정하면 된다.
gradle을 리로드하자.
라이브러리 설정
라이브러리를 스프링 빈으로 등록해서 동작하도록 만들어보자.
@Configuration
public class MemoryConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
스프링 부트 자동 구성을 사용하는 것이 아니기 때문에 빈을 직접 하나하나 등록해주어야 한다
스프링 부트는 외부 라이브러리나 자체 제공 라이브러리의 클래스를 자동으로 빈으로 등록하는 기능을 제공합니다. 이를 위해 스프링 부트는 @EnableAutoConfiguration과 @Configuration 같은 어노테이션을 활용하며, 내부적으로는 자동 구성(Auto Configuration) 메커니즘을 사용합니다.
스프링 부트의 자동 빈 등록 과정
- 라이브러리 클래스의 @ConfigurationProperties 및 @ConditionalOnClass, @ConditionalOnMissingBean 등의 어노테이션
스프링 부트는 종종 라이브러리의 특정 설정 파일이나 조건부 설정을 사용합니다. 예를 들어, 라이브러리의 클래스에 @Configuration 또는 @ConditionalOnClass 같은 어노테이션이 붙어 있으면 해당 라이브러리가 프로젝트에 포함된 상태에서만 특정 빈을 등록하도록 합니다. - @EnableAutoConfiguration과 META-INF/spring.factories 파일
스프링 부트는 @EnableAutoConfiguration을 통해 애플리케이션 시작 시 자동 구성 클래스를 로드합니다. 자동 구성 클래스들은 보통 라이브러리 내부에 정의된 META-INF/spring.factories 파일에 나열되어 있습니다. 예를 들어, spring-boot-starter-data-jpa 라이브러리를 추가하면 HibernateJpaAutoConfiguration과 같은 여러 자동 구성 클래스가 로드되어 관련 빈들이 자동으로 등록됩니다. - 빈 생성 조건 처리
많은 자동 구성 클래스들은 @ConditionalOnProperty, @ConditionalOnClass, @ConditionalOnBean 등의 조건부 어노테이션을 사용하여 빈을 등록할지 말지를 결정합니다. 예를 들어, 특정 클래스가 클래스패스에 존재하면 관련 빈을 자동으로 등록하고, 특정 설정이 누락되어 있으면 기본 설정으로 동작하게 됩니다.
예시: spring-boot-starter-data-jpa의 자동 구성
spring-boot-starter-data-jpa를 예로 들어 보면, 이 라이브러리를 추가하는 것만으로도 EntityManager, DataSource, JpaTransactionManager 같은 JPA 관련 빈들이 자동으로 등록됩니다. 스프링 부트는 이를 위해 JpaRepositoriesAutoConfiguration과 HibernateJpaAutoConfiguration 같은 자동 구성 클래스를 제공하며, 이 클래스들은 조건부 설정을 기반으로 필요한 빈을 등록합니다.
요약
스프링 부트는 라이브러리 클래스에 자동 구성 어노테이션들이 정의되어 있으면 이를 인식해 필요한 빈을 자동으로 등록합니다. 이 덕분에 개발자는 설정에 대한 부담을 덜고 필요한 의존성만 추가하면 자동으로 설정되는 이점을 누릴 수 있습니다.
메모리 조회 기능이 잘 동작하는지 확인해보자
라이브러리에 있는 클래스를 빈등록 해서 사용한 결과
메모리 조회 라이브러리 [memory-v1]이 잘 동작하는걸 확인할 수 있다.
외부 라이브러리를 직접 만들고 또 그것을 프로젝트에 라이브러리로 불러서 적용해보았다.
그런데 라이브러리를 사용하는 클라이언트 개발자 입장을 생각해보면,
라이브러리 내부에 있는 어떤 빈을 등록해야하는지 알아야 하고, 그것을 또 하나하나 빈으로 등록해야 한다.
지금처럼 간단한 라이브러리가 아니라 초기 설정이 복잡하다면 사용자 입장에서는 상당히 귀찮은 작업이 될 수 있다.
이런 부분을 자동으로 처리해주는 것이 바로 스프링 부트 자동 구성(Auto Configuration)이다.
자동 구성 라이브러리 만들기
우리가 만든 라이브러리를 사용해주는 고마운 고객 개발자를 위해,
프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리되도록 해보자.
쉽게 이야기해서 스프링 빈들이 자동으로 등록되는 것이다.
여기에 추가로 memory=on 옵션도 적용할 수 있게 해보자.
이렇게 하려면 메모리 라이브러리의 기능을 업그레이드 해야한다
프로젝트 복사시 주의할점
settings.gradle 수정
rootProject.name = 'memory-v2'
// 프로젝트 이름 변경
자동 구성 추가
MemoryAutoConfig
@AutoConfiguration
@ConditionalOnProperty(name = "memory",havingValue = "on")
public class MemoryAutoConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
자동구성이 되게끔 원하는 라이브러리 프로젝트 안에 만들어야한다.
@AutoConfiguration
스프링 부트가 제공하는 자동 구성 기능을 적용할 때 사용하는 애노테이션이다.
@ConditionalOnProperty
memory=on 이라는 환경 정보가 있을 때 라이브러리를 적용한다. (스프링 빈을 등록한다.)
라이브러리를 가지고 있더라도 상황에 따라서 해당 기능을 켜고 끌 수 있게 유연한 기능을 제공한다.
자동 구성 대상 지정
이 부분이 중요하다. 스프링 부트 자동 구성을 적용하려면, 다음 파일에 자동 구성 대상을 꼭 지정해주어야 한다.
폴더 위치와 파일 이름이 길기 때문에 주의하자
파일 생성
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
[해당 파일안에 memory.MemoryAutoConfig라고 AutoConfig 설정파일 경로를 적어주면 된다.]
앞서 만든 자동 구성인 memory.MemoryAutoConfig 를 패키지를 포함해서 지정해준다.
스프링 부트는 시작 시점에
org.springframework.boot.autoconfigure.AutoConfiguration.imports 의 정보를 읽어서 자동 구성으로 사용한다.
따라서 내부에 있는 MemoryAutoConfig 가 자동으로 실행된다.
빌드하기
다음 명령어로 빌드하자.
./gradlew clean build
빌드 결과
build/libs/memory-v2.jar
스프링 부트 자동 구성 기능이 포함된 memory-v2.jar 를 이제 프로젝트에 적용해보자
자동 구성 라이브러리 사용하기1
기존 프로젝트를 유지하기 위해 새로운 프로젝트에 자동 구성 라이브러리를 적용해보자.
똑같이 libs 디렉토리를 만든후 라이브러리 jar파일을 옮겨놓는다.
그리고 build.gradle에 의존성 추가 후
dependencies {
...
implementation files('libs/memory-v2.jar')
}
환경변수 추가해서
실행하면
수동 빈 등록할 필요없이 서버가 실행될때 알아서 라이브러리의 자동 빈 등록이 됬다.
[자동 구성 라이브러리 동작 확인 ]
memory=on 조건을 끄면 라이브러리를 사용하지 않는 것도 확인할 수 있다
스프링 부트가 제공하는 자동 구성 덕분에
복잡한 빈 등록이나 추가 설정 없이 단순하게 라이브러리의 추가만으로 프로젝트를 편리하게 구성할 수 있다.
@ConditionalOnXxx 덕분에 라이브러리 설정을 유연하게 제공할 수 있다.
[ 라이브러리를 등록하면 자동으로 사용될수도있기때문에 해당 기능으로 인해 라이브러리를 직접 빼지않고
사용할지말지 선택가능 ]
스프링 부트는 수 많은 자동 구성을 제공한다.
그 덕분에 스프링 라이브러리를 포함해서 수 많은 라이브러리를 편리하게 사용할 수 있다.
자동 구성 이해1 - 스프링 부트의 동작
스프링 부트는 다음 경로에 있는 파일을 읽어서 스프링 부트 자동 구성으로 사용한다.
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
우리가 직접 만든 memory-v2 라이브러리와
스프링 부트가 제공하는 spring-boot-autoconfigure 라이브러리의다음 파일을 확인해보면
스프링 부트 자동 구성을 확인할 수 있다.
스프링부트가 제공하는 자동 구성라이브러리를 보면 우리가 설정한 파일이 동일하게 존재하고
그안에는 스프링부트가 제공하는 자동구성 클래스들이 존재하는것을 확인할 수 있다.
그중 하나 클래스를 살펴보면
@AutoConfiguration
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"auto"},
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class ClassProxyingConfiguration {
ClassProxyingConfiguration() {
}
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry registry) {
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
AspectJAutoProxyingConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = true
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class CglibAutoProxyConfiguration {
CglibAutoProxyConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "false"
)
static class JdkDynamicAutoProxyConfiguration {
JdkDynamicAutoProxyConfiguration() {
}
}
}
}
이런식으로 우리가 했던것 처럼 @AutoConfiguration, @ConditionalOnProperty를 이용하여 구성되어있다.
이번에는 스프링 부트가 어떤 방법으로 해당 파일들을 읽어서 동작하는지 알아보자.
이해를 돕기 위해 앞서 개발한 autoconfig 프로젝트를 열어보자
스프링 부트 자동 구성이 동작하는 원리는 다음 순서로 확인할 수 있다
@SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)
스프링 부트는 보통 다음과 같은 방법으로 실행한다
@SpringBootApplication
public class AutoConfigApplication {
public static void main(String[] args) {
SpringApplication.run(AutoConfigApplication.class, args);
}
}
run() 에 보면 AutoConfigApplication.class 를 넘겨주는데, 이 클래스를 설정 정보로 사용한다는 뜻이다.
AutoConfigApplication 에는 @SpringBootApplication 애노테이션이 있는데, 여기에 중요한 설정 정보들이 들어있다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@SpringBootApplication 어노테이션 안에는
@EnableAutoConfiguration
가 있는데 이게 자동 구성을 활성화 하는 기능을 제공한다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
@Import 는 주로 스프링 설정 정보( @Configuration )를 포함할 때 사용한다.
그런데 AutoConfigurationImportSelector 를 열어보면 @Configuration가 아니다.
이 기능을 이해하려면 ImportSelector에 대해 알아야 한다
자동 구성 이해2 - ImportSelector
@Import 에 설정 정보를 추가하는 방법은 2가지가 있다.
정적인 방법
@Import (클래스) 이것은 정적이다.
코드에 대상이 딱 박혀 있다.
설정으로 사용할 대상을 동적으로 변경할 수 없다.
동적인 방법
@Import ( ImportSelector ) 코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다.
ImportSelector를 구현한 클래스를 지정해서 동적으로 사용할 대상을 선택가능하다.
정적인 방법
스프링에서 다른 설정 정보를 추가하고 싶으면 다음과 같이 @Import 를 사용하면 된다
@Configuration
@Import({AConfig.class, BConfig.class})
public class AppConfig {...}
그런데 예제처럼 AConfig , BConfig 가 코드에 딱 정해진 것이 아니라,
특정 조건에 따라서 설정 정보를 선택해야 하는 경우에는 어떻게 해야할까?
동적인 방법
스프링은 설정 정보 대상을 동적으로 선택할 수 있는 ImportSelector 인터페이스를 제공한다.
ImportSelector
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
간단하게 ImportSelector 를 사용하는 예제를 만들어보자
ImportSelector 예제
HelloBean
public class HelloBean {
}
빈으로 등록할 대상이다.
HelloConfig
@Configuration
public class HelloConfig {
@Bean
public HelloBean helloBean() {
return new HelloBean();
}
}
설정 정보이다. HelloBean 을 스프링 빈으로 등록한다
HelloImportSelector
public class HelloImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"hello.selector.HelloConfig"};
}
}
설정 정보를 동적으로 선택할 수 있게 해주는 ImportSelector 인터페이스를 구현했다.
여기서는 단순히 hello.selector.HelloConfig 설정 정보를 반환한다.
[ 원하는 대로 코드를 짜서 동적으로 반환할 수 있다.
]
이렇게 반환된 설정 정보는 선택되어서 사용된다.
여기에 설정 정보로 사용할 클래스를 동적으로 프로그래밍 하면 된다
Claude
@EnableAutoConfiguration 내부의
@Import(AutoConfigurationImportSelector.class)가 자동구성의 핵심 메커니즘입니다.
동작 과정을 상세히 설명하면:
1. @EnableAutoConfiguration의 구조
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) // 핵심
public @interface EnableAutoConfiguration {
// ...
}
2. AutoConfigurationImportSelector의 주요 로직
public class AutoConfigurationImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 1. spring.factories 파일들을 모두 로드
List<String> configurations = getCandidateConfigurations(annotationMetadata);
// 2. 중복 제거
configurations = removeDuplicates(configurations);
// 3. 제외할 설정 제거
configurations.removeAll(getExclusions(annotationMetadata));
// 4. 필터링 (조건부 어노테이션 검사)
configurations = filter(configurations);
return configurations.toArray(new String[0]);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata) {
// META-INF/spring.factories 파일에서 구성 정보 로드
return SpringFactoriesLoader.loadFactoryNames(
EnableAutoConfiguration.class,
getBeanClassLoader());
}
}
3. 실제 spring.factories 예시
![](https://blog.kakaocdn.net/dn/diQzY2/btsKBRcczL6/13Koe4lcqnYZ6u8M7KO5W1/img.png)
META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
4. 자동구성 필터링 과정
@Configuration
@ConditionalOnClass(DataSource.class) // 클래스패스에 DataSource가 있어야 함
@ConditionalOnProperty(
prefix = "spring.datasource",
name = "enabled",
matchIfMissing = true // 프로퍼티가 없으면 true로 간주
)
public class DataSourceAutoConfiguration {
// 각종 조건을 검사하여 설정 클래스의 포함 여부를 결정
}
5. 우선순위 처리
@AutoConfiguration // 스프링 부트 3.0부터
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class HighPriorityConfiguration {
// 먼저 처리되어야 하는 설정
}
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class) // 특정 설정 이후에 처리
public class JpaConfiguration {
// DataSource 설정 이후에 처리되어야 하는 JPA 설정
}
6. 제외 설정
@SpringBootApplication(
exclude = {DataSourceAutoConfiguration.class}, // 특정 자동 구성 제외
excludeName = {"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration"} // 클래스 이름으로 제외
)
public class Application {
}
7. 디버깅 지원
application.properties/yml
debug=true # 자동 구성 리포트 출력
실행 순서
1. @SpringBootApplication -> @EnableAutoConfiguration 발견
2. @Import(AutoConfigurationImportSelector.class) 처리
3. AutoConfigurationImportSelector가:
- spring.factories 파일들을 모두 로드 -> 스프링 부트 3.0 이상부터는 AutoConfiguration.imports
- 후보 설정 클래스들을 수집
- 중복 제거
- 제외 설정 적용
- 조건 평가 (@Conditional...)
4. 필터링된 설정 클래스들을 우선순위에 따라 정렬
5. 최종 선택된 설정 클래스들을 스프링 컨테이너에 등록
이 메커니즘을 통해 스프링 부트는:
- 클래스패스에 있는 라이브러리들을 자동으로 감지
- 필요한 설정만 선택적으로 활성화
- 설정 간의 의존관계를 관리
- 유연한 확장성 제공
이러한 자동구성 메커니즘은 스프링 부트의 "convention over configuration" (설정보다 관례) 철학을 잘 구현한 예시입니다.
스프링 부트 3.0 이전 버전에서는 spring.factories 파일을 사용해 자동 구성 클래스를 지정했고,
스프링 부트 3.0 이상부터는 AutoConfiguration.imports 파일이 그 역할을 담당합니다.
@Import는 지정된 클래스를 스프링 컨텍스트에 등록한다.
아래는 예시
설정클래스
// 가져올 설정 클래스
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
// Import 사용
@Configuration
@Import(DatabaseConfig.class) // DatabaseConfig의 모든 @Bean 메서드가 빈으로 등록됨, DatabaseConfig또한 빈등록됨
public class AppConfig {
// ...
}
일반클래스
// 일반 클래스
public class MyService {
// ...
}
@Configuration
@Import(MyService.class) // MyService 자체가 빈으로 등록됨
public class AppConfig {
// ...
}
ImportSelector의 구현체 등
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"com.example.ServiceA",
"com.example.ServiceB"
};
}
}
@Configuration
@Import(MyImportSelector.class) // ServiceA, ServiceB가 빈으로 등록됨
public class AppConfig {
// ...
}
다시 돌아가서 테스트를 진행해본다.
public class ImportSelectorTest {
@Test
void staticConfig() {
AnnotationConfigApplicationContext appContext =
new AnnotationConfigApplicationContext(StaticConfig.class);
// 스프링 컨테이너 생성할때 StaticConfig.class를 구성[설정]클래스로 사용한다.
HelloBean bean = appContext.getBean(HelloBean.class);
Assertions.assertThat(bean).isNotNull();
}
@Configuration
@Import(HelloConfig.class) // @Import는 지정된 클래스를 스프링 컨텍스트에 등록한다.
public static class StaticConfig {
}
}
@import로 인해 helloConfig.class가 스프링 컨텍스트에 등록되면서
helloConfig안의 @Bean 메소드가 동작해서 HelloBean의 수동 빈 등록이 진행된다.
public class ImportSelectorTest {
@Test
void staticConfig() {
AnnotationConfigApplicationContext appContext =
new AnnotationConfigApplicationContext(StaticConfig.class);
// 스프링 컨테이너 생성할때 StaticConfig.class를 구성[설정]클래스로 사용한다.
HelloBean bean = appContext.getBean(HelloBean.class);
Assertions.assertThat(bean).isNotNull();
}
@Test
void selectorConfig() {
AnnotationConfigApplicationContext appContext =
new AnnotationConfigApplicationContext(SelectorConfig.class);
HelloBean bean = appContext.getBean(HelloBean.class);
Assertions.assertThat(bean).isNotNull();
}
@Configuration
@Import(HelloConfig.class) // @Import는 지정된 클래스를 스프링 컨텍스트에 등록한다.
public static class StaticConfig {
}
@Configuration
@Import(HelloImportSelector.class)
public static class SelectorConfig {
// @Import에는 셀렉터 구현체도 넣을 수 있다. selectImports 메소드의 결과를 설정정보로 사용한다.
}
}
staticConfig()
staticConfig() 는 이해하는데 어려움이 없을 것이다.
스프링 컨테이너를 만들고, StaticConfig.class를 초기 설정 정보로 사용했다.
그 결과 HelloBean 이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있다.
selectorConfig()
selectorConfig()는 SelectorConfig를 초기 설정 정보로 사용한다.
SelectorConfig는
@Import(HelloImportSelector.class) 에서 ImportSelector 의 구현체인 HelloImportSelector를 사용했다.
스프링은 HelloImportSelector를 실행하고,
selectImports()의 리턴값인
"hello.selector.HelloConfig" 라는 문자를 반환 받는다.
스프링은 이 문자에 맞는 대상을 설정 정보로 사용한다.
[예시는 다음과 같이 하나만 반환하지만 스프링부트는 AutoConfiguration.imports파일안에 있는 내용을 가져와서 설정정보로 사용한다. ]
따라서 hello.selector.HelloConfig이 설정 정보로 사용된다.
그 결과 HelloBean 이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있다.
스프링부트는 내부동작으로 ImportSelector 구현체에서
org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일의 내용을 읽어와서
설정 정보로 사용한다.
@EnableAutoConfiguration 동작 방식
이제 ImportSelector 를 이해했으니 다음 코드를 이해할 수 있다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
AutoConfigurationImportSelector 는 ImportSelector 의 구현체이다.
따라서 설정 정보를 동적으로 선택할 수 있다.
실제로 이 코드는 모든 라이브러리에 있는 다음 경로의 파일을 확인한다.
META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports
그리고 파일의 내용을 읽어서 설정 정보로 선택한다
스프링 부트 자동 구성이 동작하는 방식은 다음 순서로 확인할 수 있다
@SpringBootApplication ->
@EnableAutoConfiguration ->
@Import(AutoConfigurationImportSelector.class)->
resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports
파일을 열어서 설정 정보 선택
해당 파일의 설정 정보가 스프링 컨테이너에 등록되고 사용
memory-v2 라이브러리의 AutoConfiguration.imports 내용을 불러와서 설정정보로 사용하려는것을 확인할 수 있다.
밑에 있는건 spring boot autoconfigure 라이브러리의 AutoConfiguration.imports 내용이다.
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).getCandidates();
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
getCandidateConfigurations() 메소드를 따라가보면 load 메소드가 보이고 AutoConfiguration.class를 전달인자로 주고있다.
즉 모든 라이브러리를 돌면서 org.springframework.boot.autoconfigure.AutoConfiguration.imports를 찾고
그 파일안의 내용[구성 정보 파일 위치]을 가져온다.
가져온 구성정보 파일 위치를 이용하여 자동 구성 설정을 진행한다.
스프링 부트는 자동 구성 설정을 위해 클래스패스에 존재하는 모든 라이브러리 JAR 파일을 스캔하면서 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 찾습니다. 이 파일이 발견되면, 그 안에 나열된 자동 구성 클래스들을 자동 구성 후보로 수집하여 설정을 진행합니다.
동작 과정은 다음과 같습니다:
- AutoConfiguration.imports 파일 스캔
스프링 부트는 클래스패스에 있는 모든 라이브러리 JAR에서 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 찾습니다. 이 파일에는 자동으로 설정할 자동 구성 클래스들이 나열되어 있습니다. - 자동 구성 클래스 목록 수집
발견된 모든 AutoConfiguration.imports 파일의 내용을 합쳐서 하나의 자동 구성 클래스 목록을 만듭니다. 이 과정에서 중복된 항목은 제거됩니다. - 제외 설정 및 조건부 필터링
spring.autoconfigure.exclude 프로퍼티나 @EnableAutoConfiguration(exclude = ...) 설정을 통해 제외할 클래스들을 필터링한 뒤, 남은 후보 클래스들에 대해 조건부 어노테이션을 평가합니다. 예를 들어, @ConditionalOnClass 또는 @ConditionalOnProperty 어노테이션이 붙어 있는 클래스는 해당 조건을 만족할 때만 자동 구성에 포함됩니다. - 자동 구성 클래스 적용
최종적으로 조건을 만족하는 클래스들은 스프링 컨텍스트에 로드되고, 각 클래스가 정의한 빈들이 컨텍스트에 등록됩니다.
요약
스프링 부트는 클래스패스에 존재하는 모든 라이브러리의 AutoConfiguration.imports 파일을 찾아 자동 구성 클래스를 수집한 후, 조건에 따라 필터링하고 남은 클래스들을 자동 구성으로 등록합니다. 이를 통해 필요한 설정을 자동으로 구성하며, 사용자는 설정 파일을 통해 제외할 구성 요소만 지정하면 됩니다.
스프링 부트의 자동 구성을 직접 만들어서 사용할 때는 다음을 참고하자.
@AutoConfiguration 에 자동 구성의 순서를 지정할 수 있다.
@AutoConfiguration 도 설정 파일이다.
내부에 @Configuration 이 있는 것을 확인할 수 있다.
하지만 일반 스프링 설정과 라이프사이클이 다르기 때문에 컴포넌트 스캔의 대상이 되면 안된다.
파일에 지정해서 사용해야 한다.
resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports
그래서 스프링 부트가 제공하는 컴포넌트 스캔에서는 @AutoConfiguration을 제외하는
AutoConfigurationExcludeFilter 필터가 포함되어 있다.
@SpringBootApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
컴포넌트스캔안에 @AutoConfiguration를 제외하는 필터를 추가하는것을 볼 수 있다.
자동 구성이 내부에서 컴포넌트 스캔을 사용하면 안된다. 대신에 자동 구성 내부에서 @Import 는 사용할 수 있다.
@AutoConfiguration
@ComponentScan
@ConditionalOnProperty(name = "memory",havingValue = "on")
public class MemoryAutoConfig {
이렇게 @Autoconfiguration이 있는 자동설정 클래스에서 컴포넌트 스캔을 사용하면 안된다는뜻
@AutoConfiguration 클래스에서 @ComponentScan을 사용하면 안 되는 이유
스프링 부트에서 @AutoConfiguration 클래스는 이미 자동으로 빈을 등록하는 방식을 사용하기 때문에, @ComponentScan을 추가하면 빈 등록 방식이 충돌할 수 있습니다. 구체적으로 다음과 같은 문제들이 발생할 수 있습니다:
- 이중 빈 등록: @AutoConfiguration 클래스 내에서 @ComponentScan을 사용하면, 자동 구성을 담당하는 클래스가 이미 스프링 컨텍스트에 빈을 등록하려고 시도합니다. 만약 해당 자동 구성 클래스가 이미 자동으로 등록된 빈들과 중복되는 빈을 등록하려고 하면, 충돌이 발생할 수 있습니다.
- 빈 등록 시점의 중복: 자동 구성 클래스는 스프링 부트의 자동 구성 시스템에 의해 관리됩니다. 즉, 애플리케이션이 시작될 때 자동으로 필요한 빈들이 등록되는데, @ComponentScan을 추가하면 스프링 부트의 자동 구성 시스템과 일반적인 빈 등록 방식이 충돌할 수 있습니다. 자동 구성을 사용하는 클래스는 @ComponentScan을 사용하여 애플리케이션 전역의 빈을 스캔하려는 의도가 아니라, 이미 필요한 빈들을 스프링 부트의 자동 구성을 통해 관리하려는 목적입니다.
@Import는 사용 가능
자동 구성 클래스 내에서 @Import는 사용 가능합니다. **@Import**는 다른 구성 클래스를 명시적으로 포함시키는 방식으로, 컴포넌트 스캔과는 다른 방식입니다. @Import를 사용하면 명시적으로 설정한 클래스를 등록할 수 있기 때문에, @ComponentScan과는 다르게 빈 등록이 충돌하지 않습니다.
자동 구성을 언제 사용하는가?
AutoConfiguration은 라이브러리를 만들어서 제공할 때 사용하고, 그 외에는 사용하는 일이 거의 없다.
왜냐하면 보통 필요한 빈들을 컴포넌트 스캔하거나 직접 등록하기 때문이다.
하지만 라이브러리를 만들어서 제공할 때는 자동 구성이 유용하다.
실제로 다양한 외부 라이브러리들이 자동 구성을 함께 제공한다.
보통 이미 만들어진 라이브러리를 가져다 사용하지, 반대로 라이브러리를 만들어서 제공하는 경우는 매우 드물다.
그럼 자동 구성은 왜 알아두어야 할까?
자동 구성을 알아야 하는 진짜 이유는
개발을 진행 하다보면 사용하는 특정 빈들이 어떻게 등록된 것인지 확인이 필요할 때가 있다.
이럴 때 스프링 부트의 자동 구성 코드를 읽을 수 있어야 한다.
그래야 문제가 발생했을 때 대처가 가능하다.
자동화는 매우 편리한 기능이지만 자동화만 믿고 있다가 실무에서 문제가 발생했을 때는
파고 들어가서 문제를 확인하는 정도는 이해해야 한다.
이번에 학습한 정도면 자동 구성 코드를 읽는데 큰 어려움은 없을 것이다.
남은 문제
그런데 이런 방식으로 빈이 자동 등록되면, 빈을 등록할 때 사용하는 설정 정보는 어떻게 변경해야 하는지
의문이 들 것 이다.
예를 들어서 DB 접속 URL, ID, PW 같은 것 말이다.
데이터소스 빈을 등록할 때 이런 정보를 입력해야 하는데, 빈이 자동으로 다 등록이 되어 버린다면
이런 정보를 어떻게 입력할 수 있을까?
다음 글을 통해 알아보자.
'인프런 > 스프링 부트 - 핵심 원리와 활용' 카테고리의 다른 글
8) 외부설정과 프로필 [2] (2) | 2024.11.12 |
---|---|
7) 외부설정과 프로필 (0) | 2024.11.11 |
5) 스프링 부트 스타터와 라이브러리 관리 (0) | 2024.11.07 |
4) 스프링 부트와 내장 톰캣 (2) (2) | 2024.11.05 |
3) 스프링 부트와 내장 톰캣 (1) (2) | 2024.11.04 |
댓글