자바의 웹 애플리케이션
•
전통적인 방식
◦
과거에 자바로 웹 애플리케이션을 개발할 때는 먼저 서버에 톰캣 같은 WAS를 설치
◦
WAS에서 동작하도록 서블릿 스펙에 맞추어 코드를 작성하고 WAR 형식으로 빌드
◦
WAR 파일을 WAS에 전달해서 배포하는 방식으로 전체 개발 주기가 동작
◦
이런 방식은 WAS 기반 위에서 개발하고 실행해야하므로 IDE에서도 WAS와 연동하는 추가 설정 필요
•
최근 방식
◦
스프링 부트 안에 WAS인 톰캣이 라이브러리로 내장되어 있음
◦
개발자는 코드를 작성하고 JAR로 빌드한 다음 원하는 위치에서 실행하기만 하면 WAS도 함께 실행
◦
main() 메서드만 실행하면 복잡한 추가 설정 필요 없이 서버 실행 가능
JAR vs WAR vs Fat JAR vs Executable JAR
•
JAR (Java Archive)
◦
구조
▪
META-INF
•
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: 우리가 만든 main() 메서드를 갖고 있는 클래스 지정
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
Plain Text
복사
▪
BOOT-INF
•
classes : 우리가 개발한 class 파일들과 리소스 파일들
•
lib : 외부 라이브러리
•
classpath.idx : 외부 라이브러리 경로
•
layers.idx : 스프링 부트 구조 경로
◦
JVM 위에서 직접 실행되거나 다른 곳에서 사용하는 라이브러리로 제공되는 파일
◦
직접 실행하는 경우 main() 메서드 필요하고 MANIFEST.MF 파일에 실행할 메인 메서드가 있는 클래스를 지정해두어야 함
◦
java -jar abc.jar
•
WAR (Web Application Archive)
◦
구조
▪
WEB-INF
•
classes : 실행 클래스 모음
•
lib : 라이브러리 모음
•
web.xml : 웹 서버 배치 설정 파일(생략 가능)
▪
index.html : 정적 리소스들
◦
WAS에 배포할 때 사용하는 파일로 JVM이 아닌 WAS 위에서 실행
◦
JAR와 비교해서 구조가 더 복잡하고 WAR의 구조를 지켜야함
•
Fat JAR or Uber JAR
task buildFatJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
duplicateStrategy = DuplicateStrategy.WARN
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
with jar
}
Java
복사
◦
WAR의 단점들을 해결하고 라이브러리의 class들을 포함시킬 수 있는 빌드 방법
◦
단점
▪
라이브러리의 구분 없이 class 폴더에 함께 풀려있어서 구분하기 어려움
▪
파일명 중복 해결 불가능
•
Executable JAR
◦
구조
▪
META-INF
•
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: 우리가 만든 main()을 갖고 있는 클래스 지정
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
Plain Text
복사
JarLauncher를 먼저 실행하여 JAR 내부의 JAR들을 읽어들이고 나서 main() 메서드를 호출한다.
▪
org/springframework/boot/loader
•
JarLauncher.class : 스프링 부트의 main() 실행 클래스
▪
BOOT-INF
•
classes : 우리가 개발한 class 파일들과 리소스 파일들
•
lib : 외부 라이브러리
•
classpath.idx : 외부 라이브러리 경로
•
layers.idx : 스프링 부트 구조 경로
◦
Fat JAR의 단점들을 해결하기 위한 자바 표준이 아닌 스프링 부트에서 만든 특별한 구조의 JAR
◦
JAR 내부에 JAR를 포함
▪
라이브러리 구분이 가능
▪
파일명 중복을 해결할 수 있음
WAR 빌드와 배포
•
빌드
◦
CLI 명령어 : ./gradlew build
◦
build/libs/server-0.0.1-SNAPSHOT.war 파일 생성
•
압축 풀기
◦
jar -xvf server-0.0.1-SNAPSHOT.war
◦
결과물로 classes와 lib를 포함한 WEB-INF와 html 파일들 생성
•
배포
◦
생성
WAR 배포 방식
•
단점
◦
톰캣 같은 WAS의 별도 설치 필요
◦
개발 환경 설정이 복잡
◦
배포 과정 복잡
◦
톰캣 버전 변경을 위해선 다시 설치 필요
•
고민
◦
단순히 자바의 main() 메서드만 실행하면 웹 서버까지 같이 실행하면 되지 않을까
◦
톰캣도 자바로 만들어졌으니 톰캣을 마치 하나의 라이브러리처럼 포함해서 사용해도 되지 않을까
JAR 배포 방식
•
단점
◦
WAR와 다르게 JAR는 라이브러리 역할을 하는 JAR를 포함할 수 없다.
톰캣 설정
•
설치 경로
◦
설치폴더/bin
•
권한 설정
◦
chmod 755 * (bin 폴더 안에서)
•
실행/종료
◦
실행 전 설치폴더/webapps 의 내용물을 전체 삭제 후 WAR를 넣고 ROOT.war로 이름 변경
◦
실행 : ./startup.sh (윈도우는 bat)
◦
종료 : ./shutdown.sh (윈도우는 bat)
•
로그
◦
설치폴더/logs/catalina.out
gradle 설정
plugins {
id 'java'
id 'war'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavnenCentral()
}
...
Java
복사
서블릿 컨테이너 초기화
•
WAS를 실행하는 시점에 필요한 초기화 작업들
◦
서비스에 필요한 필터와 서블릿을 등록
◦
스프링을 사용한다면 스프링 컨테이너를 만들고 서블릿과 스프링을 연결하는 디스패처서블릿 등록
•
서블릿 컨테이너 초기화
◦
ServletContainerInitializer
▪
서블릿은 ServletContainerInitializer 라는 초기화 인터페이스를 제공
▪
서블릿 컨테이너는 실행 시점에 초기화 메서드인 onStartup() 을 호출하고 여기서 필요한 기능들을 초기화하거나 등록할 수 있음
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
Java
복사
▪
Set<Class<?>> c : @HandlesTypes 와 함께 조금 더 유연한 초기화 기능을 제공
▪
ServletContext ctx : 서블릿 컨테이너 자체의 기능을 제공하고 이 객체를 통해 필터나 서블릿을 등록할 수 있음
◦
서블릿 컨테이너 초기화 등록
▪
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer 생성
▪
ServletContainerInitializer 인터페이스를 구현한 클래스의 패키지 경로를 작성
▪
WAS를 실행할 때 해당 클래스를 초기화 클래스로 인식하고 로딩 시점에 실행
◦
서블릿 컨테이너 초기화 과정
public interface AppInit {
void onStartup(ServletContext servletContext);
}
Java
복사
@HandleTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) {
for (Class<?> appInitClass : c) {
try {
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance(); //리플렉션을 통한 객체 생성
appInit.onStartup(ctx);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
Java
복사
1.
jakarta.servlet.ServletContainerInitializer 서블릿 컨테이너 초기화 실행
2.
@HandleTypes 어노테이션에 어플리케이션 초기화 인터페이스를 지정한다
3.
Set<Class<?>>에 어플리케이션 초기화 인터페이스 구현체들을 모두 찾아서 클래스 정보로 전달
4.
리플렉션을 통한 객체 생성
5.
리플렉션으로 생성된 객체에 서블릿 컨테이너 ctx를 전달하며 onStartup(ctx) 초기화 메서드 호출
◦
서블릿 컨테이너 초기화와 어플리케이션 초기화의 이유
▪
편리함
•
ServletContainerInitializer 인터페이스를 구현한 객체를 만들고 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 패키지 경로를 지정해야하는데 어플리케이션 초기화는 특정 인터페이스만 구현하면 된다.
▪
의존성
•
어플리케이션 초기화는 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있다.
•
특히 ServletContext 가 필요없는 어플리케이션 초기화 코드라면 의존을 완전히 제거할 수 있음
서블릿 등록
•
@WebServlet
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {}
Java
복사
•
프로그래밍 방식
public class AppInitV1Servlet implements AppInit {
@Override
public void onStartup(ServletContext servletContext) {
ServletRegistration.Dynamic helloServlet = servletContext.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
Java
복사
◦
프로그래밍 방식을 사용하는 이유
▪
어노테이션 방식은 하드코딩된 것처럼 동작하므로 mapping된 url을 변경하고 싶다면 코드를 직접 변경해야한다
▪
프로그래밍 방식을 사용하면 url mapping을 외부 설정을 읽어서 등록할 수 있고 서블릿 자체도 if를 통해 등록하거나 뺄 수 있고 생성자에 필요한 정보를 넘겨 서블릿을 직접 생성하므로 유연하다
스프링 컨테이너 등록
•
컨트롤러 생성
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
return "hello spring!";
}
}
Java
복사
•
컨트롤러 빈 등록
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController() {
return new HelloController();
}
}
Java
복사
•
스프링 컨테이너, 디스패처서블릿 생성 및 서블릿 컨테이너에 디스패처서블릿 등록
public class AppInitV2Spring implements AppInit {
@Override
public void onStartup(ServletContext servletContext) {
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
//스프링 MVC 디스패처서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처서블릿을 서블릿 컨테이너에 등록 (이름 주의 dispatcherV2)
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV2", dispatcher);
//디스패처서블릿에 url 매핑
servlet.addMapping("/spring/*");
}
}
Java
복사
•
AppInit 인터페이스를 구현한 AppInitV2Spring은 ServletContainerInitializer 서블릿 컨테이너 초기화 코드에 의해 자동으로 생성 및 메서드 호출
•
ApplicationContext 인터페이스를 구현한 AnnotationConfigWebApplicationContext 가 스프링 컨테이너가 된다
•
스프링 컨테이너의 register 메서드를 통해 스프링 설정 추가
•
디스패처서블릿을 생성할 때 생성자에 스프링 컨테이너를 전달하여 둘을 연결
•
연결하면 디스패처서블릿에 들어온 HTTP 요청들을 스프링 컨테이너에 들어있는 컨트롤러들이 처리
•
어차피 요청은 서블릿을 통해 들어오기 때문에 디스패처서블릿을 addServlet 메서드를 통해 서블릿 컨테이너에 등록해줘야함
•
서블릿을 등록할 때 원하는 이름으로 서블릿을 등록할 수 있지만 중복되지 않도록 주의
스프링 MVC의 서블릿 컨테이너 초기화
•
기존 방식
◦
ServletContainerInitializer 인터페이스를 구현하여 서블릿 컨테이너 초기화 코드 작성
◦
@HandleTypes 어노테이션을 통해 AppInit 인터페이스를 구현한 어플리케이션 초기화 코드들을 통해 서블릿 생성 및 등록
◦
/META-INF/services/jakarta.servlet.ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 패키지 경로 등록
•
스프링 MVC의 지원
◦
서블릿 컨테이너 초기화 과정을 생략하고 어플리케이션 초기화 코드만 작성
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throw ServletException;
}
Java
복사
public class AppInitV3SpringMvc implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
//스프링 MVC 디스패처서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처서블릿을 서블릿 컨테이너에 등록 (이름 주의 dispatcherV3)
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcherV3", dispatcher);
//디스패처서블릿에 url 매핑
servlet.addMapping("/");
}
}
Java
복사
•
org.springframework.spring-web 라이브러리
//buiild.gradle
dependencies {
implementation 'org.springframework:spring-webmvc:6.0.4'
}
Java
복사
//META-INF/services/jakarta.servlet.ServletContainerInitializer
org.springframework.web.SpringServletContainerInitializer
Java
복사
//SpringServletContainerInitializer.java
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
Java
복사
내장 톰캣 수동 설정
•
톰캣 의존성 추가
//build.gradle
dependencies {
implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
}
Java
복사
•
서블릿 설정
public class EmbedTomcatServletMain {
public static void main(String[] args) {
//톰캣 설정
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
//서블릿 등록
Context context = tomcat.addContext("", "/"); //contextPath와 docBase
tomcat.addServlet("", "servletName", new HelloServlet()); //contextPath와 servletName과 servlet 클래스
context.addServletMappingDecoded("/hello-servlet", "servletName");
tomcat.start();
}
}
Java
복사
◦
톰캣 설정
▪
내장 톰캣 생성 후 톰캣이 제공하는 커넥터를 8080포트에 연결
◦
서블릿 등록
▪
톰캣에 사용할 contextPath와 docBase를 지정
▪
addServlet() 메서드를 통해 서블릿을 등록
▪
addServletMappingDecoded() 메서드를 통해 등록한 서블릿에 uri 매핑
◦
톰캣 시작
▪
tomcat.start() 를 통해 톰캣 시작
•
스프링 설정
public class EmbedTomcatServletMain {
public static void main(String[] args) {
//톰캣 설정
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("", "/"); //contextPath와 docBase
tomcat.addServlet("", "dispatcher", dispatcher); //contextPath와 servletName과 servlet 클래스
context.addServletMappingDecoded("/", "dispatcher");
tomcat.start();
}
}
Java
복사
◦
톰캣 설정
▪
내장 톰캣 생성 후 톰캣이 제공하는 커넥터를 8080포트에 연결
◦
스프링 컨테이너, 디스패처서블릿 생성 및 연결
▪
AnnotationConfigWebApplicationContext 생성 및 register() 메서드를 통한 컨트롤러 빈 등록 클래스 등록
▪
디스패처서블릿 생성 시 생성자에 스프링 컨테이너를 전달해 연결
◦
디스패처서블릿 등록
▪
톰캣에 사용할 contextPath와 docBase를 지정
▪
addServlet() 메서드를 통해 서블릿을 등록
▪
addServletMappingDecoded() 메서드를 통해 등록한 서블릿에 uri 매핑
◦
톰캣 시작
▪
tomcat.start() 를 통해 톰캣 시작
스프링의 부트 클래스
•
기존 main 메서드에 있던 내용들 옮기기
public class MySpringApplication {
public static void run(Class configClass, String[] args) {
//톰캣 설정
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
connector.setPort(8080);
tomcat.setConnector(connector);
//스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(configClass);
//스프링 MVC 디스패처서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
//디스패처서블릿 등록
Context context = tomcat.addContext("", "/"); //contextPath와 docBase
tomcat.addServlet("", "dispatcher", dispatcher); //contextPath와 servletName과 servlet 클래스
context.addServletMappingDecoded("/", "dispatcher");
tomcat.start();
}
}
Java
복사
◦
기존 main 메서드에 있던 지저분한 코드를 따로 클래스로 작성
•
어노테이션 작성
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}
Java
복사
◦
기존 config 파일에서 컨트롤러 등의 빈을 등록하던 방식에서
◦
어노테이션을 통한 컴포넌트 스캔으로 자동화
◦
여러가지 기능들을 모아놓은 어노테이션
•
root에 main 메서드 작성
@MySpringBootApplication
public class MySpringBootMain {
public static void main(String[] args) {
MySpringApplication.run(MySpringBootMain.class, args);
}
}
Java
복사
◦
main 메서드만 실행시키면 다 자동으로 작동하도록 변경
스프링 부트 스타터와 라이브러리 관리
•
기존
◦
같은 종류 중 어떤 라이브러리를 사용해야할지 고민
◦
각 라이브러리의 버전과 호환성을 고민
dependencies {
//스프링 웹 MVC
implementation 'org.springframework:spring-webmvc:6.0.4'
//내장 톰캣
implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
//JSON 처리
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1'
//스프링 부트 관련
implementation 'org.springframework.boot:spring-boot:3.0.2'
implementation 'org.springframework.boot:spring-boot-autoconfigure:3.0.2'
//LOG 관련
implementation 'ch.qos.logback:logback-classic:1.4.5'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.19.0'
implementation 'org.slf4j:jul-to-slf4j:2.0.6'
//YML 관련
implementation 'org.yaml.snakeyml:1.33'
}
Plain Text
복사
•
스프링 부트
◦
라이브러리 버전 관리
plugins {
id 'io.spring.dependency-management' version '1.1.0'
}
dependencies {
//스프링 웹 MVC
implementation 'org.springframework:spring-webmvc'
//내장 톰캣
implementation 'org.apache.tomcat.embed:tomcat-embed-core'
//JSON 처리
implementation 'com.fasterxml.jackson.core:jackson-databind'
//스프링 부트 관련
implementation 'org.springframework.boot:spring-boot'
implementation 'org.springframework.boot:spring-boot-autoconfigure'
//LOG 관련
implementation 'ch.qos.logback:logback-classic'
implementation 'org.apache.logging.log4j:log4j-to-slf4j'
implementation 'org.slf4j:jul-to-slf4j'
//YML 관련
implementation 'org.yaml.snakeyml'
}
Plain Text
복사
▪
스프링 부트가 관리하지 않는 라이브러리
dependencies 블럭 안에 implementation ‘org.yaml:snakeyaml:1.30’ 처럼 직접 버전 명시
◦
스프링 부트 스타터 제공
implementation 'org.springframework.boot:spring-boot-starter-web'
=
implementation 'org.springframework.boot:spring-boot-start-json'
implementation 'org.springframework.boot:spring-boot-start-tomcat'
implementation 'org.springframework.boot:spring-boot-start'
implementation 'org.springframework:spring-web'
implementation 'org.springframework:spring-webmvc'
Plain Text
복사
▪
라이브러리 버전 변경
dependencies 블럭 밑에 ext[’tomcat.version’] = ‘10.1.4’ 처럼 직접 버전 명시
Auto Configuration
•
수동 설정
@Slf4j
@Configuration
public class DbConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setJdbcUrl("jdbc:h2:mem:test");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
public TransactionManager transactionManager() {
return new JdbcTransactionManager(dataSource());
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}
Java
복사
◦
@Transactional 어노테이션을 사용하기 위해 TransactionManager를 빈으로 등록
•
자동 설정&구성
◦
스프링 부트는 spring-boot-autoconfigure 라는 프로젝트 안에서 자동 설정&구성을 제공
◦
@AutoConfiguration
▪
자동 설정&구성을 사용하기 위한 어노테이션
▪
내부에 @Configuration 어노테이션을 갖고 있음
▪
after = DataSourceAutoConfiguration.class 를 통해 자동 설정&구성의 순서를 지정
@Conditional
•
스프링 프레임워크의 기능
•
특정 조건일 때만 해당 기능이 활성화되도록 하기 위한 어노테이션
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
Java
복사
◦
matches() 메서드가 true를 반환하면 조건에 만족해서 동작, false를 반환하면 동작하지 않음
◦
ConditionContext : 스프링 컨테이너, 환경 정보등을 담고 있음
◦
AnnotatedTypeMetadata : 어노테이션 메타 정보를 담고 있음
•
위의 인터페이스를 구현한 Condition.class를 만들고 matches() 를 오버라이딩 한 후 Config.class에 @Conditional(Condition.class) 를 붙여 사용
@ConditionalOnXXX
•
스프링 부트에서 스프링 프레임워크의 @Conditional을 확장한 기능
•
@ConditionalOnClass({DataSource.class, JdbcTemplate.class})
◦
해당 클래스가 존재하는 경우에만 설정이 동작
•
@ConditionalOnBean({Bean.class})
◦
해당 빈이 등록되어 있는 경우 동작
•
@ConditionalOnMissingBean({Bean.class})
◦
해당 빈이 등록되어 있지 않는 경우 동작
•
@ConditionalOnProperty(name = “memory”, havingValue = “on”)
◦
VM Option 또는 Arguments 로 전달되는 프로퍼티와 그 값에 따라 설정이 동작
•
@ConditionalOnResource
◦
리소스가 있는 경우 동작
•
@ConditionalOnWebApplication
◦
웹 어플리케이션인 경우 동작
•
@ConditionalOnNotWebApplication
◦
웹 어플리케이션이 아닌 경우 동작
•
@ConditionalOnExpression
◦
SpEL 표현식에 만족하는 경우 동작
라이브러리
•
라이브러리 등록
◦
root project에 libs 폴더 생성 후 build한 jar를 추가
◦
build.gradle 에 dependencies 블럭에 implementation files(’libs/test.jar) 추가
•
순수 라이브러리 사용
◦
등록된 라이브러리의 class들을 Bean으로 등록 한 후 사용
•
자동 구성 라이브러리 만들기
◦
AutoConfig.class 를 만든 후 @AutoConfiguration 붙이기
◦
@ConditionalOnXXX를 통해 원하는 조건 추가
◦
AutoConfig.class에 @Bean 등록 코드 작성
◦
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 생성 후 AutoConfig.class의 경로(package.AutoConfig)를 작성하여 인식시키기
•
원리
@SpringBootApplication
public class Application {
public static void main(Stirng[] args) {
SpringApplication.run(Application.class, args);
}
}
Java
복사
◦
run() 메서드에 설정 정보를 위해 넘겨준 class에 붙어있는 @SpringBootApplication엔 @EnableAutoConfiguration이 붙어있음
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
Java
복사
◦
이 @EnableAutoConfiguration은 Auto Configuration을 활성화하는 기능으로 안에 @Import(AutoConfigurationImportSelector.class)가 붙어있음
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
Java
복사
◦
Import는 주로 스프링 설정 정보(@Configuration)를 포함할 때 사용함
▪
정적인 방법 : @Import(클래스)
▪
동적인 방법 : @Import(ImportSelector)
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
...
}
Java
복사
◦
AutoConfigurationImportSelector.class는 ImportSelector를 구현함
◦
ImportSelector의 selectImports() 메서드를 오버라이딩하여 설정 정보의 경로를 문자로 반환해야함
◦
즉, AutoConfigurationImportSelector.class는 모든 라이브러리의 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일들을 확인하여 설정 정보를 동적으로 가져옴
외부 설정
•
과거
•
현재
•
외부 설정 종류
◦
OS 환경 변수
▪
OS에서 지원하는 외부 설정으로 해당 OS를 사용하는 모든 프로세스에서 사용
▪
윈도우 : set
▪
Mac, Linux : printenv
▪
Java : System.getenv()
◦
자바 시스템 속성
▪
자바에서 지원하는 외부 설정으로 해당 JVM 안에서 사용
▪
java -Dkey=value -jar app.jar
▪
-D 라는 VM 옵션을 통해서 key=value 형식을 줌
▪
-D 옵션이 jar 보다 앞에 있어야함
◦
자바 커맨드 라인 인수
▪
커맨드 라인에서 전달하는 외부 설정으로 실행 시 main(args) 메서드에서 사용
▪
java -jar app.jar dataA dataB
▪
스페이스로 구분해서 문자를 main() 메서드의 args 파라미터로 전달
◦
자바 커맨드 라인 옵션 인수
▪
커맨드 라인 인수를 key=value 형식으로 사용하기 위해 스프링이 제공하는 기능
▪
java -jar app.jar --key1=value1 key2=value2
▪
DefaultApplicationArguments 를 new 로 생성하면서 생성자에 args를 넘겨주면 파싱
•
getSourceArgs() : args의 인자들
•
getNonOptionArgs() : --가 붙지 않은 인자들
•
getOptionNames() : --가 붙은 인자들의 key
•
getOptionValues() : --가 붙은 인자들의 value
◦
Environment
▪
스프링은 서로 다른 외부 설정 조회 방법을 Environment와 PropertySource를 통해 추상화
•
Environment
◦
Enviroment.getProperty(key, type)를 통해 해당 타입으로 값을 조회
◦
같은 값이 있을 경우 덮어씌우는 것이 아닌 우선순위에 따라 조회
•
PropertySource
◦
각각의 외부 설정을 조회하는 XXXPropertySource 구현체 존재
▪
application.properties, application.yml도 PropertySource에 추가되므로 Environment를 통해 접근 가능
•
외부 설정 사용 - @Value
◦
필드 주입 방법
@Value("${my.datasource.url}")
private String url;
@Value("${my.datasource.url:default}")
private String url;
Java
복사
▪
키를 찾지 못한 경우 ‘:’ 뒤에 붙은 기본값을 사용
◦
파라미터 주입 방법
@Bean
public MyDataSource myDataSource(@Value("${my.datasource.url}") String url, ...) {
return new MyDataSource(url, ...);
}
@Bean
public MyDataSource myDataSource(@Value("${my.datasource.url}") @DefaultValue("default") String url, ...) {
return new MyDataSource(url, ...);
}
Java
복사
▪
키를 찾지 못한 경우 @DefaultValue를 통해 기본값을 사용
▪
객체에도 사용 가능하고 값을 지정해주지 않으면 해당 타입의 기본값을 사용
•
외부 설정 사용 - @ConfigurationProperties
◦
타입 안전하게 외부 설정을 사용할 수 있음
@Data
@ConfigurationProperties("my.datasource")
public class MyDataSourceProperties {
private String url;
private String username;
private String password;
private Etc etc;
@Data
public static class Etc {
private int maxConnection;
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
Java
복사
◦
외부 설정을 객체로 주입 받는 어노테이션
◦
기본 주입 방식은 자바빈 프로퍼티 방식이므로 Getter, Setter 필요
◦
Setter로 인한 변경 방지를 위해 생성자 주입 방식을 사용하는 것을 권함
@EnableConfigurationProperties(MyDataSoourceProperties.class)
public class MyDataSourceConfig {
private final MyDataSourceProperties properties;
public MyDataSourceConfig(MyDataSourceProperties myDataSourceProperties) {
this.properties = properties;
}
@Bean
public MyDataSource dataSource() {
return new MyDataSource(properties.getUrl(), ...);
}
}
Java
복사
◦
의존성 주입을 통해 외부 설정 객체를 주입 받아 사용
◦
외부 설정을 주입 받은 객체를 사용하기 위해선 @EnableConfigurationProperties 어노테이션에 해당 객체를 지정해서 붙여줘야함
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Java
복사
◦
외부 설정 객체를 여러 개 등록할 때는 @ConfigurationPropertiesScan을 사용
•
외부 설정 검증
@Getter
@ConfigurationProperties("my.datasource")
@Validated
public class MyDataSourceProperties {
@NotEmpty
private String url;
@NotEmpty
private String username;
@NotEmpty
private String password;
private Etc etc;
@Getter
public static class Etc {
@Min(1)
@Max(999)
private int maxConnection;
@DurationMin(seconds = 1)
@DurationMax(seconds = 60)
private Duration timeout;
private List<String> options = new ArrayList<>();
}
}
Java
복사
외부 파일
•
외부 설정을 별도의 파일로 관리하게 되면 설정 파일 자체를 관리하기 번거로운 문제
•
서버가 10대면 변경 사항이 있을 때 10대 서버의 설정 파일을 모두 변경해야 하는 불편함
•
설정 파일이 별도로 관리되기 때문에 설정값 변경 이력을 확인하기 어려움
내부 파일
•
내부 파일 분리
◦
프로젝트 안에 각 환경에 필요한 설정 파일도 함께 관리하며 빌드 시점에 모두 포함해서 빌드하고 배포
◦
실행할 때 외부 설정을 사용해서 프로필을 제공하여 읽어야 할 설정 파일을 구분
•
내부 파일 합체
◦
스프링이 하나의 내부 파일 안에서 논리적인 영역을 구분하는 방법 제공
▪
application.properties : #--- 또는 !---
▪
application.yml : ---
▪
spring.config.activate.on-profile 을 통해 프로필 지정
▪
영역 구분 기호 앞 뒤에는 주석이 없어야함
•
내부 파일의 우선순위
1.
@TestPropertySource (테스트에서 사용)
2.
커맨드 라인 옵션 인수
3.
자바 시스템 속성
4.
OS 환경변수
5.
설정 데이터 (application.properties)
a.
jar 내부 application.properties
b.
jar 내부 프로필 적용 파일 application-{profile}.properties
c.
jar 외부 application.properties
d.
jar 외부 프로필 적용 파일 application-{profile}.properties
•
우선순위 이해 방법
◦
설정 데이터는 기본적으론 단순하게 문서를 위에서 아래로 순서대로 읽으며 설정
◦
프로필을 준 부분이 기본값 보다는 우선권을 가진다
◦
더 유연한 것이 우선권을 가진다 (변경하기 어려운 것보다 쉬운 것)
◦
범위가 넓은 것보다 좁은 것이 우선권을 가진다
YAML
•
application.properties 예시
environments.dev.url=https://dev.example.com
environments.dev.name=Developer
Java
복사
•
application.yml 예시
environments:
dev:
url: "https://dev.example.com"
name: "Developer"
Java
복사
•
특징
◦
사람이 읽기 좋게 공백으로 계층 구조를 만든다
◦
구분 기호로 ‘:’를 사용하고 값은 공백을 하나 넣고 넣어준다
◦
스프링은 yml을 properties로 변환해서 읽어들인다
◦
같이 사용할 경우 properties가 우선권을 가진다
@Profile
•
사용 예시
@Configuration
public class PayConfig {
@Bean
@Profile("default")
public LocalPayClient localPayClient() {
return new LocalPayClient();
}
@Bean
@Profile("prod")
public ProdPayClient prodPayClient() {
return new ProdPayClient();
}
}
Java
복사
•
원리
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
Java
복사
◦
@Profile은 @Conditional(ProfileCondition.class) 어노테이션을 포함하고 있다
프로덕션 준비 기능
•
지표(metric), 추적(trace), 감사(auditing), 모니터링(monitoring)
스프링 엑추에이터
•
build.gradle 설정
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Java
복사
•
properties 설정
management:
endpoint: #단일 엔드포인트
jmx: #JMX
exposure:
include: "*"
endpoints: #모든 엔드포인트
web: #HTTP
exposure:
include: "*" #노출
exclude: "*" #제외
YAML
복사
◦
shutdown을 제외한 대부분의 엔드포인트는 기본적으로 활성화
◦
exposure을 통해 엔드포인트를 HTTP를 통해 노출할지, JMX를 통해 노출할지 선택 가능
◦
엔드포인트 활성화 + 엔드포인트 노출이 둘 다 적용되어있어야 사용할 수 있다
•
엔드포인트 목록
◦
beans : 스프링 컨테이너에 등록된 스프링 빈을 보여준다
◦
conditions : condition을 통해서 빈을 등록할 때 평가 조건과 일치하거나 일치하지 않는 이유를 표시한다
◦
configprops : @ConfigurationProperties를 보여준다
◦
env : Environment 정보를 보여준다
◦
health : 어플리케이션 헬스 정보를 보여준다
◦
httpexchanges : HTTP 호출 응답 정보를 보여준다 (HttpExchangeRepository 구현한 빈 등록 필요)
◦
info : 어플리케이션 정보를 보여준다
◦
loggers : 어플리케이션 로거 설정을 보여주고 변경도 할 수 있다
◦
metrics : 어플리케이션의 메트릭 정보를 보여준다
◦
mappings : @RequestMapping 정보를 보여준다
◦
threaddump : 스레드 덤프를 실행해서 보여준다
◦
shutdown : 어플리케이션을 종료한다
•
기본적인 사용
{
"_links": {
"self": {
"href": "http://localhost:8080/actuator",
"templated": false
},
"health-path": {
"href": "http://localhost:8080/actuator/health/{*path}",
"templated": true
},
"health": {
"href": "http://localhost:8080/actuator/health",
"templated": false
}
}
}
JSON
복사
◦
서버 실행 후 GET /actuator로 요청
•
health 정보
◦
설정 방법
management:
endpoint:
health:
show-details: always #모든 정보를 보여줌
show-components: always #각 component의 status만 보여줌
YAML
복사
◦
노출 정보
{
"status": "UP",
"components": {
"db": {
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 994662584320,
"free": 219767533568,
"threshold": 10485760,
"path": "/Users/user/package/.",
"exists": true
}
},
"ping": {
"status": "UP"
}
}
}
YAML
복사
◦
하나의 컴포넌트라도 문제가 생기면 전체 상태는 DOWN이 됨
•
info 정보
◦
정보 목록
▪
java : 자바 런타임 정보
▪
os : OS 정보
▪
env : Environment에서 info. 으로 시작하는 정보
▪
build : 빌드 정보 (META-INF/build-info.properties 파일이 필요함)
▪
git : git 정보 (git.properties 파일이 필요함)
◦
설정 방법
management:
info: #env, build, git은 기본적으로 활성화
java:
enabled: true
os:
enabled: true
YAML
복사
◦
커스텀 설정
management:
info:
app: {
"name": "actuator",
"company": "company"
}
YAML
복사
▪
info 밑에 설정한 정보는 전부 노출됨
•
build 정보
◦
설정 방법
▪
gradle
springBoot {
buildInfo()
}
YAML
복사
•
build/resources/main/META-INF/build-info.properties를 만들기 위한 부분
◦
노출 정보
{
"build": {
"artifact": "actuator",
"name": "actuator",
"time": "2023-02-07T01:53:43.452Z",
"version": "0.0.1-SNAPSHOT",
"group": "hello"
}
}
YAML
복사
•
git 정보
◦
설정 방법
▪
git
•
git에 커밋을 해서 git으로 관리되고 있는 프로젝트여야함
•
build/resources/main/git.properties를 통해 정보 가져옴
▪
노출 정보
{
"git": {
"branch": "main",
"commit": {
"id": "754bc78",
"time": "2023-01-01T00:00:00Z"
}
}
}
YAML
복사
•
loggers 정보
◦
노출 정보
{
"levels": [
"OFF",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"TRACE"
],
"loggers": {
"ROOT": {
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
},
"_org.springframework": {
"effectiveLevel": "INFO"
},
...
}
}
YAML
복사
▪
ROOT 부터 직접 설정한 로그레벨까지 프로젝트 전체 로그레벨을 확인할 수 있음
◦
실시간 로그 레벨 변경
▪
개발 서버는 보통 DEBUG를 사용하지만 실제 서버는 요청이 많기 때문에 INFO를 사용
▪
loggers 엔드포인트를 통해 요청을 보내면 로그 레벨을 변경할 수 있음
•
URI : /actuator/loggers/hello.controller
•
method : POST
•
content-type : application/json
•
body
{
"configuredLevel": "TRACE"
}
YAML
복사
•
httpexchanges 정보
◦
설명
▪
HTTP 요청과 응답의 과거 기록을 확인하기 위한 엔드포인트
▪
HttpExchangeRepository 인터페이스의 구현체를 빈으로 등록해야 사용할 수 있음
◦
HttpExchangeRepository 인터페이스의 구현체 설정 방법
▪
스프링 부트는 기본적으로 InMemoryHttpExchangeRepository 구현체를 제공함
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
YAML
복사
▪
최대 100개의 HTTP 요청을 제공하고 최대 요청이 넘어가면 과거 요청을 삭제
▪
setCapacity() 를 통해 최대 요청 수를 변경할 수 있음
◦
사용 방법
▪
/actuator/httpexchanges
◦
노출 정보
{
"exchanges": [
{
"timestamp": "2023-02-07T02:14:00.334293Z",
"request": {
"uri": "~~",
"method": "~~",
"headers": {
"host": [
"localhost:8080"
],
"accept": [
"*/*"
],
"accept-language": [
"ko-kr"
],
"connection": [
"keep-alive"
],
"accept-encoding": [
"gzip, defalte"
],
"user-agent": [
"~~"
]
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": [
"~~"
],
"Content-Length": [
"2"
],
"Date": [
"Tue, 07 Feb 2023 02:13:36 GMT"
],
"Keep-Alive": [
"timeout=60"
],
"Connection": [
"keep-alive"
]
}
},
"timeTake": "PT0.036144S"
}
]
}
YAML
복사
스프링 엑추에이터의 보안
•
엑추에이터를 다른 포트에서 실행
◦
설정 방법
management:
server:
port: 9292
YAML
복사
•
엑추에이터 URL 경로에 인증 설정
◦
서블릿 필터, 스프링 인터셉터 또는 스프링 시큐리티 등을 통해 인증/인가를 부여
•
엔드포인트 경로 변경
◦
설정 방법
management:
endpoints:
web:
base-path: "/actuator"
YAML
복사
모니터링 툴
•
모니터링 툴에 지표 전달
◦
CPU, JMV, 커넥션 정보 등을 JMX라는 툴에 전달한다고 하면 각각의 정보를 모니터링 툴이 정한 포멧에 맞추어 측정하고 전달해야한다.
•
모니터링 툴 변경
◦
모니터링 툴을 변경하려면 포멧이 변경되므로 측정하고 전달하는 코드까지 모두 변경해야하는 문제가 발생하는데 마이크로미터라는 라이브러리가 이를 해결해준다
마이크로미터
•
마이크로미터 추상화
◦
마이크로미터는 어플리케이션 메트릭 파사드라고 불리고 메트릭을 마이크로미터가 정한 표준 방법으로 모아서 제공한다
◦
스프링부트 엑추에이터는 마이크로미터를 기본으로 내장해서 사용한다
◦
개발자는 마이크로미터가 정한 표준 방법으로 메트릭을 전달하면 되고 모니터링 툴에 맞는 구현체를 선택하면 된다.
메트릭 확인하기
•
스프링부트 엑추에이터는 마이크로미터가 제공하는 지표 수집을 @AutoConfiguration을 통해 자동으로 등록해준다
•
메트릭 리스트 확인
◦
/actuator/metrics를 통해 확인 가능
{
"names": [
"application.ready.time",
"application.started.time",
"disk.free",
"disk.total",
...
]
}
YAML
복사
•
메트릭 디테일 확인
◦
/actuator/metrics/{name}
/actuator/metrics/jvm.memeory.used
{
"name": "jvm.memory.used",
"description": "The amount of used memory",
"baseUnit": "bytes",
"mesurements": [
{
"statistic": "VALUE",
"value": 12440428
}
],
"availableTags": [
{
"tag": "area",
"values": [
"heap",
"nonheap"
]
},
{
"tag": "id",
"values": [
"G1 Survivor Space",
"Compressed Class Space",
"Metaspace",
"CodeCache",
"G1 Old Gen",
"G1 Eden Space"
]
}
]
}
YAML
복사
◦
태그를 활용해 더 자세히 보려면 /actuator/metrics/jvm.memory.used?tag=area:heap 을 사용하면 된다
다양한 메트릭
•
JVM 메트릭 (jvm.~)
◦
메모리 및 버퍼 풀 세부 정보
◦
가비지 수집 관련 통계
◦
스레드 활용
◦
로드 및 언로드된 클래스 수
◦
JVM 버전 정보
◦
JIT 컴파일 시간
•
시스템 메트릭 (system.~, process.~, disk.~)
◦
CPU 지표
◦
파일 디스크립터 메트릭
◦
가동 시간 메트릭
◦
사용 가능한 디스크 공간
•
어플리케이션 시작 메트릭
◦
application.started.time
▪
어플리케이션을 시작하는데 걸리는 시간
▪
ApplicationStartedEvent로 측정학고 스프링 컨테이너가 완전히 실행된 상태로 이후 커맨드 라인 러너가 호출된다.
◦
application.ready.time
▪
어플리케이션이 요청을 처리할 준비가 되는데 걸리는 시간
▪
ApplicationReadyEvent로 측정하고 커맨드 라인 러너가 실행된 이후에 호출된다.
•
스프링 MVC 메트릭 (http.server.requests)
◦
TAG를 사용해서 다음 정보를 분류해서 확인할 수 있다
▪
uri : 요청 URI
▪
method : HTTP 메서드
▪
status : Http Status Code
▪
exception : 예외
▪
outcome : 상태코드를 그룹으로 모아서 확인
•
톰캣 메트릭 (tomcat.~)
◦
설정 방법
server:
tomcat:
mbeanregistry:
enabled: true
YAML
복사
◦
•
데이터 소스 메트릭 (jdbc.connections.~)
◦
DataSource, ConnectionPool에 관한 메트릭을 확인할 수 있다.
◦
최대 커넥션, 최소 커넥션, 활성 커넥션, 대기 커넥션 수 등을 확인할 수 있다.
◦
HikariCP를 사용하면 hikaricp.~ 를 통해 메트릭을 확인할 수 있다.
•
로그 메트릭 (logback.events.~)
◦
trace, debug, info, warn, error 각각의 로그 레벨에 따른 로그 수를 확인할 수 있다.
•
기타 수 많은 메트릭
◦
HTTP 클라이언트 메트릭 (RestTemplate, WebClient)
◦
로거 메트릭
◦
캐시 메트릭
◦
작업 실행과 스케줄 메트릭
◦
스프링 데이터 레포지토리 메트릭
◦
몽고DB 메트릭
◦
레디스 메트릭
프로메테우스와 그라파나 소개
•
프로메테우스
◦
어플리케이션에서 발생한 메트릭을 지속해서 수집하고 DB에 저장하는 역할을 담당
•
그라파나
◦
프로메테우스가 DB라고 한다면 DB에 있는 데이터를 대시보드로 시각화 해주는 툴
•
전체 구조
1.
스프링부트 엑추에이터와 마이크로미터를 사용하면 수 많은 메트릭을 자동으로 생성한다
a.
마이크로미터 프로메테우스 구현체는 프로메테우스가 읽을 수 있는 포멧으로 메트릭을 생성한다
2.
프로메테우스는 이렇게 만들어진 메트릭을 지속해서 수집한다
3.
프로메테우스는 수집한 메트릭을 내부 DB에 저장한다
4.
사용자는 그라파나 대시보드 툴을 통해 그래프로 편리하게 메트릭을 조회한다
•
프로메테우스 아키텍처
프로메테우스
•
어플리케이션 설정
◦
build.gradle
implementation 'io.micrometer:micrometer-registry-prometheus'
YAML
복사
▪
마이크로미터 프로메테우스 구현 라이브러리를 추가하면 스프링부트와 엑추에이터가 자동으로 마이크로미터 프로메테우스 구현체를 등록해서 동작하도록 설정해준다.
▪
엑추에이터에 프로메테우스 메트릭 수집 엔드포인트가 자동으로 추가된다. (/actuator/prometheus)
•
포멧 차이
◦
jvm.info → jvm_info
▪
프로메테우스는 . 대신에 _ 포멧을 사용한다.
◦
_total
▪
지속해서 숫자가 증가하는 메트릭을 카운터라 하고 프로메테우스는 관례상 _total을 붙인다.
◦
https.server.requests
▪
이 메트릭은 내부에 요청 수, 시간 합, 최대 시간 정보를 가지고 있었는데 프로메테우스에선 3가지로 분리된다.
▪
http_server_requests_seconds_count : 요청 수
▪
http_server_requests_seconds_sum : 시간 합 (요청 수의 시간을 합함)
▪
http_server_requests_seconds_max : 최대 시간 (가장 오래 걸린 요청 수)
•
수집 설정
◦
prometheus.yml
rule_files:
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
#추가
- job_name: "spring-actuator"
metrics_path: '/actuator/prometheus'
scrape_interval: 1s
static_configs:
- targets: ['localhost:8080']
YAML
복사
▪
job_name : 수집하는 이름으로 임의의 이름을 사용
▪
metrics_path : 수집할 경로를 지정한다.
▪
scrape_interval : 수집할 주기를 설정한다. (수집주기는 10s~1m를 권장)
▪
targets : 수집할 서버의 IP, PORT를 지정한다
◦
프로메테우스 연동 확인
▪
프로메테우스 매뉴 → Status → Configuration 에서 yml 확인 (localhost:9090/config)
▪
프로메테우스 매뉴 → Status → Targets 에서 연동 확인 (localhost:9090/targets)
•
prometheus : 프로메테우스 자체에서 제공하는 메트릭 정보
•
spring-actuator : 스프링 엑추에이터로 연동한 어플리케이션의 메트릭 정보
•
State가 UP으로 되어 있으면 정상이고 DOWN으로 되어 있으면 연동이 되지 않은 상태
◦
태그와 레이블
▪
error, method, status, uri 등 각각의 메트릭 정보를 구분하기 위해 사용하는 태그
▪
마이크로미터에서는 이것을 태그라 함
▪
프로메테우스에서는 이것을 레이블이라 함
◦
기본 기능
▪
Table → Evaluation time을 수정해서 과거 시간 조회 가능
▪
Graph → 메트릭을 그래프로 조회 가능
◦
필터
▪
레이블을 기준으로 필터를 사용할 수 있다
▪
필터는 중괄호 {} 를 사용한다
▪
레이블 일치 연산자
•
= : 제공된 문자열과 정확히 동일한 레이블 선택
•
≠ : 제공된 문자열과 같지 않은 레이블 선택
•
=~ : 제공된 문자열과 정규식 일치하는 레이블 선택
•
!~ : 제공된 문자열과 정규식 일치하지 않는 레이블 선택
▪
연산자 쿼리와 함수
•
+ : 덧셈
•
- : 빼기
•
* : 곱셈
•
/ : 분할
•
% : 모듈로
•
^ : 승수/지수
▪
sum()
•
값의 합계를 구함
▪
sum by()
•
SQL의 group by와 유사한 기능으로 인자를 통해 grouping한 sum을 구함
•
ex) sum by(method, status)(http_server_requests_seconds_count)
▪
count()
•
Table에 표시되는 result series의 갯수
▪
topk()
•
상위 3가지를 조회
•
ex) topk(3, http_server_requests_seconds_count)
▪
offset
•
SQL의 offset과 비슷한 기능으로 해당 시간만큼 뒤의 데이터들을 조회
•
ex) http_server_requests_seconds_count offset 10m
▪
[]
•
해당 시간동안의 데이터들을 조회
•
ex) http_server_requests_seconds_count[1m]
•
게이지와 카운터
◦
게이지
▪
임의로 오르내릴 수 있는 값
▪
ex) CPU 사용량, 메모리 사용량, 사용중인 커넥션
◦
카운터
▪
단순하게 증가하는 단일 누적 값
▪
ex) HTTP 요청 수, 로그 발생 수
◦
increase()[]
▪
카운터를 시간 단위별로 증가를 확인할 수 있게 바꿔줌
▪
마지막에 [시간] 을 사용해서 범위 벡터를 선택해야함
▪
ex) increase(http_server_requests_seconds_count)[1m]
◦
rate()[]
▪
카운터의 초당 평균을 나누어서 계산하여 초당 얼마나 증가하는지 나타낸다
◦
irate()[]
▪
카운터의 초당 순간 증가율을 나타낸다
그라파나
•
•
연동
1.
Configuration의 Data Source로 이동
2.
Add data source로 prometheus 선택
3.
HTTP 설정
4.
Save & Test
•
대시보드 만들기
1.
대시보드 접근
2.
새로운 대시보드 생성
3.
패널 추가 및 편집
•
type을 Code로 한 후 PromQL을 적고 Run queries 실행
•
+ Query를 통해 지표를 여러 개 추가 가능
4.
지표의 범례 변경
5.
패널의 타이틀 변경
6.
패널의 최소, 최대값과 단위 변경
•
공유 대시보드 활용
◦
설치
▪
new → import → 공유 대시보드의 ID를 입력 → Load → Datasource 선택 → import
사용자 정의 메트릭
•
MeterRegistry
◦
마이크로미터 기능을 제공하는 핵심 컴포넌트
◦
스프링을 통해 주입 받아서 사용하고 이곳을 통해서 카운터, 게이지 등을 등록한다
◦
카운터는 값을 증가하거나 0으로 초기화하는 것만 가능하다
•
Counter 만들기
@Slf4j
public class OrderServiceImpl implements OrderServer {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceImpl(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 cancle() {
log.info("취소");
stock.decrementAndGet();
Counter.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancle")
.description("order")
.register(registry)
.increment();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
Java
복사
◦
Counter.builder(name) : 카운터를 생성하고 name에는 메트릭 이름을 지정한다.
◦
tag() : 는 프로메테우스에서 필터할 수 있는 레이블로 사용한다.
◦
register(registry) : 카운터를 MeterRegistry에 등록한다
◦
increment() : 카운터의 값을 하나 증가시킨다.
•
스프링의 AOP를 이용한 @Counted
@Configuration
public class CountedConfig {
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
Java
복사
◦
CountedAspect를 등록하면 @Counted를 인지해서 Counter를 사용하는 AOP를 적용한다.
◦
CountedAspect를 빈으로 등록하지 않으면 @Counted 관련 AOP가 동작하지 않는다.
@Slf4j
public class OrderServiceImpl implements OrderServer {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceImpl(MeterRegistry registry) {
this.registry = registry;
}
@Counted("my.order")
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
}
@Counted("my.order")
@Override
public void cancle() {
log.info("취소");
stock.decrementAndGet();
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
Java
복사
◦
Counter를 만드는 부분을 AOP를 이용하는 방법이다.
◦
측정하려는 메서드에 @Counter(”메트릭 이름”)을 붙여주면 된다.
◦
어노테이션이 붙은 메서드를 기준으로 tag를 만들어서 분류해준다.
•
Timer
@Slf4j
public class OrderServiceImpl implements OrderServer {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceImpl(MeterRegistry registry) {
this.registry = registry;
}
@Counted("my.order")
@Override
public void order() {
Timer timer = Timer.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "order")
.description("order")
.register(registry);
timer.record(() -> {
log.info("주문");
stock.decrementAndGet();
sleep(500);
});
}
@Counted("my.order")
@Override
public void cancle() {
Timer timer = Timer.builder("my.order")
.tag("class", this.getClass().getName())
.tag("method", "cancle")
.description("order")
.register(registry);
timer.record(() -> {
log.info("취소");
stock.decrementAndGet();
sleep(500);
});
}
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;
}
}
Java
복사
◦
시간을 측정하는 메트릭 측정 도구이다.
◦
카운터와 유사한데 Timer를 사용하면 실행 시간도 함께 측정할 수 있다.
◦
Timer가 측정하는 내용
▪
seconds_count : 누적 실행 수 (카운터)
▪
seconds_sum : 실행 시간의 합 (sum)
▪
seconds_max : 최대 실행 시간, 가장 오래걸린 실행 시간 (게이지)
◦
내부에 타임 윈도우라는 개념이 있어서 1~3분마다 최대 실행 시간이 다시 계산된다.
•
@Timed
@Configuration
public class TimedConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
Java
복사
◦
CountedAspect와 동일하게 Bean으로 등록해주어야 하고 하지 않으면 AOP가 적용되지 않는다.
@Timed("my.order")
@Slf4j
public class OrderServiceImpl implements OrderServer {
private final MeterRegistry registry;
private AtomicInteger stock = new AtomicInteger(100);
public OrderServiceImpl(MeterRegistry registry) {
this.registry = registry;
}
@Counted("my.order")
@Override
public void order() {
log.info("주문");
stock.decrementAndGet();
sleep(500);
}
@Counted("my.order")
@Override
public void cancle() {
log.info("취소");
stock.decrementAndGet();
sleep(500);
}
@Override
public AtomicInteger getStock() {
return stock;
}
}
Java
복사
◦
@Timed(”메트릭 이름”) 은 타입이나 메서드에 적용할 수 있다.
◦
타입에 적용하면 해당 타입의 모든 public 메서드에 적용된다.
•
게이지
@Slf4j
@Configuration
public class StockConfig {
@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);
}
}
Java
복사
◦
my.stock 이라는 이름의 게이지를 등록하는 코드
◦
게이지를 만들 때 함수를 전달하는데 이 함수는 메트릭을 확인할 때마다 호출되고 이 함수의 반환값이 게이지의 값이다.
실무 모니터링 환경 구성 팁
•
모니터링 3단계
◦
대시보드
▪
전체를 한 눈에 볼 수 있는 가장 높은 뷰
▪
ex) 마이크로미터, 프로메테우스, 그라파나 등
◦
어플리케이션 추적
▪
주로 각각의 HTTP 요청을 추적, 일부는 마이크로서비스 환경에서 분산 추적
▪
ex) 핀포인트, 스카우트, 와탭, 제니퍼 등
◦
로그
▪
가장 자세한 추적, 원하는데로 커스텀 가능
▪
같은 HTTP 요청을 묶어서 확인할 수 있는 방법이 중요
▪
MDC(각각의 요청에 대해 UUID를 붙여서 트래킹 하는 방법) 적용
▪
파일로 직접 로그를 남기는 경우
•
일반 로그와 에러 로그는 파일을 구분해서 남기자
•
에러 로그만 확인해서 문제를 바로 정리할 수 있음
▪
클라우드에 로그를 저장하는 경우
•
검색이 잘 되도록 구분
•
모니터링 대상
◦
시스템 메트릭 (ex. CPU, 메모리)
◦
어플리케이션 메트릭 (ex. 톰캣 쓰레드 풀, DB 커넥션 풀, 어플리케이션 호출 수)
◦
비즈니스 메트릭 (ex. 주문수, 취소수)
•
알람
◦
모니터링 툴에서 일정 이상 수치가 넘어가면 슬랙, 문자 등을 연동
◦
알람은 2가지 종류로 꼭 구분해서 관리
▪
경고는 하루 1번 정도 사람이 직접 확인해도 되는 수준
▪
심각은 즉시 확인해야함