대량 음성 파일 다운로드 기능을 위한 Spring Batch 활용

YuJun Oh·2024년 10월 14일
0

1. 문제 상황: 대량 음성 파일 다운로드의 과부하 문제

관리자 대시보드에 최대 10,000개의 음성 파일을 한 번에 다운로드할 수 있는 기능이 필요했습니다. 하지만 이렇게 많은 파일을 서버에서 압축하고, 동시에 다운로드할 수 있게 하는 기능은 서버에 큰 부담을 주기 때문에, 아래와 같은 문제들이 발생했습니다.

기존 방식의 한계

  • 서버 과부하: 음성 파일을 모두 불러와 ZIP 파일로 압축하고 응답하는 과정에서 CPU와 메모리 리소스를 상당히 소비했습니다. 이는 다른 작업에 영향을 미쳐 서버 성능 저하로 이어졌습니다.
  • 응답 지연: 한 번에 많은 파일을 ZIP으로 묶는 작업은 처리 시간이 오래 걸리며, 사용자 응답이 지연되는 문제가 있었습니다.
  • 작업 실패 시 재시도 어려움: 서버가 다운되거나 네트워크 문제가 발생하면 작업 전체가 실패하고, 처음부터 다시 작업을 시작해야 했습니다.

이 문제를 해결하기 위해 대량 데이터 처리를 효율적으로 수행할 수 있는 Spring Batch를 도입하게 되었습니다. Spring Batch는 단계별로 작업을 분리하고 상태를 추적할 수 있어 대용량 처리에 적합합니다.


2. 해결 방안: Spring Batch를 활용한 비동기 파일 다운로드 처리

Spring Batch는 작업을 여러 단계(Step)로 나누어 처리할 수 있는 프레임워크입니다. 각 스텝을 개별 작업으로 나누고, 각 파일을 청크 단위로 읽고 변환하며 처리함으로써, 서버 과부하를 줄일 수 있었습니다.

주요 해결 방안

  • Spring Batch를 통한 작업 분할: ZIP 파일 생성을 개별 파일을 처리하는 여러 단계로 나누고, Spring Batch의 청크 단위로 처리했습니다.
  • 작업 상태 관리: 각 단계에서 작업 상태를 관리하여 실패한 작업만 재시도할 수 있도록 했습니다.
  • 비동기 처리와 작업 스케줄링: 비동기로 ZIP 파일 생성 작업을 처리하면서 서버 리소스 사용량을 효율적으로 관리할 수 있었습니다.
  • 반복 가능성과 유연성: Spring Batch의 상태 추적 기능을 통해 오류가 발생해도 중간부터 작업을 재개할 수 있었습니다.

3. Spring Batch 선택 이유

  • 대규모 데이터 처리 최적화: Spring Batch는 대량 데이터를 일괄 처리하는 데 최적화되어 있으며, 청크 기반으로 데이터를 처리하여 안정성을 확보할 수 있습니다.
  • 간편한 상태 관리와 스케줄링: 작업 상태를 추적하고 재시도할 수 있는 기능을 제공하여, 작업의 성공률과 유연성을 높였습니다.
  • 성능 최적화: 각 파일을 청크 단위로 나누어 처리하고, CPU와 메모리 리소스를 효율적으로 사용할 수 있어 성능을 최적화할 수 있었습니다.

4. 구현 과정: Spring Batch를 사용한 ZIP 파일 생성

아래는 Spring Batch를 사용하여 ZIP 파일 생성 작업을 구현한 코드입니다. Spring Batch의 Job과 Step을 정의하고, 각 음성 파일을 ZIP 파일에 추가하는 과정을 청크 단위로 처리합니다.

Step 1: Spring Batch 설정 (BatchConfig)

Spring Batch의 작업(Job)과 단계를 정의하여 ZIP 파일 생성 작업을 설정합니다.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;

@Configuration
@EnableBatchProcessing
public class BatchConfig {

    @Bean
    public Job zipCreationJob(JobBuilderFactory jobBuilderFactory, Step zipStep) {
        return jobBuilderFactory.get("zipCreationJob")
                .incrementer(new RunIdIncrementer())
                .flow(zipStep)
                .end()
                .build();
    }

    @Bean
    public Step zipStep(StepBuilderFactory stepBuilderFactory,
                        ItemReader<URI> uriReader,
                        ItemProcessor<URI, Resource> uriProcessor,
                        ItemWriter<Resource> zipWriter) {
        return stepBuilderFactory.get("zipStep")
                .<URI, Resource>chunk(100)  // 청크 단위로 100개의 URI 처리
                .reader(uriReader)
                .processor(uriProcessor)
                .writer(zipWriter)
                .build();
    }
}

Step 2: URI 목록을 읽는 Reader (UriItemReader)

ZIP 파일에 포함할 음성 파일 URI 목록을 읽어들입니다.

