Search

스프링 부트 - 핵심 원리와 활용

자바의 웹 애플리케이션

전통적인 방식
과거에 자바로 웹 애플리케이션을 개발할 때는 먼저 서버에 톰캣 같은 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()[]
카운터의 초당 순간 증가율을 나타낸다

그라파나

설치
2.
압축 풀고 bin/grafana-server 실행
3.
http://localhost:3000 으로 접근
4.
ID : admin / PW : admin 입력 후 로그인
연동
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번 정도 사람이 직접 확인해도 되는 수준
심각은 즉시 확인해야함