스프링부트 애플리케이션

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.run(args);
    }
}   

보통 IDE를 이용해서 스프링부트 프로젝트를 생성하면 메인 메소드를 포함한 애플리케이션 클래스가 자동으로 생성된다. 해당 클래스는 @SpringBootApplication를 지정하여 스프링부트 애플리케이션을 설정한다. 해당 어노테이션을 살펴보자.

@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 {
	// ...
}

먼저, 스프링부트에서 빈은 두번의 단계를 거쳐서 생성된다.

  1. @ComponentScan: 하위 패키지에서 @Component, @Resource, @Service 등으로 설정된 빈 생성
  2. @EnableAutoConfiguration: 자동 설정을 위한 빈을 로드

@Service, @Mapper, @Controller, @Component, @Repository 등의 Compoent 를 먼저 스캔하여 빈을 생성한 뒤, @EnableAutoConfiguration에서 추가적인 빈들을 생성한다.

두번째 단계인 @EnableAutoConfiguration을 자세히 살펴보자. 메이븐에서 로드된 spring-boot-autoconfigure 모듈의 META-INF폴더를 보면 spring.factories라는 파일이 존재한다.

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\

해당 파일은 자동설정을 위한 클래스들이 지정되어있다. @EnableAutoConfiguration을 사용함으로써 이 자동설정 빈을 전부 등록하게 된다. (전부는 아니고 조건에 따라서 필터링된다.)

자동설정 실습

자동설정을 위한 프로젝트를 생성한 뒤, 자동설정 라이브러리 의존성을 추가한다.

buildscript {
    ext {
        springBootVersion: '2.0.6.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.6.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'maven'

group 'jch.inflearn'
version '1.0-SNAPSHOT'

bootJar.enabled false
jar.enabled true

jar {
    enabled true
    manifest {
        attributes 'Main-Class': 'jch.inflearn.Application'
    }
}

sourceCompatibility = 1.8
targetCompatibility = 1.8


repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-autoconfigure")
    compile("org.springframework.boot:spring-boot-autoconfigure-processor")
}

참고로 그래들에서 로컬저장소에 배포하려면 repositories에 mavenLocal() 저장소를 추가해야한다. 다음으로 자동설정으로 등록할 빈의 클래스를 정의하자.

public class Holoman {

    String name;

    int howLong;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHowLong() {
        return howLong;
    }

    public void setHowLong(int howLong) {
        this.howLong = howLong;
    }

    @Override
    public String toString() {
        return "Holoman{" +
                "name='" + name + '\'' +
                ", howLong=" + howLong +
                '}';
    }
}

마지막으로 자동설정 클래스를 작성한다.

@Configuration
public class HolomanConfiguration {

    @Bean
    public Holoman holoman() {
        Holoman holoman = new Holoman();
        holoman.setName("jch");
        holoman.setHowLong(27);
        return holoman;
    }
}

해당 프로젝트를 로컬저장소에 배포한 뒤, 다른 프로젝트에서 자동설정으로 사용해보자.

buildscript {
    ext {
        springBootVersion: '2.0.6.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.6.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'maven'

group 'jch.inflearn'
version '1.0-SNAPSHOT'

bootJar.enabled false
jar.enabled true

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile('jch.inflearn:jch-spring-boot-starter:1.0-SNAPSHOT') // 자동설정 모듈 추가
}

로컬저장소에 배포된 라이브러리를 사용하는 프로젝트에서도 mavenLocal()을 추가해야 의존성을 추가할 수 있다.

@SpringBootApplication
// Compoent 스캔 후 EnableAutoConfiguration으로 추가적인 빈들을 생성
public class Application {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }
}

@Component
public class HolomanRunner implements ApplicationRunner {

    @Autowired
    Holoman holoman;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(holoman);
    }
}
// Holoman{name='jch', howLong=27} 출력

위 애플리케이션을 실행하면 HolomanRunner가 실행되면서 자동설정으로 추가된 Holoman의 빈이 출력된다. 여기까지는 아무런 문제가 없다. 하지만, 자동설정을 이용하는 프로젝트에서 새로운 Holoman을 빈으로 등록한다면 어떻게 될까?

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

    @Bean
    public Holoman holoman() {
        Holoman holoman = new Holoman();
        holoman.setName("jch");
        holoman.setHowLong(60);
        return holoman;
    }
}

