내가 만든 웹 앱은 사용자가 무언갈 검색해야 정보를 얻을 수 있다.
그러면 사용자들이 어떤 검색을 할까? 기록을 남겨볼 수 있다.
이때, 중요한 것은 어떤 사용자를 특정할 수 있다면 이런 정보를 수집하는 행위 자체가 불법이 될 수 있기에 조심해야 한다.
이런 이유가 아니더라도 에러나 경고 등을 콘솔에만 남기는 것이 아니라 외부 파일에다가 쓰기 위해서는 로깅이 필수적이다.
로깅을 어떻게 할까?
사실 로깅이라는 것은 간단하게 생각할 수 있다.
음,, 파일에 접근해서 기록할 수 있는 API를 쓰면 되겠다.
그리고 로깅이 필요한 시점에 해당 API를 호출하자.
하지만, 나도 이때까지 이렇게 생각해왔지만 이번에는 조금 다르게 생각해보기로 했다.
일단 파일을 쓴다는 것은 비교적 무거운 작업이다. 왜냐하면 메모리가 아니라 HDD나 SDD에 접근해야 하기 때문이다.
그러면 트래픽이 많은 환경에서는 파일 쓰기가 끊임없이 이뤄질 수 있는데, 이건 조금 위험할 수 있다.
동시에 하나의 파일에 접근하는 경우가 발생할 수 있고, 그에 따라서 누락되는 정보가 생길 수도 있다고 생각했다.
벡터 그래픽을 처리하는 에디터 같은 것에서는 렌더링 큐를 만들고, 변화가 생기면 그 큐에 쌓은 후 큐에 있는 것들을 주기적으로 한 번에 처리해 렌더링한다고 한다.
나는 이 방식이 맘에 들어서 로깅에도 그런 큐를 쓰기로 했다.
1. 로그할 내용 발생
2. 큐에 저장 (멀티 쓰레드 세이프한 큐)
3. n초마다 큐에 있는 내용으로 파일에 쓰기
자바에서 멀티 쓰레드에 세이프한 큐를 제공한다는 것을 알고 있기에 그건 쉽게 가져다 쓸 수 있었다.
또, n초마다 어떤 행위를 실행하는 것 역시 Spring에서 스케쥴러를 제공하고 있기에 활용하면 되었다.
구현
먼저, 로깅 책임을 가지고 있는 클래스이고, 이 클래스는 명확하게 두 가지 업무를 맡고 있는데 큐를 관리하는 것과 큐에 있는 내용물을 파일에 쓰는 것이다.
@Component
public class Logging {
private final Queue<ClassInfo> task;
private final Path logFilePath = Paths.get("/usr/local/tomcat/webapps/logs/log.txt");
public Logging() {
this.task = new ConcurrentLinkedQueue<>();
}
public void enqueue(ClassInfo info) {
task.add(info);
}
// 주기적으로 로그 처리 (예: 5초마다)
@Scheduled(fixedDelay = 10000)
public void processQueue() throws Exception {
while (!task.isEmpty()) {
try (BufferedWriter writer = Files.newBufferedWriter(
logFilePath,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND)) {
ClassInfo info;
while ((info = task.poll()) != null) {
writer.write(info.toString());
writer.newLine();
}
} catch (IOException e) {
throw new Exception("Logging Failed!!");
}
}
}
이건 내가 최초에 짰던 로깅용 코드고, 내가 원하는 곳에서 Logging 클래스의 enqueue로 원하는 정보를 넘기기만 하면 된다.
하지만 이 방식에는 문제점이 있는데, 일단 파일 명이 하드 코딩되어 있기 때문에 파일을 기준에 따라 나눠쓰기 위해서는 추가적인 처리가 필요했다.
또, 로그 파일의 경우 필요없는 것들은 제거하기도 해야 하는데 그런 것을 관리하는 것이 힘들 것 같았다.
그래서 나는 로깅용 라이브러리를 이용하기로 했다. (아래부터가 진짜)
slf4j를 사용했고, 이 녀석이 말썽을 일으키는 경우가 있다고 알고 있어서 버전을 맞추기 위해 노력을 했던 것 같다.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.12</version>
</dependency>
이 녀석을 사용하기 위해서는 설정이 필요하다.
src/resources/ 안에 logback.xml을 추가했다.
<configuration>
<appender name="DAILY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/usr/local/tomcat/webapps/logs/subject-query-history.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/usr/local/tomcat/webapps/logs/%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="DAILY_FILE"/>
</root>
</configuration>
<file>에는 경로를
<rollingPolicy> 안에는 파일이 교체되는 정책에 대해 명시한다.
어떻게 교체되는지 파일명의 패턴을 명시하고, 몇 일 동안 보관할지를 설정한다.
<encoder>에서는 받아온 메시지 내용 그리고 %n으로 줄바꿈을 하는 식으로 저장되도록 했다.
마지막으로 로깅의 레벨은 INFO로 했으며, 이러면 INFO 이상의 로그들이 기록된다.
루트 로거가 DAILY_FILE 로거를 사용하도록 연결했다.
마지막으로 Logging 클래스를 수정했다.
package cuk.api.Logging;
import cuk.api.Trinity.Entities.ClassInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
@Component
public class Logging {
private final Queue<ClassInfo> task;
private static final Logger logger = LoggerFactory.getLogger(Logging.class);
public Logging() {
this.task = new ConcurrentLinkedQueue<>();
}
public void enqueue(ClassInfo info) {
task.add(info);
}
// 주기적으로 로그 처리.
@Scheduled(fixedDelay = 10000)
public synchronized void processQueue() throws Exception {
ClassInfo info;
while (!task.isEmpty()) {
info = task.poll();
logger.info(info.toString());
}
}
}
훨신 간단하고 깔끔해졌다.
로깅에 대해서 고민하고 있다면 이런 방식도 있다는 것을 알려주고 싶었다.
문제점
일단 몇 가지 문제점이 있는데, 짚고 넘어가보겠다.
1. 큐의 사이즈가 제한이 없다.
큐에 저장된다는 것은 메모리에 담긴다는 것이기 때문에 과도한 트래픽으로 많은 로깅이 발생하면 큐가 계속 찰 것이고 결국 제한이 걸릴 것이다.
그러면 오버플로우가 나서 시스템이 망가질 수 있다.
나는 작은 사이트라서 고민하지 않았고, 좀 더 큰 트래픽이 있다면 큐의 limit을 두고 우선순위에 따라 처리하는 것을 염두해 둘 필요가 있을 것이다.
2. 스케쥴링 시간을 어떻게 정할 것인가?
1번과도 연계되는 것인데, 큐가 차는 데 얼마나 걸리는 지에 따라 스케쥴링 시간을 적절하게 정해야 한다.
하지만 그것을 완벽하게 예측하는 것은 어렵기 때문에 쉽지는 않을 것 같다.
결국 해결책이라고 한다면, 큐의 리밋을 두고 스케줄링을 빠르게 빠르게 처리하는 식으로 하거나,
카프카와 같이 메시지를 저장해두고 그걸 순서대로 읽는 식으로 조금 안전하게 처리할 수도 있다. 이 경우 카프카와 연동된 로깅용 서비스를 따로 두고 운영해도 괜찮을 것 같다.
리소스가 충분하다면 큐를 하나 더 써도 괜찮을 수 있다.
파일 쓰기 작업이 진행중인 큐와 로그가 쌓이는 큐를 따로 두고, 스위칭을 하면서 로깅을 하는 것이다.
이처럼 아이디어는 많지만, 나의 서비스의 크기에 맞게 적절한 기술을 사용하는 것이 중요하기 때문에 (배보다 배꼽이 클 수 없다,,)
무엇이 정답이라고 얘기하기는 어렵다. (이것이 소프트웨어 공학 개론,,,)
긴 글을 읽어주셔서 감사합니다.
'개발' 카테고리의 다른 글
| Nginx에서 하나의 Conf로 BE와 FE에 대한 경로 기반 라우팅 (리버스 프록시) 설정 (0) | 2026.01.12 |
|---|---|
| 2026년 30,000명이 다녀간 Trinity Parser를 리뉴얼하며,,, (5) | 2026.01.09 |
| [Gerrit, Jenkins] Gerrit과 Jenkins 연동 (1) | 2025.10.19 |
| 리액트 (FE)를 Spring (BE)에 통합해 배포하기 (2) | 2025.10.09 |
| Github Action으로 웹 앱 자동 배포하기 (with SSH 명령어) (0) | 2025.10.09 |