1. 개요

내장 Tomcat과 DispatcherServlet처럼 애플리케이션을 실행하기 위해 필요한 기술 객체까지 애플리케이션 클래스에 함께 두면, 비즈니스 코드와 기반 설정의 경계가 흐려진다.

이번 글에서는 여러 애노테이션을 하나로 묶는 합성 애노테이션(composed annotation) 을 만들고, 웹 서버와 MVC를 위한 인프라 빈 구성을 별도 클래스로 분리한다.


2. 메타 애노테이션과 합성 애노테이션

애노테이션에 붙는 애노테이션을 메타 애노테이션이라고 한다. 예를 들어 @Service는 내부에 @Component를 가지고 있으므로, @Service가 붙은 클래스도 컴포넌트 스캔 대상이 된다.

여러 메타 애노테이션을 조합해 새로운 애노테이션을 만들 수 있다. 아래 @MySpringBootApplication은 구성 정보임을 나타내는 @Configuration과 컴포넌트 스캔을 켜는 @ComponentScan을 한 이름으로 묶는다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
public @interface MySpringBootApplication {
}

애플리케이션 클래스에는 조합한 애노테이션 하나만 남는다.

@MySpringBootApplication
public class Springboot1Application {

    public static void main(String[] args) {
        SpringApplication.run(Springboot1Application.class, args);
    }
}

이 방법은 단순히 코드를 짧게 만드는 기능만 제공하지 않는다. 애플리케이션을 시작하는 데 필요한 설정 묶음에 이름을 붙이므로, 다른 프로젝트에서도 같은 의도를 재사용할 수 있다. 스프링의 @RestController도 @Controller와 @ResponseBody를 조합한 대표적인 사례다.


3. 빈의 역할을 구분하기

스프링 애플리케이션의 빈은 역할에 따라 세 부류로 나누어 볼 수 있다.

구분 예시 등록 방식
애플리케이션 로직 빈 HelloController, SimpleHelloService 컴포넌트 스캔 또는 직접 구성
애플리케이션 인프라 빈 TomcatServletWebServerFactory, DispatcherServlet 주로 자동 구성 또는 직접 구성
컨테이너 인프라 빈 ApplicationContext, BeanFactoryPostProcessor 스프링 컨테이너가 내부적으로 생성

HelloController는 서비스 규칙과 HTTP 요청 처리를 담당하는 애플리케이션 로직 빈이다. 반면 TomcatServletWebServerFactory는 내장 Tomcat을 만들고, DispatcherServlet은 Spring MVC의 프런트 컨트롤러로 요청을 전달한다. 둘은 업무 규칙을 담지 않지만 애플리케이션이 실행되려면 필요하므로 애플리케이션 인프라 빈이라고 볼 수 있다.

컨테이너 인프라 빈은 이 둘과 구분한다. 컨테이너가 빈의 등록, 의존 관계 설정, 초기화를 처리하기 위해 직접 사용하는 객체이며, 개발자가 일반적으로 구성 클래스에서 만들지 않는다.


4. @Import로 인프라 구성 정보를 분리하기

@Import는 컴포넌트 스캔 범위 밖의 구성 클래스를 현재 구성에 포함한다. 먼저 웹 서버와 MVC에 필요한 빈을 각각 별도 구성 클래스로 옮긴다.

@Configuration
public class TomcatWebServerConfig {

    @Bean
    ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
}

@Configuration
public class DispatcherServletConfig {

    @Bean
    DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
}

이 두 클래스를 가져오는 애노테이션도 만들 수 있다. @Import 역시 다른 애노테이션에 붙일 수 있는 메타 애노테이션이다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({TomcatWebServerConfig.class, DispatcherServletConfig.class})
public @interface EnableMyAutoConfiguration {
}

마지막으로 시작 애노테이션에 @EnableMyAutoConfiguration을 추가한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@EnableMyAutoConfiguration
public @interface MySpringBootApplication {
}

실행 시에는 컴포넌트 스캔이 애플리케이션 로직 빈을 찾고, @Import가 인프라 구성 클래스를 포함한다. 애플리케이션 클래스는 더 이상 Tomcat이나 DispatcherServlet을 직접 알 필요가 없다.

@MySpringBootApplication
  ├─ @ComponentScan → HelloController, SimpleHelloService
  └─ @EnableMyAutoConfiguration
       └─ @Import → TomcatWebServerConfig, DispatcherServletConfig

5. 정리

합성 애노테이션은 관련된 설정을 한 이름으로 묶어 시작 클래스를 간결하게 만들고, 설정의 의도를 드러낸다. @Configuration과 @ComponentScan을 조합하면 애플리케이션 로직 빈을 등록하는 기준점도 한곳에 모을 수 있다.

웹 서버와 DispatcherServlet처럼 기술 기반을 제공하는 빈은 별도 구성 클래스로 분리하고 @Import로 포함할 수 있다. 다음 글에서는 클래스 이름을 직접 나열하는 이 방식을 파일에서 읽어 오는 동적 자동 구성으로 바꾼다.


참고 자료