// Except: Holoman{name='jch', howLong=60}
// Actual: Holoman{name='jch', howLong=27}

Holoman{name='jch', howLong=60}의 출력을 예상했지만, Holoman{name='jch', howLong=27}가 출력된다. 이는 앞서 설명한 @SpringBootApplication에서 빈을 로드하는 두 단계와 관련이 있다.

@Service, @Mapper, @Controller, @Component, @Repository 등의 Compoent 를 먼저 스캔하여 빈을 생성한 뒤, @EnableAutoConfiguration에서 추가적인 빈들을 생성한다.

빈 덮어씌우기 방지 (ConditionOnXxxYyyZzz)

분명 우리가 등록한 Holoman이 @ComponentScan 단계에서 등록되긴 했을 것이다. 그렇다면 로그를 자세히 살펴보자.

..... Overriding bean definition for bean 'holoman' with a different definition: replacing .....

위와 같은 로그를 발견할 수있다. holoman이라는 이름의 빈이 덮어씌워졌다는 의미이다. 이는 자동설정단계에서 덮어씌워진 것이다.

이를 해결하기 전에, 자동설정 어노테이션 중 하나인 org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration을 살펴보자.

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET) // 1
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    // ...
    @Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class) // 2
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}
    // ...
}
  • 1번 라인: SpringApplication의 타입이 SERVLET일 때만 해당 설정을 빈으로 등록
  • 2번 라인: 밑에서 설명

위 클래스의 내부를 보면, Conditional이 붙은 어노테이션이 자주 사용되는 걸 알 수 있다. 이는 빈을 등록할 때, 해당 클래스의 빈이 등록되지 않은 경우 등록하기 위해 사용된다. 2번 라인에서는 HiddenHttpMethodFilter가 등록되지 않았으면 OrderedHiddenHttpMethodFilter를 새로운 빈으로 등록한다.

다시 원래 목표로 돌아가서, 우리의 자동설정 프로젝트에서 빈을 오버라이드하는 현상을 해결하기 위해 @ConditionalOnMissingBean을 이용해서 빈을 등록해보자.

@Configuration
public class HolomanConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public Holoman holoman() {
        Holoman holoman = new Holoman();
        holoman.setName("jch");
        holoman.setHowLong(27);
        return holoman;
    }
}

간단하게, @ConditionalOnMissingBean을 추가하면된다. 이제 자동설정이 실행될 때, Holoman 빈이 등록된 경우 새로 등록하지 않는다.

properties파일 이용하기

자동설정을 사용하는 프로젝트에서 등록한 Holoman이 우선시 되는건 좋다. 그러나 단순히 이름과 나이를 설정하여 빈으로 등록할 뿐인데, 굳이 장황하게 빈으로 등록해야 할까? 우리가 흔히 사용하는 properties 파일을 이용하여 자동설저되는 빈의 속성을 설정할 수 있다.

먼저 자동설정 프로젝트에 properties 파일을 사용하기 위한 프로퍼티즈 클래스를 만들고 @ConfigurationProperties어노테이션을 추가하자. (spring-boot-autoconfigure-processor 의존성이 필요하다.)

@ConfigurationProperties("holoman")
public class HolomanProperties {

    private String name;

    private int howLong;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getHowLong() {
        return howLong;
    }

    public void setHowLong(int howLong) {
        this.howLong = howLong;
    }
}

@Configuration
@EnableConfigurationProperties(HolomanProperties.class)
public class HolomanConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public Holoman holoman(HolomanProperties properties) {
        Holoman holoman = new Holoman();
        holoman.setHowLong(properties.getHowLong());
        holoman.setName(properties.getName());
        return holoman;
    }
}

설정 클래스에 @EnableConfigurationProperties 어노테이션을 이용해 프로퍼티즈 클래스를 지정하고, 빈을 등록할 때 주입된 인스턴스를 이용하여 속성 값을 설장한다.

이제 자동설정을 사용하는 프로젝트의 properties 파일에서 holoman의 속성 값을 설정할 수 있다.

holoman.name = properties 테스트
holoman.how-long = 55

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.