스프링에서는 Profile을 지정하여 어플리케이션의 구성(Configuration)을 Profile별로 가져갈 수 있도록 한다. 즉, 같은 소스코드를 두고 로컬환경, 개발환경, 운영환경별로 다른 구성(DB 접속정보, 업로드 파일의 저장위치 등)으로 실행할 수 있다.


@Profile

다르게 가져갈 구성은 @Component, @Configuration, @ConfigurationProperties 애노테이션에 @Profile을 마킹하여 지정하고, 어플리케이션 실행시 spring.profiles.active 프로퍼티로 프로파일을 지정하면 해당 프로파일이 적용된 빈만 등록된다.

아래는 test 프로파일을 적용해본 예시이다. @Component를 마킹하여 빈으로 등록될 클래스에 @Profile을 마킹했다. 이러면 정의한 프로파일에서만 빈으로 등록된다.

@Component
@Profile("test") // 이 클래스는 [test]프로파일에서만 빈으로 등록된다.
public class TestBean {
}
@Component
public class Sample implements ApplicationRunner {
    @Autowired(required = false)
    TestBean testBean;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("=====================================================");
        System.out.println("test 프로파일에서만 등록되는 빈 : " + testBean);
        System.out.println("=====================================================");
    }
}

테스트시 --spring.profiles.active=test 옵션을 주고 실행시 다음과 같이 콘솔에 남으며 TestBean 빈이 null 값이 아니라 등록 되었음을 확인할 수 있다.

=====================================================
test 프로파일에서만 등록되는 빈 : com.yang.wind.TestBean@50fb33a
=====================================================

만일 --spring.profiles.active=test 옵션을 빼고 실행하여 디폴트 프로파일로 실행하거나 --spring.profiles.active=dev 처럼 다른 프로파일을 지정하면 null 값으로 콘솔에 남는다.


Profile - Property File

스프링부트는 활성화 된 프로파일에 따라서 지정된 프로퍼티 파일을 읽는다.
즉 어떠한 프로파일도 활성화 하지 않으면 디폴트 프로퍼티 파일로 application.properties를 읽지만 test 프로파일을 활성화 하면 프로퍼티 파일로 application-test.properties를 읽어온다. 하지만 application-test.properties만 읽는것이 아니라 application.properties 파일도 같이 읽으며 중복된 값이 profile이 정의된 파일로 오버라이드 된다.

  • XXX 프로파일 활성화시 설정파일 : application.properties, application.yml, application-XXX.properties, application-XXX.yml

프로파일에 따라 어떻게 설정파일을 읽는지 알기위해 간단한 테스트를 해본다.

3개의 프로퍼티 파일을 작성 후 활성화한 프로파일에 따라 어떻게 값이 오버라이드 되는지 알아본다.

# application.yml 파일 -> 프로파일이 설정되지 않음. 우선순위가 낮아 프로파일 설정시 오버라이드 된다.

profile-common:
  name: defaultnameCommon

defaultonly:
  name: defualtname
# application-dev.yml 파일 -> dev 프로파일이 활성화시 우선순위가 높은 프로퍼티 파일

profile-common:
  name: devnameCommon

devonly:
  name: devname
# application-test.yml -> test 프로파일이 활성화시 우선순위가 높은 프로퍼티 파일

profile-common:
  name: testnameCommon

testonly:
  name: testname
  • profile-common.name는 모든 프로퍼티 파일이 공통으로 가진 프로퍼티이고, 각 파일마다 개별로 가지고있는 프로퍼티도 있다.
  • test 프로파일을 활성화하고 어플리케이션을 기동하면 공통속성인 profile-common.name은 프로파일 프로퍼티 파일(application-test.yml)이 나머지를 오버라이드하여 testnameCommon이 값이 된다.
  • application-test.yml 뿐만 아니라 디폴트 프로파일 파일인 application.yml 또한 읽으므로 defaultonly.name은 defaultname이 값이 된다.
  • 활성화 되지 않은 application-dev.yml 파일의 프로퍼티들은 읽지 않는다.

간단한 ApplicationRunner를 작성하여 프로퍼티 값을 콘솔에 찍어본다.

@Component
public class Sample implements ApplicationRunner {

    @Autowired
    Environment env;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("=====================================================");
        System.out.println("spring.profiles.active : " + args.getOptionValues("spring.profiles.active"));
        System.out.println("Enviroment's Active Profile : " + Arrays.toString(env.getActiveProfiles()));
        System.out.println("defaultonly.name : " + env.getProperty("devonly.name"));
        System.out.println("testonly.name : " + env.getProperty("testonly.name"));
        System.out.println("devonly.name : " + env.getProperty("devonly.name"));
        System.out.println("profile-common.name : " + env.getProperty("profile-common.name"));
        System.out.println("=====================================================");
    }
}
=====================================================
spring.profiles.active : [test]
Enviroment's Active Profile : [test]
defaultonly.name : defualtname
testonly.name : testname
devonly.name : null
profile-common.name : testnameCommon
=====================================================

 

 

 

외부설정(Externalized Configuration)

스프링부트는 외부설정을 통해 같은 소스코드로 각기 다른 환경에서 실행될 수 있도록 해준다.
코드의 외부설정으로는 properties 파일, YAML 파일, 환경변수, 커맨드라인 아규먼트 등으로 설정이 가능하다.

 

외부설정(Property)은 다음과 같이 소스코드에서 사용할 수 있다.

  • @Value : 소스코드에서 바로 프로퍼티에 주입받아 사용
  • Environment : 외부설정이 바인딩 된 Environment 를 주입받아 사용
  • @ConfigurationProperties : 외부설정이 바인딩 될 Bean(객체)을 생성하여 사용

외부설정을 할 수 있는 방법이 여러 가지가 있으므로 이들 사이에 우선순위도 존재한다.
같은 프로퍼티를 여기저기서 설정했다면 정의된 우선순위에 따라서 가장 높은 우선순위가 나머지를 오버라이드 한다.

 

