ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 람다로 무거운 작업 분리하기
    백엔드 : 서버공부/Spring 2026. 5. 2. 16:43
    728x90

    비사이드 코리아에서는 전자위임장을 대량으로 출력하는 기능을 제공하고 있다.

    기존 구조에서는 워드(docx) 파일을 양식으로 받아 지정된 위치에 값을 채워 넣고, 이를 PDF로 변환한 뒤 서명과 신분증 이미지를 삽입했다. 이후 여러 명의 전자위임장을 하나의 PDF 파일로 병합하는 방식이었다.

    이 구조에는 몇 가지 문제가 있었다.

    가장 큰 문제는 높은 CPU 사용량메모리 사용량이었다. 파일을 byte[] 형태로 메모리에 올려 작업했기 때문에, 출력 요청이 한 번에 몰리면 리소스 사용량이 급격히 증가했다.

    그 결과 Datadog에서 CPU 사용량 알람이 발생했고, 심한 경우에는 메모리 부족으로 서버가 다운되는 상황도 있었다.

    이 문제를 해결하기 위해 전자위임장 출력 기능을 별도의 Lambda 실행 환경으로 분리하기로 했다.

    1. 기존 로직 파악하기

    먼저 기존 로직을 살펴볼 필요가 있었다.

    Lambda로 출력 기능을 분리하더라도, 어디까지를 메인 서버가 담당하고 어디부터 Lambda가 처리할지 결정해야 했기 때문이다.

     

     

    기존 구현에서는 DB에서 사용할 전자위임장 양식을 조회한 뒤, S3에서 양식 파일을 다운로드해 메모리에 올렸다.

    이후 위임 표결 정보를 조회해 지정된 바인딩 위치에 값을 삽입하고, 서명 및 신분증 파일을 다운로드했다. 그다음 위임장 파일을 PDF로 변환한 뒤, 적절한 위치에 이미지들을 삽입했다.

    마지막으로 생성된 각 PDF 파일을 하나로 병합하고, 최종 결과물을 S3에 업로드하면서 작업이 마무리되었다.

    2. 위임장 양식을 DOCX에서 PDF로 변경하기

    사실 Lambda 분리 이전에도 구조 개선이 한 차례 있었다.

    워드 파일을 위임장 양식으로 받아 서버에서 다시 쓰는 방식은 원본 문서의 양식을 해칠 수 있었다. 그래서 위임장 양식을 PDF로 변경하자는 요구사항이 있었고, 이에 따라 아키텍처가 아래와 같이 변경되었다.

     

     

    변경된 구조에서는 템플릿 정보에 각 값이 삽입될 위치를 JSON 형태로 저장해두고, PDF 생성 요청이 들어오면 해당 위치에 필요한 값을 삽입하는 방식으로 동작했다.

    이 변경으로 워드 파일과 PDF 파일을 동시에 메모리에 올려두지 않아도 되었고, 메모리 사용량도 어느 정도 개선되었다.

    하지만 PDF 파일 자체는 여전히 메모리 위에서 처리되고 있었기 때문에, 메모리 사용량 증가와 누수 위험은 완전히 해소되지 않았다.

    3. 무거운 작업 분리하기

    이번 개선에서는 pdfTemplateEditorPort가 담당하던 렌더링 로직을 통째로 분리하기로 했다.

    그 이전 단계까지는 비즈니스 로직이 많이 포함되어 있었기 때문에 Spring 서버에서 처리하고, 최종적으로 정리된 데이터와 위임장 양식의 S3 object key만 Lambda에 전달하는 방식으로 역할을 나누었다.

     

     

    이렇게 분리하면 Spring 서버는 더 이상 대용량 파일 바이트를 메모리에 직접 적재하지 않아도 된다.

    Spring 서버는 비즈니스 로직과 상태 관리에 집중하고, 파일 생성처럼 리소스를 많이 사용하는 작업은 Lambda가 담당하도록 구조를 변경했다.

    4. lambdaClient.invoke람다 호출하기

    이제 Spring 서버와 Lambda가 실제로 통신할 수 있어야 했다.

    AWS Lambda는 API Gateway를 붙여 HTTP 엔드포인트처럼 사용할 수도 있다. 하지만 이번 기능에서는 굳이 HTTP API 형태가 필요하지 않았다.

    전자위임장 출력 요청은 외부 클라이언트가 직접 호출하는 공개 API가 아니라, 비사이드 내부 Spring 서버가 백그라운드에서 실행하는 내부 작업이기 때문이다.

    즉 브라우저나 앱이 Lambda를 직접 호출하는 구조가 아니었다. 메인 서버가 필요한 데이터를 정리한 뒤, Lambda에 “이 작업을 대신 수행해 달라”고 전달하는 구조면 충분했다.

    그래서 이번 구현에서는 API Gateway 대신 AWS SDK에서 제공하는 LambdaClient.invoke(...) 방식을 선택했다.

    이 방식의 장점은 다음과 같다.

    1. API Gateway를 따로 구성하지 않아도 된다.
      Lambda 함수명과 요청 payload만 있으면 바로 호출할 수 있어 구조가 단순하다.
    2. 내부 서버 간 통신이라는 목적에 잘 맞는다.
      외부에 공개할 URL을 만들 필요가 없고, 권한도 IAM 정책으로 제어할 수 있다.
    3. 요청과 응답 흐름이 명확하다.
      Spring 서버는 전자위임장 생성에 필요한 정보를 JSON으로 구성해 Lambda에 전달하고, Lambda는 생성 결과 파일의 S3 위치와 메타데이터를 응답하면 된다.

    실제 호출 흐름은 다음과 같다.

    1. Spring 서버가 위임장 출력에 필요한 비즈니스 데이터를 조회한다.
    2. 템플릿 PDF의 S3 object key, 서명 및 신분증 이미지의 object key, 좌표 정보, 바인딩할 텍스트 값을 하나의 요청 객체로 정리한다.
    3. lambdaClient.invoke(...)를 통해 Lambda 함수에 payload를 전달한다.
    4. Lambda는 S3에서 템플릿과 이미지를 직접 읽어 PDF를 생성한다.
    5. 최종 결과 PDF를 S3에 업로드한 뒤, 결과 파일명, object key, 파일 크기 등을 응답한다.
    6. Spring 서버는 응답을 받아 파일 정보를 저장하고, 요청 상태를 완료로 변경한다.

    코드 관점에서 보면 Spring 서버는 더 이상 PDF 바이트를 직접 메모리에 적재하거나 병합하지 않는다.

    대신 “어떤 파일을, 어떤 위치에, 어떤 값으로 렌더링해야 하는지”만 Lambda에 전달한다.

    무거운 파일 처리 책임을 Lambda로 옮기면서, 메인 서버는 비즈니스 로직과 상태 관리에 집중할 수 있게 되었다.

    5. 요청/응답 설계

    렌더링 로직을 Lambda로 분리하면서 가장 중요했던 것은 메인 서버와 Lambda 사이의 책임 경계를 명확히 나누는 것이었다.

    메인 서버는 비즈니스 데이터를 조회하고, 어떤 값을 어떤 위치에 그려야 하는지 정리한다. 반면 Lambda는 전달받은 정보를 바탕으로 실제 PDF를 렌더링하고, 결과 파일을 저장하는 역할만 수행한다.

    이 구조를 유지하기 위해 요청 payload는 파일 바이트 자체가 아니라, S3 위치 정보와 렌더링에 필요한 메타데이터 중심으로 설계했다.

    public record PdfRenderLambdaRequest(
            Long requestId,
            S3ObjectLocation output,
            String fileName,
            List<PdfRenderLambdaItemRequest> items
    ) {}

    requestId는 출력 요청을 식별하기 위한 값이고, output은 최종 결과 PDF를 업로드할 S3 위치다.

    fileName은 결과 파일명이며, items에는 실제 렌더링 대상이 되는 전자위임장 단위 정보가 담긴다.

    각 item에는 다음과 같은 정보가 포함된다.

    • 템플릿 PDF의 S3 위치
    • 서명, 신분증 이미지의 S3 위치
    • 텍스트 바인딩 값
    • 텍스트와 이미지가 삽입될 좌표 정보
    • 위임장 식별 정보

    즉 메인 서버는 “무엇을 어디에 그릴지”까지 정리해서 넘기고, Lambda는 “그리는 작업”만 수행한다.

    응답 구조는 최대한 단순하게 유지했다.

    public record PdfRenderLambdaResponse(
            boolean success,
            String fileName,
            Long fileSize,
            String objectKey,
            String errorMessage
    ) {}

    Lambda는 렌더링이 끝난 뒤 최종 결과를 S3에 업로드하고, 성공 여부와 결과 파일의 메타데이터를 응답한다.

    메인 서버는 이 응답을 기준으로 파일 정보를 등록하고, 요청 상태를 완료 또는 실패로 변경한다.

    이 방식의 장점은 다음과 같다.

    • 메인 서버와 Lambda 사이에 대용량 byte[]를 직접 주고받지 않는다.
    • 메인 서버는 S3 object key와 좌표 정보만 전달하면 된다.
    • Lambda는 독립적으로 파일을 읽고 생성한 뒤 결과만 반환하면 된다.
    • 실패 시에도 errorMessage를 통해 원인을 추적할 수 있다.

    결과적으로 요청/응답 모델을 단순한 계약 형태로 고정하면서, 메인 서버는 비즈니스 로직에 집중하고 Lambda는 렌더링 실행기처럼 동작하도록 분리할 수 있었다.

    6. 메인 서버의 상태 관리

    렌더링 작업을 Lambda로 분리하더라도, 출력 요청의 상태를 관리하는 주체는 여전히 메인 서버여야 했다.

    Lambda는 파일 생성만 담당한다. 요청 생성, 진행 상태 변경, 결과 파일 등록, 실패 처리 같은 비즈니스 흐름은 기존과 동일하게 Spring 서버가 책임지는 구조로 유지했다.

    process()의 핵심 흐름은 다음과 같다.

    1. 출력 요청에 포함된 위임장 목록을 조회한다.
    2. 각 위임장에 대해 템플릿, 이미지, 텍스트 바인딩 정보, 좌표 정보를 조합해 Lambda 요청 객체를 만든다.
    3. 모든 item을 모아 Lambda를 호출한다.
    4. Lambda가 성공 응답을 반환하면 결과 파일 정보를 저장하고, 요청 상태를 완료로 변경한다.
    5. 실패하면 요청 상태를 실패로 변경하고, 알림 이벤트를 발행한다.

    핵심 호출부는 아래와 같다.

    PdfRenderLambdaRequest request = buildPdfRenderLambdaRequest(
            processing.getId(),
            processing.getProjectId(),
            result.requests()
    );
    
    PdfRenderLambdaResponse response = pdfTemplateEditorPort.render(request);
    
    Long fileId = internalVoteFileServicePort.registerPrivateFile(
            response.fileName(),
            response.fileSize(),
            response.objectKey()
    );

    이 흐름에서 중요한 점은 메인 서버가 Lambda를 단순 실행기로 취급한다는 것이다.

    Lambda가 반환하는 것은 렌더링 결과다. 그 결과를 어떤 파일로 등록할지, 요청 상태를 어떻게 갱신할지, 후속 이벤트를 어떻게 발행할지는 모두 메인 서버가 결정한다.

    이렇게 구성하면 렌더링 책임은 분리하면서도, 시스템의 상태 일관성은 기존과 동일하게 유지할 수 있다.

    즉 파일 생성은 Lambda가 수행하지만, 출력 요청의 source of truth는 여전히 Spring 서버에 남겨두는 방식이다.

    실패 처리도 같은 방식으로 관리했다.

    Lambda가 실패 응답을 반환하거나 호출 과정에서 예외가 발생하면, 메인 서버는 해당 요청을 실패 상태로 전환하고 알림을 전송한다. 덕분에 렌더링 실행 환경이 분리되더라도, 운영 관점에서는 기존과 유사한 방식으로 상태를 추적할 수 있었다.

    7. 마무리

    이번 작업의 목적은 단순히 전자위임장 출력 기능을 Lambda로 옮기는 것이 아니었다.

    핵심은 메인 서버가 직접 부담하던 CPU와 메모리 사용량을 줄이고, 운영 안정성을 높이는 것이었다.

    기존 구조에서는 PDF 생성, 이미지 삽입, 병합 과정이 모두 Spring 서버 내부에서 수행되었다. 이 과정에서 파일 바이트가 메모리에 직접 적재되었고, 출력 요청이 많아질수록 리소스 사용량이 빠르게 증가했다.

    그 결과 메인 서버에 부하가 집중되었고, Datadog CPU 알람이나 메모리 부족 위험으로 이어질 수 있었다.

    분리 이후에는 메인 서버가 렌더링에 필요한 비즈니스 데이터와 S3 위치 정보만 정리하고, 실제 파일 생성과 병합은 Lambda가 처리하도록 역할을 나누었다. 그 결과 메인 서버의 리소스 사용량은 이전보다 안정적으로 유지되었다.

     

     

    실제 관측 결과는 다음과 같았다.

    • 메모리 사용량
      • 개선 전: 약 1000MB 수준까지 증가
      • 개선 후: 300MB 이하 유지
    • CPU 사용량
      • 개선 전: 최대 60% 수준까지 증가
      • 개선 후: 뚜렷한 피크 없이 7% 이하 유지

    즉 전자위임장 출력 요청이 들어오더라도, 메인 서버는 더 이상 대용량 PDF 바이트를 직접 메모리에 올리지 않게 되었다. CPU 사용량이 높은 렌더링 작업 역시 Lambda 실행 환경으로 분리할 수 있었다.

    결과적으로 이번 분리는 단순한 기능 이전이 아니라, 무거운 파일 처리 책임을 메인 애플리케이션 바깥으로 이동시켜 서버 안정성을 높인 작업이었다.

    메인 서버는 비즈니스 로직과 상태 관리에 집중하고, 리소스를 많이 사용하는 렌더링은 별도 실행 환경에 위임하는 구조를 갖추게 되었다.

     

     

     

Designed by Tistory.