인프런에서 백기선님의 스프링 부트 개념과 활용을 수강하고 개인적으로 공부한 내용을 정리한 글입니다.

자동 설정의 개요

스프링 프로젝트가 실행되는 지점인 메인 어플리케이션의 기본적인 형태는 다음과 같다.

@SpringBootApplication
public class Application {

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

여기서 @SpringBootApplication는 세개의 어노테이션을 하나로 합쳐놓은 형태다.

  • @SpringBootConfiguration
  • @ComponentScan
  • @EnableAutoConfiguration

스프링 부트 어플리케이션은 Bean을 두 단계에 걸쳐 등록한다.

1단계 : ComponentScan으로 등록하고

2단계 : EnableAutoConfiguration으로 추가적으로 읽어온 Bean들을 읽어서 등록한다.

@ComponentScan의 역할

@ComponentScan은 자기 자신부터 시작해서, 하위 패키지를 싹 훑어서 @Component라는 어노테이션을 붙인 클래스들을 찾아서 Bean으로 등록한다. 구체적인 검색 대상은 아래와 같다.

  • @Configuration @Repository @Service @Controller @RestController

@EnableAutoConfiguration의 역할

Configuration는 Bean을 읽어들이기 위한 조건 등이 정의된 설정 파일이다. org.springframework.boot.autoconfigure라는 패키지에서, spring.factories라는 파일을 찾아보자.

이 파일에서 EnableAutoConfiguration라는 key 하단에 정의된 수많은 XXXConfiguration들이 모두 자동 설정 대상에 해당된다.

예시로 WebMvcAutoConfiguration이라는 설정 파일을 열어보자.

@Configuration
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
    ...
}

@ConditionalXXX 형식의 어노테이션들은 어떤 경우에 객체를 Bean으로 등록할 건지 조건을 등록할 때 사용한다. 프로그램이 실행된 순간 수많은 자동 설정이 조건에 따라 적용돼서, 많은 Bean들이 자동으로 생성되고 어플리케이션이 구동되게 된다.

정리

  1. @SpringBootApplication이 붙은 어플리케이션을 실행
  2. @Component 어노테이션이 있는 클래스들을 스캔해서 Bean으로 등록 (Bean 등록 1단계)
  3. @EnableAutoConfiguration에 의해 spring.factories 안에 들어있는 수많은 자동 설정이 조건에 따라 적용 (Bean 등록 2단계)
  4. 많은 Bean들이 자동으로 생성되고 어플리케이션이 구동되게 된다.

Starter & AutoConfigure로 자동 설정 구현하기

  1. 먼저 pom.xml에서 의존성 설정을 한다.

     <?xml version="1.0" encoding="UTF-8"?>
     <project xmlns="http://maven.apache.org/POM/4.0.0"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
         <modelVersion>4.0.0</modelVersion>
    
         <groupId>spring.example</groupId>
         <artifactId>spring-boot</artifactId>
         <version>1.0-SNAPSHOT</version>
    
         <!--프로젝트에 필요한 의존성-->
         <dependencies>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-autoconfigure</artifactId>
             </dependency>
             <dependency>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-autoconfigure-processor</artifactId>
                 <optional>true</optional>
             </dependency>
         </dependencies>
    
         <!--의존성 관리에 필요한 영역-->
         <dependencyManagement>
             <dependencies>
                 <dependency>
                     <groupId>org.springframework.boot</groupId>
                     <artifactId>spring-boot-dependencies</artifactId>
                     <version>2.0.3.RELEASE</version>
                     <type>pom</type>
                     <scope>import</scope>
                 </dependency>
             </dependencies>
         </dependencyManagement>
     </project>
  2. Configutation 대상이 되는 클래스를 작성한다.

     package home;
    
     public class Person {
         private String name;
         private int age;
    
         public String getName() {
             return name;
         }
    
         public void setName(String name) {
             this.name = name;
         }
    
         public int getAge() {
             return age;
         }
    
         public void setAge(int age) {
             this.age = age;
         }
    
         @Override
         public String toString() {
             return "Person{" +
                     "name='" + name + '\'' +
                     ", age=" + age +
                     '}';
         }
     }
  3. Person 클래스의 설정 파일을 작성한다.

     package home;
    
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
    
     @Configuration
     public class PersonConfiguration {
    
         @Bean
         public Person person() {
             Person person = new Person();
             person.setAge(10);
             person.setName("코알라일락");
             return person;
         }
     }
  4. resources 폴더 하단에 META-INF라는 이름의 폴더를 하나 만들고, 그 안에 spring.factories라는 이름의 빈 파일을 하나 만들어주자.

  1. key에 해당되는 값을 읽어올 수 있도록 spring.factories에 방금 작성한 자동 설정 파일을 추가한다.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    home.PersonConfiguration
  1. mvn install 명령어를 실행하자. 빌드가 끝나면 jar 파일이 생성되고, 이 파일은 다른 maven 프로젝트에서도 사용할 수있도록 로컬 maven 저장소에 설치된다.

