#logger
Simple Logging Facade for Java
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public class TestClass {
private static final Logger log = LoggerFactory.getLogger(TestClass.class)
}
@Slf4j
public class TestClass {
}
웨이팅 등록 서비스 예시를 통해서 로그 레벨을 이해해보자.
@Slf4j
@Service
public class TestClass {
private int currentWaitingCount = 0; // 현재 대기 번호
private final Set<String> registeredPhones = new HashMap<>(); // 중복 등록 방지
public String registerWaiting(String name, String phone) {
String maskedPhone = maskPhoneNumber(phone);
// 1. [INFO] : 일반적인 비즈니스 흐름 기록
log.info("[웨이팅 요청] 이름 : {}, 연락처 : {}", name, maskedPhone);
// 2. [ERROR] : 비즈니스 오류 (중복 등록 시도)
if (registeredPhones.contains(phone)) {
log.error("[웨이팅 실패] 중복 등록 시도 발생! 연락처 : {}", maskedPhone);
throw new IllegalArgumentException("이미 대기 등록된 연락처입니다.");
}
// 3. [WARN] 시스템 경고 (대기열 마감 임박)
if (currentWaitingCount >= 45) {
log.warn("[웨이팅 경고] 대기열 마감 임박! 현재 대기 인원 : {}명", currentWaitingCount);
}
registeredPhones.add(phone);
currentWaitingCount++;
// 4. [INFO] 비즈니스 정상 처리 완료
log.info("[웨이팅 완료] 대기번호 {}번 발급 완료 (고객명 : {})", currentWaitingCount, name);
return name + "님, 대기번호 " + currentWaitingCount + "번이 발급되었습니다.";
}
private String maskPhoneNumver(String phone) {
if (phone == null || phone.length() < 13) return "****";
return phone.substring(0, 4) + "****" + phone.substring(8);
}
}
로그는 8개의 조각으로 나뉜다. 아래 로그 예시를 통해 살펴보자.
2026-03-08 17:12:03.421+09:00 INFO 18324 --- [test-server] [nio-8080-exec-1] c.k.api.user.UserController : Get user request. userId=42
2026-03-08 17:12:03.438+09:00 INFO 18324 --- [test-server] [nio-8080-exec-1] c.k.service.user.UserService : Fetching user from database. userId=42
2026-03-08 17:12:03.462+09:00 INFO 18324 --- [test-server] [nio-8080-exec-1] c.k.repository.user.UserRepository : User entity loaded. id=42
2026-03-08 17:12:03.471+09:00 INFO 18324 --- [test-server] [nio-8080-exec-1] c.k.api.user.UserController : Response success. userId=42
2026-03-08 17:12:03.421+09:00
2026-03-08 17:12:03.438+09:00
2026-03-08 17:12:03.462+09:00
2026-03-08 17:12:03.471+09:00
INFO
INFO
INFO
INFO
18324
18324
18324
18324
---
---
---
---
[test-server]
[test-server]
[test-server]
[test-server]
[nio-8080-exec-1]
[nio-8080-exec-1]
[nio-8080-exec-1]
[nio-8080-exec-1]
c.k.api.user.UserController
c.k.service.user.UserService
c.k.repository.user.UserRepository
c.k.api.user.UserController
: Get user request. userId=42
: Fetching user from database. userId=42
: User entity loaded. id=42
: Response success. userId=42
단순히 로그를 쌓기만 하는 것이 아니라, 모니터링 도구를 연동하여 관리한다.
import jakarta.servlet.Filter;
@Component
public class MdcLoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 1. 요청이 들어오면 고유한 8글자 Trace ID를 생성
String traceId = UUID.randomUUID().toString().substring(0, 8);
// 2. MDC라는 곳에 해당 아이디를 보관
MDC.put("traceId", traceId);
try {
// 3.Controller로 요청을 넘김.
chain.doFilter(request, response);
} finally {
// 요청 처리가 끝나면 반드시 비워줘야 함
// 톰캣은 재사용되기 때문에, 안 비우면 다음 사람 로그에 이전 사람 ID가 섞일 수 있음
MDC.clear();
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
MDC 필터 생성 후 로그백 설정을 추가로 진행해야한다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta([%X{traceId}]) %yellow([%thread]) %cyan(%logger{36}) : %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Logback에서는 로그를 배송하는 목적지(출력 위치)를 APPENDER가 결정한다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- property : 자바의 변수 같은 것 -->
<!-- 로그를 저장할 경로 -->
<property name="LOG_DIR" value="./logs" />
<property name="LOG_FILE_NAME" value="waiting-api-log" />
<!-- 로그를 출력할 곳을 정하는것이 appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) [%X{traceId}] %cyan(%logger{36}) : %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/${LOG_FILE_NAME}.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level [%X{traceId}] %logger{36} : %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- <fileNamePattern>${LOG_DIR}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.log.gz</fileNamePattern>-->
<!-- 이름이 MM-dd 기준으로 생성된다 즉, 날마다 로그파일을 새로 만든다! -->
<!-- gz는 압축 하겠다라는 의미이다 -->
<!-- <maxHistory>30</maxHistory>-->
<!-- 로그를 최대 30일치만 남기고 31일째 로그부터는 알아서 지워줘!-->
<fileNamePattern>${LOG_DIR}/${LOG_FILE_NAME}-%d{yyyy-MM-dd_HH-mm}.log.gz</fileNamePattern>
<!-- 이름이 HH-mm 기준으로 생성된다 즉, 분마다 로그파일을 새로 만든다! --->
<maxHistory>3</maxHistory>
<!-- 로그를 최대 3분치만 남기고 4분째 로그부터는 알아서 지워줘! --->
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>

