Spring Boot 3.4 에서 추가되는 구조화된 로깅

최근 안내된 Spring Boot 3.4 버전에서 새로 추가되는 구조화된 로깅 지원에 대한 소개글 입니다. 기본적으로 Spring Blog의 소개 글(https://spring.io/blog/2024/08/23/structured-logging-in-spring-boot-3-4)의 흐름을 따르고 있고 중간 중간 저의 의견 및 실행 결과 등을 추가했습니다.

Spring Boot 3.4에서는 구조화된 로깅(Structured logging)을 기본적으로 지원합니다. Elastic Common Schema (ECS)와 Logstash 형식을 지원하며, 사용자 정의 형식으로 확장하는 것도 가능합니다. 먼저 구조화된 로깅에 대해 알아봅시다.


구조화된 로깅이란?

로그 출력이 잘 정의된 형식으로 작성되는 기법입니다. 이 형식은 기계가 읽기 쉽도록 작성되며, ELK 스택 같은 로그 관리 시스템에서 효율적으로 처리될 수 있습니다. 대표적으로 JSON 형식이 많이 사용됩니다.

기본적인 설정 방법

Spring 3.4 버전 에서는 다음과 같은 설정이 가능합니다.

logging.structured.format.console=ecs

프로그램을 시작하면 다음과 같이 JSON 으로 로그가 포맷팅 됩니다.

{"@timestamp":"2024-07-30T08:41:10.561295200Z","log.level":"INFO","process.pid":67455,"process.thread.name":"main","service.name":"structured-logging-demo","log.logger":"com.example.structured_logging_demo.StructuredLoggingDemoApplication","message":"Started StructuredLoggingDemoApplication in 0.329 seconds (process running for 0.486)","ecs.version":"8.11"}

설정 전, 후 로그 출력 비교

  • 설정 전
  • 설정 후

파일로 로깅

콘솔 화면은 사람이 읽을 수 있는 기존 구조로 설정하고, 기계가 읽을 수 있는 구조의 로깅은 다른 특정 파일에 작성하고 싶을 수 있습니다. 다음과 같은 설정으로 파일에 구조화된 로깅을 설정할 수 있습니다.

logging.structured.format.file=ecs 
logging.file.name=log.json

추가 필드를 추가

구조화된 로깅의 강력한 기능 중 하나는 개발자가 구조화된 방식으로 로그 이벤트에 정보를 추가할 수 있다는 것입니다. 예를 들어 모든 로그 이벤트에 사용자 아이디를 추가한 다음 나중에 해당 아이디를 필터링하여 특정 사용자가 무엇을 했는지 확인할 수 있습니다.

Elastic Common Schema와 Logstash 모두 JSON에 매핑된 진단 컨텍스트(Mapped Diagnostic Context)의 콘텐츠를 포함합니다. 실제로 작동하는지 확인하기 위해 자체 로그 메시지를 생성해 보겠습니다:

@Component 
class MyLogger implements CommandLineRunner { 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class); 

    @Override public void run(String... args) { 
        MDC.put("userId", "1"); 
        LOGGER.info("Hello structured logging!"); 
        MDC.remove("userId"); 
    } 
}

로그 메시지를 로깅하기 전에 이 코드는 MDC에 “userId”도 설정합니다. Spring Boot는 JSON에 “userId”를 자동으로 포함합니다:

{ ... ,"message":"Hello structured logging!","userId":"1" ... }

또한 우아한 로깅 API를 사용하여 MDC에 의존하지 않고도 추가 필드를 추가할 수 있습니다:

@Component 
class MyLogger implements CommandLineRunner { 
    private static final Logger LOGGER = LoggerFactory.getLogger(MyLogger.class); 

    @Override public void run(String... args) { 
        LOGGER.atInfo().setMessage("Hello structured logging!").addKeyValue("userId", "1").log();
    } 
}

Elastic Common Schema는 기본적으로 많은 필드 이름을 정의하고 있습니다. Spring Boot 에서는 서비스 이름, 서비스 버전, 서비스 환경 및 노드 이름에 대한 기본 지원 기능을 갖추고 있습니다. 이러한 필드에 대한 값을 설정하려면 application.properties에서 다음을 사용할 수 있습니다:

logging.structured.ecs.service.name=MyService
logging.structured.ecs.service.version=1
logging.structured.ecs.service.environment=Production
logging.structured.ecs.service.node-name=Primary

JSON 출력에서 이제 service.name, service.version, service.environment, service.node.name와 같은 필드가 나타납니다. 이를 통해 로깅 시스템에서 노드 이름, 서비스 버전 등을 기준으로 필터링할 수 있게 됩니다. 이 네가지 필드 이외의 다른 Elastic Common Schema 필드에 대한 값은 properties 에서 설정할 수 없습니다. 다른 필드에 대한 값을 설정하려면 후에 서술할 맞춤형 로그 형식을 통해 추가해야 할 것 같습니다.

로깅 예시

{"@timestamp":"2024-08-29T11:30:41.325556Z","service.name":"MyService","service.version":"1","service.environment":"Production","service.node.name":"Primary","log.logger":"com.example.structuredlogging.StructuredLoggingApplication",...,"ecs.version":"8.11"}

맞춤형 로그 형식

각자의 프로젝트의 맞춤화된 로그 포맷을 위해 다음과 같은 단계가 필요합니다.

  1. StructuredLogFormatter 인터페이스의 커스텀 구현을 작성하세요
