Micrometer

Micrometer란?

micrometer.io에서는 Micrometer에 대해서 다음과 같이 소개하고 있다.

Micrometer provides a simple facade over the instrumentation clients for the most popular monitoring systems, allowing you to instrument your JVM-based application code without vendor lock-in. Think SLF4J, but for metrics

Micrometer는 JVM 기반의 애플리케이션에서 다양한 모니터링 도구가 제공하는 클라이언트 라이브러리에 대한 facade를 제공한다. 로깅 관련된 시스템에서는 SLF4J가 있다면 모니터링(metric) 시스템에서는 Micrometer가 있는 것이다.

즉, 모니터링 시스템을 만드는 vendor들은 Micrometer 인터페이스를 따르기 때문에 Micrometer를 사용하면 애플리케이션 내의 코드 상에서는 모니터링 시스템 클라이언트로 어떤 것을 사용할지에 대한 고민에서 벗어나 Micrometer를 이용해 애플리케이션 metric을 수집하기만 하면 된다.
(모니터링 시스템을 선택하는 것은 런타임 시점에 정해진다고 생각하면 된다.)

자세히 보기

Spring Bean LifeCycle

최근 사내에서 Kafka 관련된 설정을 리팩토링하는 작업을 했습니다. SpringBoot를 도입하며 Kafka 설정 관련된 부분들을 SpringBoot의 @ConfigurationProperties를 이용하도록 변경하고, XML 기반의 빈 생성 부분을 Java Config 기반으로 변경했습니다.
현재 프로젝트에서는 여러가지 이유로 기존의 Kafka Producer를 확장(extends)해서 사용하고 있는데, 이번에 리팩토링 관련한 PR에서 확장해서 사용하고 있는 Kafka Producer에서 close 메서드를 구현하고 있는지 확인해달라는 리뷰를 받았습니다.

컨테이너에 등록된 Bean이 DisposableBean interface를 구현하고 있거나 @PreDestroy 어노테이션 또는 destroyMethod 속성을 사용하고 있다면 Bean의 Lifecycle 마지막에 자원을 해제하거나 필요한 작업을 수행할 수 있습니다. 그러나 위의 방법 말고도 Spring container에서 Bean을 제거 할 때, close()shutdown() 메서드를 호출합니다.

Kafka ProducerCloseable interface를 구현(implement)하고 있기 때문에 close 메서드를 포함하고 있습니다. Kafka Producer가 Bean으로 등록되어 있고 후에 소멸될 때 close 메서드가 호출되어 해당 자원이 모두 해제될 것입니다.

따라서 제가 받았던 리뷰는 기존 Kafka Producer를 확장한 것을 Bean으로 등록해 사용하고 있는데, 해당 확장 클래스가 close 메서드를 제대로 구현해 Bean이 소멸 될 때 호출될 close 메서드에서 자원이 제대로 해제하고 있는지 확인해 달라는 것이었습니다.

AutoCloseabletry-with-resource 구문과 함께 사용됩니다.

이번 리뷰를 통해 Bean의 LifeCycle과 close, shutdown 메서드에 대해 다시 살펴보는 계기가 되었습니다.

Initialize 메서드

Initialize 메서드는 Bean Object가 생성되고 DI를 마친 후 실행되는 메서드입니다. 일반적으로 Object의 초기화 작업이 필요한 경우 생성자에서 처리하지만 DI를 통해 Bean이 주입된 후에 초기화할 작업이 있다면 초기화 메서드를 이용해서 초기화를 진행할 수 있습니다.

@PostConstruct

초기화 하고 싶은 메서드에 @PostConstruct 어노테이션을 붙여주면 Spring이 해당 메서드를 초기화시에 호출합니다. PostConstruct는 JSR-250 스펙에 포함되어 있기 때문에 JSR-250을 구현한 다른 프레임워크 혹은 라이브러리에서도 동작합니다. 다른 초기화 메서드에 비해 Spring에 의존적이지 않다는 장점이 있습니다.

JSR-250
JSR 250 is a Java Specification Request with the objective to develop annotations (that is, information about a software program that is not part of the program itself) for common semantic concepts in the Java SE and Java EE platforms that apply across a variety of individual technologies.

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class SimpleBean {

@PostConstruct
public void postConstruct() {
log.info("postConstruct");
}
}

InitializingBean

InitializingBean 인터페이스를 구현하면 Spring이 afterPropertiesSet 메서드를 초기화시에 호출합니다.

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class SimpleBean implements InitializingBean {

@Override
public void afterPropertiesSet() throws Exception {
log.info("afterPropertiesSet");
}
}

@Bean(initMethod)

@Bean 어노테이션을 이용해 Bean을 생성할 때, @Bean 어노테이션의 initMethod 속성을 이용해 초기화 메서드를 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class TestConfiguration {

@Bean(initMethod = "init")
public SimpleBean simpleBean() {
return new SimpleBean();
}

@Slf4j
public static class SimpleBean {

public void init() throws Exception {
log.info("init");
}
}
}

Destroy 메서드

Destroy 메서드는 스프링 컨테이너가 종료 될 때, 호출되어 Bean이 사용한 리소스들을 반환하거나 종료 시점에 처리해야할 작업이 있을 경우 사용합니다.

PreDestroy

@PreDestroy 도 PostConstruct처럼 JSR-250 스펙에 포함되어 있기 때문에 JSR-250을 구현한 다른 프레임워크 혹은 라이브러리에서도 동작합니다. 컨테이너가 종료 될 때 실행하고 싶은 메서드에 어노테이션을 붙여주면 Spring이 컨테이너 종료 시 해당 메서드를 호출합니다.

