@SpringBootApplication에 대해 알아보자

겔로그·2022년 7월 9일
1

Spring Boot

목록 보기
17/21
post-thumbnail

저번 포스팅에서는 Spring initializr를 통해 Sprign Boot 프로젝트를 생성하고, 실행해보는 시간을 가졌다. 문제는 어플리케이션이 어떻게, 왜 실행되는지를 모르는 것이다.

프로젝트를 생성할 경우 다음의 코드를 통해 어플리케이션이 실행된다.

이번 시간을 통해 Spring Boot 어플리케이션이 어떻게 실행되는지 알아보고 Spring Boot에 대해 좀 더 자세히 알아가보는 시간을 가지려고 한다.

프로젝트 생성 및 실행

처음 프로젝트를 생성하면 다음과 같은 [프로젝트명]Application.java 파일이 만들어진 것을 볼 수 있다. 해당 파일을 확인해보자

package com.example.projectspringboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProjectSpringBootApplication {

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

}

굉장히 짧은 코드였다..
해당 코드를 보면 @SpringBootApplication이라는 새로운 문법 구조를 확인할 수 있다. 그렇다면 @SpringBootApplication이 무엇인지 알아보자.

Lombok

@에 대해 알고싶다면 가장 먼저 Lombok이라는 라이브러리를 이해해야 한다. Lombok 라이브러리 설명을 본다면 다음과 같다.

Project Lombok is a java library tool that is used to minimize/remove the boilerplate code and save the precious time of developers during development by just using some annotations. In addition to it, it also increases the readability of the source code and saves space.

Lombok은 Boiler Plate한 코드를 최소화/제거 하기 위해 사용하는 java 라이브러리이며 개발자는 Lombok의 몇가지 어노테이션을 통해 이전보다 개발 시간을 단축시킬 수 있다.

BoilerPlate 코드란 모든 코드를 작성하기 위해 항상 필요한 부분을 의미한다. 

결론적으로 우리가 앞으로 분석할 @SpringBootApplication 또한 개발자가 구현해야 될 코드이지만 이를 어노테이션 하나로 편하게 이용할 수 있도록 만들어준 것이 Lombok 라이브러리라고 보면 될 것 같다.

따라서 우리는 Lombok 라이브러리를 통해 어노테이션을 이용할 수 있으며, 어노테이션을 통해 개발 시간을 단축할 수 있다.

그럼 이제 @SpringBootApplication에 대해 알아보자.

@SpringBootApplication

@SpringBootApplication은 Spring Boot에서 어플리케이션을 실행하는 중요한 역할을 담당한다. 그렇다면 어떻게 동작하는지 코드 분석을 한 번 해보도록 하자.

SpringBootApplication.class

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@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 {
    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;
}

먼저 보이는 어노테이션이 매우 많다. 각각의 어노테이션에 대해 알아봐야할 것 같다. 그 전에 Meta-annotation이라는 개념부터 잠깐 배우고 가자

Meta-annotation이란?

간단하게 말하자면, 다른 어노테이션에서도 사용되는 어노테이션을 meta-annotation이라고 부른다. meta-annotation을 통해 custom-annotation을 생성해 사용할 수 있다.

대표적인 annotation 예시로는 앞서 본 어노테이션들이 있다. 그럼 다시 각각의 어노테이션이 무슨 의미를 가지고 어떤 역할을 수행하는지 알아보자

Annotation 종류 및 의미

@Target

@Target은 Java compiler가 어노테이션이 어디에 적용될지 결정하기 위해 사용된다.
아래의 타입을 선언함을 통해 해당 어노테이션이 어떻게 선언할 때 사용되는 것인가를 알려준다.

ElementType.PACKAGE         : 패키지 선언
ElementType.TYPE            : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR     : 생성자 선언
ElementType.FIELD           : 멤버 변수 선언
ElementType.LOCAL_VARIABLE  : 지역 변수 선언
ElementType.METHOD          : 메서드 선언
ElementType.PARAMETER       : 전달인자 선언
ElementType.TYPE_PARAMETER  : 전달인자 타입 선언
ElementType.TYPE_USE        : 타입 선언
    

@Retention

