[Spring Boot] 2. 스프링부트 이해

이제 스프링 부트에 대해서 꾸준히 공부해보며 정리한다.

Spring Boot Configuration

앞에서 해봤던 스프링 부트 어플리케이션 설정을 살펴보자. (pom.xml이 나올지 build.gradle이 나올지 내마음)

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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>kr.rkaehdaos</groupId>
<artifactId>springBootMavenTest</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
</parent>

<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

spring-boot-starter-web, spring-boot-maven-plugin등은 아예 버전조차 정의되지 않고 있다. 스프링 부트가 제공하는 의존성 관리 기능 때문이다. 설정에서 <parent>로 정의된 spring-boot-starter-parent의 시작 부분을 살펴보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
<artifactId>spring-boot-starter-parent</artifactId>
<packaging>pom</packaging>
<name>Spring Boot Starter Parent</name>
<description>Parent pom providing dependency and plugin management for applications
built with Maven</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-starter-parent</url>
<properties>

<parent>로 spring-boot-dependencies가 정의된 것을 알 수 있다.
이 spring-boot-dependencies의 정의를 보면 <parent>가 없다.
즉 이 spring-boot-dependencies가 가장 최상위라고 할 수 있다.

중간에 내용을 살펴보면 버전이 정의 되어있음을 알 수 있다.

의존성_버전

또 프로퍼티 밑을 보면 <dependencyManagament>로 해서 각 의존성들이 버전까지 정의 되어 있음을 알 수 있다.
자세히 살펴보면 여기에 사용된 의존성들이 위에 <properties>에서 정의된 버전 정보를 사용하고 있음을 알 수 있다

즉 스프링 부트에서 관리하고 있는 것들은 버전명시를 안해도 자동으로 버전까지 정의되어서 충돌 걱정을 할 필요가 없다. 이로 인해서 얻을 수 있는 장점은 무엇이 있을까?

  • 관리해야할 의존성이 줄어든다.
    • 개발 외적의 일이 그만큼 줄어든다는 뜻.
  • 뜬금없는 ModelMapper
    • 가급적 도메인 활용이 좋겠지만 DTO 사용을 해야할 경우 값 복사 어떻게?
    • 보통 일일히 필드 매칭
    • modelmapper는 간단하게 이를 커버 가능
    • http://modelmapper.org/getting-started/

자동 설정의 이해

위의 예제에서 살펴봤듯이 @SpringBootApplication 한방이면
설정이 완료되었다. how?

1
2
3
4
5
6
7
8
//@SpringbootApplication
//위 어노테이션은 다음의 어노테이션의 묶음과 같다.

@SpringBootConfiguration //사실상 컨피규레이션

@ComponentScan //빈 리딩 1step

@EnableAutoConfiguration //빈 리딩 2step

@SpringBootConfiguration은 사실상 Configuration이다.
그리고 스프링 부트의 빈 등록은 두단계라고 할 수 있는데
@ComponentScan을 통해 검색된 빈등록과, @EnableAutoConfiguration을 통해 검색된 빈의 추가적인 등록이다.

@ComponentScan은 스프링의 기본 내용이라 패스…
그래도 한번 다시 되집고 가자면
@Component라는 어노테이션이 붙은 클래스들을 빈으로 등록해준다.

@Service, @Controller, @RestController, @Repository
도 @Component에 기반한다.

@ComponentScan 어노테이션이 쓰이는 곳은 아마 @Configuration이나 @SpringBootConfiguration이 붙은 컨피규레이션 자바 클래스일텐데
이때 자기 자신도 빈으로 등록한다.
중요한점은 @CompoentScan은 그 하위로 스캔하기 때문에
상위 패키지나 다른 패키지는 스캔이 안됨을 주의하자

@EnableAutoConfiguration을 주석처리하면 에러가
발생하는데 ServletWebServerFactory 빈을 찾지 못해서 ServletWebServerApplicationContext를 시작할 수 없다고 나온다. 즉 저 찾지 못한 ServletWebServerFactory빈등을 자동으로 등록시켜 주는게 @EnableAutoConfiguration의 역할이다.

만약 web 형태로 만들지 않는다면 실행을 해볼 수 있다.
밑의 소스 처럼, Application을 static이 아닌 new로 인스턴스 할당한후 .setApplicationTypeMyApplication.NONE으로 세팅하고 run해주면 console에서 Spring Boot가 실행 되는 모습을 볼 수 있다.

