네트워크 기반 애플리케이션은 실행 흐름이 복잡하고, 쓰레드가 여러 개 돌며, 예외가 다양한 위치에서 발생한다.
따라서 표준화된 로깅 구조를 반드시 도입해야 한다.
이 글에서는 Slf4J와 Logback에 대한 간단한 소개와 함께 현재 Kotlin 프로젝트에 어떻게 적용하는지 단계별로 설명한다.
1. Slf4J와 Logback

1-1. Slf4J 란?
SLF4J (Simple Logging Facade for Java)는 말 그대로 "로깅을 위한 인터페이스(Facade)"다.
특징
- 로깅 API 표준 제공 (Logger, LoggerFactory)
- 실제 로깅 엔진 구현은 포함되지 않음
- 다양한 라이브러리가 Slf4J를 사용하며, 충돌 없이 통합 가능
- Logback / Log4j2 / java.util.logging 등으로 마음껏 교체 가능
Slf4J = 추상화 계층 (표준 인터페이스)로, 실제 기록은 Logback 같은 binding 구현체가 담당한다.
JAVA 개발사인 썬마이크로시스템즈는 예나 지금이나 규모가 작은 기업이다.
JAVA 사업 초창기에는 그만큼 설계 오류가 많았는데, 대표적인 것이 logging 서비스의 부재였다.
실제 로깅 기능은 JAVA 4 이후부터 도입되었다.
1-2. Logback 이란?
Logback은 Slf4J의 공식 구현체(바인딩, binding)이며 가장 널리 사용된다.
특징
- Slf4J 권장 공식 구현체
- Log4J2 보다 안정적이고 lean한 구조
- XML 기반 설정 (logback.xml)
- 파일 rolling (용량/일자 기준), 콘솔 로깅, 레벨 제어, 패턴 지정
- 성능 좋음
Logback = 실제로 파일/콘솔에 로그를 찍는 로깅 엔진.
1-3. kotlin-logging-jvm
Kotlin 친화적인 logging wrapper 라이브러리다.
특징
- 내부적으로는 Slf4J 기반으로 동작
- Kotlin 문법으로 로그 선언, 출력이 가능하다.
- private val logger = KotlinLogging.logger {} 선언
- 로그 메시지를 람다 블록 `{}`으로 감싸면, 해당 레벨이 활성화되어 있을 때만 문자열 연산을 수행하여 불필요한 문자열 연산을 방지한다.
※Kotlin 은 Slf4J 를 바로 쓰는 경우는 드물고 “io.github.oshai:kotlin-logging-jvm” 를 주로 사용함
2. 로깅 구현 목적
현재 진행 중인 채팅 서버-클라이언트 프로젝트는 다음의 문제점들이 존재한다.
- 디버깅용 println() 출력 → 쓰레드 충돌/가독성 문제
- 서버가 여러 쓰레드(ClientHandler)를 실행 중
- 예외 발생 위치 추적이 어려움
- 상태 변경(등록, 변경, 접속, 종료) 로그가 후일 분석 불가
- 통신 문제 조사 시 패킷 수준 로깅이 필요할 수 있음
- 운영 환경에서 stdout으로만 로그를 기록하는 건 매우 불리함
keywords : 멀티쓰레드 (여러 클라이언트 처리), I/O 기반 네트워크 통신, 동시 접속/이탈, 메시지 송수신, 예외 발생
로깅 시스템은 이를 해결한다.
- println 제거 → 일관된 정보 구조 유지
- 서버 상태 추적
- 클라이언트 접속/종료 시점 기록
- 에러 발생 시 stacktrace 파일 저장
- 패킷 정보 기록 (선택)
- 개발/운영 환경별 로그 레벨 조정
- 로그 파일 분리/백업/롤링
3. Kotlin 적용 절차
3-1. Gradle 의존성 추가
dependencies {
implementation("io.github.oshai:kotlin-logging-jvm:7.0.0")
implementation("org.slf4j:slf4j-api:2.0.16")
implementation("ch.qos.logback:logback-classic:1.5.11")
}
- kotlin-logging은 Slf4J API에 의존함
- 런타임에 Logback 클래식 구현체를 함께 넣어야 로그가 기록된다.
프로젝트 환경 : openjdk 17.0.17 2025-10-21 LTS
3-2. logback.xml 설정 파일 추가
# src/main/resources/logback.xml
<configuration debug="false" scan="false">
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/server.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/server.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>10</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
- NopStatusListner : 프로그램 실행 시, configuration 설정 관련 시스템 로깅 출력 방지
- ConsoleAppender : STDOUT 형태로 CLI로 결과를 확인할 수 있는 Appender
- RollingFileAppender : FILE 형태로 로그 파일을 저장하는 Appender
3-3. Kotlin 적용 예시
- 클래스 최상단에 선언:
import io.github.oshai.kotlinlogging.KotlinLogging
private val logger = KotlinLogging.logger { }
- 이후 로그 출력 :
logger.info { "User $userName connected." }
logger.debug { "Received packet: $packet" }
logger.error(exception) { "Unexpected error in ClientHandler" }
- 메시지를 람다 `{}`로 감쌌기 대문에 로그 레벨이 맞지 않을 때는 문자열 연산이 전혀 수행되지 않아 성능 측면에서 안전하다.
- 예외 발생 시, logger.error(e) { "..." } 형태로 stack trace 포함 로깅이 가능하다.
4. 프로젝트 적용
4-1. 서버 (ClientHandler 내부)
class ClientHandler(...) : Thread() {
companion object {
private val log = KotlinLogging.logger {}
}
override fun run() = try {
log.info { "Client [$clientId] connected (awaiting name)" }
sendPacket(...)
listenForMessages()
} catch (e: Exception) {
log.error(e) { "Client $clientId unexpected exception" }
} finally {
if (conn.isConnected()) {
handleClientDisconnect()
}
}
private fun handleClientDisconnect() {
val name = clientData.name ?: clientId
log.info { "Client [$name] disconnected (sent=${clientData.sentCount.get()}, received=${clientData.receivedCount.get()})" }
conn.close()
}
}
4-2. 클라이언트 (ClientSession 내부)
class ClientSession(...) {
companion object {
private val log = KotlinLogging.logger {}
}
fun receivePacket() {
try {
while (conn.isConnected() && !shutdownFlag.isIntentional) {
val packet = conn.readPacket()
log.debug { "Received packet: type=${packet.type}, length=${packet.length}" }
handlePacket(packet)
}
} catch (e: IOException) {
log.warn(e) { "Server disconnected unexpectedly" }
} catch (e: Exception) {
log.error(e) { "Unexpected error in receive thread" }
}
}
}
5. Logging 레벨 설계
Slf4j에는 로그 이벤트와 연동된 보안 레벨이 이미 설정되어 있다.
5-1. TRACE 레벨
- 미세 단위의 로그 이벤트 추적
- 앱과 사용 중인 제 3자 라이브러리에서 발생하는 것에 대한 가시성이 존재한다면 필요 없음
- 매우 장황함(verbose)
5-2. DEBUG 레벨 -- 개발/디버깅 환경 전용
- TRACE 레벨보다 깊은 진단과 트러블슈팅 필요 시 사용
- 패킷 header / body dump
- 클라이언트 상태 변화 상세
- read/write count 증가
- 수신 쓰레드 polling 정보
5-3. INFO 레벨 -- 일반 운영 로그
- 뭐가 발생했는지, 앱은 요청을 처리했는지 등을 가리키는 로그 레벨
- 정기적으로 확인하지 않았다고 문제가 발생하지 않는 수준에 사용됨
- 접속 (connect / disconnect)
- 로그인 / 이름 등록 성공
- whisper / 채팅 전송 성공
- 파일 송수신 시작/완료
5-4. WARN 레벨 -- 비정상 시도/예상된 오류
- 예기치 않은 상황이 발생했을 때를 가리키는 로그 레벨
- 이름 중복
- 메시지 형식 오류
- 존재하지 않는 사용자에게 whisper 시도
- 패킷 파싱 실패 (recoverable)
5-5. ERROR 레벨 -- 운영 오류 (예외 던지는 케이스)
- 하나 이상의 앱 기능이 정상 동작하지 못하는 경우를 가리키는 로그 레벨
- IOException (← 최근에는 해커들의 DDoS, 악성코드 공격이 횡행하다 보니 error 처리하지 않는 경우가 많아짐)
- 소켓 강제 종료
- JSON 직렬화/역직렬화 오류
- 예상치 못한 RuntimeException
※ 가장 심각한 경우를 지칭하는 FATAL 레벨도 존재하지만, 로깅을 못할 정도로 예측 불가능한 상황을 지칭하는데 사용되는 것으로 Web 환경이 일반적인 현대에서는 잘 사용되지 않는다.
6. 결론
Kotlin 환경에 Slf4J와 Logback을 적용하여 다음의 효과를 얻었다.
- println 기반 출력 제거 → 구조적/일관된 로깅
- 쓰레드 기반 서버에서 디버깅 난이도 대폭 감소
- 에러 발생 지점 및 원인 파악 용이
- 운영/개발 환경별 레벨 조정 가능
- 파일 로그 롤링 지원 → 로그 보존 체계 가능
- 나중에 ELK, Grafana, Loki, CloudWatch, Splunk 전송에도 호환
결과적으로, 멀티 쓰레드 기반 Chat Server에서 "누가 언제 접속/종료/메시지 전송/예외가 발생했는지" 추적 가능해졌다.
참고 출처
- Kotlin Tips and Tricks You May Not Know: #1 — Kotlin Logging
- SLF4J Tutorial: Loggers, Levels & How to Configure for Java Applications with Examples
- Logger는 사드세요... 제발: SLF4J와 Logback -- github.io
- [Java] Log..그리고 slf4j와 logback
- Java 로그(Log)를 위한 SLF4J & Logback - 기본
- [Spring Boot] Kotlin으로 REST API 만들기(3) - Logback 설정 [너나들이 개발 이야기:티스토리]
Powered By. ChatGPT
'Backend' 카테고리의 다른 글
| [Backend] Kotlin tips (0) | 2025.11.24 |
|---|---|
| [Backend] Jackson 기반 직렬화·역직렬화의 원리와 적용 절차 (0) | 2025.11.17 |
| Mockito-inline 5.2.0 설정 시 문제 해결 가이드 (Kotlin + Gradle + JDK17) (0) | 2025.10.30 |
| [Backend] JVM의 의존성 관리와 빌드 도구 분석 (0) | 2025.10.17 |
| [Backend] ByteArray class 이해하기 (1) | 2025.10.16 |