[SpringBoot] @Bean, @Configuration, @Component 어노테이션

찌글렛·2022년 2월 8일
1

java

목록 보기
2/15

기존의 Spring MVC에서는 xml을 활용하여 Bean을 등록하고 있었다. 하지만 프로젝트의 규모가 커짐에 따라 사용하는 요소들을 xml에 등록하는 것이 상당히 번거로워 져서 어노테이션(Annotation, @)를 활용한 Bean 등록 방법이 탄생하게 되었다. 이번에는 Spring에서 Bean을 등록하기 위해 활용가능한 @Bean, @Component, @Configuration 어노테이션에 대해서 알아보도록 하겠다.

  1. Spring Bean이란?
    [ Spring Bean 이란? ]
    Spring에서는 Spring의 DI Container에 의해 관리되는 POJO(Plain Old Java Object)를 Bean이라고 부르며,
    이러한 Bean들은 Spring을 구성하는 핵심 요소이다. Spring의 Bean을 정리하면 아래와 같다.

POJO(Plain Old Java Object)로써 Spring 애플리케이션을 구성하는 핵심 객체이다.
Spring IoC 컨테이너(또는 DI 컨테이너)에 의해 생성 및 관리된다.
class, id, scope, constructor-arg 등을 주요 속성으로 지닌다.

[ Spring Bean 구성 요소 ]

class: Bean으로 등록할 Java 클래스
id: Bean의 고유 식별자
scope: Bean을 생성하기 위한 방법(singleton, prototype 등)
constructor-arg: Bean 생성 시 생성자에 전달할 파라미터
property: Bean 생성 시 setter에 전달할 인수

Spring에서는 기본적으로 Singleton 방식으로 Bean이 생성되며, 아래와 같이 사용자의 Session을 관리하는 AuthenticateInterceptor를 xml 기반의 Bean으로 구성해보도록 하자.

아래의 AuthenticateInterceptor는 해당 요청을 처리하기 전에 Session에 user 객체가 저장되어 있는지를 검사하여 없을 경우 로그인 화면으로 redirect를 보내고 있다.

import lombok.extern.log4j.Log4j2;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Log4j2
public class AuthenticateInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        if(request.getSession().getAttribute("user") == null){
            response.sendRedirect("/user/loginView");
            return false;
        }
        return true;
    }

}
 

위의 AuthenticateInterceptor를 xml 기반으로 구성하면 아래와 같다.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
       					   http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
                           
	<mvc:annotation-driven/>
	
	<mvc:interceptors>
		<mvc:interceptor>
		
			<mvc:mapping path="/**"/>
			
			<!-- static resource -->
			<mvc:exclude-mapping path="/resource/**"/>
			
			<!-- login -->
			<mvc:exclude-mapping path="/cmmn/**"/>
			
			<!-- 회원가입신청 -->
			<mvc:exclude-mapping path="/user/signIn"/>
			<mvc:exclude-mapping path="/user/signOut"/>
			<mvc:exclude-mapping path="/user/signUp"/>
			
			<bean id="authenticateInterceptor" class="com.example.AuthenticateInterceptor"/>
			
		</mvc:interceptor>
	</mvc:interceptors>
    
</beans>

만약 Bean으로 등록해주어야 하는 클래스가 1개라면 위와 같이 Bean을 등록하면 되겠지만, 실제 개발을 하다 보면 상당히 많은 양의 클래스를 Bean으로 등록하게 되고, 해당 클래스를 일일이 xml을 통해 Bean으로 등록하려면 상당히 번거로운 작업이 된다. 이러한 번거로움을 덜기 위해 Spring에서는 Annotation을 활용해 Bean을 등록하고 사용하는 방법을 제공하고 있다.

  1. Spring Bean 등록 방법(@Bean, @Configuration, @Component)
    [ @Bean 어노테이션과 @Configuration 어노테이션 ]
    위에서 보여준 AuthenticateInterceptor를 Bean으로 등록하고자 한다면 @Bean 어노테이션을 활용하면 된다. 아래의 예제에서 authenticateInterceptor()라는 메소드를 통해서 AuthenticateInterceptor를 Bean으로 등록하고 있다.
import com.example.HeaderFilter;
import com.example.SessionInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;

import java.util.Properties;

// 1개 이상 Bean을 등록하고 있음을 명시하는 어노테이션
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    ...

    @Bean
    public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
        return new ResourceUrlEncodingFilter();
    }

    @Bean
    public HeaderFilter createHeaderFilter() {
        return new HeaderFilter();
    }

    @Bean
    public FilterRegistrationBean<HeaderFilter> getFilterRegistrationBean() {
        FilterRegistrationBean<HeaderFilter> registrationBean = new FilterRegistrationBean<>(createHeaderFilter());
        registrationBean.setOrder(Integer.MIN_VALUE);
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }

    @Bean
    public BCryptPasswordEncoder createBCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

