
MPRESSER는 SEMES 기업 연계 프로젝트로 현재 SEMES는 반도체 잉크젯 설비에서 디스플레이용 RGB 픽셀을 프린트하고 기판 위에 얇은 막을 형성하는 소프트웨어를 개발하고 있습니다.

큰 기판 위에 고해상도 이미지를 인쇄하는 프린터이므로 사용되는 이미지의 용량이 큽니다. 이 과정에서 대형 패턴 이미지 BMP 를 TIFF 파일로 압축 변화하는 과정은 느리다는 문제가 발생하여
IMPRESSER는 초대형 BMP 이미지를 고속으로 TIFF 타입으로 압축·변환하기 위해 개발된 SEMES 기업 연계 프로젝트입니다.
잉크젯 프린팅 공정 특성상 하나의 기판에 매우 높은 해상도의 패턴 BMP 이미지를 출력해야 했고, 이 과정에서 이미지 생성 자체가 병목이 되는 문제가 있었습니다.
요청 처리 → 비동기 파이프라인 → 연산 엔진 분리 → 대용량 업로드 전략까지
이미지 생성 전 과정을 단계적으로 정리를 담았습니다
이미지 생성 요청은 다음과 같은 특징을 가지고 있었습니다.
- 이미지 크기가 매우 큼 (2GB 의 BMP 이미지)
- 생성 및 변환에 수 초 이상 소요
- 요청 수가 늘어날 경우 동시 처리 필요
- 실패 시 재시도 및 상태 추적 필요
즉, 동기 요청으로 처리하기에는 너무 무거운 작업이며 서버가 파일을 직접 다루면 병목이 발생하는 구조였습니다.
이 문제를 해결하기 위해 관리 가능한 비동기 작업으로 다루는 방향으로 설계를 시작했습니다.
이미지 생성 파이프라인은 다음과 같은 흐름으로 구성되어 있습니다.
- Client에서 이미지 생성 요청
- API Server에서 요청 정보 DB 저장 및 jobId 발급
- Presigned Multipart URL 발급 후 클라이언트가 S3 직접 업로드
- 비동기 워커가 C++ 연산 엔진으로 작업 전달
- CUDA 기반 병렬 이미지 생성 수행
- 결과 BMP 파일을 S3에 업로드
- 완료 콜백을 통해 DB 상태 갱신 및 SSE 알림 전송
핵심은 API 서버가 직접 연산이나 대용량 파일 처리를 하지 않는다는 점입니다.
이미지 생성 작업을 등록하는 작업입니다.
@PostMapping
@Operation(summary = "패턴 생성")
public ResponseEntity<BaseResponse<CreateBmpImageResponse>> createBmpImage(
@RequestBody @Valid CreateBmpImageRequest createBmpImageRequest) {
CreateBmpImageResponse createBmpImageResponse = generationHistoryService.createBmpImage(
createBmpImageRequest);
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.onSuccess(createBmpImageResponse));
}
서비스 레이어에서는 다음 작업을 수행합니다.
GenerationHistory generationHistory =
createBmpImageRequest.toEntity(requestedAt, user);
GenerationHistory saved = generationHistoryRepository.save(generationHistory);
이미지 생성 파라미터(R/G/B 패턴, 간격, 크기 등), 요청 시간, 초기 상태를 GenerationHistory 엔티티로 DB에 먼저 저장합니다.
DB 저장 후, 서버는 곧바로 연산을 수행하지 않습니다.
대신 비동기 워커를 호출합니다.
generateImageWorker.createBmpImageAsync(
savedGeneratedHistory.getId(),
userUuid,
accessToken
);
이 시점에서 API 서버의 역할은 끝납니다.
요청을 검증하고 작업을 등록하고 비동기 처리를 트리거
👉 연산은 절대 API 스레드에서 수행하지 않습니다.
@Async("imageGenerationExecutor")
public void createBmpImageAsync(
Long generationHistoryId,
UUID userUuid,
String accessToken) {
비동기 워커는 다음 순서로 동작합니다.
① 작업 상태 RUNNING 전환
txService.markRunning(generationHistoryId);
DB 기준으로 이 작업은 실행을 알림
② S3 Multipart 업로드 초기화
InitMultipartUploadResponse init =
filePresignedService.initMultipartUpload("bmp", bmpFileName);
String uploadId = init.uploadId();
String bmpKey = S3Util.extractKeyFromUrl(init.imageUrl());
서버는 BMP 파일을 직접 받지 않습니다.
대신 Presigned Multipart 업로드를 위한 준비만 수행합니다.
long volumeBytes = generationHistory.getBmpVolume();
int partCount = (int) Math.ceil(
(double) volumeBytes / S3_PART_SIZE);
이미지 크기를 기준으로 멀티파트 개수를 계산합니다.
③ Presigned URL 목록 생성
PresignedUrlListResponse presigned =
filePresignedService.createPartPresignedUrls(
objectName, uploadId, partCount);
List<String> partUploadUrls = presigned.urls();
이 URL 목록은 C++ 연산 엔진으로 전달됩니다.
마지막으로, 서버는 외부 C++ 연산 엔진 API를 호출합니다.
GenerateImageApiRequest apiRequest =
GenerateImageApiRequest.from(
partUploadUrls,
uploadId,
objectName,
auth,
generationHistory
);
externalApiClient.requestGenerate(apiRequest);
@Component
public class ExternalApiClient {
private final RestClient rest;
public ExternalApiClient(RestClient externalApiRestClient) {
this.rest = externalApiRestClient;
}
public GenerateImageApiResponse requestGenerate(GenerateImageApiRequest generateImageRequest) {
GenerateImageApiResponse response = rest.post()
.uri("/generate")
.contentType(MediaType.APPLICATION_JSON)
.body(generateImageRequest)
.retrieve()
.onStatus(HttpStatusCode::isError, (r, res) ->
new ResponseStatusException(res.getStatusCode(), "API 호출 실패"))
.body(GenerateImageApiResponse.class);
return response;
}
}
이를 통해 API 서버는
👉 연산(CUDA)과 완전히 분리된 오케스트레이터 역할만 수행합니다.
C++ 연산이 끝나면 /bmp/{generationUuid}/complete API로 콜백이 들어옵니다.
@PostMapping("/{generationUuid}/complete")
public void completeBmpGeneration(
@PathVariable UUID generationUuid,
@RequestBody CompleteBmpGernerationRequest request) {
generationHistoryService
.processGenerationCompletion(generationUuid, request);
}
성공/실패에 따라 DB 상태 갱신, SSE를 통한 사용자 실시간 알림을 처리합니다.
sseService.sentToClient(
userUuid,
GENERATE_BMP_SUCCESS,
response
);
정리하며 IMPRESSER의 이미지 생성 백엔드 파이프라인은 다음 원칙을 중심으로 설계되었습니다.
장시간 연산은 비동기 처리
대용량 파일은 서버를 거치지 않음
API 서버와 연산 엔진의 책임 분리
이 구조 덕분에 서버 부하 없이 대용량 이미지 처리, 안정적인 상태 관리,연산 서버 독립 확장이 가능했습니다.