본문 바로가기

기술 블로그 (Tech Blog)/Project-coopang

@Component, @Configuration + @ConfigurationProperties

@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**를 참조하고 사용할 수 있습니다.