@Component와 @Configuration은 둘 다 Spring에서 **빈(Bean)**을 정의하고 관리하는 데 사용되지만,
역할과 적용 범위에서 중요한 차이점이 있습니다.
정리
@Component 가 @Configuration 둘다 자동 빈을 등록하지만
@Configuration 주된 목적은 이 클래스 안에서 빈을 정의하고 설정하는 특성이 크다고 생각합니다.
1. @Component
- 역할: Spring의 **구성 요소(Bean)**로 등록하기 위해 사용됩니다. 기본적으로 Spring의 스캔 범위 안에 있는 클래스가 @Component로 마킹되어 있으면, Spring은 해당 클래스를 빈으로 등록하고 관리합니다.
- 적용 대상: 주로 서비스 클래스, 리포지토리 클래스, 도메인 클래스 등에 사용됩니다.
- 기본 사용: @Component는 Spring 컨텍스트에서 자동으로 빈을 등록하기 위한 가장 일반적인 어노테이션입니다.
import org.springframework.stereotype.Component;
@Component
public class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}
주요 특징:
- 기본적인 빈 등록: @Component는 빈을 Spring 컨텍스트에 등록할 때 사용되며, 특별한 설정 없이 빈으로 관리됩니다.
- 스프링 컨테이너에서 자동 감지: Spring은 @ComponentScan을 통해 해당 어노테이션이 붙은 클래스를 자동으로 찾아 빈으로 등록합니다.
2. @Configuration
- 역할: Java 기반의 설정 클래스임을 명시합니다. 이 어노테이션이 붙은 클래스는 하나 이상의 빈을 생성하고 관리하는 데 사용되며, 해당 클래스 내의 메서드를 통해 빈을 정의할 수 있습니다.
- 적용 대상: 주로 설정 클래스에서 사용되며, 여러 빈을 정의하고 해당 빈들을 구성할 때 사용합니다.
- 기본 사용: @Configuration을 사용하여 직접 빈을 정의하고, 해당 빈들이 **싱글턴(Singleton)**으로 관리될 수 있도록 보장합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
주요 특징:
- Java 설정 클래스: @Configuration은 설정 클래스임을 나타내며, 클래스 내부에서 여러 빈을 정의할 수 있습니다.
- @Bean 메서드: @Configuration 클래스 내에 정의된 메서드들은 @Bean 어노테이션을 사용하여 빈으로 관리됩니다.
- 싱글턴 보장: Spring은 @Configuration 클래스 내에서 정의된 빈들이 싱글턴임을 보장합니다. 동일한 빈이 여러 번 생성되지 않도록 관리됩니다.
김영한님의 Configuration 정리
Auto Configuration
Auto Configuration은 주로 다음 두 용어로 번역되어 사용된다.
- 자동 설정
- 자동 구성
김영한님 정리
- Auto Configuration은 **자동 구성**이라는 단어를 주로 사용하고, 문맥에 따라서 자동 설정이라는 단어도 사용
- Configuration이 단독으로 사용될 때는 **설정**이라는 단어를 사용하겠다.
쿠팡 프로젝트 적용 예시
현재 제가 작업하고 있는 쿠팡 프로젝트에서는 api-config에 공통으로 설정할 수 있는 파일들을 넣어놨습니다.
각각의 msa-server-app 에서 api-config모듈에 있는 config Bean을 선택적으로 등록할 수 있습니다.
FilterConfig
- @Configuration 을 적용하여 @Condtional 을 적용하여 선택적으로 @Bean을 등록합니다.
package com.coopang.apiconfig.security.config;
import com.coopang.apiconfig.security.filter.CommonApiHeaderFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
private final SecurityFilterProperties securityFilterProperties;
public FilterConfig(SecurityFilterProperties securityFilterProperties) {
this.securityFilterProperties = securityFilterProperties;
}
@Bean
@ConditionalOnProperty(name = "common.api.filter.enabled", havingValue = "true", matchIfMissing = false)
@ConditionalOnMissingBean(CommonApiHeaderFilter.class)
public CommonApiHeaderFilter commonHeaderFilter() {
return new CommonApiHeaderFilter(securityFilterProperties.getPaths());
}
}
@ConditionalOnProperty
- application.yml 또는 application.properties에서 common.api.filter.enabled=true로 설정된 경우에만 빈이 생성됩니다.
- 예를 들어, 설정 파일에 아래와 같이 명시되어 있으면 빈이 생성됩니다.
- 이 설정이 **false**이거나 설정 파일에서 누락되어 있다면, 해당 빈은 생성되지 않습니다.
common:
api:
filter:
enabled: true
@ConditionalOnMissingBean
- CommonApiHeaderFilter 타입의 빈이 이미 등록되어 있는 경우 새로운 빈이 생성되지 않습니다.
- 이 조건은 같은 타입의 빈이 중복 생성되는 것을 방지합니다.
SecurityFilterProperties
- spring boot에서 bean이 등록되도록 @Component 을 적용했습니다.
package com.coopang.apiconfig.security.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "common.api.filter")
public class SecurityFilterProperties {
private List<String> paths;
public List<String> getPaths() {
return paths;
}
public void setPaths(List<String> paths) {
this.paths = paths;
}
}
CommonApiHeaderFilter
- msa-server-app 에서 CommonApiHeaderFilter 를 수동으로 bean을 등록할 수 있습니다.
package com.coopang.apiconfig.security.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Slf4j(topic = "CommonApiHeaderFilter")
public class CommonApiHeaderFilter extends OncePerRequestFilter {
private static final String USER_ID_HEADER = "X-User-Id";
private static final String USER_ROLE_HEADER = "X-User-Role";
private final List<String> excludedPaths;
public CommonApiHeaderFilter(List<String> excludedPaths) {
this.excludedPaths = excludedPaths != null ? excludedPaths : new ArrayList<>();
}
/**
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String userId = request.getHeader(USER_ID_HEADER);
final String role = request.getHeader(USER_ROLE_HEADER);
if (StringUtils.hasText(userId)) {
final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(request.getUserPrincipal(), userId,
Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role)));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
log.info("Authentication set for user: " + userId);
} else {
SecurityContextHolder.getContext().setAuthentication(null);
log.info("No authentication set due to missing headers");
}
try {
filterChain.doFilter(request, response);
} catch (Exception e) {
log.error("Exception during filter chain", e);
throw e;
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return excludedPaths.stream().anyMatch(path::startsWith);
}
}
@ConfigurationProperties
-> @EnableConfigurationProperties & @ConfigurationPropertiesScan
@ConfigurationProperties(prefix = "common.api.filter")
- 외부 설정을 주입 받는 객체 라는 뜻이다
- gettert & setter 가 필요하다
- setter는 생성자로 대체 가능하다
**Type-safe**
스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다.
이것을 **타입 안전한 설정 속성**이라 한다.
객체를 사용하면 타입을 사용할 수 있다.
따라서 실수로 잘못된 타입이 들어오는 문제도 방지할 수 있고,
객체를 통해서활용할 수 있는 부분들이 많아진다.
쉽게 이야기해서 외부 설정을 자바 코드로 관리할 수 있는 것이다.
그리고 설정 정보 그 자체도 타입을 가지게 된다.
@EnableConfigurationProperties(SecurityFilterProperties.class)
- @ConfigurationProperties가 붙은 클래스 하나를 수동으로 bean을 등록한다
- 즉 하나하나 일일이 bean 등록한다(강조)
@ConfigurationPropertiesScan
- 시작점을 정해서 하위로 @ConfigurationProperties가 붙은 클래스들을 자동으로 bean을 등록한다
- @EnableConfigurationProperties은 하나의 빈을 수동으로 등록한다면,
- ConfigurationPropertiesScan은 특정 범위부터 다수의 bean을 자동으로 등록할때 사용한다
SecurityFilterProperties 개선
@ConfigurationProperties, @EnableConfigurationProperties, @ConfigurationPropertiesScan 의 차이를 구분하게 되어서 쿠팡 프로젝트를 개선하려고 한다.
SecurityFilterProperties 개선 전 코드
@Component
@ConfigurationProperties(prefix = "common.api.filter")
public class SecurityFilterProperties {
private List<String> paths;
public List<String> getPaths() {
return paths;
}
public void setPaths(List<String> paths) { // setter 해놓으면 신입개발자가 변경할수 있음, 속성값은 변경하면 안됨.
this.paths = paths;
}
}
변경 해야 할 것
1. @Component 제거
2. 생성자 생성
3. gettter만 가능
- @ConfigurationProperties 으로 인해 setter 제거
- setter 해놓으면 신입개발자가 변경할수 있음, 속성값은 변경하면 안됨
SecurityFilterProperties 개선 후 코드
package com.coopang.apiconfig.security.config;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
//@Component 제거
@Getter // getter만 가능
@ConfigurationProperties(prefix = "common.api.filter")
public class SecurityFilterProperties {
private List<String> paths;
public SecurityFilterProperties(List<String> paths) { // 생성자로 set
this.paths = paths;
}
}
ApiConfigApplication.java(@EnableConfigurationProperties)
- ApiConfigApplication 에 이미 @EnableConfigurationProperties(SecurityFilterProperties.class) 가 존재함
- @EnableConfigurationProperties이 Bean 자동으로 등록해준다
package com.coopang.apiconfig;
import com.coopang.apiconfig.security.config.SecurityFilterProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(SecurityFilterProperties.class)
public class ApiConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ApiConfigApplication.class, args);
}
}
hub app에서도 SecurityFilterProperties(@ComponentScan)
- ApiConfig 는 쿠팡프로젝트에서 라이브러리처럼 사용되고 있다
- hub app에서도 SecurityFilterProperties 될까?
- 결론 : 된다
- 이유 : @ComponentScan 때문에
package com.coopang.hub;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@EnableFeignClients(basePackages = {"com.coopang.apiconfig.feignClient", "com.coopang.apicommunication.feignClient"})
@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
@ComponentScan(basePackages = {"com.coopang.hub", "com.coopang.apiconfig", "com.coopang.apicommunication"})
@EnableMethodSecurity(securedEnabled = true)
public class HubApplication {
public static void main(String[] args) {
SpringApplication.run(HubApplication.class, args);
}
}
- @ComponentScan(basePackages = {"com.coopang.hub", "com.coopang.apiconfig", "com.coopang.apicommunication"}) 을 하고 있다
이유:
- **HubApplication**은 **ComponentScan**을 통해 com.coopang.apiconfig 패키지를 스캔하고 있습니다.
- 이로 인해 **ApiConfigApplication**에서 등록된 빈과 설정을 허브 애플리케이션에서 사용할 수 있게 됩니다.
- 즉, HubApplication에서는 이미 **ApiConfigApplication**에 의해 빈으로 등록된 **SecurityFilterProperties**를 참조하고 사용할 수 있습니다.
'기술 블로그 (Tech Blog) > Project-coopang' 카테고리의 다른 글
gateway 라우팅 에러 해결하기 (0) | 2024.10.16 |
---|---|
협업 과정에서의 문제 해결과 카프카 메시징 구조 개선 (0) | 2024.10.14 |
페이징 처리 (0) | 2024.09.23 |
DDD Layered Architecture의 application service와 domain service에 어떤 코드를 넣어야 하는 걸까? (0) | 2024.09.23 |
DDD Architecture (0) | 2024.09.14 |