위의 예제에서는 AuthenticateInterceptor 외에도 HeaderFilter, BCrpytPasswordEncoder 등을 Bean으로 등록하고 있는데, 이러한 클래스들을 xml에 일일이 적어서 등록한다고 하면 상당히 많은 시간을 차지할 것이고, 생산력 저하를 야기할 것이다. 그렇기 때문에 반드시 어노테이션을 활용한 Bean의 등록을 지향해야 한다.

하지만 어떤 임의의 클래스를 만들어서 @Bean 어노테이션을 붙인다고 되는 것이 아니고, @Bean을 사용하는 클래스에는 반드시 @Configuration 어노테이션을 활용하여 해당 클래스에서 Bean을 등록하고자 함을 명시해주어야 한다.

위의 예제에서도 클래스 이름 위에 @Configuration 어노테이션을 명시하여 해당 클래스에서 1개 이상의 Bean을 생성하고 있음을 명시하고 있다. 그렇기 때문에 @Bean 어노테이션을 사용하는 클래스의 경우 반드시 @Configuration과 함께 사용해주어야 한다.

이러한 @Bean 어노테이션의 경우 아래와 같은 상황에서 주로 사용한다.

개발자가 직접 제어가 불가능한 라이브러리를 활용할 때
초기에 설정을 하기 위해 활용할 때
위의 예제에서는 Spring에서 제공하여 개발자가 직접 제어할 수 없는 BCryptPasswordEncoder, ResourceUrlEncodingFilter, FilterRegistration를 위해 @Bean을 사용하고 있고, 설정을 위해 개발자가 개발한 AuthenticateInterceptor, HeaderFilter를 위해 @Bean을 사용하고 있다.

@Configuration없이 @Bean만 사용해도 스프링 빈으로 등록이 된다. 대신 메소드 호출을 통해 객체를 생성할 때 싱글톤을 보장하지 못한다. 그렇기 때문에 Spring 설정 정보를 위해서는 반드시 @Configuration을 사용해주어야 한다.

그리고 여기서 빈의 설정을 담당하는 @Configuration 어노테이션도 내부적으로 @Component를 가지고 있어 @Configuration이 붙은 클래스도 Spring의 빈으로 등록이 되는 것 역시 알아 둘 필요가 있다.

[ @Component 어노테이션 ]
개발자가 직접 개발한 클래스를 Bean으로 등록하고자 하는 경우에는 @Component 어노테이션을 활용하면 된다. 예를 들어 폴더 생성, 파일 저장 등을 처리하기 위해 직접 개발한 FileUtils를 Bean으로 등록하고자 한다면 아래와 같이 @Component를 사용하면 된다.

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Component
@Log4j2
public class FileUtils {

    public boolean createDirectory(String dirPath){
        Path path = Paths.get(dirPath);
        if(!Files.exists(path)){
            try {
                Files.createDirectories(path);
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }

    public String uploadFile(MultipartFile file, String dirPath){
        String fileName = null;
        if(!file.isEmpty()){
            fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
            try {
                file.transferTo(new File(dirPath + fileName));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return fileName;
    }
    
}
 

물론 다음과 같은 방법으로도 빈 등록을 할 수 있다.

@Bean
public FileUtils fileUtils() {
    return new FileUtils();
}

하지만 이렇게 직접 개발한 클래스 단위의 빈을 등록할 때에는 @Component를 통해 해당 클래스를 빈으로 등록함을 노출하는 것이 좋을 것이다. 왜냐하면 해당 클래스에 있는 @Component 만 보면 해당 빈이 등록되도록 잘 설정되었는지를 찾지 않아고 확인할 수 있기 때문이다. 반면에 @Bean을 이용하면 해당 메소드를 찾는 번거로움이 생길 수 있다.

추가로 @Component를 이용한다면 Main 또는 App 클래스에서 @ComponentScan으로 컴포넌트를 찾는 탐색 범위를 지정해주어야 한다. 하지만 SpringBoot를 이용중이라면 @SpringBootConfiguration 하위에 기본적으로 포함되어 있어 별도의 설정이 필요 없다.

  1. 요약
    [ @Bean, @Configuration ]
    개발자가 직접 제어가 불가능한 외부 라이브러리 또는 설정을 위한 클래스를 Bean으로 등록할 때 @Bean 어노테이션을 활용
    1개 이상의 @Bean을 제공하는 클래스의 경우 반드시 @Configuration을 명시해 주어야 함
    [ @Component ]
    개발자가 직접 개발한 클래스를 Bean으로 등록하고자 하는 경우 @Component 어노테이션을 활용

0개의 댓글