[ Spring Boot ] 스트리밍 서비스에서 Buffer Pool로 ByteBuffer 관리

2025. 7. 29. 20:47·Spring

현재 프로젝트의 문제점

현재 실시간 스트리밍 프로젝트를 하던 중 메모리 관리가 잘 되지 않는다는 문제가 있었다.

 

메모리가 일정 이상으로 증가하면 GC가 메모리를 정리해주지만 보다 효율적인 코드를 짜보려고 했다.

현재 코드 분석

우선 현재 코드는 아래와 같이 동작한다.

  1. 프레임 생성자 스레드에서 ByteArrayOutputStream에 바이트 단위로 값을 넣어서 jpeg 프레임 이미지 생성
  2. 프레임을 byte[]에 복사 후 프레임 소비자 스레드로 전달
  3. 프레임 소비자 스레드는 데이터를 받아서 클라이언트에 전달

코드 문제점 분석

하지만 이 코드에는 몇 가지 문제가 있다.

  • ByteArrayOutputStream은 byte를 추가할 때 공간이 부족하면 새로운 배열을 만들고 값을 복사함
  • ByteArrayOutputStream을 byte[]로 바꿀 때 복사가 발생함

이러한 잦은 복사는 실시간성이 중요한 실시간 스트리밍에서 성능 저하로 이어질 수 있기에 자제해야 한다.

따라서 zero-copy의 효과를 내기 위해 기존의 코드를 ByteBuffer 기반의 코드로 재구성했다.

 


[ 대안 1 ] 단일 ByteBuffer 기반의 실시간 스트리밍 서비스

ByteBuffer 기반의 스트리밍 동작 원리

우선 해당 방법은 단일 ByteBuffer를 프레임 생성자와 소비자 스레드가 공유하며 프레임을 관리하는 방식이다.

ByteBuffer는 초기화 비용이 비싸고 메모리를 많이 차지하기 때문에 해당 방법을 구상했다.

 

해당 방식은 아래와 같이 동작한다

  1. 스트리밍 시작 시 ByteBuffer 초기화
  2. 생성자 스레드는 jpeg 프레임의 각 바이트를 ByteBuffer에 put하여 jpeg 프레임 생성
  3. 생성자 스레드는 생성된 프레임을 소비자 스레드에게 전달
  4. 소비자 스레드는 프레임을 클라이언트에게 전달
  5. 소비자 스레드가 ByteBuffer 내부의 값을 clear

ByteBuffer 사용 시 이점

우선 ByteBuffer를 사용했을 때 얻을 수 있는 이점은 zero-copy 효과이다.

보다 자세히 말하자면, ByteBuffer는 내부적으로 byte[]를 관리하고, 해당 배열을 외부에서 참조할 수 있는 수단을 제공한다.

따라서 기존 방식과 달리 복사 비용이 거의 들지 않기 때문에 스트리밍 시 실시간성에 도움이 된다.

 

다음으로 기존 방식과 달리 NIO(논블로킹) 방식이기에 보다 빠른 I/O 속도를 제공한다.

이는 속도가 중요한 실시간 스트리밍 서비스에서 보다 효과적으로 프레임들을 사용자에게 전달 가능하다.

현재 방식의 문제점

하지만 현재 방식에는 매우 심각한 문제점이 있다.

하나의 ByteBuffer를 사용하기에 Race Condition이 발생할 수 있다는 것이다.

 

우선 아래 수도코드를 확인해보자.

ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

Queue<byteBuffer> frameQueue;

// frame consumer thread
while (doStreaming) {
    Frame frame = frameQueue.poll();
    sendFrame(frame);
    frame.clear();
}

// frame producer thread
while (doStreaming) {
    readFrame(byteBuffer);
    frameQueue.add(byteBuffer);
}

 

이 코드는 얼핏 봤을 때 잘 동작하는 것으로 보인다.

하지만 아래와 같은 상황에서 race condition이 발생할 수 있기에 주의해야 한다.

  1. 프레임 생성 스레드가 프레임(ByteBuffer)을 read 후 큐에 push
  2. 프레임 소비 스레드가 프레임(ByteBuffer)을 큐에서 poll 및 사용자에게 전달
  3. 프레임 생성자 스레드가 프레임(ByteBuffer)을 read
  4. 프레임 소비 스레드가 프레임(ByteBuffer)을 초기화

여기서 4번에서는 race condition이 발생한다.

아직 큐에 프레임을 넣기 전에 frame을 clear했기 때문이다.

 

따라서 단일 ByteBuffer를 관리하는 것이 아닌 여러개의 ByteBuffer를 효과적으로 재사용 및 관리할 수 있는 방법이 필요하다.

 

 


[ 대안 2 ] Buffer Pool과 ByteBuffer 기반의 실시간 스트리밍 서비스

Buffer Pool 코드 작성하기

우선 여러개의 ByteBuffer를 효과적으로 관리하기 위해 Buffer Pool을 사용할 예정이다.

 

Buffer Pool은 여러개의 버퍼들을 관리하기 위한 기술이다.

대표적으로 Java 기반의 Kafka와 같은 기술들이 해당 기술을 채택하고 있고 상당히 높은 성능을 나타내고 있다.

 

실시간 스트리밍에서는 일정 수준의 프레임 버리기는 허용되기에 아래와 같이 최대한 간단하게 코드를 구성했다.

