
스프링 부트 소개
스프링 프레임워크
핵심 기술 - 스프링 DI컨테이너, AOP, 이벤트
웹 기술 - 스프링 MVC, 스프링 WebFlux
데이터 접근 기술 - 트랜잭션, JDBC, ORM, XML
기술 통합 - 캐시, 이메일, 원격접근, 스케줄링
테스트 - 스프링 기반 테스트 지원
스프링 생태계
- 스프링 데이터, 스프링 세션, 스프링 시큐리티, 스프링 Rest Docs, 스프링 배치, 스프링 클라우드
설정이 점점 늘어나고, 어려워짐
스프링 부트
- 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
- 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
- 관례에 의한 간결한 설정
스프링 부트 - 5가지 핵심 기능
- 내장 서버 (WAS)
- Tomcat같은 웹서버를 내장해서 별도의 웹 서버를 설치하지 않아도 된다.
- 자동 라이브러리 관리
- 손쉬운 빌드 구성을 위한 스타터 종속성 제공
- 스프링과 외부 라이브러리의 버전을 자동으로 관리
- 자동 구성
- 프로젝트 시작에 필요한 스프링과 외부 라이브러리의 빈을 자동 등록
- 외부 설정
- 환경에 따라 달라져야 하는 외부 설정 공통화
- 모니터링과 관리 기능 (프로덕션 준비)
- 모니터링을 위한 메트릭, 상태확인 기능 제공
웹 서버와 서블릿 컨테이너
전통적인 방식
- WAS설치
- WAS에서 동작하도록 서블릿 스펙에 맞춰 코드 작성
- WAR형식으로 빌드해서 WAR파일 생성
최근 방식
- 스프링 부트가 내장 톰캣을 포함
- 코드 작성, JAR로 빌드 -> JAR을 원하는 위치에서 실행
- WAS도 함께 실행된다.
JAR vs WAR
JAR
- 자바는 여러 클래스와 리소스를 묶어서 JAR라고 하는 압축 파일을 만들 수 있다.
- 이 파일은 JVM위에서 직접 실행되거나 또는 다른 곳에서 사용하는 라이브러리로 제공된다.
- 직접 실행하는 경우 main() 메서드가 필요하고,
MANIFEST.MF
파일에 실행할 메인 메서드가 있는 클래스에 지정해두어야 한다. - 쉽게 이야기 해서 클래스와 관련 리소스를 압축한 단순한 파일
WAR(Web Application Archive)
- 웹 어플리케이션 서버(WAS)에 배포할 때 사용하는 파일
- JAR파일이 JVM위에서 실행된다면, WAR는 웹 애플리케이션 서버 위에서 실행
- WAR구조를 지켜야 한다.
- WEB-INF
- classes : 실행 클래스 모음
- lib : 라이브러리 모음
- web.xml : 웹 서버 배치 설정 파일(생략 가능)
- index.html : 정적 리소스
- WEB-INF 폴더 하위는 자바 클래스와 라이브러리, 그리고 설정 정보가 들어가는 곳이다.
- WEB-INF 를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역이다.
- WEB-INF
WAR파일 배포 순서
- 톰캣 서버를 종료한다. ./shutdown.sh
- 톰캣폴더/webapps 하위를 모두 삭제한다.
- 빌드된 server-0.0.1-SNAPSHOT.war 를 복사한다.
- 톰캣폴더/webapps 하위에 붙여넣는다.
- 톰캣폴더/webapps/server-0.0.1-SNAPSHOT.war
- 이름을 변경한다.
- 톰캣폴더/webapps/ROOT.war
- 톰캣 서버를 실행한다. ./startup.sh
서블릿 컨테이너 초기화
- WAS를 실행하는 시점에 필요한 초기화 작업들이 있다S.
- 서비스에 필요한 필터와 서블릿 등록
- 스프링 컨테이너 생성
- 서블릿과 스프링을 연결하는 디스패처 서블릿 등록
- WAS가 제공하는 초기화 기능을 사용하면, WAS실행 시점에 이러한 초기화 과정을 진행할 수 있다.
- 과거에는
web.xml
을 사용해 초기화했지만, 지금은 서블릿 스펙에서 자바 코드를 사용한 초기화도 지원한다.
서블릿을 등록하는 2가지 방법 존재
- @WebServlet 애노테이션
- 프로그래밍 방식
초기화는 다음 순서로 진행된다.
- 서블릿 컨테이너 초기화 실행
- resources/META-INF/services/
- jakarta.servlet.ServletContainerInitializer
- 애플리케이션 초기화 실행
- @HandlesTypes(AppInit.class)
서블릿 컨테이너 초기화만 있어도 될 것 같은데, 왜 이렇게 복잡하게 애플리케이션 초기화라는 개념을 만들었을까?
편리함
- 애플리케이션 초기화는 특정 인터페이스만 구현하면 된다.
의존성
- 애플리케이션 초기화는 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있다.
- 이를 통해 애플리케이션 초기화 코드가 서블릿 컨테이너에 대한 의존을 줄일 수 있다.
필요한 과정은 애플리케이션 초기화에 모두 넣어주면 된다.
스프링 컨테이너 등록
앞서 배운 서블릿 컨테이너 초기화와 애플리케이션 초기화를 활용하면 된다.
다음과 같은 과정이 필요할 것이다.
- 스프링 컨테이너 만들기
- 스프링MVC 컨트롤러를 스프링 컨테이너에 빈으로 등록하기
- 스프링MVC를 사용하는데 필요한 디스패처 서블릿을 서블릿 컨테이너 등록하기
스프링 컨테이너 생성
- AnnotationConfigWebApplicationContext 가 바로 스프링 컨테이너이다.
- AnnotationConfigWebApplicationContext 부모를 따라가 보면
- ApplicationContext 인터페이스를 확인할 수 있다.
- 이 구현체는 이름 그대로 애노테이션 기반 설정과 웹 기능을 지원하는 스프링 컨테이너로 이해하면 된다.
- appContext.register(HelloConfig.class)
- 컨테이너에 스프링 설정을 추가한다.
스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
- new DispatcherServlet(appContext)
- 코드를 보면 스프링 MVC가 제공하는 디스패처 서블릿을 생성하고, 생성자에 앞서 만든 스프링 컨테이너를 전달하는 것을 확인할 수 있다. 이렇게 하면 디스패처 서블릿에 스프링 컨테이너가 연결된다.
- 이 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.
디스패처 서블릿을 서블릿 컨테이너에 등록
- servletContext.addServlet(“dispatcherV2”, dispatcher)
- 디스패처 서블릿을 서블릿 컨테이너에 등록한다.
- /spring/_ 요청이 디스패처 서블릿을 통하도록 설정
- /spring/_ 이렇게 경로를 지정하면 /spring 과 그 하위 요청은 모두 해당 서블릿을 통하게 된다.
- /spring/hello-spring
- /spring/hello/go
- /spring/_ 이렇게 경로를 지정하면 /spring 과 그 하위 요청은 모두 해당 서블릿을 통하게 된다.
/spring/hello-spring 실행을 /spring/* 패턴으로 호출했기 때문에 다음과 같이 동작한다.
- dispatcherV2 디스패처 서블릿이 실행된다. ( /spring )
- dispatcherV2 디스패처 서블릿은 스프링 컨트롤러를 찾아서 실행한다. ( /hello-spring )
- 이때 서블릿을 찾아서 호출하는데 사용된 /spring 을 제외한 /hello-spring 가 매핑된 컨트롤러 HelloController )의 메서드를 찾아서 실행한다. (쉽게 이야기해서 뒤에 * 부분으로 스프링 컨트롤러를 찾는다.)
스프링 MVC 서블릿 컨테이너 초기화 지원
서블릿 컨테이너 초기화 과정은 상당히 번거롭고 반복되는 작업이다.
스프링 MVC는 이러한 서블릿 컨테이너 초기화 작업을 이미 만들어두었다. 덕분에 개발자는 서블릿 컨테이너 초기화 과정은 생략하고, 애플리케이션 초기화 코드만 작성하면 된다.
스프링이 지원하는 애플리케이션 초기화를 사용하려면 다음 인터페이스를 구현하면 된다.
package org.springframework.web;
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
package hello.container;
import hello.spring.HelloConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class AppInitV3SpringMvc implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException
{
System.out.println("AppInitV3SpringMvc.onStartup");
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
//스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처 서블릿을 서블릿 컨테이너에 등록 (이름 주의! dispatcherV3)
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV3", dispatcher);
//모든 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/");
}
}
현재 등록된 서블릿 다음과 같다.
- / = dispatcherV3
- /spring/* = dispatcherV2
- /hello-servlet = helloServlet
- /test = TestServlet
이런 경우 우선순위는 더 구체적인 것이 먼저 실행된다.
일반적으로는 스프링 컨테이너를 하나 만들고, 디스패처 서블릿도 하나만 만든다. 그리고 디스패처 서블릿의 경로 매핑도 / 로 해서 하나의 디스패처 서블릿을 통해서 모든 것을 처리하도록 한다.
spring-web 라이브러리를 열어보면 서블릿 컨테이너 초기화를 위한 등록 파일을 확인할 수 있다. 그리고 이곳에 서블릿 컨테이너 초기화 클래스가 등록되어 있다.
org.springframework.web.SpringServletContainerInitializer 코드를 확인해보자.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements
ServletContainerInitializer {}
- 코드를 보면 우리가 앞서 만든 서블릿 컨테이너 초기화 코드와 비슷한 것을 확인할 수 있다.
- @HandlesTypes 의 대상이 WebApplicationInitializer 이다. 그리고 이 인터페이스의 구현체를 생성하고 실행하는 것을 확인할 수 있다. 우리는 앞서 이 인터페이스를 구현했다.
정리
스프링MVC도 우리가 지금까지 한 것 처럼 서블릿 컨테이너 초기화 파일에 초기화 클래스를 등록해두었다. 그리고 WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해두고, 이것을 생성해서 실행한다.
따라서 스프링 MVC를 사용한다면 WebApplicationInitializer 인터페이스만 구현하면 AppInitV3SpringMvc 에서 본 것 처럼 편리하게 애플리케이션 초기화를 사용할 수 있다.
지금까지 알아본 내용은 모두 서블릿 컨테이너 위에서 동작하는 방법이다. 따라서 항상 톰캣 같은 서블릿 컨테이너에 배 포를 해야만 동작하는 방식이다.
과거에는 서블릿 컨테이너 위에서 모든 것이 동작했지만, 스프링 부트와 내장 톰캣을 사용하면서 이런 부분이 바뀌기 시 작했다.
스프링 부트와 내장 톰캣
웹 애플리케이션을 개발하고 배포하려면 다음과 같은 과정을 거쳐야 한다.
- 톰캣 같은 웹 애플리케이션 서버(WAS)를 별도로 설치해야 한다.
- 애플리케이션 코드를 WAR로 빌드해야 한다.
- 빌드한 WAR 파일을 WAS에 배포해야 한다.
웹 애플리케이션을 구동하고 싶으면 웹 애플리케이션 서버를 별도로 설치해야 하는 구조이다.
단점
- 톰캣 같은 WAS를 별도로 설치해야 한다.
- 개발 환경 설정이 복잡하다.
- 단순한 자바라면 별도의 설정을 고민하지 않고, main() 메서드만 실행하면 된다.
- 웹 애플리케이션은 WAS 실행하고 또 WAR와 연동하기 위한 복잡한 설정이 들어간다.
- 배포 과정이 복잡하다. WAR를 만들고 이것을 또 WAS에 전달해서 배포해야 한다.
- 톰캣의 버전을 변경하려면 톰캣을 다시 설치해야 한다.
=> 이런 문제를 해결하기 위해 톰캣을 라이브러리로 제공하는 내장 톰캣(embed tomcat) 기능을 제공한다
- 왼쪽 그림은 웹 애플리케이션 서버에 WAR 파일을 배포하는 방식, WAS를 실행해서 동작한다.
- 오른쪽 그림은 애플리케이션 JAR 안에 다양한 라이브러리들과 WAS 라이브러리가 포함되는 방식, main() 메서드를 실행해서 동작한다.
내장 톰캣 설정
톰캣을 라이브러리로 포함하고 자바 코드로 직접 실행하는 것
package hello.embed;
import hello.servlet.HelloServlet;
import hello.spring.HelloConfig;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
public class EmbedTomcatServletMain {
public static void main(String[] args) throws LifecycleException {
System.out.println("EmbedTomcatSpringMain.main");
//톰캣 설정
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new
AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
//스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처 서블릿 등록
Context context = tomcat.addContext("", "/");
tomcat.addServlet("", "dispatcher", dispatcher);
context.addServletMappingDecoded("/", "dispatcher");
tomcat.start();
}
}
스프링 컨테이너를 생성하고, 내장 톰캣에 디스패처 서블릿을 등록했다
main() 메서드를 실행하면 다음과 같이 동작한다.
- 내장 톰캣을 생성해서 8080 포트로 연결하도록 설정한다.
- 스프링 컨테이너를 만들고 필요한 빈을 등록한다.
- 스프링 MVC 디스패처 서블릿을 만들고 앞서 만든 스프링 컨테이너에 연결한다.
- 디스패처 서블릿을 내장 톰캣에 등록한다.
- 내장 톰캣을 실행한다.
내장 톰캣 - 빌드와 배포
자바의 main() 메서드를 실행하기 위해선 jar
형식으로 빌드해야 한다.
그리고 jar
안에 META-INF/MANIFEST.MF
파일에 실행 할 main()
메서드의 클래스를 지정해주어야 한다.
Gradle의 도움을 받으면 이 과정을 쉽게 진행할 수 있다.
task buildJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
with jar
}
jar파일을 압축을 풀고 살펴보면 spring이나 tomcat관련 코드들이 없다.
jar 파일은 jar파일을 포함할 수 없다.
- WAR와 다르게 JAR 파일은 내부에 라이브러리 역할을 하는 JAR 파일을 포함할 수 없다.
- 포함한다고 해도 인식이 안된다. 이것이 JAR 파일 스펙의 한계이다.
- 그렇다고 WAR를 사용할 수 도 없다. WAR는 웹 애플리케이션 서버(WAS) 위에서만 실행할 수 있다.
대안으로는 fat jar 또는 uber jar 라고 불리는 방법이다.
- Jar 안에는 Jar를 포함할 수 없다. 하지만 클래스는 얼마든지 포함할 수 있다.
- 라이브러리에 사용되는 jar 를 풀면 class 들이 나온다. 이 class 를 뽑아서 새로 만드는 jar 에 포함하는 것이다.
- 이렇게 하면 수 많은 라이브러리에서 나오는 class 때문에 뚱뚱한(fat) jar 가 탄생한다
task buildFatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
duplicatesStrategy = DuplicatesStrategy.WARN
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it :
zipTree(it) } }
with jar
}
Fat Jar의 장점
- Fat Jar 덕분에 하나의 jar 파일에 필요한 라이브러리들을 내장할 수 있게 되었다.
- 내장 톰캣 라이브러리를 jar 내부에 내장할 수 있게 되었다.
- 덕분에 하나의 jar 파일로 배포부터, 웹 서버 설치+실행까지 모든 것을 단순화 할 수 있다.
Fat Jar의 단점
Fat Jar는 완벽해 보이지만 몇가지 단점을 여전히 포함하고 있다.
- 어떤 라이브러리가 포함되어 있는지 확인하기 어렵다.
- 모두 class 로 풀려있으니 어떤 라이브러리가 사용되고 있는지 추적하기 어렵다.
- 파일명 중복을 해결할 수 없다.
편리한 부트 클래스 만들기
지금까지 만든 것을 라이브러리로 만들어서 배포한다면? 그것이 바로 스프링 부트이다
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
스프링 부트는 보통 예제와 같이 SpringApplication.run() 한줄로 시작한다.
스프링 부트와 웹서버
스프링 부트는 지금까지 고민한 문제를 깔끔하게 해결해준다.
- 내장 톰캣을 사용해서 빌드와 배포를 편리하게 한다.
- 빌드시 하나의 Jar를 사용하면서, 동시에 Fat Jar 문제도 해결한다.
- 지금까지 진행한 내장 톰캣 서버를 실행하기 위한 복잡한 과정을 모두 자동으로 처리한다.
실행과정
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
- 스프링 부트를 실행할 때는 자바
main()
메서드에서SpringApplication.run()
을 호출해주면 된다. - 여기에 메인 설정 정보를 넘겨주는데, 보통
@SpringBootApplication
애노테이션이 있는 현재 클래스를 지정해주면 된다. - 참고로 현재 클래스에는
@SpringBootApplication
애노테이션이 있는데, 이 애노테이션 안에는 컴포넌트 스캔을 포함한 여러 기능이 설정되어 있다. 기본 설정은 현재 패키지와 그 하위 패키지 모두를 컴포넌트 스캔한다.
이 단순해 보이는 코드 한 줄안에서는 수 많은 일이 발생하지만 핵심은 2가지다.
- 스프링 컨테이너를 생성한다.
- WAS(내장 톰캣)을 생성한다.
스프링 부트도 우리가 앞서 내장 톰캣에서 진행했던 것과 동일한 방식으로 스프링 컨테이너를 만들고, 내장 톰캣을 생성 하고 그 둘을 연결하는 과정을 진행한다.
내장 톰캣이 포함된 스프링 부트를 빌드해보자
- JAR를 푼 결과를 보면 Fat Jar가 아니라 처음보는 새로운 구조로 만들어져 있다.
- jar 내부에 jar를 담아서 인식하는 것이 불가능한데, jar가 포함되어 있고, 인식까지 되었다.
스프링 부트 실행 가능 Jar
스프링 부트는 이런 문제를 해결하기 위해 jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고 동시에 만든 jar를 내부 jar를 포함해서 실행할 수 있게 했다. 이것을 **실행 가능 Jar(Executable Jar)**라 한다. 이 실행 가능 Jar를 사용하면 다음 문제들을 깔끔하게 해결할 수 있다.
문제: 어떤 라이브러리가 포함되어 있는지 확인하기 어렵다.
해결: jar 내부에 jar를 포함하기 때문에 어떤 라이브러리가 포함되어 있는지 쉽게 확인할 수 있다.
문제: 파일명 중복을 해결할 수 없다.
해결: jar 내부에 jar를 포함하기 때문에 a.jar , b.jar 내부에 같은 경로의 파일이 있어도 둘다 인식할 수 있다.
참고로 실행 가능 Jar는 자바 표준은 아니고, 스프링 부트에서 새롭게 정의한 것이다.
jar 실행 정보
META-INF/MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.2
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17
- Main-Class
- 우리가 기대한 main() 이 있는 hello.boot.BootApplication 이 아니라 JarLauncher 라는 전혀 다른 클래스를 실행하고 있다.
- JarLauncher 는 스프링 부트가 빌드시에 넣어준다. org/springframework/boot/loader/JarLauncher 에 실제로 포함되어 있다.
- 스프링 부트는 jar 내부에 jar를 읽어들이는 기능이 필요하다. 또 특별한 구조에 맞게 클래스 정보도 읽어들여야 한다. 바로 JarLauncher 가 이런 일을 처리해준다. 이런 작업을 먼저 처리한 다음 StartClass: 에 지정된 main() 을 호출한다.
- Start-Class : 우리가 기대한 main() 이 있는 hello.boot.BootApplication 가 적혀있다.
- 기타: 스프링 부트가 내부에서 사용하는 정보들이다.
- Spring-Boot-Version : 스프링 부트 버전
- Spring-Boot-Classes : 개발한 클래스 경로
- Spring-Boot-Lib : 라이브러리 경로
- Spring-Boot-Classpath-Index : 외부 라이브러리 모음
- Spring-Boot-Layers-Index : 스프링 부트 구조 정보
- 참고: Main-Class 를 제외한 나머지는 자바 표준이 아니다. 스프링 부트가 임의로 사용하는 정보이다.
BOOT-INF
- classes : 우리가 개발한 class 파일과 리소스 파일
- lib : 외부 라이브러리
- classpath.idx : 외부 라이브러리 모음
- layers.idx : 스프링 부트 구조 정보
WAR구조는 WEB-INF 라는 내부 폴더에 사용자 클래스와 라이브러리를 포함하고 있는데, 실행 가능 Jar도 그 구조를 본따서 만들었다. 이름도 유사하게 BOOT-INF 이다.
JarLauncher 를 통해서 여기에 있는 classes 와 lib 에 있는 jar 파일들을 읽어들인다
스프링 부트 어플리케이션 실행 과정 정리
- java -jar xxx.jar
- MANIFEST.MF 인식
- JarLauncher.main() 실행
- BOOT-INF/classes/ 인식
- BOOT-INF/lib/ 인식
- BootApplication.main() 실행
참고
- 실행 가능 Jar가 아니라, IDE에서 직접 실행할 때는 BootApplication.main() 을 바로 실행한다.
- IDE가 필요한 라이브러리를 모두 인식할 수 있게 도와주기 때문에 JarLauncher 가 필요하지 않다.
스프링 부트 스타터와 라이브러리 관리
- 웹 프로젝트를 하나 설정하기 위해서는 수 많은 라이브러리를 알아야 한다. 여기에 추가로 각각의 라이브러리의 버전까지 골라서 선택해야 한다.
- 가장 어려운 문제는 각 라이브러리들 간에 서로 호환이 잘 되는 버전도 있지만 호환이 잘 안되는 버전도 있다는 점이다.
스프링 부트는 개발자 대신에 수 많은 라이브러리의 버전을 직접 관리해준다.
버전 관리 기능을 사용하려면 io.spring.dependency-management 플러그인을 사용해야 한다.
plugins {
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0' //추가
id 'java'
}
버전 정보 bom
- 해당 build.gradle 문서안에 보면 bom 이라는 항목이 있다.
- 각각의 라이브러리에 대한 버전이 명시되어 있는 것을 확인할 수 있다
참고 - 스프링 부트가 관리하지 않는 라이브러리
스프링 부트가 관리하지 않는 외부 라이브러리도 있다. 특히 아직 잘 알려지지 않거나 대중적이지 않은 경우가 그러한데, 이때는 다음과 같이 라이브러리의 버전을 직접 적어주어야 한다.
dependencies {
//3. 스프링 부트 스타터
implementation 'org.springframework.boot:spring-boot-starter-web'
}
- spring-boot-starter-web 이 라이브러리 하나로 지금까지 우리가 직접 넣어주었던 모든 라이브러리가 포함된다.
- 이것은 사용하기 편리하게 의존성을 모아둔 세트이다.
자동 구성
스프링 부트는 자동 구성(Auto Configuration)이라는 기능을 제공하는데, 일반적으로 자주 사용하는 수 많은 빈들을 자동으로 등록해주는 기능이다.
JdbcTemplate , DataSource , TransactionManager 모두 빈으로 등록을 안해도 스프링 부트가 자동 구성을 제공해서 자동으로 스프링 빈으로 등록된다.
(있으면 자동 등록을 안해준다.)
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ DatabaseInitializationDependencyConfigurer.class,
JdbcTemplateConfiguration.class,
NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {
}
@AutoConfiguration : 자동 구성을 사용하려면 이 애노테이션을 등록해야 한다.
- 자동 구성도 내부에 @Configuration 이 있어서 빈을 등록하는 자바 설정 파일로 사용할 수 있다.
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
- IF문과 유사한 기능을 제공한다. 이런 클래스가 있는 경우에만 설정이 동작한다. 만약 없으면 여기 있는 설정들이 모두 무효화 되고, 빈도 등록되지 않는다
자동 등록 설정
다음과 같은 자동 구성 기능들이 다음 빈들을 등록해준다.
- JdbcTemplateAutoConfiguration : JdbcTemplate
- DataSourceAutoConfiguration : DataSource
- DataSourceTransactionManagerAutoConfiguration : TransactionManager
Auto Configuration
자동 설정 Configuration 이라는 단어가 컴퓨터 용어에서는 환경 설정, 설정이라는 뜻으로 자주 사용된다. Auto Configuration은 크게 보면 빈들을 자동으로 등록해서 스프링이 동작하는 환경을 자동으로 설정해주기 때문에 자동 설정이라는 용어도 맞다.
자동 구성
Configuration 이라는 단어는 구성, 배치라는 뜻도 있다. 예를 들어서 컴퓨터라고 하면 CPU, 메모리등을 배치해야 컴퓨터가 동작한다. 이렇게 배치하는 것을 구성이라 한다.
스프링도 스프링 실행에 필요한 빈들을 적절하게 배치해야 한다. 자동 구성은 스프링 실행에 필요한 빈들을 자동으로 배치해주는 것이다
자동 설정, 자동 구성 두 용어 모두 맞는 말이다. 자동 설정은 넓게 사용되는 의미이고, 자동 구성은 실행에 필요한 컴포넌트 조각을 자동으로 배치한다는 더 좁은 의미에 가깝다.
정리
스프링 부트가 제공하는 자동 구성 기능을 이해하려면 다음 두 가지 개념을 이해해야 한다.
@Conditional : 특정 조건에 맞을 때 설정이 동작하도록 한다.
@AutoConfiguration : 자동 구성이 어떻게 동작하는지 내부 원리 이해
@Condition 인터페이스
package org.springframework.context.annotation;
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
- matches() 메서드가 true 를 반환하면 조건에 만족해서 동작하고, false 를 반환하면 동작하지 않는다.
- ConditionContext : 스프링 컨테이너, 환경 정보등을 담고 있다.
- AnnotatedTypeMetadata : 애노테이션 메타 정보를 담고 있다.
@ConditionalOnXxx
스프링은 @Conditional 과 관련해서 개발자가 편리하게 사용할 수 있도록 수 많은 @ConditionalOnXxx 를 제공한다.
@ConditionalOnClass , @ConditionalOnMissingClass
- 클래스가 있는 경우 동작한다. 나머지는 그 반대
@ConditionalOnBean , @ConditionalOnMissingBean
- 빈이 등록되어 있는 경우 동작한다. 나머지는 그 반대
@ConditionalOnProperty
- 환경 정보가 있는 경우 동작한다.
@ConditionalOnResource
- 리소스가 있는 경우 동작한다.
@ConditionalOnWebApplication , @ConditionalOnNotWebApplication
- 웹 애플리케이션인 경우 동작한다.
@ConditionalOnExpression
- SpEL 표현식에 만족하는 경우 동작한다.
Tip!!
@Conditional
자체는 스프링 부트가 아니라 스프링 프레임워크의 기능이다. 스프링 부트는 이 기능을 확장해서 @ConditionalOnXxx
를 제공한다.
순수 라이브러리 정리
순수 라이브러리를 사용하면 스프링 부트 자동 구성을 사용하는 것이 아니기 때문에 빈을 직접 하나하나 등록해주어야 한다.
- 외부 라이브러리를 직접 만들고 또 그것을 프로젝트에 라이브러리로 불러서 적용해보았다
- 그런데 라이브러리를 사용하는 클라이언트 개발자 입장을 생각해보면, 라이브러리 내부에 있는 어떤 빈을 등록해야하는지 알아야 하고, 그것을 또 하나하나 빈으로 등록해야 한다. 지금처럼 간단한 라이브러리가 아니라 초기 설정이 복잡하다면 사용자 입장에서는 상당히 귀찮은 작업이 될 수 있다.
- 이런 부분을 자동으로 처리해주는 것이 바로 스프링 부트 자동 구성(Auto Configuration)이다.
자동 구성 라이브러리 만들기
프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리되도록 해보자
쉽게 이야기해서 스프링 빈들이 자동으로 등록되는 것이다
자동 구성 추가
@AutoConfiguration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryAutoConfig {
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
}
@AutoConfiguration
- 스프링 부트가 제공하는 자동 구성 기능을 적용할 때 사용하는 애노테이션이다.
@ConditionalOnProperty
- memory=on 이라는 환경 정보가 있을 때 라이브러리를 적용한다. (스프링 빈을 등록한다.)
- 라이브러리를 가지고 있더라도 상황에 따라서 해당 기능을 켜고 끌 수 있게 유연한 기능을 제공한다.
자동 구성 대상 지정
이 부분이 중요하다. 스프링 부트 자동 구성을 적용하려면, 다음 파일에 자동 구성 대상을 꼭 지정해주어야 한다.
폴더 위치와 파일 이름이 길기 때문에 주의하자
파일 생성
src/main/resources/META-INF/spring/
폴더 생성
org.springframework.boot.autoconfigure.AutoConfiguration.imports
파일 생성
org.springframework.boot.autoconfigure.AutoConfiguration.imports안에 아래 클래스를 작성한다.
memory.MemoryAutoConfig
- 앞서 만든 자동 구성인 memory.MemoryAutoConfig 를 패키지를 포함해서 지정해준다.
- 스프링 부트는 시작 시점에
org.springframework.boot.autoconfigure.AutoConfiguration.imports
의 정보를 읽어서 자동 구성으로 사용한다. - 따라서 내부에 있는 MemoryAutoConfig 가 자동으로 실행된다.
앞서 project-v1 에서는 memory-v1 을 사용하기 위해 스프링 빈을 직접 등록했다.
project-v2 에서 사용하는 memory-v2 라이브러리에는 스프링 부트 자동 구성이 적용되어 있다. 따라서 빈을 등록하는 별도의 설정을 하지 않아도 된다.
정리
- 스프링 부트가 제공하는 자동 구성 덕분에 복잡한 빈 등록이나 추가 설정 없이 단순하게 라이브러리의 추가만으로 프로젝트를 편리하게 구성할 수 있다.
- @ConditionalOnXxx 덕분에 라이브러리 설정을 유연하게 제공할 수 있다.
자동 구성의 이해
스프링 부트는 다음 경로에 있는 파일을 읽어서 스프링 부트 자동 구성으로 사용한다.
resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
스프링 부트 자동 구성이 동작하는 원리는 다음 순서로 확인할 수 있다.
@SpringBootApplication
-> @EnableAutoConfiguration
-> @Import(AutoConfigurationImportSelector.class)
@EnableAutoConfiguration이 어노테이션이 AutoConfiguration을 활성화하는 기능
@Import 는 주로 스프링 설정 정보( @Configuration )를 포함할 때 사용한다.
ImportSelector의 이해
@Import 에 설정 정보를 추가하는 방법은 2가지가 있다.
- 정적인 방법: @Import (클래스) 이것은 정적이다. 코드에 대상이 딱 박혀 있다. 설정으로 사용할 대상을 동적으로 변경할 수 없다.
- 동적인 방법: @Import ( ImportSelector ) 코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다.
정적인 방법
@Configuration
@Import({AConfig.class, BConfig.class})
public class AppConfig {...}
스프링에서 다른 설정 정보를 추가하고 싶으면 다음과 같이 @Import 를 사용하면 된다.
동적인 방법
특정 조건에 따라서 설정 정보 선택
public class HelloImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"hello.selector.HelloConfig"};
}
}
- 설정 정보를 동적으로 선택할 수 있게 해주는 ImportSelector 인터페이스를 구현했다.
- 여기서는 단순히 hello.selector.HelloConfig 설정 정보를 반환한다.
- 이렇게 반환된 설정 정보는 선택되어서 사용된다.
- 여기에 설정 정보로 사용할 클래스를 동적으로 프로그래밍 하면 된다.
public class ImportSelectorTest {
@Test
void staticConfig() {
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(StaticConfig.class);
HelloBean bean = appContext.getBean(HelloBean.class);
assertThat(bean).isNotNull();
}
@Test
void selectorConfig() {
AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(SelectorConfig.class);
HelloBean bean = appContext.getBean(HelloBean.class);
assertThat(bean).isNotNull();
}
@Configuration
@Import(HelloConfig.class)
public static class StaticConfig {
}
@Configuration
@Import(HelloImportSelector.class)
public static class SelectorConfig {
}
}
staticConfig()
- 스프링 컨테이너를 만들고, StaticConfig.class 를 초기 설정 정보로 사용했다. -> 스프링 컨테이너에 잘 등록된다.
selectorConfig()
- selectorConfig() 는 SelectorConfig 를 초기 설정 정보로 사용
- SelectorConfig 는 @Import(HelloImportSelector.class) 에서 ImportSelector 의 구현체인 HelloImportSelector 를 사용했다.
- 스프링은 HelloImportSelector 를 실행하고, “hello.selector.HelloConfig” 라는 문자를 반환 받는다
- 스프링은 이 문자에 맞는 대상을 설정 정보로 사용한다. 따라서 hello.selector.HelloConfig 이 설정 정보로 사용된다.
@EnableAutoConfiguration 동작 방식
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {…}
- AutoConfigurationImportSelector 는 ImportSelector 의 구현체이다. 따라서 설정 정보를 동적으로 선택할 수 있다
실제로 이 코드는 모든 라이브러리에 있는 다음 경로의 파일을 확인한다
META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
@SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)
-> resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 열어서 설정 정보 선택
-> 해당 파일의 설정 정보가 스프링 컨테이너에 등록되고 사용
정리
스프링 부트의 자동 구성을 직접 만들어서 사용할 때는 다음을 참고하자.
- @AutoConfiguration 에 자동 구성의 순서를 지정할 수 있다.
- @AutoConfiguration 도 설정 파일이다. 내부에 @Configuration 이 있는 것을 확인할 수 있다.
- 하지만 일반 스프링 설정과 라이프사이클이 다르기 때문에 컴포넌트 스캔의 대상이 되면 안된다.
- 파일에 지정해서 사용해야 한다.
- resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 그래서 스프링 부트가 제공하는 컴포넌트 스캔에서는 @AutoConfiguration 을 제외하는
AutoConfigurationExcludeFilter
필터가 포함되어 있다. - 자동 구성이 내부에서 컴포넌트 스캔을 사용하면 안된다. 대신에 자동 구성 내부에서 @Import 는 사용할 수 있다
자동 구성을 언제 사용하는가?
자동 구성을 알아야 하는 진짜 이유는 개발을 진행 하다보면 사용하는 특정 빈들이 어떻게 등록된 것인지 확인이 필요할 때가 있다.
이럴 때 스프링 부트의 자동 구성 코드를 읽을 수 있어야 한다.
그래야 문제가 발생했을 때 대처가 가능하다.
자동화는 매우 편리한 기능이지만 자동화만 믿고 있다가 실무에서 문제가 발생했을 때는 파고 들어가서 문제를 확인하는 정도는 이해해야 한다.
다음장에선 이런 방식으로 빈이 자동 등록되면, 빈을 등록할 때 사용하는 설정 정보는(ex - DB접속 URL, ID, PW) 어떻게 변경해야 하는지 알아본다.
외부 설정과 프로필 1
외부 설정이란?
환경에 따라 변하는 설정값을 실행 시점에 주입
외부 설정은 일반적으로 4가지 방법이 있다.
- OS 환경 변수: OS에서 지원하는 외부 설정, 해당 OS를 사용하는 모든 프로세스에서 사용
- 자바 시스템 속성: 자바에서 지원하는 외부 설정, 해당 JVM안에서 사용
- 자바 커맨드 라인 인수: 커맨드 라인에서 전달하는 외부 설정, 실행시 main(args) 메서드에서 사용
- 외부 파일(설정 데이터): 프로그램에서 외부 파일을 직접 읽어서 사용
- 애플리케이션에서 특정 위치의 파일을 읽도록 해둔다. 예) data/hello.txt
- 그리고 각 서버마다 해당 파일안에 다른 설정 정보를 남겨둔다.
OS 환경 변수
@Slf4j
public class OsEnv {
public static void main(String[] args) {
Map<String, String> envMap = System.getenv();
for (String key : envMap.keySet()) {
log.info("env {}={}", key, System.getenv(key));
}
}
}
필요한 곳에서 System.getenv()
로 외부 설정을 사용할 수 있다.
자바 시스템 속성
- 자바 시스템 속성(Java System properties)은 실행한 JVM 안에서 접근 가능한 외부 설정이다.
- 자바가 내부에서 미리 설정해두고 사용하는 속성들도 있다.
@Slf4j
public class JavaSystemProperties {
public static void main(String[] args) {
Properties properties = System.getProperties();
for (Object key : properties.keySet()) {
log.info("prop {}={}", key,
System.getProperty(String.valueOf(key)));
}
String url = System.getProperty("url");
String username = System.getProperty("username");
String password = System.getProperty("password");
log.info("url={}", url);
log.info("username={}", username);
log.info("password={}", password);
}
}
예) java -Durl=dev -jar app.jar
System.getProperties() 를 사용하면 Map 과 유사한( Map 의 자식 타입) key=value 형식의 Properties 를 받을 수 있다.
jar 로 빌드되어 있다면 실행시 다음과 같이 자바 시스템 속성을 추가할 수 있다.
java -Durl=devdb -Dusername=dev_user -Dpassword=dev_pw -jar app.jar
커맨드 라인 인수
커맨드 라인 인수(Command line arguments)는 애플리케이션 실행 시점에 외부 설정값을 main(args) 메서드의 args 파라미터로 전달하는 방법이다.
예) java -jar app.jar dataA dataB
@Slf4j
public class CommandLineV1 {
public static void main(String[] args) {
for (String arg : args) {
log.info("arg {}", arg);
}
}
}
커맨드라인 옵션 인수
스프링에서는 커맨드 라인 인수를 key=value 형식으로 편리하게 사용할 수 있도록 스프링 만의 표준 방식을 정의
--key=value
형식으로 사용한다.
@Slf4j
public class CommandLineV2 {
public static void main(String[] args) {
for (String arg : args) {
log.info("arg {}", arg);
}
ApplicationArguments appArgs = new DefaultApplicationArguments(args);
log.info("SourceArgs = {}", List.of(appArgs.getSourceArgs()));
log.info("NonOptionArgs = {}", appArgs.getNonOptionArgs());
log.info("OptionNames = {}", appArgs.getOptionNames());
Set<String> optionNames = appArgs.getOptionNames();
for (String optionName : optionNames) {
log.info("option args {}={}", optionName,
appArgs.getOptionValues(optionName));
}
List<String> url = appArgs.getOptionValues("url");
List<String> username = appArgs.getOptionValues("username");
List<String> password = appArgs.getOptionValues("password");
List<String> mode = appArgs.getOptionValues("mode");
log.info("url={}", url);
log.info("username={}", username);
log.info("password={}", password);
log.info("mode={}", mode);
}
}
Tip!
참고로 옵션 인수는 –username=userA –username=userB 처럼 하나의 키에 여러 값을 포함할 수 있기 때문에 appArgs.getOptionValues(key) 의 결과는 리스트( List )를 반환한다.
커맨드 라인 옵션 인수와 스프링 부트
- 스프링 부트는 커맨드 라인을 포함해서 커맨드 라인 옵션 인수를 활용할 수 있는 ApplicationArguments 를 스프링 빈으로 등록해둔다.
- 그 안에 입력한 커맨드 라인을 저장해둔다.
- 해당 빈을 주입 받으면 커맨드 라인으로 입력한 값을 어디서든 사용할 수 있다.
@Slf4j
@Component
public class CommandLineBean {
private final ApplicationArguments arguments;
public CommandLineBean(ApplicationArguments arguments) {
this.arguments = arguments;
}
@PostConstruct
public void init() {
log.info("source {}", List.of(arguments.getSourceArgs()));
log.info("optionNames {}", arguments.getOptionNames());
Set<String> optionNames = arguments.getOptionNames();
for (String optionName : optionNames) {
log.info("option args {}={}", optionName,
arguments.getOptionValues(optionName));
}
}
}
스프링 통합
정책에 따라서 설정값을 사용하는 방법이 달라질 수 있고, 잘못된 방법으로 사용할 수 있다.
스프링은 이 문제를 Environment 와 PropertySource 라는 추상화를 통해서 해결한다.
PropertySource
- org.springframework.core.env.PropertySource
- 스프링은 PropertySource 라는 추상 클래스를 제공하고, 각각의 외부 설정를 조회하는 XxxPropertySource 구현체를 만들어두었다.
- 스프링은 로딩 시점에 필요한 PropertySource 들을 생성하고, Environment 에서 사용할 수 있게 연결해둔다.
Environment
- org.springframework.core.env.Environment
- Environment 를 통해서 특정 외부 설정에 종속되지 않고, 일관성 있게 key=value 형식의 외부 설정에 접근할 수 있다.
- environment.getProperty(key) 를 통해서 값을 조회할 수 있다.
설정 데이터(파일)
여기에 우리가 잘 아는 application.properties , application.yml 도 PropertySource 에 추가된다.
따라서 Environment 를 통해서 접근할 수 있다.
@Slf4j
@Component
public class EnvironmentCheck {
private final Environment env;
public EnvironmentCheck(Environment env) {
this.env = env;
}
@PostConstruct
public void init() {
String url = env.getProperty("url");
String username = env.getProperty("username");
String password = env.getProperty("password");
log.info("env url={}", url);
log.info("env username={}", username);
log.info("env password={}", password);
}
}
정리
- 커맨드 라인 옵션 인수, 자바 시스템 속성 모두 Environment 를 통해서 동일한 방법으로 읽을 수 있다
- 스프링은 Environment 를 통해서 외부 설정을 읽는 방법을 추상화했다.
- 덕분에 자바 시스템 속성을 사용하다가 만약 커맨드 라인 옵션 인수를 사용하도록 읽는 방법이 변경되어도, 개발 소스 코드는 전혀 변경하지 않아도 된다.
우선순위
- 더 유연한 것이 우선권을 가진다
- 범위가 넒은 것 보다 좁은 것이 우선권을 가진다.
설정 데이터1 - 외부파일
지금까지 살펴본 방법은 관리가 어렵다
등장하는 대안으로는 설정값을 파일에 넣어서 관리하는 방법이다
.properties
라는 파일은 key=value 형식을 사용해서 설정값을 관리하기에 적합하다.
- 예를 들면 개발 서버와 운영 서버 각각에 application.properties 라는 같은 이름의 파일을 준비해둔다.
- 그리고 애플리케이션 로딩 시점에 해당 파일을 읽어서 그 속에 있는 값들을 외부 설정값으로 사용하면 된다.
Tip!
지금부터 설명할 내용은 application.properties 대신에 yml 형식의 application.yml 에도 동일하게 적용된다
남은 문제
- 외부 설정을 별도의 파일로 관리하게 되면 설정 파일 자체를 관리하기 번거로운 문제가 발생한다.
- 서버가 10대면 변경사항이 있을 때 10대 서버의 설정 파일을 모두 각각 변경해야 하는 불편함이 있다.
- 설정 파일이 별도로 관리되기 때문에 설정값의 변경 이력을 확인하기 어렵다.
- 특히 설정값의 변경 이력이 프로젝트 코드들과 어떻게 영향을 주고 받는지 그 이력을 같이 확인하기 어렵다.
설정 데이터2 - 내부파일
이 문제를 해결하는 간단한 방법은 설정 파일을 프로젝트 내부에 포함해서 관리하는 것이다. 그리고 빌드 시점에 함께 빌드되게 하는 것이다.
외부 설정으로 넘어온 프로필 값이 dev 라면 application-dev.properties 를 읽고 prod 라면 application-prod.properties 를 읽어서 사용하면 된다.
프로필
- 스프링은 이런 곳에서 사용하기 위해 프로필이라는 개념을 지원한다.
- spring.profiles.active 외부 설정에 값을 넣으면 해당 프로필을 사용한다고 판단한다.
- 프로필에 따라서 다음과 같은 규칙으로 해당 프로필에 맞는 내부 파일(설정 데이터)을 조회한다.
application-{profile}.properties
하나의 파일로 통합
주의!
- 속성 파일 구분 기호에는 선행 공백이 없어야 하며 정확히 3개의 하이픈 문자가 있어야 한다.
- 구분 기호 바로 앞과 뒤의 줄은 같은 주석 접두사가 아니어야 한다.
- 파일을 분할하는 #— 주석 위 아래는 주석을 적으면 안된다
우선순위
url=local.db.com
username=local_user
password=local_pw
#---
spring.config.activate.on-profile=dev
url=dev.db.com
username=dev_user
password=dev_pw
#---
spring.config.activate.on-profile=prod
url=prod.db.com
username=prod_user
password=prod_pw
#---
url=hello.db.com
단순하게 문서를 위에서 아래로 순서대로 읽으면서 값을 설정한다. 이때 기존 데이터가 있으면 덮어쓴다.
우선순위는 위에서 아래로 적용된다. 아래가 더 우선순위가 높다.
자주 사용하는 우선순위
- 설정 데이터( application.properties )
- OS 환경변수
- 자바 시스템 속성
- 커맨드 라인 옵션 인수
- @TestPropertySource (테스트에서 사용)
설정 데이터 우선순위
- jar 내부 application.properties
- jar 내부 프로필 적용 파일 application-{profile}.properties
- jar 외부 application.properties
- jar 외부 프로필 적용 파일 application-{profile}.properties
정리
- 실무에서 대부분의 개발자들은 applicaiton.properties 에 외부 설정값들을 보관한다.
- 사용하다가 일부 속성을 변경할 필요가 있다면 더 높은 우선순위를 가지는 자바 시스템 속성이나 커맨드 라인 옵션 인수를 사용하면 되는 것이다.
외부 설정과 프로필 2
외부 설정 사용 - Environment
다음과 같은 외부 설정들은 스프링이 제공하는 Environment 를 통해서 일관된 방식으로 조회할 수 있다.
스프링이 지원하는 다양한 외부 설정 조회 방법
- Environment
- @Value - 값 주입
- @ConfigurationProperties - 타입 안전한 설정 속성
외부 속성을 사용하기 위해 preperties를 작성
my.datasource.url=local.db.com
my.datasource.username=local_user
my.datasource.password=local_pw
my.datasource.etc.max-connection=1
my.datasource.etc.timeout=3500ms
my.datasource.etc.options=CACHE,ADMIN
@Slf4j
@Configuration
public class MyDataSourceEnvConfig {
private final Environment env;
public MyDataSourceEnvConfig(Environment env) {
this.env = env;
}
@Bean
public MyDataSource myDataSource() {
String url = env.getProperty("my.datasource.url");
String username = env.getProperty("my.datasource.username");
String password = env.getProperty("my.datasource.password");
int maxConnection = env.getProperty("my.datasource.etc.max-connection", Integer.class);
Duration timeout = env.getProperty("my.datasource.etc.timeout", Duration.class);
List<String> options = env.getProperty("my.datasource.etc.options", List.class);
return new MyDataSource(url, username, password, maxConnection, timeout, options);
}
}
- Environment 를 사용하면 외부 설정의 종류와 관계없이 코드 안에서 일관성 있게 외부 설정을 조회할 수 있다.
- Environment.getProperty(key, Type) 를 호출할 때 타입 정보를 주면 해당 타입으로 변환해준다. (스프링 내부 변환기가 작동한다.)
- env.getProperty(“my.datasource.etc.max-connection”, Integer.class) : 문자 숫자로 변환
- env.getProperty(“my.datasource.etc.timeout”, Duration.class) : 문자 Duration (기간) 변환
- env.getProperty(“my.datasource.etc.options”, List.class) : 문자 List 변환 ( A,B [A,B] )
외부설정 사용 - @Value
- @Value 에 ${} 를 사용해서 외부 설정의 키 값을 주면 원하는 값을 주입 받을 수 있다.
- @Value 는 필드에 사용할 수도 있고, 파라미터에 사용할 수도 있다.
- 만약 키를 찾지 못할 경우 코드에서 기본값을 사용하려면 다음과 같이 : 뒤에 기본값을 적어주면 된다.
- 예)
@Value("${my.datasource.etc.max-connection:1}")
- 예)
외부설정 사용 - @ConfigurationProperties
Type-safe Configuration Properties
- 스프링은 외부 설정의 묶음 정보를 객체로 변환하는 기능을 제공한다. 이것을 타입 안전한 설정 속성이라 한다
- 외부 설정을 자바 코드로 관리
- 설정 정보 그 자체도 타입을 가지게 된다.
@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV1 {
private String url;
private String username;
private String password;
private Etc etc = new Etc();
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
- 외부 설정을 주입 받을 객체를 생성한다. 그리고 각 필드를 외부 설정의 키 값에 맞추어 준비한다.
- @ConfigurationProperties 이 있으면 외부 설정을 주입 받는 객체라는 뜻이다. 여기에 외부 설정 KEY의 묶음 시작점인 my.datasource 를 적어준다.
- 기본 주입 방식은 자바빈 프로퍼티 방식이다. Getter , Setter 가 필요하다. (롬복의 @Data 에 의해 자동 생성된다.)
@Slf4j
@EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
public class MyDataSourceConfigV1 {
private final MyDataSourcePropertiesV1 properties;
public MyDataSourceConfigV1(MyDataSourcePropertiesV1 properties) {
this.properties = properties;
}
@Bean
public MyDataSource dataSource() {
return new MyDataSource(
properties.getUrl(),
properties.getUsername(),
properties.getPassword(),
properties.getEtc().getMaxConnection(),
properties.getEtc().getTimeout(),
properties.getEtc().getOptions());
}
}
- @EnableConfigurationProperties(MyDataSourcePropertiesV1.class)
- 스프링에게 사용할 @ConfigurationProperties 를 지정해주어야 한다. 이렇게 하면 해당 클래스는 스프링 빈으로 등록되고, 필요한 곳에서 주입 받아서 사용할 수 있다.
- private final MyDataSourcePropertiesV1 properties 설정 속성을 생성자를 통해 주입 받아서 사용한다.
타입 안전
- ConfigurationProperties 를 사용하면 타입 안전한 설정 속성을 사용할 수 있다
- 타입이 다르면 오류 발생 (ex - Int인데 String인 경우)
정리
application.properties 에 필요한 외부 설정을 추가하고, @ConfigurationProperties 를 통해서 MyDataSourcePropertiesV1 에 외부 설정의 값들을 설정
표기법 변환
스프링은 캐밥 표기법을 자바 낙타 표기법으로 중간에서 자동으로 변환해준다.
- application.properties 에서는 max-connection
- 자바 코드에서는 maxConnection
@ConfigurationPropertiesScan
- @ConfigurationProperties 를 하나하나 직접 등록할 때는 @EnableConfigurationProperties 를 사용한다.
- @ConfigurationProperties 를 특정 범위로 자동 등록할 때는 @ConfigurationPropertiesScan 을 사용하면 된다.
여기서 문제 상황
- Setter 를 가지고 있기 때문에 누군가 실수로 값을 변경하는 문제가 발생할 수 있다.
- Setter 를 제거하고 대신에 생성자를 사용하면 중간에 데이터를 변경하는 실수를 근본적으로 방지할 수 있다.
- 좋은 프로그램은 제약이 있는 프로그램이다
@ConfigurationProperties 는 Getter, Setter를 사용하는 자바빈 프로퍼티 방식이 아니라 생성자를 통해서 객체를 만드는 기능도 지원한다.
@Getter
@ConfigurationProperties("my.datasource")
public class MyDataSourcePropertiesV2 {
private String url;
private String username;
private String password;
private Etc etc;
public MyDataSourcePropertiesV2(String url, String username, String password, @DefaultValue Etc etc) {
this.url = url;
this.username = username;
this.password = password;
this.etc = etc;
}
@Getter
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options;
public Etc(int maxConnection, Duration timeout, @DefaultValue("DEFAULT") List<String> options) {
this.maxConnection = maxConnection;
this.timeout = timeout;
this.options = options;
}
}
}
- 생성자를 만들어 두면 생성자를 통해서 설정 정보를 주입한다.
- @Getter 롬복이 자동으로 getter 를 만들어준다.
- @DefaultValue : 해당 값을 찾을 수 없는 경우 기본값을 사용한다.
@DefaultValue Etc etc
- etc 를 찾을 수 없을 경우 Etc 객체를 생성하고 내부에 들어가는 값은 비워둔다. ( null , 0 )
@DefaultValue("DEFAULT") List<String> options
- options 를 찾을 수 없을 경우 DEFAULT 라는 이름의 값을 사용한다.
정리
application.properties 에 필요한 외부 설정을 추가하고, @ConfigurationProperties 의 생성자 주입을통해서 값을 읽어들였다. Setter 가 없으므로 개발자가 중간에 실수로 값을 변경하는 문제가 발생하지 않는다.
문제
- 숫자에 문자가 들어오는 것 같은 기본적인 타입 문제들은 해결이 되었따.
- 숫자의 범위 지정같은 경우는 어떻게 할까
@ConfigurationProperties 은 자바 객체이기 때문에 스프링이 자바 빈 검증기를 사용할 수 있도록 지원
- @Validated 애노테이션이 있어야 검증을 실행한다. (클래스에 달아줘야 함)
- 필드에 어노테이션을 추가한다
- @NotEmpty url , username , password 는 항상 값이 있어야 한다. 필수 값이 된다.
- @Min(1) @Max(999) maxConnection : 최소 1 , 최대 999 의 값을 허용한다.
- @DurationMin(seconds = 1) @DurationMax(seconds = 60) : 최소 1, 최대 60초를 허용한다.
jakarta.validation.constraints.Max
패키지 이름에 jakarta.validation 으로 시작하는 것은 자바 표준 검증기에서 지원하는 기능이다
ConfigurationProperties 장점
- 외부 설정을 객체로 편리하게 변환해서 사용할 수 있다.
- 외부 설정의 계층을 객체로 편리하게 표현할 수 있다.
- 외부 설정을 타입 안전하게 사용할 수 있다.
- 검증기를 적용할 수 있다.
YAML
스프링은 설정 데이터를 사용할 때 application.properties 뿐만 아니라 application.yml 이라는 형식도 지원한다.
my:
datasource:
url: local.db.com
username: local_user
password: local_pw
etc:
maxConnection: 2
timeout: 60s
options: LOCAL, CACHE
---
spring:
config:
activate:
on-profile: dev
my:
datasource:
url: dev.db.com
username: dev_user
password: dev_pw
etc:
maxConnection: 10
timeout: 60s
options: DEV, CACHE
---
spring:
config:
activate:
on-profile: prod
my:
datasource:
url: prod.db.com
username: prod_user
password: prod_pw
etc:
maxConnection: 50
timeout: 10s
options: PROD, CACHE
주의 application.properties , application.yml 을 같이 사용하면 application.properties가 우선권을 가진다
@Profile
- 프로필과 외부 설정을 사용해서 각 환경마다 설정값을 다르게 적용하는 것은 이해했다.
- 설정값이 다른 정도가 아니라 각 환경마다 서로 다른 빈을 등록해야 한다면 어떻게 해야할까?
- ex) 로컬 개발 환경에서는 실제 결제가 발생하면 문제가 되니 가짜 결제 기능이 있는 스프링 빈을 등록하고, 운영 환경에서는 실제 결제 기능을 제공하는 스프링 빈을 등록한다고 가정
@Slf4j
@Configuration
public class PayConfig {
@Bean
@Profile("default")
public LocalPayClient localPayClient() {
log.info("LocalPayClient 빈 등록");
return new LocalPayClient();
}
@Bean
@Profile("prod")
public ProdPayClient prodPayClient() {
log.info("ProdPayClient 빈 등록");
return new ProdPayClient();
}
}
- @Profile 애노테이션을 사용하면 해당 프로필이 활성화된 경우에만 빈을 등록한다.
- 스프링은 @Conditional 기능을 활용해서 개발자가 더 편리하게 사용할 수 있는 @Profile 기능을 제공하는 것이다.
@Profile 을 사용하면 각 환경 별로 외부 설정 값을 분리하는 것을 넘어서, 등록되는 스프링 빈도 분리할 수 있다
액츄에이터
프로덕션 준비 기능이란?
-
서비스에 문제가 없는지 모니터링하고 지표들을 심어서 감시하는 활동
-
운영 환경에서 서비스할 때 필요한 이런 기능들을 프로덕션 준비 기능이라 한다.
-
프로덕션을 운영에 배포할 때 준비해야 하는 비 기능적 요소들을 뜻한다
-
애플리케이션이 현재 살아있는지, 로그 정보는 정상 설정 되었는지, 커넥션 풀은 얼마나 사용되고 있는지 등
-
지표(metric), 추적(trace), 감사(auditing)
-
모니터링
스프링 부트가 제공하는 액추에이터는 이런 프로덕션 준비 기능을 매우 편리하게 사용할 수 있는 다양한 편의 기능들을 제공한다.
더 나아가서 마이크로미터, 프로메테우스, 그라파나 같은 최근 유행하는 모니터링 시스템과 매우 쉽게 연동할 수 있는 기능도 제공한다.
- localhost:8080/actuator/health -> 이 기능은 현재 서버가 잘 동작하고 있는지 애플리케이션의 헬스 상태를 나타낸다.
- 액츄에이터는 /actuator 경로를 통해서 기능을 제공한다 -> 액츄에이터가 제공하는 수 많은 기능을 확인할 수 있다.
- 액츄에이터가 제공하는 기능 하나하나를 엔드포인트라 한다. health 는 헬스 정보를, beans 는 스프링 컨테이너에 등록된 빈을 보여준다
액츄에이터 기능을 웹에 노출
management:
endpoints:
web:
exposure:
include: '*'
엔드 포인트 설정
엔드포인트를 사용하려면 다음 2가지 과정이 모두 필요하다.
- 엔드포인트 활성화
- 엔드포인트 노출
- 엔드포인트를 활성화 한다는 것은 해당 기능 자체를 사용할지 말지 on , off 를 선택
- 엔드포인트를 노출하는 것은 활성화된 엔드포인트를 HTTP에 노출할지 아니면 JMX에 노출할지 선택
- 엔드포인트는 대부분 기본으로 활성화
- 따라서 어떤 엔드포인트를 노출할지 선택하면 된다
- 위의 yml파일이 웹에 노출하는 설정이다.
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: '*'
shutdown기능은 기본적으로 꺼져있는데 키면 /actuator/shutdown시 서버가 종료된다.
다양한 엔드포인트
- beans : 스프링 컨테이너에 등록된 스프링 빈을 보여준다.
- conditions : condition 을 통해서 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유를 표시한다.
- configprops : @ConfigurationProperties 를 보여준다.
- env : Environment 정보를 보여준다.
- health : 애플리케이션 헬스 정보를 보여준다.
- httpexchanges : HTTP 호출 응답 정보를 보여준다. HttpExchangeRepository 를 구현한 빈을 별도로 등록해야 한다.
- info : 애플리케이션 정보를 보여준다.
- loggers : 애플리케이션 로거 설정을 보여주고 변경도 할 수 있다.
- metrics : 애플리케이션의 메트릭 정보를 보여준다.
- mappings : @RequestMapping 정보를 보여준다.
- threaddump : 쓰레드 덤프를 실행해서 보여준다.
- shutdown : 애플리케이션을 종료한다. 이 기능은 기본으로 비활성화 되어 있다.
health , info , loggers , httpexchanges , metrics 는 더 자세히 알아본다.
헬스 정보
헬스 정보를 사용하면 애플리케이션에 문제가 발생했을 때 문제를 빠르게 인지할 수 있다
- 헬스 정보는 단순히 애플리케이션이 요청에 응답을 할 수 있는지 판단하는 것을 넘어서 애플리케이션이 사용하는 데이터베이스가 응답하는지, 디스크 사용량에는 문제가 없는지 같은 다양한 정보들을 포함해서 만들어진다.
헬스 정보를 더 자세히 보려면 다음 옵션을 지정한다.
management:
endpoint:
health:
show-details: always
# 간단하게 노출
# show-components: always
Tip!
- 자세한 헬스 기본 지원 기능은 다음 공식 메뉴얼을 참고하자
애플리케이션 정보
info 엔드포인트는 애플리케이션의 기본 정보를 노출한다.
- java : 자바 런타임 정보
- os : OS 정보
- env : Environment 에서 info. 로 시작하는 정보
- build : 빌드 정보, META-INF/build-info.properties 파일이 필요하다.
- git : git 정보, git.properties 파일이 필요하다.
java, os, env 정보
management:
endpoint:
shutdown:
enabled: true
health:
show-details: always
endpoints:
web:
exposure:
include: '*'
info:
java:
enabled: true
os:
enabled: true
env:
enabled: true
info:
app:
name: hello-actuator
company: yh
build정보
- 빌드 정보를 노출하려면 빌드 시점에 META-INF/build-info.properties 파일을 만들어야 한다.
- gradle 을 사용하면 다음 내용을 추가하면 된다
springBoot {
buildInfo()
}
이렇게 하고 빌드를 해보면 build 폴더안에 resources/main/META-INF/build-info.properties 파일을 확인할 수 있다
git
```groovy
plugins {
...
id "com.gorylenko.gradle-git-properties" version "2.4.1" //git info
}
빌드를 해보면 build 폴더안에 resources/main/git.properties 파일을 확인할 수 있다
더 자세한 정보를 보고 싶으면 다음 옵션을 적용한다.
management:
info:
git:
mode: 'full'
로거
loggers 엔드포인트를 사용하면 로깅과 관련된 정보를 확인하고, 또 실시간으로 변경할 수도 있다. 코드를 통해서 알아보자.
logging:
level:
hello.controller: debug
hello.controller 패키지와 그 하위는 debug 레벨을 출력하도록 했다. 이제 앞서 만든 LogController 클래스도 debug 레벨의 영향을 받는다.
- 로그를 별도로 설정하지 않으면 스프링 부트는 기본으로 INFO 를 사용한다. 실행 결과를 보면 ROOT 의 configuredLevel 가 INFO 인 것을 확인할 수 있다. 그 하위도 모두 INFO 레벨이 적용된다.
- 앞서 우리는 hello.controller 는 DEBUG 로 설정했다. 그래서 해당 부분에서 configuredLevel이 DEBUG 로 설정된 것을 확인할 수 있다. 그 하위도 DEBUG 레벨이 적용된다.
실시간 로그 레벨 변경
개발 서버는 보통 DEBUG 로그를 사용하지만, 운영 서버는 보통 요청이 아주 많다. 따라서 로그도 너무 많이 남기 때문에 DEBUG 로그까지 모두 출력하게 되면 성능이나 디스크에 영향을 주게 된다. 그래서 운영 서버는 중요하다고 판단되는 INFO 로그 레벨을 사용한다.
loggers 엔드포인트를 사용하면 애플리케이션을 다시 시작하지 않고, 실시간으로 로그 레벨을 변경할 수 있다.
HTTP 요청 응답 기록
HTTP 요청과 응답의 과거 기록을 확인하고 싶다면 httpexchanges 엔드포인트를 사용하면 된다
스프링 부트는 기본으로 InMemoryHttpExchangeRepository 구현체를 제공한다
참고로 이 기능은 매우 단순하고 기능에 제한이 많기 때문에 개발 단계에서만 사용하고, 실제 운영 서비스에서는 모니터링 툴이나 핀포인트, Zipkin 같은 다른 기술을 사용하는 것이 좋다.
액츄에이터와 보안
액츄에이터의 엔드포인트들은 외부 인터넷에서 접근이 불가능하게 막고, 내부에서만 접근 가능한 내부망을 사용하는 것이 안전하다.
액츄에이터 포트 설정
management.server.port=9292
액츄에이터 URL 경로에 인증 설정
포트를 분리하는 것이 어렵고 어쩔 수 없이 외부 인터넷 망을 통해서 접근해야 한다면 /actuator 경로에 서블릿 필터, 또는 스프링 시큐티리를 통해서 인증된 사용자만 접근 가능하도록 추가 개발이 필요하다.
제일 좋은 방법은 내부망에서만 사용 가능하게 설정하는 것이다.
정리
마이크로미터, 프로메테우스, 그라파나
마이크로미터
모니터링 툴이 작동하려면 시스템의 다양한 지표들을 각각의 모니터링 툴에 맞도록 만들어서 보내주어야 한다
모니터링 툴을 변경하면 기존 측정에 사용한 코드를 전부 변경해야 하는데 이러한 문제를 해결하는 것이 마이크로미터 라이브러리다.
- 마이크로미터는 애플리케이션 메트릭 파사드라고 불리는데, 애플리케이션의 메트릭(측정 지표)을 마이크로미터가 정한 표준 방법으로 모아서 제공
- 마이크로미터가 추상화를 통해서 구현체를 쉽게 갈아끼울 수 있도록 해두었다.
- 스프링 부트 액츄에이터는 마이크로미터를 기본으로 내장해서 사용
- 개발자는 마이크로미터가 정한 표준 방법으로 메트릭(측정 지표)를 전달하면 된다. 그리고 사용하는 모니터링 툴에 맞는 구현체를 선택하면 된다. 이후에 모니터링 툴이 변경되어도 해당 구현체만 변경하면 된다. 애플리케이션 코드는 모니터링 툴이 변경되어도 그대로 유지할 수 있다.
메트릭 확인하기
스프링 부트 액츄에이터는 마이크로미터가 제공하는 지표 수집을 @AutoConfiguration 을 통해 자동으로 등록
http://localhost:8080/actuator/metrics
metrics 엔드포인트를 사용하면 기본으로 제공되는 메트릭들을 확인할 수 있다.
http://localhost:8080/actuator/metrics/{name}
metrics 엔드포인트는 다음과 같은 패턴을 사용해서 더 자세히 확인할 수 있다.
해당 Tag를 기반으로 정보를 필터링해서 확인할 수 있다.
tag=KEY:VALUE 과 같은 형식을 사용해야 한다.
다양한 메트릭
- JVM 메트릭
- jvm으로 시작한다
- 메모리 및 버퍼 풀 세부 정보
- 가비지 수집 관련 통계
- 스레드 활용
- 로드 및 언로드된 클래스 수
- JVM 버전 정보
- JIT 컴파일 시간
- 시스템 메트릭
- system. , process. , disk. 으로 시작한다.
- CPU 지표
- 파일 디스크립터 메트릭
- 가동 시간 메트릭
- 사용 가능한 디스크 공간
- 애플리케이션 시작 메트릭
- 시작 시간 메트릭을 제공
- 스프링 MVC 메트릭
- 스프링 MVC 컨트롤러가 처리하는 모든 요청을 다룬다.
- 메트릭 이름: http.server.requests
- 톰캣 메트릭
- 톰캣 메트릭은 tomcat. 으로 시작한다.
- 톰캣 메트릭을 모두 사용하려면 옵션을 켜야한다.
- 데이터 소스 메트릭
- DataSource , 커넥션 풀에 관한 메트릭을 확인할 수 있다.
- jdbc.connections. 으로 시작한다.
- 최대 커넥션, 최소 커넥션, 활성 커넥션, 대기 커넥션 수 등을 확인할 수 있다.
- 로그 메트릭
- logback.events : logback 로그에 대한 메트릭을 확인할 수 있다.
- trace , debug , info , warn , error 각각의 로그 레벨에 따른 로그 수를 확인할 수 있다.
- 사용자 정의 메트릭
- 기타 메트릭
- HTTP 클라이언트 메트릭( RestTemplate , WebClient )
- 캐시 메트릭
- 작업 실행과 스케줄 메트릭
- 스프링 데이터 리포지토리 메트릭
- 몽고DB 메트릭
- 레디스 메트릭
server:
tomcat:
mbeanregistry:
enabled: true
tomcat 메트릭 사용 옵션
프로메테우스와 그라파나
프로메테우스
- 과거 이력까지 함께 확인
- 메트릭을 지속적으로 수집해 DB에 저장
그라파나
- 데이터를 불러서 사용자가 보기 편하게 보여주는 대시보드
- 데이터를 그래프로 보여주는 툴
- 스프링 부트 액츄에이터와 마이크로미터를 사용하면 수 많은 메트릭을 자동으로 생성
- 마이크로미터 프로메테우스 구현체는 프로메테우스가 읽을 수 있는 포멧으로 메트릭을 생성
- 프로메테우스는 이렇게 만들어진 메트릭을 지속해서 수집
- 프로메테우스는 수집한 메트릭을 내부 DB에 저장
- 사용자는 그라파나 대시보드 툴을 통해 그래프로 편리하게 메트릭 조회
프로메테우스
키면 기본 경로 - http://localhost:9090/
2가지 작업이 필요하다
- 애플리케이션 설정: 프로메테우스가 애플리케이션의 메트릭을 가져갈 수 있도록 애플리케이션에서 프로메테우스 포멧에 맞추어 메트릭 만들기
- 프로메테우스 설정: 프로메테우스가 우리 애플리케이션의 메트릭을 주기적으로 수집하도록 설정
어플리케이션 설정
프로메테우스가 어플리케이션의 메트릭을 가져가려면 프로메테우스가 사용하는 포멧에 맞춰 메트릭을 만들어야 한다.
actuator/metrics
에서 보았던 포맷(JSON)은 이해하지 못한다. -> 마이크로미터가 이런부분 해결
각각의 메트릭들은 내부에서 마이크로미터 표준 방식으로 측정되고 있다. 따라서 어떤 구현체를 사용할지 지정만 해주면 된다.
build.gradle 추가
implementation 'io.micrometer:micrometer-registry-prometheus' //추가
- 스프링 부트와 액츄에이터가 자동으로 마이크로미터 프로메테우스 구현체를 등록해서 동작하도록 설정해준다.
- 액츄에이터에 프로메테우스 메트릭 수집 엔드포인트가 자동으로 추가
- /actuator/prometheus
포맷 차이
- jvm.info jvm*info : 프로메테우스는 . 대신에 * 포멧을 사용한다. . 대신에 _ 포멧으로 변환된 것을 확인할 수 있다.
- logback.events logback_events_total : 로그수 처럼 지속해서 숫자가 증가하는 메트릭을 카운터라 한다. 프로메테우스는 카운터 메트릭의 마지막에는 관례상 _total 을 붙인다.
- http.server.requests
- http_server_requests_seconds_count : 요청 수
- http_server_requests_seconds_sum : 시간 합(요청수의 시간을 합함)
- http_server_requests_seconds_max : 최대 시간(가장 오래걸린 요청 수)
프로메테우스 - 수집 설정
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: "prometheus"
# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
static_configs:
- targets: ["localhost:9090"]
- job_name: "spring-actuator"
metrics_path: "/actuator/prometheus"
scrape_interval: 1s
static_configs:
- targets: ["localhost:8080"]
- job_name : 수집하는 이름이다. 임의의 이름을 사용하면 된다.
- metrics_path : 수집할 경로를 지정한다.
- scrape_interval : 수집할 주기를 설정한다.
- targets : 수집할 서버의 IP, PORT를 지정한다
Tip!
- scrape_interval 여기서는 예제를 빠르게 확인하기 위해서 수집 주기를 1s 로 했지만, 수집 주기의 기본 값은 1m 이다.
- 수집 주기가 너무 짧으면 애플리케이션 성능에 영향을 줄 수 있으므로 운영에서는 10s ~ 1m 정도를 권장한다. (물론 시스템 상황에 따라서 다르다.)
프로메테우스 기본 기능
- Table -> Evaluation time을 수정해서 과거 시간 조회 가능
- Graph -> 메트릭을 그래프로 조회 가능
필터
레이블을 기준으로 필터를 사용할 수 있다. 필터는 중괄호 ({})문법을 사용한다.
레이블 일치 연산자
- = 제공된 문자열과 정확히 동일한 레이블 선택
- != 제공된 문자열과 같지 않은 레이블 선택
- =~ 제공된 문자열과 정규식 일치하는 레이블 선택
- !~ 제공된 문자열과 정규식 일치하지 않는 레이블 선택
ex
- http_server_requests_seconds_count{uri=“/log”, method=“GET”}
- http_server_requests_seconds_count{uri!=“/actuator/prometheus”}
- http_server_requests_seconds_count{method=~“GET|POST”}
- http_server_requests_seconds_count{uri!~“/actuator.*“}
연산자와 쿼리 함수
- +, -, *, /, %, ^(승수/지수) 지원
- sum : 값의 합계
- ex) sum(http_server_requests_seconds_count)
- sum by : SQL의 group by와 유사
- ex) sum by(method, status)(http_server_requests_seconds_count)
- count : 메트릭 자체의 수 카운트
- ex) count(http_server_requests_seconds_count)
- topk : 상위 3개 메트릭 조회
- ex) topk(3, http_server_requests_seconds_count)
- 오프셋 수정자 : 현재를 기준으로 특정 과거 시점의 데이터를 반환
- ex) http_server_requests_seconds_count offset 10m
- 범위 벡터 선택기
- ex) http_server_requests_seconds_count[1m]
- 마지막에 [1m] , [60s] 와 같이 표현한다. 지난 1분간의 모든 기록값을 선택한다.
- 범위 벡터 선택기는 차트에 바로 표현할 수 없다. 데이터로는 확인할 수 있다.
- 범위 벡터 선택의 결과를 차트에 표현하기 위해서는 약간의 가공이 필요한데, 조금 뒤에 설명하는 상대적인 증가 확인 방법을 참고하자.
- ex) http_server_requests_seconds_count[1m]
프로메테우스 - 게이지와 카운터
- 게이지(Gauge)
- 임의로 오르내릴 수 있는 값
- ex) CPU사용량, 메모리 사용량, 사용중인 커넥션
- 카운터(Counter)
- 단순하게 증가하는 단일 누적 값
- ex) HTTP 요청 수, 로그 발생 수
카운터는 단순하게 증가하는 값이기 때문에 특정 시간에 얼마나 고객의 요청이 들어왔는지 확인하기 매우 어렵다
이런 문제를 해결하기 위해 increase(), rate(), irate()같은 함수를 지원한다
- increase() : 지정한 시간 단위별로 증가를 확인할 수 있다.
- ex) increase(http_server_requests_seconds_count{uri=“/log”}[1m])
- rate() : 범위 벡터에서 초당 평균 증가율을 계산한다.
- ex) rate(data[1m])
- irate() : 범위 벡터에서 초당 순간 증가율을 계산한다.
- ex) irate(http_server_requests_seconds_count{uri=“/log”}[1m])
Tip!!
더 자세한 내용은 다음 프로메테우스 공식 메뉴얼을 참고하자
기본기능: https://prometheus.io/docs/prometheus/latest/querying/basics/
연산자: https://prometheus.io/docs/prometheus/latest/querying/operators/
함수: https://prometheus.io/docs/prometheus/latest/querying/functions/
그라파나
그라파나에서 프로메테우스를 데이터소스로 사용해서 데이터를 읽어야 한다.
- 대시보드 생성
- 왼쪽 Dashboard 메뉴 선택
- new 버튼 -> New Dashboard선택
- save dashboard
- 이름 지정 후 저장
- 대시보드에 패널 만들기
- 대시보드가 큰 틀이라면 패널은 그 안에 모듈처럼 들어가는 실제 그래프를 보여주는 컴포넌트
- 오른쪽 상단의 Add panel 버튼(차트 모양) 선택
- Add a new panel 메뉴 선택
- 패널의 정보를 입력할 수 있는 화면이 나타난다.
- 아래에 보면 Run queries 버튼 오른쪽에 Builder , Code 라는 버튼이 보이는데, Code 를 선택
- Enter a PromQL query… 이라는 부분에 메트릭을 입력하면 된다
공유 대시보드 활용
https://grafana.com/grafana/dashboards/ -> 접속
JVM (Micrometer), springBoot 2.1 System Monitor 이 두가지가 많이 사용된다.
그라파나 - 메트릭을 통한 문제 확인
실무에서 주로 많이 발생하는 다음 4가지 대표적인 예시를 확인해보자.
- CPU 사용량 초과
- JVM 메모리 사용량 초과
- 커넥션 풀 고갈
- 에러 로그 급증
정리
메트릭을 보는 것은 정확한 값을 보는 것이 목적이 아니다. 대략적인 값과 추세를 확인하는 것이 주 목적이다.
모니터링 메트릭 활용
- 비즈니스에 특화된 부분을 모니터링 하고 싶을 때 메트릭을 만들어 등록
메트릭 등록1 - 카운터
마이크로미터를 사용해 메트릭을 직접 등록이 가능
MeterRegistry
- 마이크로미터 기능을 제공하는 핵심 컴포넌트
- 스프링을 통해서 주입 받아 사용하고, 이곳을 통해서 카운터, 게이지 등을 등록한다.
Counter(카운터)
- 단조롭게 증가하는 단일 누적 측정항목
- 단일 값
- 보통 하나씩 증가
- 누적이므로 전체 값을 포함
- 관례상 마지막에
_total
붙인다
- 값은 증가하거나 0으로 초기화하는 것만 가능
package hello.order.v1;
import hello.order.OrderService;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV1 implements OrderService {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceV1(MeterRegistry registry) {
this.registry = registry;
}
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "order")
.description("order")
.register(registry).increment();
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancel")
.description("order")
.register(registry).increment();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- Counter.builder(name) 를 통해서 카운터를 생성한다. name 에는 메트릭 이름을 지정한다.
- tag 를 사용했는데, 프로메테우스에서 필터할 수 있는 레이블로 사용된다.
- 주문과 취소는 메트릭 이름은 같고 tag 를 통해서 구분하도록 했다.
- register(registry) : 만든 카운터를 MeterRegistry 에 등록한다. 이렇게 등록해야 실제 동작한다.
- increment() : 카운터의 값을 하나 증가한다.
액츄에이터 메트릭 확인 http://localhost:8080/actuator/metrics/my.order
프로메테우스 포멧 메트릭 확인 http://localhost:8080/actuator/prometheus
참고: 카운터는 계속 증가하기 때문에 특정 시간에 얼마나 증가했는지 확인하려면 increase() , rate() 같은 함수와 함께 사용하는 것이 좋다.
메트릭 등록 - @Counted
OrderServiceV1 의 가장 큰 단점은 메트릭을 관리하는 로직이 핵심 비즈니스 개발 로직에 침투했다는 점이다.
스프링 AOP를 사용하면 해결된다.
마이크로미터는 이런 상황에 맞추어 AOP구성요소를 이미 다 만들어두었다.
package hello.order.v2;
import hello.order.OrderService;
import io.micrometer.core.annotation.Counted;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class OrderServiceV2 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Counted("my.order")
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Counted("my.order")
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
- @Counted 애노테이션을 측정을 원하는 메서드에 적용한다. 주문과 취소 메서드에 적용했다.
- 그리고 메트릭 이름을 지정하면 된다. 여기서는 이전과 같은 my.order 를 적용했다.
- 참고로 이렇게 사용하면 tag 에 method 를 기준으로 분류해서 적용한다.
@Configuration
public class OrderConfigV2 {
@Bean
public OrderService orderService() {
return new OrderServiceV2();
}
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
- CountedAspect 를 등록하면 @Counted 를 인지해서 Counter 를 사용하는 AOP를 적용한다.
- 주의! CountedAspect를 빈으로 등록하지 않으면 @Counted 관련 AOP가 동작하지 않는다.
메트릭 등록3 - Timer
Timer
- Timer는 좀 특별한 메트릭 측정 도구인데, 시간을 측정하는데 사용된다.
- 카운터와 유사한데, Timer 를 사용하면 실행 시간도 함께 측정할 수 있다.
- Timer 는 다음과 같은 내용을 한번에 측정해준다.
- seconds_count : 누적 실행 수 - 카운터
- seconds_sum : 실행 시간의 합 - sum
- seconds_max : 최대 실행 시간(가장 오래걸린 실행 시간) - 게이지
- 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산된다.
코드 적용 후 액츄에이터 메트릭 확인
{
'name': 'my.order',
'description': 'order',
'baseUnit': 'seconds',
'measurements':
[
{ 'statistic': 'COUNT', 'value': 5 },
{ 'statistic': 'TOTAL_TIME', 'value': 1.929075042 },
{ 'statistic': 'MAX', 'value': 0.509926375 },
],
'availableTags':
[
{ 'tag': 'method', 'values': ['cancel', 'order'] },
{ 'tag': 'class', 'values': ['hello.order.v3.OrderServiceV3'] },
],
}
- measurements 항목을 보면 COUNT , TOTAL_TIME , MAX 이렇게 총 3가지 측정 항목을 확인할 수 있다.
- COUNT : 누적 실행 수(카운터와 같다)
- TOTAL_TIME : 실행 시간의 합(각각의 실행 시간의 누적 합이다)
- MAX : 최대 실행 시간(가장 오래 걸린 실행시간이다)
프로메테우스 포멧 메트릭 확인
# HELP my_order_seconds order
# TYPE my_order_seconds summary
my_order_seconds_count{class="hello.order.v3.OrderServiceV3",method="order",}
3.0
my_order_seconds_sum{class="hello.order.v3.OrderServiceV3",method="order",}
1.518434959
my_order_seconds_count{class="hello.order.v3.OrderServiceV3",method="cancel",}
2.0
my_order_seconds_sum{class="hello.order.v3.OrderServiceV3",method="cancel",}
0.410640083
# HELP my_order_seconds_max order
# TYPE my_order_seconds_max gauge
my_order_seconds_max{class="hello.order.v3.OrderServiceV3",method="order",}
0.509926375
my_order_seconds_max{class="hello.order.v3.OrderServiceV3",method="cancel",}
0.20532925
- 프로메테우스로 다음 접두사가 붙으면서 3가지 메트릭을 제공한다.
- seconds_count : 누적 실행 수
- seconds_sum : 실행 시간의 합
- seconds_max : 최대 실행 시간(가장 오래걸린 실행 시간), 프로메테우스 gague
- 참고: 내부에 타임 윈도우라는 개념이 있어서 1~3분 마다 최대 실행 시간이 다시 계산된다.
- 여기서 평균 실행 시간도 계산할 수 있다.
- seconds_sum / seconds_count = 평균 실행시간
메트릭 등록4 - @Timed
타이머는 @Timed 라는 애노테이션을 통해 AOP를 적용할 수 있다.
package hello.order.v4;
import hello.order.OrderService;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@Timed("my.order")
@Slf4j
public class OrderServiceV4 implements OrderService {
private AtomicInteger stock = new AtomicInteger(100);
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
sleep(500);
}
@Override
public void cancel() {
log.info("취소");
stock.incrementAndGet();
sleep(200);
}
private static void sleep(int l) {
try {
Thread.sleep(l + new Random().nextInt(200));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
package hello.order.v4;
import hello.order.OrderService;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OrderConfigV4 {
@Bean
OrderService orderService() {
return new OrderServiceV4();
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
TimedAspect 를 적용해야 @Timed 에 AOP가 적용된다.
메트릭 등록5 - 게이지
- Gauge(게이지)
- https://prometheus.io/docs/concepts/metric_types/#gauge
- 게이지는 임의로 오르내릴 수 있는 단일 숫자 값을 나타내는 메트릭
- 값의 현재 상태를 보는데 사용
- 값이 증가하거나 감소할 수 있음
- 예) 차량의 속도, CPU 사용량, 메모리 사용량
게이지 단순하게 등록하기
@Slf4j
@Configuration
public class StockConfigV2 {
@Bean
public MeterBinder stockSize(OrderService orderService) {
return registry -> Gauge.builder("my.stock", orderService, service -> {
log.info("stock gauge call");
return service.getStock().get();
}).register(registry);
}
}
정리
메트릭은 100% 정확한 숫자를 보는데 사용하는 것이 아니다. 약간의 오차를 감안하고 실시간으로 대략의 데이터를 보는 목적으로 사용한다.
마이크로미터 핵심 기능 Counter, Gauge, Timer, Tags
Tag, 레이블
- Tag를 사용하면 데이터를 나누어서 확인할 수 있다.
- Tag는 카디널리티가 낮으면서 그룹화 할 수 있는 단위에 사용해야 한다.
- 예) 성별, 주문 상태, 결제 수단[신용카드, 현금] 등등
- 카디널리티가 높으면 안된다. 예) 주문번호, PK 같은 것
실무 모니터링 화면 팁
모니터링 3단계
- 대시보드
- 애플리케이션 추적 - 핀포인트
- 로그
대시보드
전체를 한눈에 볼 수 있는 가장 높은 뷰
제품
- 마이크로미터, 프로메테우스, 그라파나 등등
모니터링 대상
- 시스템 메트릭(CPU, 메모리)
- 애플리케이션 메트릭(톰캣 쓰레드 풀, DB 커넥션 풀, 애플리케이션 호출 수)
- 비즈니스 메트릭(주문수, 취소수)
애플리케이션 추적
주로 각각의 HTTP 요청을 추적, 일부는 마이크로서비스 환경에서 분산 추적
제품
- 핀포인트(오픈소스), 스카우트(오픈소스), 와탭(상용), 제니퍼(상용)
로그
- 가장 자세한 추적, 원하는데로 커스텀 가능
- 같은 HTTP 요청을 묶어서 확인할 수 있는 방법이 중요, MDC 적용
파일로 직접 로그를 남기는 경우
- 일반 로그와 에러 로그는 파일을 구분해서 남기자
- 에러 로그만 확인해서 문제를 바로 정리할 수 있음
클라우드에 로그를 저장하는 경우
- 검색이 잘 되도록 구분
모니터링 정리
- 각각 용도가 다르다.
- 관찰을 할 때는 전체 점점 좁게
- 핀포인트는 정말 좋다. 강추 마이크로 서비스 분산 모니터링도 가능, 대용량 트래픽에 대응
알람
모니터링 툴에서 일정 이상 수치가 넘어가면, 슬랙, 문자 등을 연동
경고와 심각을 잘 나누어서 업무와 삶에 방해가 되지 않도록 해야함