Application1.java
1
2
3
4
5
6
7
8
9
10
@Configuration
@CompoentScan
//@EnableAutoConfiguration
public class Application1 {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application1.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.run(args);
}
}

@EnableAutoConfiguration은 Spring의 메타파일을 읽어들인다.
@EnableAutoConfiguration 의 풀패키지명은
org.springframework.boot.autoconfigure.EnableAutoConfiguration
spring-boot-autoconfigure-<version>.RELEASE.jar 파일을 참조 한다. 해당 jar파일의 META-INF를 보면 spring.factories라는 파일이 존재한다.
(이 spring.factories는 Spring Boot 한정이 아닌 일반적인 Spring Framework에 포함되어 있는 것이다)

spring.factories

spring.factories를 열어보면 다음과 같이 키가 설정되어 있고
그 키에는 여러개의 컨피규레이션들이 지정되어 있다.

spring.factories detail

하위에 보면 키 이름이 EnableAutoConfiguration이 있고 하위에 정말 수많은 Configuration 클래스의 이름이 들어 있다. 열어보면 @Configuration이 붙어 있는 스프링 Configuration 클래스임을 알 수 있다.
@EnableAutoConfiguration을 사용하면 이 키값의 모든 컨피규레이션을 참조하여 빈을 등록하는 것이다.

그러면 @EnableAutoConfiguration을 하면 이 수많은 클래스에서 설정한 더 수많은 빈들을 모조리 등록하는 것일까?
결론부터 말하면 No.
일단 EnableAutoConfiguration의 컨피규레이션중 하나를 열어보자.

WebMvcAutoConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

public static final String DEFAULT_PREFIX = "";

public static final String DEFAULT_SUFFIX = "";

private static final String[] SERVLET_LOCATIONS = { "/" };

@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

@Bean
@ConditionalOnMissingBean(HttpPutFormContentFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.formcontent.putfilter", name = "enabled", matchIfMissing = true)
public OrderedHttpPutFormContentFilter httpPutFormContentFilter() {
return new OrderedHttpPutFormContentFilter();
}
//...(계속)

@Configuration이 설정되어 있는 WebMvcAutoConfiguration 설정 클래스의 첫 부분이다.
살펴보면 @ConditionalOn으로 시작하는 어노테이션이 엄청 붙어 있는 것들을 볼 수 있다. @EnableAutoConfiguration에 설정된 수많은 Configuration 클래스에는 이런식으로 다시 수많은 @ConditionalOn~ 어노테이션이 붙어 있으며 여기에 설정된 condition에 따라서 빈이 등록되고 혹은 등록이 되지 않기도 한다.

Configuration 클래스위에 붙어 있는 @ConditionalOn~ 어노테이션은
현재 설정 클래스 자체를 사용할지 안할지를 결정하며,
하위 @Bean에 붙어있는 @ConditionalOn~ 어노테이션은
해당 빈을 등록할지 안할지 결정한다.

예를들어서 위 클래스에 설정되어있는

1
@ConditionalOnWebApplication(type = Type.SERVLET)

의 경우

Applicaion1.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package kr.rkaehdaos;

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

@SpringBootApplication
public class Application1 {
public static void main(String[] args) {

//SpringApplication.run( Application1.class, args);
SpringApplication application = new SpringApplication(Application1.class);

//여기서 SERVLET 타입일때 작동!!!
application.setWebApplicationType(WebApplicationType.SERVLET);

application.run(args);
}
}

위의 예처럼 타입이 Servlet 일때 이 설정 클래스를 사용하라라는 뜻이다.
이런식의 설정이 컨피규레이션 클래스마다 많은 룰로 정의되어 있어서
전부 다 빈으로 등록되는 것은 아니고 필요한 빈들 위주로
등록이 된다는 점을 알아두자.

자동 설정(AutoConfiguration) 이해

자동 설정(AutoConfiguration)의 이해를 위해
커스텀한 자동 설정을 직접 정의하여 프로젝트에서 사용해 보자

Spring Boot 의 모듈 프로젝트는 보통 2가지 프로젝트로 나눈다.

  • XXX-Spring-Boot-AutoConfigure 모듈 : 자동설정
  • XXX-Spring-Boot-Starter 모듈 : 필요한 의존성 정의

자동 설정은 위 2가지중 AutoConfigure에 넣는것이 일반적이다.

내용이 작을 경우 굳이 분리하지 않고 XXX-Spring-Boot-Starter만 만들고 AutoConfigure 부분도 안에 그냥 써준다.

starter는 pom파일이 핵심이라 보통 소스코드가 없는 경우가 많다.
내용이 작은 경우 Starter하나만 만들고 Auto부분도 다 넣어버리면 된다.

일반적으로 실제 개발에서
현재 만드는 이 프로젝트는 자동설정의 타겟이 되는 타겟 프로젝트이며
이 프로젝트에서 만드는 클래스들(여기선 Person하나)은
실제적으로 다른 프로젝트에서 참조하는 라이브러리가 될 것이다.

구현 방법

1. 의존성 추가

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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>kr.rkaehdaos</groupId>
<artifactId>SpringBootStarter1</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 의존성은 autoconfigure,autoconfigure-processor 2가지 추가 -->
<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>

<!-- 위 2 의존성의 버전 관리를 위해 의존성을 관리하는 -->
<!-- dependencyManagement 를 추가 하였다.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

맨처음 포스트에서 봤던 <parent>의 상속을 쓰지 않고
spring-boot-dependencies를 이용하던 방법을 이용하여 의존성을 설정 하였다.

의존성 추가후 다음 스텝으로 넘어가기전에
자동설정을 지정할 타겟 클래스를 만들어본다. Person을 예로 들어보자

Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package me.rkaehdaos;

public class Person {
String name;
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 +
'}';
}
}

