Avartar
@deuk9

성장과 배움, 그리고 공유의 공간입니다.

📌 Recent Posts

Spring Boot 3.4 + Log4j2로 Structured Logging 적용하기

2025-05-17

운영 환경에서 로그를 수집하고 분석하는 과정에서, 특정 키워드나 필드 값을 기준으로 로그를 필터링하거나 분류하고 싶을 때가 자주 발생한다. 예를 들어 "level": "error" 같은 값을 기준으로 로그를 검색하려면, 단순 텍스트 로그보다 구조화된 JSON 로그가 훨씬 유리하다. 보다 안정적이고 일관된 로그 구조를 만들기 위해 JSON 로그 포맷을 본격적으로 검토하였고, 그 과정에서 Spring Boot 3.4에서 새롭게 추가된 structured logging 기능을 알게 되었다.📦 사전 준비
  • Spring Boot 3.4.0 이상
  • 로깅 구현체: spring-boot-starter-log4j2
  • Java 17 이상
  • 🧱 StructuredLogFormatter란?Spring Boot 3.4부터 도입된 org.springframework.boot.logging.structured.StructuredLogFormatter<T>
    인터페이스는 구조화된 로그 출력을 위한 포맷터 역할을 한다.Spring Boot는 사용하는 로깅 구현체에 맞춰 해당 포맷터를 자동 연동하며,
    JSON 형태의 로그 생성을 쉽게 도와주는 JsonWriter<T> 유틸리티도 함께 제공한다.🛠 Log4j2 기반 커스텀 포맷터 구현다음은 LogEvent를 기반으로 JSON 로그를 생성하는 커스텀 포맷터 클래스 구현 예시다:package org.example.jsonloggingsmple.config.log; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import org.apache.logging.log4j.core.LogEvent; import org.springframework.boot.json.JsonWriter; import org.springframework.boot.logging.structured.StructuredLogFormatter; public class MyStructuredLoggingFormatter implements StructuredLogFormatter<LogEvent> { private final JsonWriter<LogEvent> writer = JsonWriter.<LogEvent>of((members) -> { members.add("time", event -> { Instant javaInstant = Instant.ofEpochMilli(event.getInstant().getEpochMillisecond()); return LocalDateTime.ofInstant(javaInstant, ZoneId.systemDefault()); }); members.add("level", LogEvent::getLevel); members.add("marker", LogEvent::getMarker).whenNotNull(); members.add("thread", LogEvent::getThreadName); members.add("message", (event) -> event.getMessage().getFormattedMessage()); }).withNewLineAtEnd(); @Override public String format(LogEvent event) { return this.writer.writeToString(event); } }
  • logback 으로 적용은 spring-boot-logback-샘플 을 참고한다. (제너릭 인터페이스가 다름)
  • ⚙️ 설정 방법logging: structured: format: console: org.example.jsonloggingsmple.config.log.MyStructuredLoggingFormatter config: classpath:log4j2-spring.xml 📄 로그 출력 예시{"time":"2025-05-21T23:54:07.368","level":"WARN","thread":"http-nio-8080-exec-1","message": "hi."} 📚 참고 자료
  • Spring Boot Structured Logging 공식 문서
  • html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}

    Openfeign QueryDsl JPA 설정하기

    2025-04-20

    기존에 Querydsl 프로젝트가 버전 업그레이드가 잘 되지 않고 있다는 점은 알고 있었지만, 그동안은 큰 문제가 없어 그냥 사용해왔다.그러던 중, 새로 진행하는 토이 프로젝트에서 한번 설정해 보고싶어 테스트 해봤다. 적용 과정은 의외로 간단했고, 특히 Spring Boot 3.x + Jakarta EE + Hibernate 6 환경에서의 호환성 문제도 자연스럽게 해결할 수 있어 만족스러웠다.게다가 Spring Data 공식 문서에서도 해당 포크에 대해 언급하고 있다는 점에서, 앞으로의 표준처럼 느껴진다.1. 환경 구성은 아래와 같다.
  • java 21
  • gradle(kotlin)
  • spring boot 3.4
  • 2. Gradle(kotlin) 설정하는 방법val queryDslVersion = "6.11" implementation("io.github.openfeign.querydsl:querydsl-core:${queryDslVersion}") implementation("io.github.openfeign.querydsl:querydsl-jpa:$queryDslVersion") annotationProcessor("io.github.openfeign.querydsl:querydsl-apt:$queryDslVersion:jpa") 3. maven 설정 방법<dependency> <groupId>io.github.openfeign.querydsl</groupId> <artifactId>querydsl-core</artifactId> <version>6.11</version> </dependency> <dependency> <groupId>io.github.openfeign.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${openfeign.querydsl.version}</version> </dependency> <dependency> <groupId>io.github.openfeign.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${openfeign.querydsl.version}</version> <classifier>jpa</classifier> <scope>compile</scope> </dependency>
  • 기존에 비해 정말 간단하게 설정이 가능함(플러그인 설치할 필요 없음.)
  • 참고 자료
  • OpenFeign
  • Spring Data Extensions
  • html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}

    RabbitMQ 에서 MQTT와 AMQP 혼용 통신

    2025-04-10

    🧐 조사하게 된 계기기존에는 RabbitMQ를 AMQP 기반으로만 사용하고 있었으나, MQTT 지원 요구가 생기며 조사하게 되었다. RabbitMQ가 MQTT 플러그인을 통해 AMQP와 MQTT를 혼용할 수 있다는 사실을 확인하였다. 이에 따라 Docker 환경에서 MQTT → AMQP 메시지 흐름까지 테스트하였다.✅ MQTT와 AMQP 0.9.1 비교항목MQTTAMQP 0.9.1설명목적IoT, 저전력 장치 통신엔터프라이즈 메시징, 시스템 간 통합경량 통신 vs 신뢰성 위주의 통신구조Topic 기반 Pub/SubExchange → Queue 기반라우팅 방식 차이메시지 흐름Publisher → Broker → SubscriberPublisher → Exchange → Queue → ConsumerAMQP는 중간에 Exchange가 있음토픽 구분자/ (슬래시). (점)계층 구분 방식 차이싱글 레벨 와일드카드+ (플러스)* (애스터리스크)하나의 레벨만 매칭멀티 레벨 와일드카드# (해시)# (해시)0개 이상의 모든 레벨 매칭대표 사용처IoT 센서, 게이트웨이, 모바일금융, 백엔드 마이크로서비스적용 영역 차이브로커 예시Mosquitto, EMQX, RabbitMQ (MQTT 플러그인)RabbitMQ, ActiveMQ공통 브로커도 존재함 (예: RabbitMQ)✅ RabbitMQ 실행version: '3.8' services: rabbitmq: image: rabbitmq:3.12-management container_name: rabbitmq ports: - "5672:5672" # AMQP - "15672:15672" # 관리 콘솔 - "1883:1883" # MQTT environment: - RABBITMQ_DEFAULT_USER=guest - RABBITMQ_DEFAULT_PASS=guest volumes: - ./rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf command: > sh -c " rabbitmq-plugins enable --offline rabbitmq_mqtt && rabbitmq-server "
  • MQTT 프로토콜을 사용하기 위해서는 rabbitmq_mqtt 플러그인을 활성화해야 한다.
  • MQTT 기본 포트는 1883번이며, 이를 통해 메시지를 송수신한다.
  • 해당 플러그인을 통해 MQTT <-> AMQP 간 통신이 가능하다.
  • 각 프로토콜은 각자의 topic 규칙으로 송수신하면 RabbitMQ가 이를 변환하여 처리한다.
  • ✅ RabbitMQ 도식화
    RabbitMQ 도식화

    RabbitMQ 도식화

  • MQTT 메시지는 test/topic/A 형태의 토픽으로 발행되며,
    RabbitMQ에서는 이를 test.topic.A 라우팅 키로 변환하여 처리한다.
  • Exchange는 amq.topic, Queue는 test.queue로 설정한다.
  • ✅ 코드로 보기@Configuration class RabbitMqConfig { companion object { const val QUEUE_NAME = "test.queue" const val EXCHANGE_NAME = "amq.topic" const val ROUTING_KEY = "test.topic.A" } @Bean fun testQueue(): Queue { return Queue(QUEUE_NAME, true) } @Bean fun topicExchange(): TopicExchange { return TopicExchange(EXCHANGE_NAME) } @Bean fun binding(queue: Queue, exchange: TopicExchange): Binding { return BindingBuilder .bind(queue) .to(exchange) .with(ROUTING_KEY) .noargs() } } @Component class MqttAmqpConsumer { @RabbitListener(queues = ["test.queue"]) fun receive(message: String) { println("메시지 수신: $message") } }
  • Queue와 Exchange 를 묶는 설정이다.
  • Exchange 는 topic 타입으로 설정하고 test.topic.Atest.queue를 바인딩한다.
  • AMQP를 사용하는 Consumer에서 test.queue를 바라보고 있으면 MQTT -> AMQP 메시지 전송이 가능하다.
  • ✅ AMQP → MQTT 메시지 흐름이번에는 반대 방향, 즉 AMQP에서 발행된 메시지를 MQTT 클라이언트가 수신하는 경우를 살펴보겠다. 이 경우에는 AMQP Publisher가 메시지를 amq.topic exchange에 발행하고,
    MQTT 클라이언트가 구독하고 있는 토픽과 일치하는 라우팅 키로 메시지를 전달해야 한다.라우팅 키는 MQTT 토픽과의 호환을 위해 .(dot) 형식으로 작성해야 한다.🧑‍💻샘플코드https://github.com/deuk9/mqtt-amqp-integrationhtml pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}