스프링부트 공식문서에 소개된 우선순위는 17가지이므로 우선순위에 대한 개념만 알고 넘어가며 상황에 따라 찾아보면 될 듯하다.
기본적인 classpath 이하에 application.properties 파일을 두는 것은 우선순위가 15위로 하위권이다. 당연히 3순위인 Command line argument를 부여하면 오버라이드 된다.
예를 들어application.properties foo = bar1로 작성하고 java -jar myproject.jar --foo=bar2 로 애플리케이션을 실행하면 애플리케이션에서 foo에 맵핑된 프로퍼티 값은 ‘bar2’가 된다.

- 외부설정 및 우선순위에 대한 스프링 문서 :

https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/html/spring-boot-features.html#boot-features-external-config

 

Spring Boot Features

If you need to call remote REST services from your application, you can use the Spring Framework’s RestTemplate class. Since RestTemplate instances often need to be customized before being used, Spring Boot does not provide any single auto-configured Res

docs.spring.io


외부 프로퍼티 파일

여러 가지 방법으로 외부설정이 가능하다 하더라도 가장 많이 사용할 외부설정 방식은 프로퍼티 파일일 것이다.
주로 classpath:/application.properties 경로에 설정파일을 두고 사용하고 있지만 여러 경로에 두는 것이 가능하다.

프로퍼티 파일의 위치에 따라서 정의된 우선순위가 있다. 마찬가지로 같은 프로퍼티가 여러 파일에 정의되어 있다면 우선순위가 높은 경로에 있는 파일이 오버라이드 한다.

공식문서에서 소개하는 우선순위이다. classpath 바로 이하에 두는 것은 우선순위가 제일 낮은 6위이다. classpath 이하에 디폴트 설정 파일을 두고 필요에 따라 실제 프로퍼티 값을 가진 파일을 상위 우선순위 위치에 두는 전략을 생각해볼 수 있다.

  1. file:./custom-config/
  2. classpath:custom-config/
  3. file:./config/
  4. file:./
  5. classpath:/config/
  6. classpath:/

위 경로 중에custom-config 디렉터리는 개발자가 직접 설정이 가능한 경로라는 의미이다.
spring.config.name와 spring.config.location 프로퍼티로 커스텀한 파일경로를 지정한다. 프로퍼티 파일경로에 대한 설정은 기동시 이른 시점에 결정되어야 하므로 OS환경변수나 시스템 프로퍼티 또는 커맨드라인 아규먼트로 지정해줘야 한다.
아래는 커맨드라인 아규먼트로 지정한 예시이다. 경로(파일도 가능)는 컴마(,)를 구분자로 여러 개를 지정할 수 있다.

 

spring.config.name 프로퍼티로 파일명만 지정한다.

$ java -jar myproject.jar --spring.config.name=myproject

 

spring.config.location 프로퍼티로 경로와 파일명까지 지정한다.

$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

요약하면

  • 스프링부트는 코드 밖에서 외부 프로퍼티 설정이 가능하다.
  • 외부는 properties 파일, YAML 파일, 환경변수, 커맨드라인 아규먼트 등을 말하며 이들 사이에는 우선순위가 있어 프로퍼티 값이 오버라이드 된다.
  • 외부설정 중 많이 쓰이는 ‘파일 외부설정’은 파일이 위치하는 경로에 따라 우선순위가 있어 프로퍼티 값이 오버라이드 된다.
  • 파일 외부설정시 디폴트 파일명, 경로 외에 spring.config.name, spring.config.location 프로퍼티 설정으로 커스텀한 파일명 또는 경로를 지정할 수 있다.

Property Binding

스프링부트 프로젝트를 생성하면 외부 프로퍼티 설정파일을 만들게 된다.(application.yml, application.properties ...) 여기 정의한 키-값들이 결국 어플리케이션이 기동되면서 클래스로 바인딩 되는데 이 과정에 대해 알아본다.

 

먼저 클래스에서 외부 프로퍼티 값들을 가져오는 방식을 살펴보자. 아래 3가지를 이용할 수 있다.

  • @Value : 소스코드에서 바로 프로퍼티에 주입받아 사용
  • Environment : 외부설정이 바인딩 된 Environment 빈을 주입받아 사용
  • @ConfigurationProperties : 외부설정이 바인딩 될 Bean(객체)을 생성하여 사용

가장 빈번하게 사용하는 방식은 @Value("${property}") 방식일 듯 싶지만 불러올 프로퍼티의 갯수가 많거나, 계층형의 구조를 가지고 있는 경우 등에는 적절하지 않다. 또한 프로퍼티명이나 타입을 직접 입력해야 하므로 오류에 대한 여지도 있다. @Value와 Environment 빈에 대한 예시는 아래와 같다.

application.properties에 다음과 같이 프로퍼티 키-값 정의 : yangs.name=brand

@Component
public class SimpleRunner implements ApplicationRunner {
    @Autowired
    Environment environment; // Environment 타입의 빈 주입

    @Value("${yangs.name}") // ${}내부에 작성된 프로퍼티키에 해당하는 값을 세팅
    String name1; 

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("Value 애노테이션의 yangs.name : " + name1);

        String name2 = environment.getProperty("yangs.name");
        System.out.println("Environment bean의 yangs.name : " + name2);
    }
}

// 아래는 콘솔 출력결과
Value 애노테이션의 yangs.name : brand

Environment bean yangs.name : brand


간단한 몇 개의 값이라면 @Value를 사용하여 바인딩할 수 있지만, 검증이나 Type-safe, 계층형 구조 등을 처리할 수 있는 @ConfigurationProperties를 이용한 방법이 있다,

application.yml의 예시와 이 값을 @ConfigurationProperties를 어떻게 사용하여 바인딩 하는지 예제로 살펴본다.


application.yml 예시

일반적으로 프로퍼티명은 아래 is-main-tester처럼 소문자와 대쉬(-)를 구분자로 하는 명칭이 권장된다.
프로퍼티명 앞의 대쉬(-)는 리스트형을 나타낼때 사용한다.

test:
  name: tester
  cnt: 99
  is-main-tester: true
  sub-testers:
    - name: sub-tester1
       cnt: 11
       is-main-tester: false
    - name: sub-tester2
       cnt: 22
       is-main-tester: false


@ConfigurationProperties(prefix = “test”) 로 프로퍼티클래스에 값 바인딩하기