이름과 나이를 멤버로 갖는 Person 클래스에 Getter/Setter와 toString까지만 만든 상태다.

2. @Configuration 파일 작성

PersonConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package me.rkaehdaos;

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(39);
person.setName("GeunChang Ahn");
return person;
}
}

Person타입의 Bean을 리턴하는 설정 파일을 만들었다.

3. src/main/resource/META-INF에 spring.factories 파일 만들기

src/main/resource/META-INF에 spring.factories 파일을 생성한다.
IDE 사용중이면 resource까진 있을테니 밑에 META-INF 폴더 생성후 파일을 만든다.
이 파일은 스프링 부트 전용이 아닌 스프링 프레임워크에 원래 사용되는 파일이다.

이펙티브 자바의 service Provider참조

4. spring.factories 안에 자동 설정 파일 추가

아까 만들었던 설정파일을 명시적으로 지정한다.

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
me.rkaehdaos.PersonConfiguration

이러면 앞에서 봤던것처럼 스프링의 EnableAutoConfiguration이 켜져있으면
알아서 위에 추가한 자동설정 파일을 읽게 된다.

5. mvn install

maven install plugin은 install phase에서 동작하는 플러그인으로
local repository에 artifact를 추가하는데 사용된다.

maven deploy plugin은 deploy phase에서 동작하는 플러그인으로
remote repository에 artifact를 올리는 역할

mvn install 하면 build 플러그인후 install 플러그인이 실행된다.

이러면 위의 프로젝트가 jar로 묶인후 나의 로컬 repo(기본적으로 개발자 pc)
에 올라가게 되며 이제 다른 프로젝트에서
이 프로젝트를 참조하여 사용할 수 있게 된다.

맨위에서 만들었던 Application1에서 사용해보자.
Application1의 의존성관리에 방금 만든 프로젝트를 추가해보자
방금만든 프로젝트(person포함한)의 pom.xml을 열어보면
맨위에 나온 artifactId와 groupId가 있는데 이것을 사용할
프로젝트(Application1)의 pom.xml의 의존성 관리에 추가한다.

불러오는 클라이언트 프로젝트(application1)의 pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--의존성부분만 보면-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 여기에 이렇게 포함시켜 준다 -->
<dependency>
<groupId>me.rkaehdaos</groupId>
<artifactId>ahn-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

이렇게 하면 왼쪽의 External Lib(IDE에 따라서 maven Lib)에 내가 만든 프로젝트가
참조 됨을 알 수 있다.

Application1의 메인 메소드에 application타입을 WebApplicationType.NONE
로 설정해서 로딩이 좀더 빠르게 해보자.

Application1.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package me.rkaehdaos;

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

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

타겟(Person)의 확인을 위해 ApplicationRunner 구현 클래스를 하나 만든다.

PersonRunner.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.rkaehdaos;

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;

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