그런데... mvn install을 실행했을 때 이런 에러가 떴다. 원인은 아마 내가 brew로 maven을 설치했기 때문인듯.

[ERROR] Source option 5 is no longer supported. Use 7 or later.
[ERROR] Target option 5 is no longer supported. Use 7 or later.

pom.xml에 아래의 태그를 작성하면 해결된다.

<properties>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
</properties>

자동 설정이 적용되고 있는지 로그로 확인해보자. ApplicationRunner를 활용해서 Bean을 출력하는 코드를 작성했다.

package home;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class PersonRunner implements ApplicationRunner {

    @Autowired
    Person person;

    public void run(ApplicationArguments args) throws Exception {
        System.out.println(person);
    }
}

person 객체가 잘 주입되어 로그가 찍히는 것을 확인할 수 있다.

이 경우 발생할 수 있는 문제

@SpringBootApplication
public class Application {

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

    @Bean
    public Person person() {
        Person person = new Person();
        person.setAge(20);
        person.setName("개발자");
        return person;
    }
}

만약 이런식으로 Application에서 동일한 Person 객체를 Bean으로 등록했다면, @AutoConfiguration이 우선순위가 더 높아서 개발자가 직접 등록한 Bean이 적용되지 않는다. 다시 프로젝트를 실행해 보면 로그는 여전히 Person{name='코알라일락', age=10}로 출력된다.

@ConfigurationProperties로 자동 설정 구현하기

위에서 발생한 문제를 해결하려면 컴포넌트 스캔으로 읽은 Bean이 항상 우선순위가 더 높아야 한다. 이를 위해 @ConditionalOnMissingBean 어노테이션을 사용한다.

컴포넌트 스캔 단계에서 이미 등록된 Bean이라면, AutoConfiguration 단계에선 Bean으로 등록하지 않는다.

@Configuration
public class PersonConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public Person person() {
        Person person = new Person();
        person.setAge(10);
        person.setName("코알라일락");
        return person;
    }
}

다시 한 번 실행해보면 Application에서 직접 등록한 Bean이 출력된다. 이걸 활용하면 스프링에서 제공하는 각종 기능을 내 입맛대로 바꿀 수 있게된다.

나에게 필요한 Bean을 하나하나 모두 통째로 생성해서 등록하는 작업은 번거롭다. Bean을 직접 정의하지 않고 properties만 정의해서 간편하게 갖다 쓰는 방법이 있다.

  1. resources 폴더 하단에 application.properties라는 파일을 하나 만든다.

  1. application.properties안에 Bean에 설정할 값을 key-value 형식으로 작성한다.

     person.name = koalailac
     person.age = 30
  2. @ConfigurationProperties("prefix 이름") 어노테이션을 붙인 Properties 파일을 만든다.

     @ConfigurationProperties("person")
     public class PersonProperties {
         private String name;
         private int age;
    
         public String getName() {
             return name;
         }
    
         public void setName(String name) {
             this.name = name;
         }
    
         public int getAge() {
             return age;
         }
    
         public void setAge(int age) {
             this.age = age;
         }
     }
  3. Configuration metadata file를 생성할 수 있도록 pom.xml의 <dependencies></dependencies> 태그 내부에 새로운 의존성을 추가한다. (참고)

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-configuration-processor</artifactId>
         <optional>true</optional>
     </dependency>
  4. properties 파일을 읽어와 값을 사용할 수 있도록 Configuration 파일에 @EnableConfigurationProperties 어노테이션을 붙인다.

     @Configuration
     @EnableConfigurationProperties(PersonProperties.class)
     public class PersonConfiguration {
    
         @Bean
         @ConditionalOnMissingBean
         public Person person(PersonProperties properties) {
             Person person = new Person();
             person.setAge(properties.getAge()); // properties value를 가져옴
             person.setName(properties.getName()); // properties value를 가져옴
             return person;
         }
     }

로컬 저장소의 jar 파일에 변경사항이 반영되도록 다시 한 번 mvn install을 실행해야 한다.
다시 빌드해보면 properties에 작성한 값이 로그에 찍히는 것을 확인할 수 있다.