프로퍼티가 맵핑될 클래스(아래의 TesterProperties)를 작성하고 @ConfigurationProperties을 마킹한다. 이 클래스는 프로퍼티값이 맵핑될 클래스라는 의미로 애노테이션에는 프로퍼티명의 prefix(아래의 prefix = "test")를 지정할 수 있다.
프로퍼티 파일에서 리스트형으로 작성한 sub-testers를 맵핑할 수 있도록 List<Subster> subTesters 로 선언한 것을 볼 수 있고,
작성된 위의 프로퍼티 파일은 kebab case(- 가 구분자)로 작성하였으나 클래스 변수명은 camel case이다.
스프링부트에서는 프로퍼티명의 camel case, kebab case, underscore notation 간의 변환을 모두 지원한다.
subTesters - sub-testers - sub_testers 모두 변환하여 바인딩이 가능하다.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

// 읽어들인 프로퍼티 중 해당 prefix의 프로퍼티값을 바인딩한다.
@ConfigurationProperties(prefix = "test")
public class TesterProperties {
    private String name;
    private int cnt;
    private boolean isMainTester;
    private List<SubTester> subTesters = new ArrayList<>();

    // Getter, Setter, ToString 생략
    
    static class SubTester {
        private String name;
        private int cnt;
        private boolean isMainTester;
            
        // Getter, Setter, ToString 생략
    }
}

@EnableConfigurationProperties(TesterProperties.class)로 프로퍼티클래스 사용하기

바인딩이 완료된 TesterProperties를 빈으로 등록하여 사용하기 위해 @EnableConfigurationProperties를 마킹한다.
주로 프로퍼티 값을 사용할 @Configuration 파일에 마킹하며, @ConfigurationProperties가 마킹된 클래스를 빈으로 등록해주는 역할이다. 만일 프로퍼티 클래스가 다수라면 @ConfigurationPropertiesScan({"base.package1", "base.package2"}) 으로 여러 패키지를 스캔하여 등록할 수도 있다.
프로퍼티가 제대로 바인딩 됬는지 테스트를 위해 ApplicationRunner를 작성 후 콘솔로그를 찍어본다.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@EnableConfigurationProperties(TesterProperties.class)
public class PropertyRunner implements ApplicationRunner {
    private final TesterProperties testerProperties; // 값이 바인딩된 빈이 의존성 주입된다.

    public PropertyRunner(TesterProperties testerProperties) {
        this.testerProperties = testerProperties;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 바인딩이 제대로 되었는지 콘솔 출력
        System.out.println("=================================");
        System.out.println("TesterProperties : " + testerProperties);
        System.out.println("=================================");
    }
}

=================================
TesterProperties : TesterProperties{name='tester', cnt=99, isMainTester=true, 
subTesters=[SubTester{name='sub-tester1', cnt=11, isMainTester=false}, 
                    SubTester{name='sub-tester2', cnt=22, isMainTester=false}]}
=================================

String, int, boolean 및 List 형까지 바인딩이 제대로 된 것을 확인할 수 있다. 혹여 바인딩시 Validation을 하고 싶다면 아래와 같이 스프링의 @Validated 와 JSR-303에 정의된 javax.validation 애노테이션을 사용할 수 있다.

@ConfigurationProperties(prefix = "test")
@Validated // 검증대상임을 표시
public class TesterProperties {
    @NotNull
    private String name;
    @Max(90)
    private int cnt;
    private boolean isMainTester;
    @Valid // 내부 프로퍼티에 검증이 적용되려면 @Valid를 마킹한다.
    private List<SubTester> subTesters = new ArrayList<>();

    static class SubTester {
        @NotNull
        private String name;
        @Min(20)
        private int cnt;
        private boolean isMainTester;
    }
    
    // Getter, Setter, ToString 생략
}

요약하면

  • 외부에서 설정된 프로퍼티 파일을 코드에서 사용하는 방법은 여러가지가 있다.
  • 만일 읽어올 프로퍼티가 많거나 계층형이라면 객체에 바인딩하여 사용하는 것을 고려하자.
  • 프로퍼티값을 객체에 바인딩하여 빈으로 등록 후 사용을 위해서는 @ConfigurationProperties(prefix = "test"), @EnableConfigurationProperties, @ConfigurationPropertiesScan({"base.package1", "base.package2"}) 같은 애노테이션을 조합한다.
  • 프로퍼티 바인딩시 검증을 위해서는 @Validated와 javax.validation 애노테이션을 사용한다.

SpringApplication이 시작한 시점에 특정 코드를 실행하고자 한다면, ApplicationRunner나 CommandLineRunner를 활용할 수 있다.
두 개의 Runner 인터페이스 중 하나를 선택하여 implements하여 메서드를 오버라이드하고, 빈으로 등록해주면 사전 준비는 완료된다.


ApplicationRunner

run(ApplicationArguments args) 메서드 하나만 오버라이드하면 되며 파라미터인 ApplicationArguments에 바인딩된 어플리케이션 아규먼트들에 접근이 가능하다.
ApplicationArguments는 여러가지 편리한 메서드를 제공하고 있다. 그러므로 아래에서 설명할 CommandLineRunner보다 사용이 권장된다. 아래는 커맨드로 Option, Non-Option Argument를 설정 후 기동 했을 때의 테스트 코드이다.

어플리케이션 기동 커맨드 : java -jar MySpringBootApp.jar alone1 alone2 --key1=value1 --key2=value2

  @Component
  public class SimpleApplicationRunner implements ApplicationRunner {
      @Override
      public void run(ApplicationArguments args) {
          System.out.println("======================================================");
          System.out.println("ApplicationRunner - ApplicationArguments ");
          System.out.println("NonOption Arguments : " + args.getNonOptionArgs());
          System.out.println("Option Arguments Names : " + args.getOptionNames());
          System.out.println("key1의 value : " + args.getOptionValues("key1"));
          System.out.println("key2의 value : " + args.getOptionValues("key2"));
          System.out.println("======================================================");
      }
  }
======================================================
ApplicationRunner - ApplicationArguments
NonOption Arguments : [alone1, alone2]
Option Arguments Names : [key1, key2]
key1의 value : [value1]
key2의 value : [value2]
======================================================

CommandLineRunner

