오늘은 Spring boot Actuator의 마지막 시간이다. 그때 말했던 것처럼 오늘은 trace를 좀 더 영구적으로 사용할 수 있도록 해볼 예정이다.
일단 몽고DB와 redis로 저장할 예정인데 redis 경우에는 시리얼라이즈 할때 좀 귀찮은게 있다. 그래서 좀 설정이 들어간다. 일단 trace에 대해서 좀 더 알아보자.

InMemoryTraceRepository

기본적으로 trace 엔드포인트는 100개 까지만 저장 가능하다. 그래서 100개 이상 저장되면 이전에 저장되었던 오래된 데이터들은 사라진다. 이를 해결하기 위해 저장 개수를 늘릴 수 있다.

@Bean
fun traceRepository(): TraceRepository {
  val traceRepository = InMemoryTraceRepository()
  traceRepository.setCapacity(10000)
  return traceRepository;
}

위와 같이 하면 InMemoryTraceRepository를 사용해서 만개의 데이터를 저장 할 수 있다. 하지만 이것도 문제가 있다. 요즘 같은 트래픽이 많은 시대에 만개의 데이터도 금방 쌓인다. 그렇다고 해서 마냥 용량을 백만개 천만개로 늘릴 수 도 없다. 그만큼 메모리를 사용하기 때문이다. 그래서 좀 더 영구적인 곳에 저장하면 메모리 걱정도 없을 것이다.
그리고 인메모리에 저장되어 서버가 내려가면 메모리에 저장되니 사라지기 마련이다. 물론 개발중에는 상관없겠지만 운영중에서 사용할 경우 서버가 내려가면 모든 데이터가 지워지니 trace 할 데이터도 모두 사라진다.

TraceRepository

우리는 다른 곳에 trace를 간단한 코딩만으로 저장할 수 있다. Spring boot 에서 지원해주는 TraceRepository만 구현해주면 끝난다. TraceRepository에는 두개의 추상메서드가 존재한다. 한개는 저장할 때 사용되며 다른 한개는 저장한 목록을 불러오는데 사용된다.

public interface TraceRepository {

    List<Trace> findAll();

    void add(Map<String, Object> traceInfo);
}

우리는 findAll() 메서드와 add()라는 메서드만 구현해주면 trace 엔드포인트의 저장 공간을 변경할 수 있다. 그럼 어디 한번 해보자.

MongoTraceRepository

우리는 커스텀하게 MongoDB로 저장할 수 있는 TraceRepository를 만들 수 있다. 일단 Spring boot의 data-mongodb starter를 디펜더시 받자.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

그러고 난 후 몽고 서버를 띄우면 되는데 몽고서버를 다운로드 받기 귀찮으면 embed 몽고DB를 디펜더시 받으면 된다.

<dependency>
  <groupId>de.flapdoodle.embed</groupId>
  <artifactId>de.flapdoodle.embed.mongo</artifactId>
  <scope>runtime</scope>
</dependency>

위의 디펜더시를 테스트 용도 이니 운영에서는 사용하지 말자. 한번 MongoTraceRepository를 만들어 보자.

class MongoTraceRepository(val mongoOperations: MongoOperations) : TraceRepository {

  override fun add(traceInfo: Map<String, Any>?) {
    mongoOperations.save(Trace(Date(), traceInfo))
  }

  override fun findAll(): List<Trace> {
    return mongoOperations.findAll(Trace::class.java)
  }
}

소스 또한 엄청 짧다. TraceRepository 인터페이스만 구현해주면 된다. 위와 같이 구현을 한 후에 MongoTraceRepository 클래스를 빈으로 등록 시켜주면 된다.

@Bean
fun traceRepository(mongoOperations: MongoOperations) = MongoTraceRepository(mongoOperations)

이제 서버를 띄어서 확인해보자.

RedisTraceRepository

redis 또한 Mongo와 마찬가지로 RedisTraceRepository라는 클래스를 만들어서 구현해보자. redis도 디펜더시 받자.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

위와 같이 디펜더시를 받고 RedisTraceRepository 구현하자.

class RedisTraceRepository(val redisTemplate: RedisTemplate<String, Trace>) : TraceRepository {

  override fun add(traceInfo: Map<String, Any>?) {
    redisTemplate.opsForList().rightPush("redisTrace", Trace(Date(), traceInfo))
  }