이제 실행해보면 Application1의 스프링부트가 실행되면서
ApplicationRunner에 의해 Person에 대해 println하게 된다.
보면 타겟프로젝트 자동설정에서 세팅한 대로
빈에 값이 들어가 있는 것을 볼 수 있다.

빈 설정 오버라이딩 문제

사실 여기에 문제가 있는데 다음과 같이 새빈을 등록시킨다고 가정하자.

자동설정이 아닌 새로운 빈으로 오버라이딩을 시도하는 Application1.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package me.rkaehdaos;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

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

@Bean
public Person person() {
Person person = new Person();
person.setAge(10);
person.setName("Another Ahn");
return person;
}
}

이렇게 코드를 만드는 개발자는 빈설정이 오버라이딩 되서
새로운 설정에 따른 빈이 생성됨을 기대할 것이다.
허나 결과는 앞과 동일하게 자동생성에 만든대로 빈이 나온다.
즉 새로 작성한 빈 코드가 무시가 된다. 왜그럴까?

맨 위의 자동설정의 이해에서..
@SpringbootApplication은 다음의 페이즈의 묶음이라고 하였다.

  1. @SpringBootConfiguration //사실상 컨피규레이션
  2. @ComponentScan //빈 리딩 1step
  3. @EnableAutoConfiguration //빈 리딩 2step

위에서 보이듯이 @ComponentScan가 먼저 이뤄지기 떄문이다.
이 빈리딩 1step에서 내가 Application1에 생성한 Bean이 만들어진 후
그다음의 2step에서 자동설정의 빈이 불러와서 1step에서 만들었던 빈을
덮어씌운것이다. 즉 개발자의 의도와 정 반대로
내가 만든 빈 설정이 라이브러리의 자동설정의 빈설정이 덮어 씌웠다.
이것을 어떻게 해결할 수 있을까?
이것은 자동 설정쪽에 하나만 추가 해주면 된다.

해결책이 적용된 PersonConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.rkaehdaos;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PersonConfiguration {

@Bean
@ConditionalOnMissingBean // <--해결책
public Person person() {
Person person = new Person();
person.setAge(39);
person.setName("GeunChang Ahn");
return person;
}
}

@ConditionalOnMissingBean

위에서 스프링 부트의 설정 클래스를 열었을때 붙어있는 수많은 @Conditional~
어노테이션을 기억하는가? 그중에서 @ConditionalOnMissingBean@Bean에 붙었다.
이는 빈 설정에 적용이 되며 해당 빈이 없을때에만 이 설정이 작동이 된다.

위처럼 선택하고 다시 mvn install하면 이제 정상적으로 새로 만든 빈 설정이 적용된다.
1step에서 생성된 새로 설정된 빈이 2step에서 저 어노테이션을 만나고
그시점에서 해당 타입의 빈이 이미 1step에서 생성되었으므로 넘어가게 되는 것이다.

자 이제 빈 설정 오버라이딩 문제가 해결되었다.
그런데 꼭 빈 설정을 오버라이딩 해야하는 것일까?

빈 재정의 간소화

현재 코드에서는 @Bean이 붙은 Person 빈을 리턴하는 메소드를 똑같이 붙여넣었다.
그런데 이러면 중복코드가 된다. 예제처럼 짧지 않고 빈생성 메소드가 엄청 길다면?
그리고 그런 메소드가 수십개가 된다면?? 전부 복사해서 붙여야할까?
만약 자동설정의 설정대로 빈 설정후 setter만 실행해서 내가 원하는 값을 set할 수
있다면?

만약에 프로퍼티파일에 우리가 원하는 값을 세팅해놓고 자동설정에서 프로퍼티를 참조해서
우리가 원하는 값을 찾으면 그값으로 세팅하고 빈을 등록 할 수 있게 된다면?

프로퍼티파일 생성

스프링 부트는 application.properties 파일을 이용해서 설정을 제공한다.
스프링 부트가 제공하는 프로퍼티를 설정 하는 파일이지만 커스텀한 프로퍼티도
설정이 가능하다.

application.properties
1
2
Person.name = Ahn_Properties
Person.age = 18

properties keyvalue auto complete

위의 프로퍼티에는 새로운 빈 설정 값만 프로퍼티로 들어가 있다.

이 커스텀 프로퍼티를 사용하는 방법들이 다수 존재하는데
설정 프로퍼티 클래스를 사용하면 관련 설정을 한 클래스에서 관리할 수 있어서 편하다.