@Retention은 해당 어노테이션이 실제로 적용되고 유지되는 범위를 의미합니다.
다음과 같은 종류의 타입이 있으며, 해당 타입을 통해 정책을 가질 수 있습니다.

@Retention 타입 종류

RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM읠 통해 계속 참조가 가능. 대표적으로는 리플렉션이나 로깅에 사용
RetentionPolicy.CLASS   : 컴파일러가 클래스를 참조할 때까지만 유효
RetentionPolicy.SOURCE  : 컴파일 전까지만 유효. 컴파일 이후에는 사라지게 된다.

@Documented

@Documented 선언시 선언한 어노테이션을 사용한 클래스가 문서화 될 경우 해당 어노테이션이 적용되었음을 명시하도록 한다.

다음과 같이 @SpringBootApplication내에 선언한 어노테이션들이 @Documented를 선언했을 경우, 다음과 같이 docs에 명시된다.

@Inherited

@Inherited을 이용할 경우 해당 어노테이션을 적용한 클래스를 상속받을 경우, 해당 클래스에도 어노테이션이 적용된다. 다음의 예시를 보자

@Inherited
@Target(ElementType.TYPE)
public @interface Example{}

@Example
public class Test{}

public class InheritTest extends Test{}

다음의 경우 Test 클래스의 @Inherited에 의해 InheritTest에도 해당 어노테이션이 적용된다.

@SpringBootConfiguration

@SpringBootConfiguration은 어플리케이션의 구성을 제공하는 class-level의 어노테이션이다.

해당 어노테이션을 통해 Spring 컨테이너는 @Bean으로 정의된 클래스 메소드를 처리해 Bean을 생성한다.
생성된 Bean을 통해 @Autowired 또는 @Inject 어노테이션을 사용하여 앞서 공부했던 DI.
즉 Spring 컨테이너의 의존성 주입을 가능하게 만들어 준다.

@SpringBootConfiguration 내부에는 다음과 같은 어노테이션이 추가로 구현되어져 있다.

  • @Indexed

@Indexed 적용시, 해당 어노테이션을 적용한 클래스(인터페이스)가 있을 경우 해당 클래스를 상속(extends)하거나 구현(implements)할 경우 해당 클래스(인터페이스)의 스테레오 타입에 자동으로 포함된다.

 @Indexed
 public interface AdminService { ... }
 
 public class ConfigurationAdminService implements AdminService { ... }

다음 예시를 볼 경우 @Indexed가 적용된 AdminService를 ConfigurationAdminService가 implemets한 것을 볼 수 있는데, 이 때 ConfigurationAdminService의 스테레오 타입에 AdminService가 자동으로 포함됨을 의미한다.

  • @Configuration

@Configuration 적용시 해당 클래스에 구현한 @Bean을 Spring 컨테이너에 포함시킬 수 있다.

@EnableAutoConfiguration

@EnableAutoConfiguration을 사용할 경우 Spring Boot가 어플리케이션 컨텍스트를 자동 구성할 수 있도록 도움을 준다.
@EnableAutoConfiguration 주석을 선언한 클래스의 패키지가 기본값으로 간주되며 자동 구성하고 싶지 않는 클래스의 경우, exclude 또는 excludeName 옵션을 통해 제외할 수 있다.

@Configuration
@EnableAutoConfiguration(excludeName = {"org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration"},
exclude={JdbcTemplateAutoConfiguration.class} )
public class EmployeeApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(EmployeeApplication.class, args);
        // ...
    }
}

추가로 @Import(AutoConfigurationImportSelector.class)가 선언되어 있는데
AutoConfigurationImportSelector의 정의를 타고 올라가보면 getCandidateConfigurations라는 메소드가 있다.
이 메소드는 자동 설정을 하고자 하는 후보군 클래스의 이름 목록을 반환한다.

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

Assert의 메시지를 보면 META-INF/spring.factories에 클래스 정보가 있어야 함을 알 수 있으며 해당 파일은 spring-boot-autoconfigure 패키지 구조에 존재한다.

따라서, @EnableAutoConfiguration 적용시 spring-boot-autoconfigure의 spring.factories에 작성된 basepackages를 불러들여 해당 경로에 있는 Bean을 자동으로 등록해준다. spring.factories에는 다음과 같은 내용이 담겨있다.

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
... 등등