public class BufferPool {
    private final ArrayBlockingQueue<ByteBuffer> bufferPool;

    public BufferPool(int maxSize, int sizeOfBuffer) {
        bufferPool = new ArrayBlockingQueue<>(maxSize);
        for(int i = 0; i < maxSize; i++) {
            bufferPool.add(ByteBuffer.allocate(sizeOfBuffer));
        }
    }

    public ByteBuffer acquire() {
        ByteBuffer buffer = bufferPool.poll();
        if(buffer != null) {
            buffer.clear();
        }
        return buffer;
    }

    public void release(ByteBuffer buffer) {
        bufferPool.offer(buffer);
    }
}

 

 

Buffer Pool은 미리 사용할 ByteBuffer를 생성해두고 이를 재사용하는 원리이다.

하지만 해당 기술을 사용함으로써 기존 단일 ByteBuffer 방식의 문제를 해결하고 성능을 향상시킬 수 있다.

Buffer Pool 기반의 스트리밍 동작 원리

Buffer Pool을 사용하여 기존 수도 코드의 동작 방식을 바꿔보았다

BufferPool bufferPool = new BufferPool(3, 1024);

Queue<byteBuffer> frameQueue;

// frame consumer thread
while (doStreaming) {
    Frame frame = frameQueue.poll();
    sendFrame(frame);
    bufferPool.release(frame);
}

// frame producer thread
while (doStreaming) {
    ByteBuffer frame = bufferPool.aquire();
    readFrame(frame);
    frameQueue.add(frame);
}

 

현재 코드에서 수정된 부분은 ByteBuffer를 재사용하는 것과 ByteBuffer 해제 대신 release하는 것이다.

 

하지만 아래와 같은 이점을 얻을 수 있다.

  • race condition 해결을 위한 새로은 ByteBuffer 할당을 미리 진행하므로 오버헤드가 적다
  • 특정 ByteBuffer에 대한 작업이 완전히 종료된 후에 release하므로 race condition에서 자유롭다
  • ByteBuffer의 수와 크기를 일괄적으로 제한할 수 있기에 메모리 관리가 편하다 

따라서 기존 코드의 단점을 보완하면서 장점을 부각할 수 있는 것이 현재 코드이다.

현재 코드의 문제점

하지만 현재 코드도 주의해야 하는 부분이 있다.

바로 Buffer Pool의 크기와 각각의 ByteBuffer의 크기를 지정하는 것이다.

 

이는 효과적으로 GC가 동작하고 메모리를 사용하는데 있어서 매우 중요하므로 Buffer Pool의 목적과 동시 접근 스레드 수에 따라서 적절하게 설정해야 한다.

 

현재 내가 개발하고 있는 서비스에서는 1024 * 128 크기를 갖는 3개의 ByteBuffer를 관리하기 위한 Buffer Pool이면 충분하다

 


마무리

이번에는 실시간성이 중요한 서비스에서 어떻게 시간을 절약하고 어떻게 데이터를 담기 위한 버퍼를 관리할지에 대해서 알아보았다.

 

처음에는 어떻게 코드를 구현할지 막막했는데 열심히 인터넷을 찾아보고 고민하면서 현재 선택할 수 있는 최적의 방안을 떠올릴 수 있었던 것 같다.

 

결과적으로 현재 프로젝트의 목적에 맞는 최적의 메모리 관리를 할 수 있었고 메모리 사용량이 기존에 비해서 감소하면서 성능은 향상되는 효과로 이어졌다

 

무엇보다 이전에 배웠던 Race Condition이나 쓰레드 등의 CS 지식을 활용하는 빈도가 늘어남에 따라서 해당 개념들에 대한 심도있는 지식이 필요할 때라고 느껴졌다.

'Spring' 카테고리의 다른 글

[ Spring Boot ] JPA 연관 관계 매핑  (1) 2025.08.26
[ Spring Boot ] 웹소켓 통신에서 메모리 누수 제어  (3) 2025.07.21
[ Spring ] Spring의 디자인 패턴과 아키텍처  (1) 2025.07.14
[Spring Boot] Spring Boot 설정 파일 분리 : submodule  (0) 2025.03.31
'Spring' 카테고리의 다른 글
  • [ Spring Boot ] JPA 연관 관계 매핑
  • [ Spring Boot ] 웹소켓 통신에서 메모리 누수 제어
  • [ Spring ] Spring의 디자인 패턴과 아키텍처
  • [Spring Boot] Spring Boot 설정 파일 분리 : submodule
코드래곤
코드래곤
코드래곤 님의 블로그 입니다.
  • 코드래곤
    코드래곤 님의 블로그
    코드래곤
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 알고리즘 (3)
        • 그리디 (1)
        • 그래프 (2)
      • 시스템 설계 (6)
      • CS 및 기본 개념 (17)
      • Docker (5)
      • Spring (23)
        • 백준 서비스 구현하기 (1)
        • 기초 개념 (14)
        • MSA (2)
        • JPA (1)
      • Dart (3)
      • Flutter (1)
      • Kubernetes (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
코드래곤
[ Spring Boot ] 스트리밍 서비스에서 Buffer Pool로 ByteBuffer 관리
상단으로

티스토리툴바