ApplicationRunner와 기능은 같지만 전달되는 아규먼트가 더 불친절하다. 그리고 Option과 Non-Option 아규먼트의 구분도 없고 아규먼트도 단순 배열로 전달된다.
위와 동일한 커맨드로 기동 했을 때의 코드와 로그이다. 

어플리케이션 기동 커맨드 : java -jar MySpringBootApp.jar alone1 alone2 --key1=value1 --key2=value2

  @Component
  public class SimpleCommandLineRunner implements CommandLineRunner {
      @Override
      public void run(String... args) throws Exception {
          System.out.println("======================================================");
          System.out.println("CommandLineRunner - String... ");
          for(String s : args){
              System.out.println("argument : " + s);
          }
          System.out.println("======================================================");
      }
  }
======================================================
CommandLineRunner - String...
argument : alone1
argument : alone2
argument : --key1=value1
argument : --key2=value2
======================================================

 

SpringApplication은 main() 메서드에서 시작되는 스프링 어플리케이션의 초기 설정을 제어할 수 있는 클래스이다.
일반적으로는 main() 메서드 안에서 어플리케이션의 실행을 SpringApplication.run() 메서드에게 위임하거나, SpringApplication 객체를 만들어 run() 메서드를 실행할 수 있다.

public static void main(String[] args) {
    SpringApplication.run(MySpringConfiguration.class, args); // static 메서드로 기동
}
public static void main(String[] args) {
    SpringApplication app = new SpringApplication(App.class); // 객체를 생성하여 기동
    app.run(args);
}

 

SpringApplication이 초기화 되면서 어플리케이션이 구동될 때 발생하는 이벤트나 에러시 동작하는 FailureAnalyzer 등에 대해 먼저 알아본다. 


FailureAnalyzers

어플리케이션 실행시 오류가 발생한 경우에 동작하며, 에러에 대한 내용과 에러를 고치기 위한 Action을 출력한다.
이미 많은 FailureAnalyzers가 등록되어 있고, 추가도 가능하다.

***************************
APPLICATION FAILED TO START
***************************

Description:
Embedded servlet container failed to start. Port 8080 was already in use.

Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

Application Events and Listeners

스프링에서 제공하는 ContextRefreshedEvent 외에도 스프링부트의 SpringApplication은 추가적인 어플리케이션 이벤트들을 보낸다.
어플리케이션 기동이 시작되면 각 단계에 따라서 ApplicationStartingEvent를 시작으로 이벤트들이 발생하고, 발생된 이벤트들은 이벤트리스너에 전달된다. 각 단계별로 어떤 이벤트가 발생하는지는 아래 스프링부트 공식문서를 참고하자.

https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners

 

이벤트리스너가 빈으로 등록되어 있고, 리스너의 표적 이벤트가 발생하면 자동으로 처리메서드가 실행된다.
아래 코드와 로그는 빈으로 등록된 ApplicationStartedEvent리스너가 어플리케이션 기동시 출력된 로그이다.
SampleListener 클래스가 @Component 애노테이션으로 빈등록 되었으며, 기동시 ApplicationStartedEvent 이벤트가 발생하면서 자동으로 onApplicationEvent() 메서드가 호출된다.

@Component // 빈으로 등록한다.
public class SampleListener implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("ApplicationStartedEvent가 발생!");
    }
}

어플리케이션 기동시 마지막에 콘솔로그가 출력된다. (ApplicationStartedEvent가 기동시 제일 마지막에 발생하기 때문이다.)

2019-12-26 16:09:49.823 INFO 4212 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-12-26 16:09:49.828 INFO 4212 --- [ main] com.yang.wind.App : Started App in 2.1 seconds (JVM running for 3.235)
ApplicationStartedEvent가 발생!

하지만 ApplicationStartingEvent의 경우 ApplicationContext가 초기화 되기 전에 발생하는 이벤트기 때문에, 빈 등록으로는 리스너가 동작하지 않는다.(ApplicationContext에 빈이 등록되는데, 이 과정보다 먼저 이벤트가 발생하므로 이벤트 발생시점에는 리스너가 있는지도 모르는 상태이다.) 이런 경우에는 SpringApplication addListeners() 메서드로 리스너를 추가하면 된다.
아래 코드는 SpringApplication.run()으로 바로 시작하지 않고, 객체 생성을 통해 설정을 추가했다.

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(App.class);
        app.addListeners(new SampleListener()); // 리스너를 코드로 추가한다.
        app.run(args);
    }
}
// 이미 SpringApplication에 리스너를 등록했으므로 빈등록이 필요없다.
public class SampleListener implements ApplicationListener<ApplicationStartingEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
        System.out.println("ApplicationStartingEvent 발생!");
    }
}

어플리케이션 기동시 처음에 콘솔로그가 출력된다. 로그를 복붙하다보니 좀 깨지지만 그건 중요한게 아니다.

ApplicationStartingEvent 발생!

  .   ____          _            __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )

  '  |____| .__|_| |_|_| |_\__, | / / / /

 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::        (v2.2.0.RELEASE)

....


WebApplicationType

SpringApplication에서는 ApplicationContext의 타입이 내부적으로 확정된다.
3개의 타입이 존재하는데 다음과 같이 자동설정되며 SpringApplication의 setWebApplicationType() 메서드를 이용하여 코드에서 특정타입으로 오버라이드가 가능하다.

  • WebApplicationType.SERVLET : Spring MVC가 존재하면 설정됨
  • WebApplicationType.REACTIVE : Spring MVC가 없고, Spring WebFlux가 존재하면 설정됨
  • WebApplicationType.NONE : 위의 경우에 해당이 안되면 설정됨. 어플리케이션이 기동 되었다가 바로 종료된다.
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(App.class);
        // ApplicationType 설정을 오버라이드 한다.
        app.setWebApplicationType(WebApplicationType.SERVLET);
        app.run(args);
    }
}

스프링부트에서 의존성을 추가하고 싶을 때 주로 스프링부트에서 제공하는 starter 라이브러리를 의존성 설정파일에 추가한다.
예를들어 Mybatis 라이브러리를 추가하고자 하면 제공되는 starter 라이브러리를 의존성설정파일(메이븐의 경우 pom.xml)에 아래처럼 추가한다. starter 라이브러리는 단순히 의존성만 추가되는 것이 아니라 자동설정을 포함 하는데, 이것에 대해 자세히 알아본다.

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.1</version>
</dependency>

mybatis starter 라이브러리를 기준으로 포스팅할 예정이며 다른 라이브러리도 비슷하다.


mybatis-spring-boot-starter

스프링부트 프로젝트의 pom.xml에 추가한 mybatis-spring-boot-starter 자체에는 많은 파일이 있지는 않다.
META-INF 폴더에 pom.xml과 pom.properties 파일만 있는 정도이다.
아래는 mybatis-spring-boot-starter의 파일구성과 pom.xml이다. starter의 pom.xml에는 사용될 다른 라이브러리와 autoconfigure(mybatis-spring-boot-autoconfigure) 라이브러리가 포함되어 있는 것을 볼 수 있다.

mybatis-spring-boot-starter

<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>
  <parent>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot</artifactId>
    <version>2.1.1</version>
  </parent>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <name>mybatis-spring-boot-starter</name>
  <properties>
    <module.name>org.mybatis.spring.boot.starter</module.name>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
    </dependency>
  </dependencies>
</project>

mybatis-spring-boot-autoconfigure

pom.xml 중간에 추가된 autoconfigure는 자동설정 클래스를 포함하고 있다.(spring.factories  XxxAutoConfiguration.class 포함)

스프링부트 프로젝트 기동시 메인 클래스의 @EnableAutoConfiguration 애노테이션이 spring.factories 파일을 읽어들여 자동설정을 진행하는데 자세한 내용은 이전 포스팅을 참고하자.

이전 포스팅 링크 :

2020/12/01 - [Dev/스프링부트] - @SpringBootApplication에 대해서(Springboot 기본구조)

 

@SpringBootApplication에 대해서(Springboot 기본구조)

스프링부트 기본 프로젝트를 생성하면 main메서드를 포함하는 시작 클래스가 있고, 이 클래스에는 @SpringBootApplication이 마킹되어 있다. @SpringBootApplication는 다수의 애노테이션으로 이루어진 메타

yangbox.tistory.com

mybatis-spring-boot-autoconfigure

mybatis-spring-boot-autoconfigure의 내용(위 사진)을 보면 MybatisAutoConfiguration 자동설정클래스도 보이고, MybatisProperties 프로퍼티 클래스도 보인다. 이 중 프로퍼티 클래스에 대해 좀 더 살펴보면 보통 어플리케이션에 프로퍼티 설정을 위해 application.yml, application.properties와 같은 파일을 두고 키-값으로 프로퍼티를 작성한다. 이 값들을 스프링부트가 읽어 들여 변수에 세팅하는 클래스가 MybatisProperties 같은 프로퍼티 클래스이다.

좀 더 자세히 보자. MybatisProperties의 소스코드 일부이다. @ConfigurationProperties는 외부 프로퍼티를 사용하는 클래스라는 의미이고 prefix값을 지정할 수 있다. 예를들어 application.properties에서 'mybatis'로 시작하는 프로퍼티를 작성하면 읽어 들여 클래스의 변수에 바인딩된다.
mybatis.configLocation 프로퍼티 작성 -> MybatisProperties 클래스의 configLocation 변수에 바인딩

  @ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
  public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";
    private String configLocation; // 프로퍼티 설정파일에서 값을 읽어들여 바인딩된다.
    // 생략
  }

변수가 바인딩된 프로퍼티 클래스를 AutoConfiguration 자동설정 클래스에서 @EnableConfigurationProperties(MybatisProperties.class) 애노테이션을 마킹하여 바인딩된 변수(프로퍼티 설정파일의 값)를 이용하여 자동설정 한다.

  @org.springframework.context.annotation.Configuration
  @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
  @ConditionalOnSingleCandidate(DataSource.class)
  @EnableConfigurationProperties(MybatisProperties.class)
  @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
  public class MybatisAutoConfiguration implements InitializingBean {
      private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);
      private final MybatisProperties properties; // 바인딩된 properties
      // 생략
  }

요약

스프링부트에서 의존성을 추가하고 싶다면 제공되는 stater가 있는지 알아본다.
starter는 단순히 라이브러리를 추가하는 것에 더해서 autoconfigure 라이브러리를 참조한다.
autoconfigure의 spring.factories에 정의된 자동설정 클래스들이 실행되고 스트링부트 프로젝트의 application.properties 같은 프로퍼티 설정파일에서 값을 읽어 들여 자동설정에 사용한다.

 

스프링부트 기본 프로젝트를 생성하면 main메서드를 포함하는 시작 클래스가 있고, 이 클래스에는 @SpringBootApplication이 마킹되어 있다.

@SpringBootApplication는 다수의 애노테이션으로 이루어진 메타애노테이션으로 상세 애노테이션들이 실행되어 빈 등록 및 자동설정을 수행한다. 주요 기능을 수행하는 상세 애노테이션은 3가지다.


@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan

 

아래는 @SpringbootApplication의 코드 일부로 구성하는 3개의 애노테이션을 확인할 수 있다. @SpringbootApplication은 이 3가지 애노테이션을 동작시켜 스프링부트 기동시 기반작업(빈등록 등)을 수행하게 하는 역할이다.

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
	@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	...
}

@SpringBootConfiguration은 그저 자바 설정파일임을 마킹하는 애노테이션이고 아래 두개의 애노테이션이 스프링 Bean등록의 핵심이 된다. Bean 등록은 두 단계로 나뉘어져 진행되는데 다음과 같다.

 

1단계 @ComponentScan : 개발자가 지정한(애노테이션으로 마킹한) 클래스를 빈으로 등록

2단계 @EnableAutoConfiguration : 기타 라이브러리의 클래스를 자동으로 빈 등록

 

하나씩 살펴본다.

@ComponentScan

해당 애노테이션이 마킹된 자바파일의 패키지를 기본패키지로 하위 패키지의 컴포넌트들을 모두 빈으로 등록한다. 빈 등록 대상은 개발자가 애노테이션을 마킹한 클래스들이고, 이 때 마킹에 사용되는 주요 애노테이션은 아래와 같다.

클래스의 성격에 따라 맞는 애노테이션을 마킹한다.

 

- @Component

- @Configuration, @Repository, @Service, @Controller, @RestController 등


@EnableAutoConfiguration