설정 프로퍼티 클래스 생성(ConfigurationProperties class)

먼저 설정 프로퍼티로 사용할 클래스를 생성한다.

PersonProperties.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package me.rkaehdaos;

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

@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;
}
}

설정 프로퍼티로 사용할 PersonProperties 클래스를 생성하였다.
클래스에 @ConfigurationProperties를 붙인 후
application.properties에서 사용할 접두어(prefix)를 정한다.
설정 프로퍼티 클래스에는 파일 속의 프로퍼티 값을 전달받을 Setter와
프로퍼티 참조시 사용할 getter가 필요하다.

application.properties의 프로퍼티는 다음과 같이 매칭된다.
application.properties 프로퍼티 이름 = prefix + “.” + setter 프로퍼티 이름

위처럼 하면 인텔리 J에서 에러가 나며 다음 문서로 연결된다.
Spring Boot Configuration Annotation Processor not found in classpath

해석 그대로 해당 스프링 부트 어노테이션 프로세서를 못찾았으니
의존성에 추가해달라는 이야기

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

적용하면 에러가 사라진다.

@ConfigurationProperties를 적용한 클래스를 만들었다면,
다음 할 일은 빈으로 등록하는 것이다.
스프링 빈으로 등록하는 방법은 두 가지가 있다.

  1. 설정 프로퍼티 클래스에 @Configuration 적용하기
    (또는 설정 프로퍼티 클래스를 @Bean으로 등록하기)
  2. @EnableConfigurationProperties을 이용해서 지정하기

1번의 방법은 설정 프로퍼티 클래스를 빈으로 등록하는 방법이다.
@Configuration이 붙어있으면 콤포넌트 스캔시 빈으로 등록된다.
스프링부트는 해당 빈에 @ConfigurationProperties가 적용되어 있다면
프로퍼티값을 할당해 준다.

여기는 자동설정 공부이므로 2번 방법으로 한다.

@EnableConfigurationProperties을 설정 클래스에 추가,

설정 클래스(여기서는 PersonConfiguration)에 @EnableConfigurationProperties를 추가한다.
어노테이션의 값에는 @ConfigurationProperties를 적용한 클래스를 지정한
설정 프로퍼티 클래스(여기선 PersonProperties)를 지정하면 된다.

이 경우 @EnableConfigurationProperties는 해당 클래스를 빈으로 등록하고
프로퍼티 값을 할당한다.

@EnableConfigurationProperties 적용한 설정 클래스 PersonConfiguration.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package me.rkaehdaos;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(PersonProperties.class)
public class PersonConfiguration {

@Bean
@ConditionalOnMissingBean
public Person person(PersonProperties properties) {
Person person = new Person();
person.setAge(properties.getAge());
person.setName(properties.getName());
return person;
}
}

클라이언트 프로젝트에서 refresh후 사용해보자
위의 설정이 모두 끝난 상태라면
인텔리J엔터프라이즈 버전인 경우 properties도 자동완성이 가능하다.

인텔리J에서 프로퍼티 인코딩을 UTF-8로 해도 한글이 깨지는 경우
File Encoding에서 밑에 보면 properties 따로 체크 하는 부분이 나온다.
여기서 Transparent native-to-ascii conversion을 체크하면 한글이
올바로 표시된다.

\0A등의 문자표로 저장이 되더라도 보여지기는 한글이나 아스키로 보여지게
하는 옵션이다.


내장 웹 서버의 이해

스프링 부트는 서버가 아니다.

스프링 부트 실행시 자동으로 뜨는 서버를 보고 서버인 줄 착각하면 안된다.
스프링 부트는 서버가 아니다.

코드상에서 톰캣실행시 다음의 7단계가 필요하다.

  1. 톰캣 객체 생성
  2. 포트 설정
  3. 톰캣에 컨텍스트 추가
  4. 서블릿 만들기
  5. 톰캣에 서블릿 추가
  6. 컨텍스트에 서블릿 맵핑
  7. 톰캣 실행 및 대기

이해를 위해 위의 단계를 수동으로 했다고 생각하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package me.rkaehdaos.springbootstudyembeddedtomcat;

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;