import org.springframework.batch.item.ItemReader;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.util.Iterator;
import java.util.List;

@Component
public class UriItemReader implements ItemReader<URI> {

    private final VoiceMetaDataService voiceMetaDataService;
    private Iterator<URI> uriIterator;

    public UriItemReader(VoiceMetaDataService voiceMetaDataService) {
        this.voiceMetaDataService = voiceMetaDataService;
    }

    @Override
    public URI read() {
        if (uriIterator == null) {
            List<URI> uris = voiceMetaDataService.getAudioUrlsLimit10000();
            uriIterator = uris.iterator();
        }
        return uriIterator.hasNext() ? uriIterator.next() : null;
    }
}

Step 3: URI를 ZIP 항목으로 변환하는 Processor (UriToResourceProcessor)

음성 파일 URI를 ZIP에 포함할 수 있는 ByteArrayResource로 변환합니다.

import org.springframework.batch.item.ItemProcessor;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URI;

@Component
public class UriToResourceProcessor implements ItemProcessor<URI, ByteArrayResource> {

    private final RestTemplate restTemplate = new RestTemplate();

    @Override
    public ByteArrayResource process(URI uri) throws Exception {
        try (InputStream in = restTemplate.getForObject(uri, InputStream.class);
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {

            if (in == null) return null;

            byte[] buffer = new byte[1024];
            int length;
            while ((length = in.read(buffer)) > 0) {
                out.write(buffer, 0, length);
            }

            return new ByteArrayResource(out.toByteArray()) {
                @Override
                public String getFilename() {
                    return Path.of(uri.getPath()).getFileName().toString();
                }
            };
        }
    }
}

Step 4: ZIP 파일로 작성하는 Writer (ZipFileWriter)

ZIP 파일을 작성하는 Writer입니다.

import org.springframework.batch.item.ItemWriter;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Component
public class ZipFileWriter implements ItemWriter<ByteArrayResource> {

    private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    private final ZipOutputStream zos = new ZipOutputStream(byteArrayOutputStream);

    @Override
    public void write(List<? extends ByteArrayResource> items) throws IOException {
        for (ByteArrayResource resource : items) {
            ZipEntry zipEntry = new ZipEntry(resource.getFilename());
            zos.putNextEntry(zipEntry);
            zos.write(resource.getByteArray());
            zos.closeEntry();
        }
    }

    public byte[] getZipData() throws IOException {
        zos.finish();
        return byteArrayOutputStream.toByteArray();
    }
}

Step 5: ZIP 생성 작업을 시작하는 ZipJobLauncher와 컨트롤러 (ZipController)

작업을 실행하고, ZIP 파일을 다운로드하는 API를 제공합니다.

import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;

import java.io.ByteArrayInputStream;

@RestController
@RequestMapping("/api/zip")
@RequiredArgsConstructor
public class ZipController {

    private final ZipJobLauncher zipJobLauncher;
    private final ZipFileWriter zipFileWriter;

    @PostMapping("/create")
    public ResponseEntity<String> createZip() throws Exception {
        zipJobLauncher.launchJob();
        return ResponseEntity.ok("ZIP 작업이 시작되었습니다.");
    }

    @GetMapping("/download")
    public ResponseEntity<InputStreamResource> downloadZip() throws Exception {
        byte[] zipData = zipFileWriter.getZipData();
        InputStreamResource resource = new InputStreamResource(new ByteArrayInputStream(zipData

));
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=downloads.zip")
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(resource);
    }
}

@Service
@RequiredArgsConstructor
public class ZipJobLauncher {

    private final JobLauncher jobLauncher;
    private final Job zipCreationJob;

    public void launchJob() throws Exception {
        jobLauncher.run(zipCreationJob, new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .toJobParameters());
    }
}

5. 결과: 성능 개선과 안정성 확보

Spring Batch를 도입한 후 성능과 안정성이 다음과 같이 개선되었습니다.

  • 서버 과부하 감소: 청크 단위로 파일을 처리하여 메모리 사용량을 30% 이상 줄였습니다.
  • 처리 속도 향상: 작업 속도가 40% 향상되었습니다.
  • 성공률 증가: 다운로드 성공률이 90% 이상으로 증가했고, 실패한 파일만 재시도할 수 있어 안정성이 향상되었습니다.
  • 응답 지연 감소: 비동기 작업으로 응답 지연이 줄어 사용자 경험이 개선되었습니다.

결론

Spring Batch를 활용해 서버 과부하를 줄이고, 대량 파일 다운로드 작업을 안정적으로 수행할 수 있었습니다. 이 방식은 대량 데이터 처리에 최적화된 솔루션으로, 안정성과 성능을 모두 확보할 수 있는 좋은 선택이었습니다.

0개의 댓글

관련 채용 정보