  override fun findAll(): List<Trace> {
    return redisTemplate.opsForList().range("redisTrace", 0, 100)
  }
}

위와 같이만 할 경우에는 제대로 동작하지 않는다. 왜냐하면 Spring boot 에서 기본적인 RedisTemplate은 RedisTemplate<Object,Object>StringRedisTemplate만 빈으로 등록된다. 그래서 우리는 RedisTemplate<String, Trace>을 빈으로 등록시켜줘야 한다.

@Bean
fun redisTemplate(
    redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Trace> {
  val template = RedisTemplate<String, Trace>()
  template.connectionFactory = redisConnectionFactory
  template.valueSerializer = Jackson2JsonRedisSerializer<Trace>(Trace::class.java)
  return template
}

우리는 Jackson2JsonRedisSerializer 를 이용해서 value에 Trace 클래스를 시리얼라이즈한다고 정의해 놨다. 위와 같이 한다해도 서버는 정상적으로 동작하지만 findAll() 메서드를 호출하면 에러가 발생한다. 그 이유는 jackjson의 Deserializer할 때는 디폴트 생성자가 있어야 한다. 하지만 Trace 클래스에는 디폴트 생성자가 존재하지 않는다. 그래서 어떻게 할까 고민을 하다가 상속을 받아서 해볼까 했는데 그 또한 되지 않는다. 안타깝게 Trace 클래스는 final 클래스이다. 점차 산으로 간다.. 그래서 또 생각한게 Deserializer를 커스텀하게 만들어서 넣으면 되지 않을까 해서 그렇게 만들었다.

@Bean
fun redisTemplate(
    redisConnectionFactory: RedisConnectionFactory): RedisTemplate<String, Trace> {
  val template = RedisTemplate<String, Trace>()
  template.connectionFactory = redisConnectionFactory
  val jackson2JsonRedisSerializer = Jackson2JsonRedisSerializer<Trace>(Trace::class.java)
  val objectMapper = ObjectMapper()
  val simpleModule = SimpleModule()
  simpleModule.addKeyDeserializer(Trace::class.java, TraceDeserializer(objectMapper))
  jackson2JsonRedisSerializer.setObjectMapper(objectMapper)
  template.valueSerializer = jackson2JsonRedisSerializer
  return template
}

대충 위와 같은 코드이다. 배보다 배꼽이 더 큰 듯하다. TraceDeserializer를 좀 더 자세히 보고 싶다면 여기에서 확인은 가능하다.

Include

마지막으로 /trace 엔드포인트의 저장할 데이터를 정해진 필드에 추가할 수 있다. 기본적으로 request, response의 헤더, 쿠키, 에러, 시간만 저장이 되는데 path, user principal, parameters등 다양한 정보를 넣을 수 있다. 추가로 넣고 싶은 정보는 아래와 같다.

REQUEST_HEADERS, RESPONSE_HEADERS, COOKIES, AUTHORIZATION_HEADER, ERRORS, PATH_INFO, PATH_TRANSLATED, CONTEXT_PATH, USER_PRINCIPAL, PARAMETERS, QUERY_STRING, AUTH_TYPE, REMOTE_ADDRESS, SESSION_ID, REMOTE_USER, TIME_TAKEN

꽤 많은 정보를 넣을 수 있다. 위의 정보를 넣고 싶으면 아래와 같이 프로퍼티에 작성하면 된다.

management.trace.include=REQUEST_HEADERS, PATH_INFO, PARAMETERS

include를 사용해서 사용자가 원하는 데이터를 손쉽게 넣을 수 있게 잘 만들어 놨다.

이렇게 trace 엔드포인트를 손쉽게 커스텀 할 수 있다. 물론 다른 nosql을 사용해서 위와 비슷하게 구현해도 괜찮다. 꼭 nosql만 사용하라는 법도 없고 RDB에 넣어도 괜찮겠지만 뭐 굳이 그럴필요까지 있나 싶기도 하다..

오늘 마지막으로 Spring boot Actuator를 살펴봤다. 더 궁금한게 있다면 문서를 찾아보는 것도 좋은 방법이다. 마지막으로 Spring boot Actuator 프로젝트는 괜찮은 프로젝트라고 생각된다.