public class SpringbootStudyEmbeddedtomcatApplication {

public static void main(String[] args) throws LifecycleException, IOException {
Tomcat tomcat = new Tomcat(); // 1단계
tomcat.setPort(8080); //2단계
//mac linux는 docBase대신 '/'해도 돌아가나 윈도우는 이게 필요.
String docBase = Files.createTempDirectory("tomcat-basedir").toString();
Context context = tomcat.addContext("/", docBase); //3단계

//doget()만 구현
HttpServlet servlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//super.doGet(req, resp);
PrintWriter writer = resp.getWriter();
writer.println("<html><head><title>title </title></head><body>");
writer.println("hello tomcat");
writer.println("</body></html>");

}
}; //4단계
String servletName = "helloServlet";
tomcat.addServlet("/", servletName, servlet); //5단계
context.addServletMappingDecoded("/hello", servletName); //6단계
tomcat.start(); //7단계
tomcat.getServer().await();
}
}

위 코드를 실행하고 브라우저에서 /hello로 접근하면 서블릿 응답을 볼 수 있다.

이 모든 과정을 보다 상세히 또 유연하고 설정하고 실행해주는게
바로 스프링 부트의 자동 설정이다.

역시 org.springframework.boot.autoconfigure 안의 spring.factories
보면 설정파일 키값중 org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration 키가 존재한다. 해당 설정 클래스를 살펴보면 서블릿 웹서버 생성을 설정하는 것을 확인 할 수 있다. 살펴보면 TomcatServletWebServerFactory 를 참조하는 것을 확인할 수 있는데 이부분을 보면 앞에서 만들었던 비슷하게 톰캣을 생성한후 커스터마이징을 하는 것을 알 수 있다.

그러면 서블릿등록 부분은 어디 있을까?

일반적으로 많이 쓰이는 웹 MVC 구조를 위해선
첫단에 DispatcherServlet이 필요하다.(프론트 컨트롤러 패턴)

역시 spring.factoriesorg.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration 키값을 찾아가면 설정 클래스를 확인할 수 있다. 클래스 내용을 보면 해당 클래스에서 dispatcher 서블릿을 만들고 클래스에 등록하는 부분이 설정 되어 있음을 확인할 수 있다.

이처럼 컨테이너 생성과 서블릿 등록이 분리되어 있기 떄문에
컨테이너 생성과 서블릿등록이 서로 의존되지 않고 각자 설정이 가능하다.

컨테이너와 포트

1) 다른 컨테이너로 변경

스프링부트의 기본 컨테이너는 톰캣임을 의존성에서 확인할 수 있다.
이는 앞에서 살펴보았던 자동 설정에 의해서 (ConditionalOnClass)
톰캣용 자동 파일이 읽어지게 되는 것이다.

참조 레퍼런스 : https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-web-servers.html

pom.xml에 설정했던 spring-boot-starter-webspring-boot-starter-tomcat를 포함하기 때문에 톰캣을 포함하게 된다. 이 톰캣을 exclude하고 spring-boot-starter-jettyspring-boot-starter-undertow를 대신 사용할 수 있다.

pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

2) web server 사용하지 않기

참조 레퍼런스 : https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-web-servers.html#howto-disable-web-server

위에서 해봤던 것처럼 SpringApplication에서

1
application.setWebApplicationType(WebApplicationType.NONE);

해서 웹서버를 사용하지 않기도 했다.
하지만 이렇게 코드에 써놓을 필요없이 프로퍼티만으로도 가능하다.

application.properties
1
spring.main.web-application-type=none

3) port 변경

standalone 어플리케이션에서 메인 HTTP port는 8080이 기본이다. 바꿀떄는

application.properties
1
server.port=7070

0을 지정하면 random port로 서버를 올릴때마다 포트가 바뀐다.

4) 런타임시 포트 알아내는 법

프로그램 코드내에서 포트 정보가 필요한 경우 만약 랜덤포트나 특정 주입등으로
포트가 결정 되는 경우에 어떻게 얻어야할까?

참조 : https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-web-servers.html#howto-discover-the-http-port-at-runtime

가장 쉬운 방법은 콘솔에 찍힌 로그를 확인하는 것이다.
하지만 위 참조에서 보듯이 가장 최선의 방법(Best way)은 ApplicationListener<ServletWebServerInitializedEvent>타입의 빈을 등록 후
가져오는 것이다.

ApplicationListener 인터페이스를 통해 이벤트를 수신할 수 있는 데 수신한 이벤트는 ServletWebServerInitializedEvent다. 이 이벤트는 스프링 부트에서 ApplicationContext가 갱신되고 WebServer가 ready되었을 때 publish된다.