@EnableAutoConfiguration은 @ComponentScan이 동작한 이후 자동설정이 동작한다. 이름에서 알 수 있듯이 라이브러리에 추가한 클래스들을 빈으로 자동등록 및 추가 자동설정을 해준다.

spring-boot-starter 안에는 자동설정을 위해 spring-boot-autoconfigure 라이브러리가 포함되어 있다. @EnableAutoConfiguration도 여기 라이브러리에 포함되어 있고, 다른 주요파일로는 META-INF/spring.factories 파일이 있다.

META-INF 디렉터리에 대해서는 이전에 정리한 글을 참고한다.

2018/04/12 - [Dev/기타] - META-INF 디렉터리에 대하여

spirng-boot-autoconfgure 라이브러리 및 spring.factories 파일의 일부

spring.factories을 열어보면 여러 클래스들이 나열되어 있는데, 그 중 org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티값으로 작성된 AutoConfiguration 클래스들이 모두 자동설정의 대상이 된다. 살펴보면 tomcat 자동설정, DispatcherServlet 자동설정 등 많은 수의 자동설정을 기본으로 제공하는 걸 볼 수 있다.

spring.factories에 작성된 자동설정 대상이 되는 클래스들은 모두 @Cofiguration이 마킹되어 있다. 당연히 스프링부트 기동시 설정파일로 읽어들여 실행된다.

일반적으로 스프링부트 환경에서 A 라이브러리를 추가한다고하면, A라이브러리의 자동설정파일이 spring.factories에 정의되어 있고, 이 자동설정파일을 스프링부트가 기동시 실행하여 빈등록이나 추가 초기화 작업을 수행하게 된다. 라이브러리만 추가했을 뿐인데 빈등록이 되었다면 이 과정이 수행되었을 가능성이 크다.

org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티값이 선언되어 있는 자동설정

AutoConfiguration 클래스에는 @ConditionalOnXxxYyyZzz 과 비슷한 유형의 애노테이션들이 작성되어 있다. 이는 모든 클래스들이 다 적용되는 것이 아니라 자동설정 클래스별로 조건에 따라 자동설정 되거나 무시된다. 예를들어 @ConditionalOnMissingBean 조건에는 특정 빈이 등록되어 있지 않은 경우에만 해당 설정파일이 적용된다.


정리하면 스프링부트의 시작점인 메인 클래스에는 @SpringBootApplication라는 메타 애노테이션이 마킹되어 있다.

이 애노테이션을 통해 개발자가 작성한 클래스들이 자동으로 읽어져 빈으로 등록되고,(@ComponentScan의 기능)

의존성에 추가한 라이브러리의 자동 설정 및 빈등록도 진행된다.(@EnableAutoConfiguration의 기능)

Thread를 구현할 때 Runnable 인터페이스를 구현하거나 Thread 클래스를 상속하여 구현한다. 구현한 Thread를 new MyThread().start() 와 같이 호출하여 직접적으로 실행할 수도 있지만 기본 JDK의 java.util.concurrent패키지의 ExcecutorService를 이용하여 실행할 수도 있다.


ExecutorService

ExecutorService는 재사용이 가능한 ThreadPool로 Executor 인터페이스를 확장하여 Thread의 라이프사이클을 제어한다.
Thread를 활용하여 다수의 작업(Task)들을 비동기로 수행한다는 것은 간단하지 않다. Thread의 라이프사이클(생성과 제거 등)이나 발생할 수 있는 여러가지 low level의 고려사항들이 존재하는데 이를 개발자가 신경쓰지 않도록 편리하게 추상화한 것이 ExecutorService이다.

ExecutorService에 Task(작업)를 지정해주면 가진 ThreadPool을 이용하여 Task를 실행한다. Task는 큐(Queue)로 관리되기 때문에 ThreadPool의 Thread 갯수보다 실행할 Task가 많은경우 미실행된 Task는 큐에 저장되어 실행을 마친 Thread가 생길 때까지 기다린다.

 


ExecutorService 초기화

ExecutorService를 초기화 하는 방법에는 2 가지 방법이 있다.

1. 직접 new 키워드를 사용

ExecutorService는 인터페이스이기 때문에 구현체인 ThreadPoolExecutor를 new키워드로 초기화한다. (필요에 따라 다른 구현체를 초기화해도 된다.)

아래의 초기화 코드에서 10개의 core thread, 10개의 max thread, 0 밀리세크의 keepAliveTime, 작업 큐로는 LinkedBlockingQueue가 초기화되었다. Task(작업)을 위한 Queue에는 Runnable과 Callable 인터페이스를 구현한 클래스를 받을 수 있는데 return값이 있냐(Callable) 없냐(Runnable)에 따라 선택하면 된다.

ExecutorService executorService = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

2. Executors 클래스에서 제공하는 Factory method를 사용

제공되는 3가지의 factory method를 이용한 초기화이다. 메서드명에서 생성되는 ThreadPool의 성향을 유추할 수 있으며 실행하고자 하는 Task에 따라 선택하여 사용한다.

// 1. 10개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newFixedThreadPool(10);

// 2. 1개 고정 사이즈의 ThreadPool 생성
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 3. 유동적으로 증가하고 줄어드는 ThreadPool 생성
ExecutorService executorService = Executors.newCachedThreadPool();

new 키워드를 사용하는 것이 좀 더 세부적인 설정이 가능하지만 Executors를 사용하는 것이 더 간편하다. 대부분의 경우 간편한 설정으로 원하는 작업이 가능하다.


ExecutorService Task 할당

ExecutorService를 초기화 했다면 ThreadPool에 원하는 Task(작업)을 할당해야 한다. 일단 Task를 Callable / Runnable 인터페이스를 구현하여 생성하고, ExecutorService의 메서드를 호출하여 실행한다.