1
2
3
4
5
6
7
8
9
@Slf4j
@Component
public class SimpleBean {

@PreDestroy
public void preDestroy() {
log.info("preDestroy");
}
}

DisposableBean

DisposableBean 인터페이스를 구현하면 Spring이 destroy 메서드를 호출합니다.

1
2
3
4
5
6
7
8
9
lf4j
@Component
public class SimpleBean implements DisposableBean {

@Override
public void destroy() throws Exception {
log.info("destroy");
}
}

@Bean(destroyMethod)

@Bean 어노테이션을 이용해 Bean을 생성할 때, @Bean 어노테이션의 destroyMethod 속성을 이용해 컨테이너 종료시 실행하고자 하는 메서드를 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class TestConfiguration {

@Bean(destroyMethod = "destroy")
public SimpleBean simpleBean() {
return new SimpleBean();
}

@Slf4j
public static class SimpleBean {

public void destroy() throws Exception {
log.info("destroy");
}
}
}

close & shutdown

close와 shutdown 메서드는 DisposableBeanAdapter에 의해 실행됩니다.

@Bean Lite Mode & inter-bean references

@Bean Methods in @Configuration Classes

일반적으로 @Bean 어노테이션은 @Configuration 어노테이션이 사용된 클래스 내의 메서드에 선언이 됩니다. 이 경우 @Bean 어노테이션을 사용하는 메서드는 같은 클래스의 다른 @Bean 메소드를 직접 호출하여 참조할 수 있습니다. 이렇게하면 bean 간의 참조(reference)가 강하게 만들어집니다.

아래 코드의 실행 후 로그를 통해 a() 메서드의 결과로 생성되는 A 클래스의 빈은 b() 와 c() 메서드에서 a() 메서드를 직접 호출해 참조가 되는 것을 확인할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@SpringBootApplication
public class TestApplication {

@Autowired
private ApplicationContext context;

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(TestApplication.class, args);
}

@Configuration
public static class TestConfiguration {

@Bean
public A a() {
return new A();
}

@Bean
public B b() {
A a = a();
System.out.println(a);
return new B();
}

@Bean
public C c() {
A a = a();
System.out.println(a);
return new C();
}
}

public static class A {
}

public static class B {
}

public static class C {
}
}


// 샐행 결과
2019-04-30 19:41:04.837 INFO 24509 --- [ main] com.example.test.TestApplication : Starting TestApplication on AL01297960.local with PID 24509 (/Users/user/work/test/build/classes/java/main started by user in /Users/user/work/test)
2019-04-30 19:41:04.841 INFO 24509 --- [ main] com.example.test.TestApplication : No active profile set, falling back to default profiles: default
2019-04-30 19:41:06.229 INFO 24509 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-04-30 19:41:06.262 INFO 24509 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-04-30 19:41:06.262 INFO 24509 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.17]
2019-04-30 19:41:06.353 INFO 24509 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-04-30 19:41:06.353 INFO 24509 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1441 ms
com.example.test.TestApplication$A@42163c37
com.example.test.TestApplication$A@42163c37
2019-04-30 19:41:06.620 INFO 24509 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-04-30 19:41:06.846 INFO 24509 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-30 19:41:06.850 INFO 24509 --- [ main] com.example.test.TestApplication : Started TestApplication in 2.489 seconds (JVM running for 3.45)

이러한 관계를 **빈 간의 참조(inter-bean references)**라 부릅니다. 이러한 빈 간의 참조는 @Configuration 클래스의 @Bean이 cglib wrapper에 의해 래핑되기 때문에 동작하게 됩니다.(@Bean 메서드에 대한 호출을 가로채고 Bean 인스턴스를 컨텍스트에서 반환하게 됩니다.)

@Bean Lite Mode

처음 알게된 분도 계실 수 있을텐데요, @Bean 메소드는 @Configuration으로 주석을 달지 않은 클래스 내에서도 선언 될 수도 있습니다. 이런 경우, @Bean 메서드는 **lite mode**로 처리됩니다.

lite mode의 Bean 메서드는 스프링 컨테이너에 의해 일반 팩토리 메서드로 처리됩니다. 그렇기 때문에, lite mode에서는 빈 간의 참조가 지원되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@SpringBootApplication
public class TestApplication {

@Autowired
private ApplicationContext context;

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(TestApplication.class, args);
}

@Configuration
public static class TestConfiguration {

public A a() {
return new A();
}

@Bean
public B b() {
A a = a();
System.out.println(a);
return new B();
}

@Bean
public C c() {
A a = a();
System.out.println(a);
return new C();
}
}

public static class A {
}

public static class B {
}

public static class C {
}
}


// 실행 결과
```java
2019-04-30 20:02:17.530 INFO 65524 --- [ main] com.example.test.TestApplication : Starting TestApplication on AL01297960.local with PID 65524 (/Users/user/work/test/build/classes/java/main started by user in /Users/user/work/test)
2019-04-30 20:02:17.534 INFO 65524 --- [ main] com.example.test.TestApplication : No active profile set, falling back to default profiles: default
2019-04-30 20:02:18.857 INFO 65524 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-04-30 20:02:18.891 INFO 65524 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-04-30 20:02:18.891 INFO 65524 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.17]
2019-04-30 20:02:18.973 INFO 65524 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-04-30 20:02:18.973 INFO 65524 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1368 ms
com.example.test.TestApplication$A@919d542c
com.example.test.TestApplication$A@414fc49c
2019-04-30 20:02:19.240 INFO 65524 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-04-30 20:02:19.471 INFO 65524 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-30 20:02:19.474 INFO 65524 --- [ main] com.example.test.TestApplication : Started TestApplication in 2.406 seconds (JVM running for 3.2)