PortListener.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.rkaehdaos;

import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class PortListener implements ApplicationListener<ServletWebServerInitializedEvent> {
@Override
//이벤트에 해당하는 콜백 호출됨
public void onApplicationEvent(ServletWebServerInitializedEvent servletWebServerInitializedEvent) {
//이벤트에서 ServletWebServerApplicationContext를 꺼낸다.
ServletWebServerApplicationContext applicationContext = servletWebServerInitializedEvent.getApplicationContext();
//ServletWebServerApplicationContext는 webserver를 멤버로 참조하며 이를 통해 포트를 접근 할 수 있다..
int port = applicationContext.getWebServer().getPort();
System.out.println("port:"+port);
}
}

내장 웹서버에서 HTTPS와 HTTP2 사용하기

HTTPS 설정하기

키스토어 만들기

generate-keystore.sh
1
2
3
4
5
6
7
8
keytool -genkey \
-alias tomcat \
-storetype PKCS12 \
-keyalg RSA \
-keysize 2048 \
-keystore keystore.p12 \
-validity 4000 \
# 패스워드를 물으면 암호를 입력
application.properties
1
2
3
4
server.ssl.key-store: keystore.p12
server.ssl.key-store-password: q1w2e3 #위에서 넣었던 암호
server.ssl.keyStoreType: PKCS12
server.ssl.keyAlias: tomcat

스프링 부트는 기본적으로 하나의 커넥터만 등록이 되는데
그 커넥터에 SSL이 적용되게 된다.
이제 http로 하면 400에러 뜨고 https로만 접속이 된다.
https접속하면 인증서 신뢰에 관한 에러가 뜨게 된다.
지금 사용한 인증서는 브라우저가 가지고 있는 CA의 pub키가
아니기 때문에 당연히 뜨는 것이다.
무시하고 들어가는것을 선택하면 이제 만들어놓은 사이트에 접속이 된다.

참조: curl 사용법 (https://www.lesstif.com/pages/viewpage.action?pageId=14745703)

1
2
3
4
5
6
7
8
curl -I  http://localhost:8080
#400 에러뜸.
curl -I --http2 https://localhost:8080
#curl: (77) schannel: next InitializeSecurityContext failed:
#SEC_E_UNTRUSTED_ROOT (0x80090325)
#신뢰되지 않은 기관에서 인증서 체인을 발급했습니다.
curl -I -k --http2 https://localhost:8080
#200 정상뜸

하나인 커넥터가 https가 되었기 떄문에 http를 사용 할 수 없어서
http 시 400 에러가 뜨고 https로 접속하면 인증서 신뢰에 대한 오류 가 뜬다.
이를 무시하기 위해 k옵션을 붙이면 정상적으로 200이 뜬다.
현재 http2가 아니므로 htt2옵션을 보내도 헤더가 HTTP/1.1인 것을 알 수 있다.

만약 http와 https를 동시에 사용하려면?
커넥터를 하나 만들어서 추가하면 된다.

SpringbootSslHttp2TestApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package me.rkaehdaos.springbootsslhttp2test;

import org.apache.catalina.connector.Connector;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class SpringbootSslHttp2TestApplication {

@GetMapping("/")
public String root() {
return "thisis root";
}
@GetMapping("/hello")
public String hello() {
return "hello spring";
}

@Bean
public ServletWebServerFactory servletWebServerFactory() {
TomcatServletWebServerFactory tomcatFactory = new TomcatServletWebServerFactory();
tomcatFactory.addAdditionalTomcatConnectors(createStandardConnector());
return tomcatFactory;
}

private Connector createStandardConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setPort(8080);
return connector;
}


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

위처럼 8080 포트를 사용하는 커넥터를 추가한다.
원래 https를 사용하는 기본 커넥터는 8443으로 바꾼다.

application.properties
1
server.port=8443

이렇게 하면 http(8080),https(8443) 둘다 사용이 가능하다.

http2 사용하기

https://developers.google.com/web/fundamentals/performance/http2/?hl=ko
https://www.popit.kr/%EB%82%98%EB%A7%8C-%EB%AA%A8%EB%A5%B4%EA%B3%A0-%EC%9E%88%EB%8D%98-http2/
(HTTP/2를 사용하려면 SSL은 적용이 되어있어야 함을 명심하자.)

스프링 부트 어플리케이션에서 server.http2.enabled설정으로
HTTP/2 사용을 가능하게 할 수 있다. 어떤 컨테이너를 사용하느냐에 따라서
이후 제약사양이 달라진다.

  • 제약사양
    • Undertow: 아무런 추가 설정이 필요 없다.
    • tomcat: 조올라복잡하다..쓰지말자..
      다만 링크 밑에 보면 알듯이 tomcat 9.0.x와 JDK9이상을 사용하는 경우에는 설정없이 가능하다.
      ()그러니 이경우가 아니면 톰캣에서 HTTP/2를 사용하는 것은 지양하자.)
