2025-05-17
"level": "error"
같은 값을 기준으로 로그를 검색하려면, 단순 텍스트 로그보다 구조화된 JSON 로그가 훨씬 유리하다.
보다 안정적이고 일관된 로그 구조를 만들기 위해 JSON 로그 포맷을 본격적으로 검토하였고, 그 과정에서 Spring Boot 3.4에서 새롭게 추가된 structured logging
기능을 알게 되었다.📦 사전 준비3.4.0
이상spring-boot-starter-log4j2
org.springframework.boot.logging.structured.StructuredLogFormatter<T>
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);
}
}
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."}
📚 참고 자료2025-04-20
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>
2025-04-10
/
(슬래시).
(점)+
(플러스)*
(애스터리스크)#
(해시)#
(해시)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
"
rabbitmq_mqtt
플러그인을 활성화해야 한다.1883
번이며, 이를 통해 메시지를 송수신한다.RabbitMQ 도식화
test/topic/A
형태의 토픽으로 발행되며,test.topic.A
라우팅 키로 변환하여 처리한다.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")
}
}
topic
타입으로 설정하고 test.topic.A
와 test.queue
를 바인딩한다.AMQP
를 사용하는 Consumer에서 test.queue
를 바라보고 있으면 MQTT -> AMQP 메시지 전송이 가능하다.amq.topic
exchange에 발행하고,.(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}