☘️ Spring Boot

[Spring] LMAX Disruptor

별빛난무 2025. 7. 1. 17:19
728x90

💁‍♂️ 들어가기 전

이번에 필자는 Spring AOP 어노테이션을 기반으로 로깅 라이브러리를 구현했었다. 그 때 사용했던 커스텀 로거가 LMAX의 Disruptor로 구현되어 있어서 Disruptor에 대해서 알아 가볼려고 한다. (잠시 라이브러리 홍보 좀.. 🤣)

https://github.com/kyungmin08g/zephyro

 

GitHub - kyungmin08g/zephyro: 🍀 라이브러리 → Spring AOP 어노테이션 기반 로깅 라이브러리

🍀 라이브러리 → Spring AOP 어노테이션 기반 로깅 라이브러리. Contribute to kyungmin08g/zephyro development by creating an account on GitHub.

github.com


🍀 LMAX Disruptor란?

LMAX에서 만든 고성능 저지연 메시징 라이브러리금융 거래 시스템, 주식과 같은 고성능 시스템에서 저지연 처리를 목표로 개발됐다.

기존 자바의 큐(queue) 기반 메시징 시스템보다 더 빠른 성능을 보여준다. 미리 말하자면 Disruptor는 이벤트 기반으로 동작한다.

그래서 내가 만든 라이브러리에서 System.out.println();문과 성능 비교한 결과 커스텀 로거(Disruptor 사용한 로거)가 압도적으로 빨랐다^^

 

✔️ Disruptor 특징

1. Ring Buffer 기반 데이터 구조

  • Disruptor는 배열 기반의 고정 크기 링 버퍼(Ring Buffer)를 활용하여 데이터가 메모리에 미리 할당되도록 한다.
  • 새로운 데이터가 들어오면 오래된 데이터가 덮어쓰기 되는 구조이며, 캐시 친화적인 설계가 가능하다. 그래서 가비지 컬렉션(GC)의 영향을 받지 않는다.

2. 메모리 할당 최소화 및 GC 부담 감소

  • Disruptor는 객체 할당을 미리 하고, 데이터는 재사용되므로 GC 오버헤드가 거의 없다.
  • 따라서 GC로 인해 발생하는 성능 저하를 줄일 수 있다.

3. 성능 (Throughput & Latency)

  • 기존의 BlockingQueue 기반 구조보다 최대 10~100배 빠른 처리 성능을 제공한다.
  • 일반적으로 마이크로초(µs) 단위의 지연시간을 유지하며, 수백만 TPS를 처리할 수 있다.

🚀 Disruptor 사용

아래는 로깅 라이브러리 구현할때 사용하던 일부 코드이다. (자세한 코드는 위 Github에서 보실 수 있습니다..!)

 

1. 이벤트

먼저 Disruptor는 이벤트 기반으로 동작하기 때문에 발생할 이벤트 객체를 만들어야 한다.

@Setter
@NoArgsConstructor
public class ZephyroLogEvent {

  private Object message;
  private Class<?> clazz;
  private Color color;
  private LogLevel level;
  private Color defaultColor;

  public String getFormatMessage() {
    return this.getMessageFormat(message, clazz, color, level);
  }
 }

 

 

2. 필드 선언

그 다음 EventFactory, Disruptor, RingBuffer를 만들어야 한다.

private static final Integer BUFFER_SIZE = 1024;

private final EventFactory<ZephyroLogEvent> factory = ZephyroLogEvent::new;
private final Disruptor<ZephyroLogEvent> handler = new Disruptor<>(factory, BUFFER_SIZE, Thread::new);
private final RingBuffer<ZephyroLogEvent> buffer = handler.getRingBuffer();

EventFactory는 제네릭으로 이벤트 객체를 받는다. 사실 모든 객체를 받기는 한데 이벤트 관련 객체를 넣어줘야 잘 작동한다.

* 이해하기 쉽게 설명하면 EventFactory는 이벤트 객체를 생상하는 생산자라고 생각해도 무방할 것 같다. (제네릭 타입으로 넣어준 이젠트 객체를 생산하는 EventFactory)

 

Disruptor는 new를 선언하여 Disruptor 객체를 만들어 준다. 첫번째 인자에는 해당 이벤드 객체가 담겨진 Factory를 넘기고 두번째 인자에는 버퍼 사이즈를 넣어준다. 마지막 인자에는 Disruptor가 동작할 Thread를 넣어주면 된다.

* Disruptor는 제어자 역할이라고 생각하면 된다. (중심 | 뼈대)

 

RingBuffer는 해당 Disruptor의 RingBuffer를 꺼내서 넣어준다. 그럼 필드 선언은 끝났다.

* RingBuffer는 사용자 입력과 관련한 작업을 수행하는 친구이다. (삽입)

 

3. 실제 이벤트 전달

이제 이벤트를 핸들러에게 전달하고 실행시키는 작업이 남았다.

buffer.next();를 통해 다음 buffer 위치를 가져온다. 그리고 buffer.get(위치)를 호출해 해당 버퍼 위치에 이벤트 객체를 넣어준다.

마지막으로 buffer.publish(버퍼 위치);를 호출하여 실제로 버퍼에 이벤트를 등록해준다.

 // 다음 로그 이벤트를 기록할 버퍼 위치(sequence) 가져오기
long sequence = this.buffer.next();
try {
  // 해당 sequence 위치의 로그 이벤트 객체 가져오기
  ZephyroLogEvent event = this.buffer.get(sequence);
  // 로그 이벤트에 필요한 값 설정
  event.setMessage(message);
  event.setLevel(level);
  event.setColor(color);
  event.setClazz(clazz);
  event.setDefaultColor(
    (defaultColor == Color.RESET) ? this.defaultColor : defaultColor
  );
} finally {
  // 설정된 로그 이벤트를 버퍼에 푸시
  this.buffer.publish(sequence);
}

 

 

4. 이벤트 핸들러

이벤트를 들을 리스너 작업을 아래에 친구가 해준다. 

/**
* 이벤트 핸들러 등록 메서드
*/
private void eventHandleRegister() {
    this.handler.handleEventsWith((event, sequence, endOfBatch) -> {
      // true라면 포맷된 로그 사용, false면 기본 로그 형식 사용
      if (this.isFormat) {
        System.out.write((event.getFormatMessage() + "\n").getBytes());
      } else {
        System.out.write((event.getDefaultMessage() + "\n").getBytes());
      }
    });
    this.handler.start(); // 이벤트 핸들러 실행
}

event 파라미터가 버퍼에서 온 이벤트 객체이다. 그래서 event에 점(.)을 찍어보면 이벤트 객체에 있는 필드나 메서드가 나오는걸 볼 수 있다. 그리고 진짜 마지막으로 핸들러를 시작해주면 끝이다.


 

나도 처음에는 GPT한테 "로깅 라이브러리를 구현하려고 하는데 sout문 보다 빠르게 만들순 없을까?"라고 질문하니 Disruptor가 나왔다.

열심히 구글링해서 공식 문서 보면서 개발했다. 그러면서 Disruptor 매력에 빠져보린게 아닐까 싶다ㅎㅎ

이해 안되는 부분 있으시면 편하게 댓글로 말씀해주세용~ 😊 감사합니다..!!

728x90