pom.xml
1
2
3
4
5
6
7
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>9</java.version><!-- 1.9가 아닌 9임을 명심하자-->
<tomcat.version>9.0.12</tomcat.version>
</properties>

인텔리 J에서 platform jdk만 9로 바꿔주면 되는 것이 아니라
modules의 dependency도 9버전으로 올려줘야함을 명심하자.

독립 실행 가능한 JAR

스프링 부트 보다는 maven 플러그인에 더 가까운 내용이다.
간단한 스프링 예제 만든후 mvn package를 하면 target 안에 jar파일이 생기는데
이를 java -jar <jar파일이름>.jar를 하는 것만으로도 스프링 부트 어플리케이션의
실행이 가능하다. 크기는 아마 16메가정도로 기억된다.

그럼 안의 내용은 어떻게 되는가? 내가 만든 클래스는 다 포함이 되어 있겠지만
나머지 그 수많은 의존성파일들도 다 jar에 lib로 포함이 되어있다.

그래서 과거에는 “uber”jar를 사용하였다.
uber jar라는 것은 의존성 및 어플리케이션을
jar파일 하나로 압축하는 방법이다.
이렇게 되면 무슨 라이브러리를 쓰는 것인지 뭐가 어디에서 오는 것인지 알 수가 없다.
특히 내용이 다른데 이름이 같은 파일의 경우에는 처리가 어렵다.

이런 까닭은 바로
자바에는 jar를 읽을 수 있는 표준 방법이 없기 때문이다.

스프링 부트는 내장 Jar로 Jar 파일안에 Jar를 묶어놓고
어플리케이션 클래스와 라이브러리 위치를 분리한다.
그리고 그걸 읽을 수 있는 기능을 추가하였다.
만들어진 jar를 보면
org.springframework.boot.loader가 있는 부분이 바로 그것이다.
org.springframework.boot.loader.jar.JarFile을 사용해 내장 JAR를 로딩한다.
org.springframework.boot.loader.Launcher를 사용해서 실행한다.
위의 경우에는 Jar파일이라 JarLaunch를 사용해서 실행하게 된다. 실제로 jar파일의
MANIFEST.MF 파일을 보면 메인 클래스에
org.springframework.boot.loader.JarLauncher로 되어 있음을 볼 수 있다.

MANIFEST.MF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Manifest-Version: 1.0
Implementation-Title: springboot-ssl-http2-test
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: Ahn
Implementation-Vendor-Id: me.rkaehdaos
Spring-Boot-Version: 2.0.6.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: me.rkaehdaos.springbootsslhttp2test.SpringbootSslHttp2Tes
tApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_172
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/springboot-ssl-http2-test

JarLauncher는 Start-Class로 정의된 내가 만든 클래스를 타겟으로 실행을 하게 된다.

이 독립적으로 실행가능한 어플리케이션을 만드는 것도 스프링 부트의 특징이다.

원리 정리

  • 의존성 관리
    • 스프링 부트 스타터 덕분에 의존성 내용이 엄청 줄었다.
      정말 조금만 적어도 알아서 다 가져온다.
  • 자동 설정
    • @EnableAutoConfiguration로 자동 설정을 읽어오는 부분 기억하자.
    • 자동설정을 이용해서 개발자가 재정의하거나 설정해야할 부분을 최대한 줄여보자.
  • 내장 웹 서버
    • 이것도 스프링 부트의 goal의 하나인 독립 실행 가능한 어플리케이션에 이바지
    • 톰캣,제티,언더토우, 얼마든지 커스터마이징 할 수 있음을 기억하자.
  • 독립적 실행 가능한 Jar
    • 이것도 독립 실행 가능한 어플리케이션에 이바지한다.
    • 위에서 봤다시피 라이브러리와 어플리케이션을 구분해서 완전한 관리가 가능하다.

Related POST

공유하기