// Runnable 인터페이스로 Task 정의
Runnable runnableTask = () -> {
	try{
		System.out.println(Thread.currentThread().getName() + " start");
		TimeUnit.MILLISECONDS.sleep(500);
		System.out.println(Thread.currentThread().getName() + " end");
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
};

// Callable 인터페이스로 Task 정의
Callable callableTask = () -> {
	TimeUnit.MILLISECONDS.sleep(5000);
	return "Task's execution";
};

// 정의한 Task를 List에 추가
List<Callable> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

 

아래는 위에서 정의한 작업(Task)을 할당(실행)하기 위해서 제공되는 메서드들이다.

 

1. execute() : 리턴타입이 void로 Task의 실행결과나 Task의 상태(실행중 or 실행완료)를 알 수 없다.

executorService.execute(runnableTask);

 

2. submit() : Task를 할당하고 Future 타입의 결과값을 받는다. 결과가 리턴되어야 하므로 주로 Callable을 구현한 Task를 인자로 준다.

Future future = executorService.submit(callableTask);

 

3. invokeAny() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 실행에 성공한 Task 중 하나의 리턴값을 반환한다.

String result = executorService.invokeAny(callableTasks);

 

4. invokeAll() : Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 모든 Task의 리턴값을 List<Future<>> 타입으로 반환한다.

List<Future> futures = executorService.invokeAll(callableTasks);

ExcecutorService 종료

실행 명령한 Task가 모두 수행되어도 ExecutorService는 자동으로 종료되지 않는다. 앞으로 들어올 Task를 처리하기 위해 Thread는 wait 상태로 대기한다. 그러므로 종료를 위해서는 제공되는 shutdown() 이나 shutdownNow() API를 사용해야 한다.

 

1. executorService.shutdown()

실행중인 모든 Task가 수행되면 종료한다.

 

2. List<Runnable> notExecutedTasks = executorService.shutDownNow()

실행중인 Thread들을 즉시 종료시키려고 하지만 모든 Thread가 동시에 종료되는 것을 보장하지는 않고 실행되지 않은 Task를 반환한다.

여기에 추가로 두 개의 shutdown 메서드가 결합된 awaitTermination()을 사용하는 것이 추천된다. 이 메서드는 먼저 새로운 Task가 실행되는 것을 막고, 일정 시간동안 실행중인 Task가 완료되기를 기다린다. 만일 일정 시간동안 처리되지 않은 Task에 대해서는 강제로 종료시킨다.

executorService.shutdown();
try {
	if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
		executorService.shutdownNow();
	} 
} catch (InterruptedException e) {
	executorService.shutdownNow();
}

Future 인터페이스

submit()invokeAll() 메서드를 호출할때 반환하는 Future 객체로 Task의 결과값이나 상태(실행중 또는 실행완료)를 알 수 있다. 또한 Future 인터페이스는 Blocking method인 get()을 제공하는데 Task 실행 결과를 얻을 수 있다.(Runnable을 구현한 Task라면 null이 반환된다.)

Blocking이기 때문에 실행중에 get()이 호출되는 경우 실행이 끝날 때까지 대기한다. 이는 성능저하를 불러올 수 있으므로 Timeout을 설정하여 일정 시간이 지나면 TimeoutException이 발생하도록 유도할 수 있다.

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get(); // Task가 실행중이면 여기서 대기한다.
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// Timeout 설정, 지정된 시간이 지나면 TimeoutException이 발생한다.
String result = future.get(200, TimeUnit.MILLISECONDS); 

이 밖에도 isDone(), cancel(), isCancelled() 메서드가 있다.

boolean isDone = future.isDone(); // Task가 실행되었는지?
boolean canceled = future.cancel(true); // Task를 취소
boolean isCancelled = future.isCancelled(); // Task가 취소되었는지?

'개발 > JAVA' 카테고리의 다른 글

equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
Comparator, Comparable 어떻게 사용할까?  (0) 2020.11.29
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09

자바에서 정렬에 관련된 작업을 할 경우 떠올릴 수 있는 대표적인 인터페이스가 두가지 있다.

Comparator와 Comparable이다.

두 가지 인터페이스의 차이점과 사용법에 대해 알아보자.

 

두 인터페이스가 사용되는 이유는 공통적이다.

Primitive 타입의 숫자나 문자를 비교할 경우 명확한 기본 비교로직이 존재한다. 오름차순이라면 '1'보단 '2'가 크고, '가' 보다는 '나'가 크다. 하지만 비교하는 대상이 객체라면 기본 비교로직으로는 비교가 불가능하다.

예를들어 Game 클래스로 만든 객체들을 정렬한다고 가정한다면 여러가지 정렬을 위한 기준이 있을 수 있다. (발매년도, 타이틀명 등등) 이 때, 정렬 기준에 대한 명확한 로직을 정의할 수 있도록 도와주는 것이 Comparator와 Comparable 인터페이스이다. 

 

하나씩 살펴보자.

 

Comparator

'비교기'라는 이름에서 알 수 있듯이 비교를 위한 보조로직을 따로 작성하여 정렬시 인자로 건네준다.

보조로직 작성은 Comparator의 int compare(obejct args1, object args2) 메서드에 작성한다.

리스트의 정렬은 Collections.sort() 메서드를 사용하는데, 여기의 인자로 전달한다.

public class GameComparatorTest {
    @Test
    public void test1(){
        Comparator<Game> gameComparator = new Comparator<Game>() {
            // 비교를 위한 로직을 작성
            public int compare(Game game1, Game game2) {
                // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
                if(game1.year != game2.year){
                    return Integer.compare(game1.year, game2.year) * (-1);
                }else {
                    return game1.title.compareTo(game2.title);
                }
            }
        };
 
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games, gameComparator);
 
        System.out.println(games);
    }
}
 
class Game {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
}

// 출력결과

[Game{year=2000, title='Assassin Creed'}, Game{year=2000, title='League Of Legends'}, Game{year=1990, title='A Power'}, Game{year=1990, title='Overwatch'}]

발매년도로 내림차순 되었고, 발매년도가 같은경우 타이틀명으로 오름차순 되었다. 

 

 

Comparable

Comparator와 유사하게 비교를 위한 보조로직을 정의하지만 작성하는 위치가 다르다. '비교할 수 있는'이라는 뜻과 같이 비교하고자 하는 클래스에 비교로직을 정의한다. 마찬가지로 Collections.sort() 메서드로 정렬할 수 있고 이 때는 두번째 인자가 필요하지 않는다.

public class GameComparableTest {
    @Test
    public void test1(){
        List<Game> games = new ArrayList<>();
        games.add(new Game(2000"Assassin Creed"));
        games.add(new Game(1990"Overwatch"));
        games.add(new Game(2000"League Of Legends"));
        games.add(new Game(1990"A Power"));
 
        Collections.sort(games);
 
        System.out.println(games);
    }
}
 