해당 코드 내부에는 @AutoConfigurationPackage 및 @Import 어노테이션이 선언되어 있는데 의미는 다음과 같다.

  • @AutoConfigurationPackage
    @AutoConfigurationPackage는 패키지를 자동으로 구성해주는 어노테이션이다.
    해당 코드를 볼 경우 @Import({Registrar.class})라고 되어 있는데 Registrar.class에 대해 알아봐야 정확히 어떻게 돌아가는지 알 수 있을 것 같다.
 static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
    }

Registrar클래스에는 두 가지 메소드가 정의되어져 있는데 registerBeanDefinitions과 determineImports로 정의되어져 있다. Bean으로 정의된 내용을 등록하는 registerBeanDefinitions와 Import로 정의된 내용을 등록하는 determineImports이 있다.

  • @Import

@Import 주석 사용시 하나 이상의 다른 @Configuration 파일 또는 구성 요소에서 빈을 로드할 수 있다.
대표적으로 하나의 구성 파일에서 모든 bean을 로드하고 싶지 않을 때 @Import 어노테이션을 사용하여 로드하는 bean을 설정할 수 있다.

@Configuration
public class FishConfig {
  @Bean
  public GoldFish goldFish() {
    return new GoldFish();
  }
  
  @Bean
  public Guppy guppy() {
    return new Guppy();
  }
  
  @Bean
  public Salmon salmon() {
    return new Salmon();
  }
  
}
-----------------------------------------------
@Configuration
public class BirdConfig {
  @Bean
  public Eagle eagle() {
    return new Eagle();
  }
  
  @Bean
  public Ostrich ostrich() {
    return new Ostrich();
  }
  
  @Bean
  public Peacock peacock() {
    return new Peacock();
  }
}
-----------------------------------------------
@Configuration
@Import({FishConfig.class, BirdConfig.class})
public class ImportBeansConfig {
  @Bean
  public ExampleBean exampleBean() {
    return new ExampleBean();
  }
  
  @Bean
  public SampleBean sampleBean() {
    return new SampleBean();
  }
}

@ComponentScan

@ComponentScan을 사용할 경우, @Component를 명시한 클래스들을 Scan하여 Bean을 생성한다.

주의할 점은 @ComponentScan을 사용할 경우 Scan범위를 지정하지 않을 경우 지정된 패키지에서 스캔을 시작한다. 따라서, 스캔 범위를 변경하고 싶을 경우 basePakages 또는 basePackageClasses 명령어를 통해 범위를 지정해라.

@Configuration
@ComponentScan(basePackages = {"project.com.test1", "project.com.test2"},
  basePackageClasses = Test.class)
public class TestApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(TestApplication.class, args);
    }
}
  • @Filter

해당 기능을 통해 특정 패턴을 매칭하여 컴포넌트를 scan할 수 있다. REGET 정규식 패턴이나 ASPECTJ의 표현식, 커스텀 등 다양한 방식으로 필터를 설정할 수 있으며, 이를 통해 스캔 범위를 지정할 수 있다.

@AliasFor

@AliasFor를 사용할 경우, 해당 클래스, 변수 등에 칭을 선언할 수 있다.

결론

@SpringBootApplication은 이와같이 다양한 어노테이션을 통해 구성되어져 있다. SpringBootConfiguration과 ComponentScan을 통해 Spring 컨테이너에 Component와 Configuration을 탐색하여 Bean을 생성해주고,@EnableAutoConfiguration을 통해 base package로 정의된 경로의 모든 Bean을 자동으로 구성해준다.

쉽게 말하자면
타입 선언시 적용되며, 런타임간 JVM에서 참조가 되며 @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan 조합으로 어플리케이션에 필요한 모든 Configuration 또는 Component들을 Bean으로 생성해 Spring Container에 담고, 이를 이용하여 어플리케이션을 실행시킨다.

Reference

geeksforgeeks - lombok
Boilerplate code
tistory - @SpringBootApplication이란?
tistory - Meta Annotation
baeldung - SpringBootConfiguration
docs.spring.io - Indexed
baeldung - spring-componentscan-vs-enableautoconfiguration

profile
Gelog 나쁜 것만 드려요~

0개의 댓글