class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> { 

    @Override public String format(ILoggingEvent event) { 
        return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n"; 
    } 
}
  1. application.properties에서 해당 커스텀 구현을 참조하세요
logging.structured.format.console=com.example.structuredlogging.MyStructuredLoggingFormatter

Spring Boot 3.4에 새로 추가되는 JsonWriter 를 사용해 JSON 으로 출력할 수도 있습니다.

class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> {  
    private final JsonWriter<ILoggingEvent> writer = JsonWriter.<ILoggingEvent>of((members) -> {  
       members.add("time", (event) -> event.getInstant());  
       members.add("level", (event) -> event.getLevel());  
       members.add("thread", (event) -> event.getThreadName());  
       members.add("message", (event) -> event.getFormattedMessage());  
       members.add("application").usingMembers((application) -> {  
          application.add("name", "StructuredLoggingDemo");  
          application.add("version", "1.0.0-SNAPSHOT");  
       });  
       members.add("node").usingMembers((node) -> {  
          node.add("hostname", "node-1");  
          node.add("ip", "10.0.0.7");  
       });  
    }).withNewLineAtEnd();  

    @Override  
    public String format(ILoggingEvent event) {  
       return this.writer.writeToString(event);  
    }  
}

마무리

Spring Boot 3.4 에서 강화된 구조화된 로깅 지원 기능은 매우 편리하게 로그에 다양한 정보를 추가할 수 있도록 도와줍니다. 로그를 남길때 특정 로그가 작성되는 시점의 맥락을 로그에 담았으면 좋겠다는 생각을 많이 하는데 이럴 때 유용하게 사용할 수 있을 것 같습니다. 특히 코드로 맞춤형 로그 구조를 직접 만들 수 있는 기능은 개인정보 마스킹등의 로그 요구사항도 비교적 손쉽게 구현이 가능해 질 것 같아서 기대가 됩니다.

추가 자료 링크

Spring Boot 프로젝트에서 초간단 docker compose 사용하기

Spring Boot를 사용해서 서비스를 만들 때 데이터베이스 같은 외부 서비스 들을 보통 한 개 이상 사용하게 되는데요, 이 때 docker compose를 사용하면 개발 과정에서 외부 서비스 들을 쉽게 관리할 수 있습니다.

docker-compose.yml 파일에 사용할 외부 서비스들을 정의하고 프로젝트 시작 전에 “docker-compose up” 프로젝트 종료 후 “docker-compose down” 명령어를 실행하는 방식이었죠.

Spring Boot 3.1 버전 부터는 docker compose 지원이 강화되어 프로젝트의 docker compose 파일을 감지하고 프로젝트가 기동 될 때 up, 중지 될 때 down 명령을 자동으로 실행해 줍니다.

바로 살펴봅시다!


필요 사항

  • Spring Boot 3.1 이상
  • docker-compose 2.2.0 이상

사용 방법

services:
database:
image: 'postgres'
ports:
– '5432'
environment:
– 'POSTGRES_USER=admin'
– 'POSTGRES_DB=mydatabase'
– 'POSTGRES_PASSWORD=password'

위와 같은 docker-compose.yml 이 프로젝트에 있다고 가정합니다.

developmentOnly 'org.springframework.boot:spring-boot-docker-compose'

build.gradle 파일에 spring-boot-docker-compose 라이브러리를 추가합니다. 개발환경에만 활성화되도록 developmentOnly 를 적용시킵니다.

끝!

직접 실행해서 결과를 확인해보겠습니다.

실행 결과

Spring Boot 실행 로그 중 3번째 라인에서 “Using Docker Compose file …” 을 확인 할 수 있습니다. 이후 Container 를 띄우고 Healthy 상태까지 기다린 이후에 프로젝트가 시작 되는 것을 확인 할 수 있네요!

정말 간단하죠? 심지어 데이터베이스 연결 정보를 애플리케이션 프로퍼티에 지정할 필요도 없습니다. spring-boot-docker-compose 에서 docker-compose.yml 을 읽어서 연결 정보가 필요하다면 ConnectionDetails 을 자동으로 만들어 주기 때문입니다. (자동으로 연결해주는 서비스 목록)

추가 설정

만약 커넥션 주소에 옵션 값을 추가하려고 한다면 docker compose yml 파일에 다음과 같이 추가 할 수 있습니다.

services:
postgres:
image: 'postgres:15.3'
environment:
– 'POSTGRES_USER=myuser'
– 'POSTGRES_PASSWORD=secret'
– 'POSTGRES_DB=mydb'
ports:
– '5432:5432'
labels:
org.springframework.boot.jdbc.parameters: 'ssl=true&sslmode=require'

마치며

Spring Boot 3.1 부터 강화된 docker compose 지원은 로컬 개발 시 정말 손쉽게 docker compose 를 다룰 수 있도록 도와줍니다. 그동안 외부 서비스들을 별도로 띄워서 사용했다면 docker compose 를 한번 사용해 보시는 것도 좋을 것 같고 이미 docker compose를 사용하고 있었다면 Spring Boot 버전을 3.1 이상으로 올리는 것도 고려해 볼 만 할 것 같습니다. 이미 boot 버전이 3.1 이상이라면 이 기능은 안 쓸 이유가 없습니다!

참고