class Game implements Comparable<Game> {
    int year;
    String title;
 
    public Game(int year, String title) {
        this.year = year;
        this.title = title;
    }
 
    @Override
    public String toString() {
        return "Game{" +
                "year=" + year +
                ", title='" + title + '\'' +
                '}';
    }
 
    @Override
    public int compareTo(Game game1) {
        // 발매년도는 내림차순으로 하면서, 같은 발매년도에서는 타이틀명을 오름차순으로 하고싶다.
        if(this.year != game1.year){
            return Integer.compare(this.year, game1.year) * (-1);
        }else {
            return this.title.compareTo(game1.title);
        }
    }
}

출력결과는 Comparator의 출력결과와 동일하다.

 

즉, 두 가지 인터페이스 모두 객체 비교를 위한 보조로직을 정의하는데 사용되는 인터페이스이다. 상황에 맞게 선택하여 사용하자.

 

 

 

 

'개발 > JAVA' 카테고리의 다른 글

JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01
LocalDateTime, ZonedDateTime  (0) 2019.10.11
Collections를 이용한 정렬(sort method) 활용  (0) 2017.05.09
BigDecimal & BigInteger  (0) 2017.04.18

목표

  • Maven에 대한 기본개념 이해
  • Maven의 의존성 관리 빌드 자동화 이해

Maven

의존성 관리와 빌드 자동화 기능을 제공하는 툴이며, 프로젝트 내에서 pom.xml을 주설정파일로 하여 동작한다.


의존성 관리

개발시 사용하는 라이브러리에 대해 pom.xml을 기준으로 메이븐 저장소로부터 다운받아 개발자의 프로젝트에 추가해 준다. (로컬저장소의 .m2 폴더에 다운로드 되며 가끔 의존성이 꼬일경우 .m2 폴더 내용을 날려주면 해결되는 경우가 있었다.)

라이브러리 추가를 위해서는 <dependencies/> 이하에 <dependency/> 태그에 작성하며 groupId, artifactId, version 세가지 정보가 필요하다. 보통 maven repository 웹사이트에서 라이브러리를 검색하여 등록한다,

dependency에 <scope>의 경우 compile, runtime, provided, test등이 올 수 있는데 해당 라이브러리가 언제 필요한지, 언제 제외되는지를 나타낸다.


빌드 자동화

메이븐은 자바 프로젝트의 빌드(Build)를 자동화해 주는 빌드 툴(Build Tool)이다. 즉, 자바 소스를 컴파일하고 패키징하여 배포까지 자동으로 해주는 도구이다.

컴파일->패키징->배포 과정(maven build)에 대한 세부 설정값들은 pom.xml에 작성한다.

 

maven build는 LifeCycle이 존재하며, 메이븐에 내장된 라이프사이클은 default, clean, site 3가지가 있다.

라이프사이클은 세부적으로 페이즈(phase)로 구성되어 있다. 예를 들어 default 라이프사이클은 다음과 같이 7개의 페이즈로 구성되며(전부는 아니고 큰 줄기로하면) 이들 사이에는 실행순서가 존재한다.

mvn install 명령어로 install 페이즈를 실행하면 1번 validate 페이즈부터 6번 install 페이즈까지 순차적으로 실행되는 구조다. (deploy는 실행되지 않는다.)

  1. validate
  2. compile
  3. test
  4. package
  5. verify
  6. install
  7. deploy

mvn 명령은 스페이스를 구분자로 여러동작을 한번에 지정할 수 있다.

mvn clean deploy : clean 페이즈 실행 후 1번 페이즈 validate부터 7번 페이즈 deploy까지 실행한다.

 

위의 Build Phase는 다시 Plugin Goal의 모음으로 이루어져 있다. Phase는 구체적인 작업을 나타내지는 않는다. 예를들어 package는 패키징된 결과물이 jar 파일이냐 war 파일이냐에 따라 다르게 수행되어야 하지만 Phase는 세부정보를 표시하지 않는 상위 개념이다.

pom.xml의 설정에 따라서 Phase마다 수행되는 세부작업을 Goal이라 부르고, Goal들의 집합을 Plugin이라 한다. 각 Phase마다 수행되어야할 Goal들이 맵핑되어 있고, Phase를 실행하면 설정에 맞는 Goal이 수행되는 구조다.

Goal은 하나 이상의 Phase에 맵핑되어 있거나, 맵핑이 되어 있지 않은 것도 있는데 맵핑된 Phase가 없다면 해당 Goal을 직접적으로 호출하여 실행할 수 있다.

 

mvn clean dependency:copy-dependencies package

  1. clean phase 실행(라이프사이클 순서에 따라 순차적으로)
  2. dependency:copy-dependencies : dependency plugin의 copy-dependencies goal 실행
  3. package phase 실행(라이프사이클 순서에 따라 순차적으로)

 

메이븐의 빌드자동화를 정리하면,

  • LifeCycle : 순서를 가지는 Phase의 집합
  • Phase : LifeCycle의 구성요소로 Phase간에는 정해진 순서가 있다.
  • Goal : 메이븐이 수행할 구체적인 작업으로 하나의 페이즈는 하나의 골과 맵핑된다.
  • Plugin : Goal의 집합

라이프사이클에 따른 페이즈 구성이나 설정에 따라 수행되는 골의 종류 등은 메이븐의 공식 사이트를 참고하고 대략적인 개념정리는 여기서 마무리 하도록 한다.


참고

메이븐 웹 - http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference

메이븐파헤치기(김우용) - https://www.slideshare.net/ssuser5445b7/ss-56566336?qid=927855f5-7c8a-4f88-a834-d31292324fd2&v=&b=&from_search=4

 

 


 

 

'개발 > 기타' 카테고리의 다른 글

패스워드 암호화에 대해서  (0) 2020.12.06
LastModified 헤더를 이용한 파일변경 체크  (0) 2020.12.06
UTC, GMT, Epoch Time  (0) 2019.10.11
Ajax에 관하여  (0) 2018.12.13
META-INF 디렉터리에 대하여  (1) 2018.04.12

+ Recent posts