방대한 로그 데이터를 실시간으로 수집, 검색, 분석 및 시각화하는 오픈소스 플랫폼
Logstash는 세 가지 단계(input, filter, output)를 통해 움직인다.
.log 파일을 한 줄씩 실시간으로 읽어온다.logstash.conf 파일 예시를 살펴보자.
# 1. Input
input {
file {
# 도커 컨테이너 안에서 바라보는 로그 파일의 위치
path => "/usr/share/logstash/logs/waiting-api-log.log"
# 처음 켤 때 파일의 맨 처음부터 끝까지 싹 다 읽어오라는 뜻
start_position => "beginning"
# 파일이 끝났는지 확인하는 주기 (기본 1초에 한 번씩 감시)
stat_interval => 1
}
}
# 2. Filter
filter {
# logstash grok 검색후 적용해보기
}
# 3. Output
output {
# 목적지인 엘라스틱서치로 보냄
elasticsearch {
hosts => ["http://elasticsearch:9200"]
# 엘라스틱서치에 저장될 인덱스 이름! 날짜별로 분류함
index => "waiting-api-logs-%{+YYYY.MM.dd}"
}
# 잘 전송되고 있는지 도커 콘솔 화면에서도 확인하기 위해 추가
stdout { codec => rubydebug }
}
Elasticsearch에 쌓인 데이터를 시각화하여 그래프와 대시보드로 보여주는 분석 도구
Elasticsearch에 쿼리를 실행하고, 결과를 다양한 형태로 보여준다.
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.1.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
logstash:
image: docker.elastic.co/logstash/logstash:9.1.0
container_name: logstash
volumes:
- ./logs:/usr/share/logstash/logs
- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
ports:
- "5044:5044"
depends_on:
- elasticsearch
kibana:
image: docker.elastic.co/kibana/kibana:9.1.0 # 무조건 ES, Logstash와 버전을 맞춰야 한다! (9.1.0)
container_name: kibana
ports:
- "5601:5601" # 키바나 포트 번호
environment:
# 키바나가 데이터를 가져올 주소
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch