서비스가 급하게 만들어진 것도 있었고.. 오픈 초기에는 cs문의나 시스템 에러도 별로 안 터지는 상황이였어서 로그 시스템 구축을 살짝 미루고 있었다.. 하지만 (비즈니스에서 제일 중요한) 결제 관련 cs가 올라오니 허겁지겁 로그를 뒤지게 되느 상황이 발생했고... 로그 파일을 하나씩 열어보며 grep 명령어를 치는 재앙을 맞이한 후에야 

로그를 손봐야겠다...

라는 생각이 제대로 들어서 

 

사실 이전 회사에서 es를 구축해본 경험이 있었고 클라우드 환경이라면 가장 간편하게 aws cloudwatch를 사용하면 됐지만

온프레미스 환경이였고.. 인프라팀에서 그라파나를 구축해놨긴 했지만 로그 검색하는 방법이 통 맘에 들지 않았음 (관리자 권한이 없어서 어떻게 커스텀해보지 못했던게 컸다)

 

먼거 가장 먼저 한 일은 로그를 어떻게 다룰것인가.. 였는데

팀이 원하는 로그 시스템의 기준은 명확했다

 

 

  • 로그는 중앙에서 한 번에 볼 수 있어야 한다
  • 에러 로그는 즉시 감지 가능해야 한다
  • 로그는 실시간으로 흘러야 한다
  • 애플리케이션 로직과 강하게 결합되지 않아야 한다

이 기준을 놓고 봤을 때 로그를 파일 단위로 수집하는 방식보다는 이벤트 스트림 기반 구조가 더 적합하다고 판단했다.

그래서 api 통계 수집용으로 깔아놓은 카프카를 연동해서

로그서비스를 구축해보자! < 까지 오게됐다는

 

Kafka 토픽 설계

Kafka를 도입하면서 가장 먼저 고민한 건 토픽 설계였다.

 

  • ebook_store_trace
    • 일반 요청 흐름 추적용 로그
    • 파라미터, 처리 시간, 비즈니스 흐름
  • ebook_store_critical
    • 에러 및 장애 대응용 로그
    • 즉각적인 확인이 필요한 이벤트

 

이렇게 분리함으로써

  • Critical 로그만 별도 Consumer로 즉시 감지 가능
  • 장애 대응과 일반 분석 로그를 명확히 분리
  • 로그 소비 전략을 로그 성격에 맞게 가져갈 수 있음

 

 

애플리케이션 코드와 로그 수집을 분리

Kafka 연동에서 가장 중요하게 본 포인트는 애플리케이션 코드 침투를 최소화하는 것이었다.

비즈니스 로직에서 직접 Kafka Producer를 호출하는 방식은 곧바로 제외했다.

  • 로그와 로직이 강하게 결합됨
  • 유지보수 난이도 증가
  • 로깅 정책 변경 시 코드 수정 필요

대신 선택한 방식이 Custom Logback Appender였다.

public class KafkaLogAppender extends AppenderBase<ILoggingEvent> {

    private KafkaProducer<String, String> producer;

    @Override
    protected void append(ILoggingEvent event) {
        String message = convertToJson(event);
        producer.send(new ProducerRecord<>(topic, message));
    }
}

 

이 방식을 채택함으로서 Slf4j의 log.info,. log.error() 코드는 그대로 사용할 수 있었고,

로그 전송 정책은 이 Appender에서만 관리 할 수 있다는 분리성을 가질 수 있었다

 

 

로그가 모이자, 컨텍스트가 보이지 않았다

로그를 Kafka로 모으기 시작하자 곧바로 다음 문제가 드러났다.

이 로그가 어떤 요청에서 나온 로그인지 알 수 없다.

 

서버가 여러 대이고, 동시에 수많은 요청이 처리되는 환경에서
단순한 로그 메시지만으로는 흐름을 추적하기 어려웠다.

그래서 MDC(Mapped Diagnostic Context)를 적극 활용했다.

 

MDC 기반 요청 단위 로그 추적

요청 진입 시점에 컨텍스트 정보를 MDC에 설정했다.

MDC.put("traceId", UUID.randomUUID().toString()); MDC.put("userId", userId);

그리고 로그 포맷에 MDC 값을 포함시켰다.

[%X{traceId}] [%X{userId}] %msg

이 구조를 통해

  • 하나의 요청에서 발생한 로그를 traceId로 묶을 수 있었고
  • 서버가 달라도 동일한 요청 흐름을 추적할 수 있었고
  • Kafka 이후 단계에서도 일관된 분석이 가능해졌다

 

AOP 기반 자동 로깅 도입

로그 시스템을 구축하면서 request log 나 시스템 에러로그같은 공통적인 요청들은 자동으로 로깅되므로   Controller 전반에 AOP 기반 자동 로깅을 적용했다.

@Around("execution(* com.xxx.controller..*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();

    try {
        Object result = joinPoint.proceed();
        log.info("Request success");
        return result;
    } catch (Exception e) {
        log.error("Critical error", e);
        throw e;
    } finally {
        log.info("Elapsed time: {}ms", System.currentTimeMillis() - start);
    }
}
 
 

이 구조의 효과는 명확했다.

  • 모든 요청이 자동으로 로깅됨
  • 예외 발생 시 Critical 로그가 즉시 Kafka로 전송
  • 장애 발생을 “로그 발생 순간”에 인지 가능

 

로그 포맷 표준화: 마지막으로 남은 문제

Kafka에 로그가 쌓이기 시작하자 서버마다, 서비스마다 로그 포맷이 다르다는 문제가 드러났다.

이 상태에서는 검색이 어렵고 분석은 거의 불가능하다는 결론으로 이어져 로그 포맷을 구조적으로 표준화하기로 했음

{
  "timestamp": "...",
  "level": "ERROR",
  "service": "ebook-store",
  "traceId": "...",
  "message": "...",
  "stackTrace": "..."
}
 
  • JSON 기반 포맷 통일
  • MDC 필드 강제 포함
  • 이후 Consumer, 분석 파이프라인에서 일관성 확보

 

정리하며

이 로그 시스템 구축은 단순히 Kafka를 도입한 작업이 아니었다.

  • 로그를 파일이 아닌 이벤트로 바라보는 관점
  • 장애 대응을 사후 분석에서 실시간 감지로 전환
  • 운영과 개발 모두의 피로도를 줄인 구조 개선

무엇보다 가장 큰 변화는 이거였다.

장애가 발생하면 서버 접속부터 하던 흐름이 로그 스트림과 대시보드부터 보는 흐름으로 바뀌었다.

 

로그는 남기는 것보다 어떻게 흐르게 할 것인가가 훨씬 중요하다는 걸 이 작업을 통해 확실히 체감했다.

 

+ Recent posts