다량의 엑셀 정보는 어떻게 저장할 수 있을까?
- 개발/이력관리
- 2026. 6. 6. 16:03
현재 SI 프로젝트를 진행하고 있습니다. 프로젝트의 상세한 내용은 보안상 말씀드릴 수 없지만, 업무를 진행하면서 확인하고 싶은 기술적인 내용을 정리해보려고 합니다. 이번에 다루려는 주제는 프론트 화면에서 엑셀 파일을 업로드하면, 해당 데이터를 서버에서 읽어 DB에 저장하는 흐름입니다. 단순히 엑셀을 업로드하고 저장하는 기능처럼 보일 수 있지만, 실제로는 파일 파싱 방식, 데이터 검증, 대량 데이터 처리, 트랜잭션 범위, 실패 처리 방식 등 여러 가지 선택지가 존재합니다. 따라서 이번 글에서는 특정 실무 코드를 공개하기보다는, 예제 코드를 바탕으로 어떤 기술을 선택할 수 있는지, 그리고 그 선택에 어떤 트레이드오프가 있는지를 정리해보려고 합니다. 여기에 등장하는 코드는 실제 프로젝트 코드가 아니며, 개념 설명을 위해 별도로 작성한 예제 코드입니다.
일단, 100만건의 데이터를 엑셀업로드를 진행해봅시다.
* 실무에서 정했던 엑셀 데이터 수보다 더 많이 진행할 예정입니다.
엑셀에는 100만건 이상의 데이터가 존재합니다. 이것을 바로 파일 업로드를 시켜 바로 DB에 넣다보면 서버에서 OOM이 발생할 수 있습니다.
테스트 결과 생각보다 100만건의 데이터는 업로드에 성공하였습니다.

하지만, 업로드가 완료가 될때까지 약 25분 정도 흘렀습니다. 또한, 동기 방식으로 데이터를 전송하기 때문에, 모든 데이터가 업로드가 종료가 되어야 DB에 반영이 되는 문제도 있었습니다.
이러한 문제를 해결하기 위해서는 어떻게 해야 할까요?
전송되는 데이터를 쪼개서 처리하면 됩니다. 이를 청크 단위 처리라고 볼 수 있습니다.
예를 들어 100만 건의 데이터를 하나의 큰 그룹으로 보고 한 번에 전송하면, 업로드 요청 한 번에 100만 건이 모두 실리게 됩니다. 하지만 이 데이터를 10개 또는 100개의 그룹으로 나누면, 한 번에 전송되는 데이터 수는 각각 10만 건, 1만 건 수준으로 줄어들게 됩니다.
그렇다면 데이터를 쪼개는 작업은 어느 위치에서 수행할 수 있을까요?
데이터를 쪼개는 위치는 크게 세 가지로 볼 수 있습니다.
첫 번째는 프론트 화면에서 엑셀 데이터를 읽고, 일정 단위로 나누어 서버에 전송하는 방식입니다.
두 번째는 프론트에서는 엑셀을 단순한 파일로만 업로드하고, 서버에서 파일을 읽으면서 데이터를 나누어 처리하는 방식입니다.
마지막은 현재 구조와 동일하게 프론트에서 서버로 데이터를 전달한 뒤, DB 적재 단계에서 프로시저를 활용하여 데이터를 나누어 처리하는 방식입니다.
이제 각각의 방식이 어떤 장단점을 가지는지, 그리고 현재 상황에서는 어떤 방식이 가장 적합한지 정리해보겠습니다.
프론트에서 데이터를 쪼개보자.

프론트에서 데이터를 쪼개기 위한 전제조건은 브라우저에서 엑셀 파일을 직접 읽어야 한다는 것입니다.
즉, 프론트는 엑셀 파일을 단순히 서버로 전송하는 것이 아니라, 파일 내부의 행 데이터를 파싱한 뒤 일정 단위로 나누어 서버에 전송해야 합니다.
프론트에서 데이터를 쪼개는 방식은 크게 두 가지로 볼 수 있습니다.
행 단위로 분할 하는 방법
먼저 SheetJs같은 클라이언트 파서를 준비해줍니다.

예를 들어 엑셀에 100만 건의 데이터가 있다면, 이를 1만 건씩 나누어 서버에 100번 요청하는 방식입니다.
100만 건 엑셀
→ 프론트에서 엑셀 파싱
→ 1만 건씩 분할
→ API 100번 호출
→ 서버 저장
이 방식의 장점은 한 번에 서버로 전송되는 데이터 양을 줄일 수 있다는 점입니다.
네트워크가 불안정하더라도 전체 100만 건이 한 번에 실패하는 것이 아니라, 일부 청크만 실패할 수 있습니다.

위에서 프론트에서 Excel을 읽을 수 있어야 한다고 했기 때문에 SheetJs 같은 클라이언트 파서가 필요합니다.
실행해보겠습니다.

또한 업로드 진행 상황을 확인할 수 있다는 장점도 있습니다.
그 이유는 데이터를 청크 단위로 나누어 전송하기 때문입니다. 각 청크가 서버로 전송될 때마다 API 호출 결과를 확인할 수 있으며, 이를 통해 전체 데이터 중 몇 건이 업로드되었는지 사용자에게 진행률 형태로 제공할 수 있습니다.


약 총 337초 정도 걸린것을 확인할 수 있었습니다. 5분정도 걸렸습니다.
테스트 결과, 아무런 분할 없이 데이터를 전송했을 때는 약 25분 정도가 소요되었습니다. 반면 데이터를 청크 단위로 분할하여 전송했을 경우 약 5분 내외로 처리되었으며, 이를 통해 전체 처리 시간이 약 80% 감소한 것을 확인할 수 있었습니다.
이렇게 보면 나쁘지 않아 보이지만, 단점도 명확합니다.
우선 브라우저가 100만 건의 엑셀 데이터를 직접 읽고 파싱해야 합니다. 데이터의 양이 많아질수록 클라이언트의 메모리 사용량도 함께 증가하게 되며, 경우에 따라서는 브라우저가 일시적으로 응답하지 않거나 화면이 멈추는 현상이 발생할 수 있습니다.
결국, 서버의 부담을 줄일 수 있다는 장점은 있지만, 그 부담이 사용자 브라우저로 이동하는 구조라고 볼 수 있습니다. 특히 대용량 데이터를 처리해야 하는 환경에서는 사용자의 PC 성능이나 브라우저 상태에 따라 업로드 품질이 달라질 수 있다는 점도 고려해야 합니다.
ai를 시켜 벤치마킹을 해본결과

크롬이 살짝 빠른것을 알 수 있었습니다. 물론 테스트를 많이 해본것이 아니기 때문에 오차는 있을 수 있지만, 브라우저마다 속도가 다르다는 느낌은 받을 수 있었습니다.
두 번째 방법은 "이진 단위(바이트 단위)로 분할"하는 방식입니다.
이진 단위로 분할하는 방식
앞선 방식이 브라우저에서 엑셀 파일을 직접 파싱한 뒤, 데이터를 JSON 형태로 분할하여 서버에 전송하는 방식이었다면, 이번 방식은 엑셀 파일 자체를 바이트 단위로 나누어 서버에 전송하는 방식입니다.
예를 들어 file.slice()를 활용하면 파일을 1MB, 5MB와 같은 일정 크기의 조각으로 분할할 수 있습니다. 이후 분할된 파일 조각들을 서버로 전송하고, 서버에서는 이를 다시 하나의 파일로 재조립합니다. 재조립이 완료되면 Apache POI와 같은 라이브러리를 이용해 엑셀을 파싱하고 DB에 데이터를 저장하게 됩니다.

이 방식은 브라우저에서 엑셀 파일을 직접 파싱하지 않기 때문에 엑셀 파싱 라이브러리에 대한 의존도가 낮습니다. 또한 엑셀 데이터를 메모리에 모두 올려 처리하지 않으므로, 행 단위 분할 방식에 비해 브라우저의 메모리 부담이 상대적으로 적습니다.

가장 큰 특징은 업로드가 바이트 단위로 이루어진다는 점입니다. 따라서 마지막으로 성공한 파일 조각 정보를 관리하고 있다면, 업로드 도중 네트워크 오류가 발생하더라도 처음부터 다시 전송할 필요 없이 실패한 지점부터 업로드를 재개할 수 있습니다. 실제로 Google Drive와 같은 대용량 파일 업로더나 AWS S3 Multipart Upload 역시 이와 유사한 방식을 사용하고 있습니다.
실제로 구동을 시켜보겠습니다.

테스트 결과, 처리 시간은 약 360초로 측정되었습니다. 이는 앞서 프론트에서 엑셀을 직접 파싱한 뒤 데이터를 분할하여 전송했던 방식보다 약 24초 정도 느린 결과였습니다.
이쯤 되니 브라우저의 메모리 사용량이 얼마나 차이 나는지도 궁금해집니다. 비록 처리 시간은 조금 더 소요되었지만, 사용자 입장에서 체감하는 경험은 오히려 더 나았기 때문입니다.
이 방식의 가장 큰 특징은 프론트만으로는 구현이 어렵다는 점입니다. 파일을 바이트 단위로 분할하여 전송하는 것은 프론트에서 수행할 수 있지만, 분할된 파일 조각들을 다시 하나의 파일로 재조립하는 작업은 서버에서 담당해야 합니다. 즉, 프론트와 백엔드가 함께 구현되어야만 완성할 수 있는 구조입니다.

반면 프론트 입장에서는 상대적으로 부담이 적습니다. 브라우저가 엑셀 데이터를 직접 읽고 파싱할 필요가 없으며, 단순히 파일을 일정 크기의 조각으로 나누어 전송하기만 하면 됩니다. 따라서 대용량 엑셀 파일을 처리하는 과정에서도 브라우저가 멈추거나 과도한 메모리를 사용하는 현상이 상대적으로 적어, 사용자 입장에서는 보다 쾌적하게 동작한다고 느껴졌습니다.
다만, 이 방식은 대용량 파일 전송의 안정성을 높이는 데 초점이 맞춰져 있습니다. 서버에 파일이 모두 업로드된 이후에는 여전히 파일 재조립, 엑셀 파싱, DB 적재 과정을 거쳐야 하므로 데이터 처리 성능 자체를 직접적으로 개선하는 방식은 아닙니다. 즉, 데이터 처리 최적화보다는 파일 업로드 안정성 향상에 더 적합한 방법이라고 볼 수 있습니다.
개인적으로 엑셀 업로드에 이 방식을 사용하는 것은 비효율적이라고 생각합니다. 엑셀 데이터는 결국 row 단위로 파싱되고 처리되어야 합니다. 따라서 바이트 단위로 파일을 분할하여 업로드하더라도, 서버에서는 다시 파일을 재조립한 뒤 row 단위로 파싱해야 합니다. 결국 업로드 안정성은 높일 수 있지만, 엑셀 데이터 처리 자체를 개선하는 방식은 아니라고 판단이 되어집니다.
서버에서 데이터를 쪼개보자.
앞서 살펴본 방식들은 데이터를 분할하는 주체가 프론트였습니다. 엑셀을 직접 파싱하여 JSON 형태로 분할하는 방법도 있었고, 파일 자체를 바이트 단위로 분할하는 방법도 있었습니다. 이번에는 관점을 바꿔보겠습니다. 프론트는 엑셀 파일을 그대로 서버에 전달하고, 데이터를 분할하는 작업은 서버에서 수행하는 방식입니다. 즉, 데이터를 쪼개는 주체가 프론트가 아닌 서버가 되는 것입니다.
찾아보니 서버에서는 기술적으로 쪼개는 방법도 존재하지만, 프로토콜 또는 스펙 기반으로, 인프라 아키텍처 레벨로 , 언어/ JVM 기능적으로 쪼개는 방법들도 다양하게 존재합니다.
기술적 관점

기술적으로 엑셀 데이터를 쪼개는 방법에 대해 고민해보았습니다. 크게 보면 처리량을 높이는 방법과, 대용량 데이터를 안정적으로 처리하는 방법으로 나누어 볼 수 있습니다. 이번에 적용해볼 기술은 Spring Batch입니다. Spring Batch는 데이터를 일정한 chunk 단위로 나누어 처리할 수 있기 때문에 대량 데이터 적재에 적합하다고 판단하였습니다. 또한 Apache POI SAX도 함께 적용해보려고 하였지만, 확인해보니 이미 적용된 상태였습니다. Apache POI SAX는 엑셀 전체를 메모리에 올리지 않고 row 단위로 읽는 방식이기 때문에, OOM이 발생하지 않았던 이유도 이와 관련이 있었습니다.
spring batch

이번에는 프론트에서는 단순히 파일 업로드만 수행하고, 서버에서 Spring Batch를 이용해 엑셀 데이터를 분할 처리하기로 하였습니다.
어떻게 보면 서버에서 엑셀 파일을 읽고 바로 저장하는 방식에 비해 row를 chunk 단위로 나누는 과정이 추가되었을 뿐입니다. 하지만 실제 테스트 결과 처리 시간에는 차이가 있었습니다. 약 25분 정도 소요되던 작업이 7분 내외로 단축된 것을 확인할 수 있었습니다.

이렇게 보면 프론트에서 데이터를 미리 분할하여 전송하는 방식이 가장 좋아 보일 수 있습니다. 하지만 서버에서 row를 분할하여 처리하는 방식도 분명한 장점이 존재합니다. (336s -> 360s -> 420s)
가장 큰 장점은 처리 현황을 서버에서 직접 관리할 수 있다는 점입니다. 프론트에서 데이터를 분할하여 전송하는 방식은 업로드 진행률은 확인할 수 있지만, 실제 데이터가 얼마나 저장되었는지는 확인하기 어려웠습니다. 반면, 서버에서 Spring Batch를 통해 데이터를 처리하는 경우에는 현재 몇 건이 처리되었고, 몇 건이 남았는지와 같은 실제 처리 현황을 확인할 수 있습니다.

즉, 단순히 파일 업로드 진행 여부가 아니라 실제 데이터 적재 진행 상황을 확인할 수 있다는 점은 운영 관점에서도 의미가 있다고 생각합니다.
언어 관점

현재는 서버(JVM)에서 엑셀 데이터를 읽고 처리한 뒤 DB에 저장하는 구조를 사용하고 있습니다. 한 행씩 순차적으로 진행되는 이 흐름을 JVM 차원에서 어떻게 쪼갤 수 있는지 InputStream을 통한 스트리밍 입력, ExecutorService를 통한 병렬 처리, Parallel Stream을 통한 선언적 분산 등의 도구를 중심으로 정리합니다.
InputStream을 통한 스트리밍 입력
사실 이 부분은 이미 적용이 되어 있습니다. 그렇다면, 이 방식으로 왜 적용해야 하는지 생각해봅시다.
MultipartFile.getInputStream()으로 받은 입력을 그대로 POI에 흘려보내고, POI는 한 행씩 SAX 이벤트로 파싱합니다. 메모리에 파일 전체를 올리지 않습니다.
만약 같은 동작을 스트리밍 없이 짠다고 가정해봅시다.

두 단계에서 메모리가 부풀어 오릅니다.
1. getBytes() — 파일 크기만큼 byte[] 점유 (28MB)
2. WorkbookFactory.create() — 모든 셀을 객체로 메모리에 보관 (수 GB 가능)
100만 행짜리 파일이면 JVM 힙 한도를 쉽게 넘겨 OutOfMemoryError가 발생합니다. 28MB → 28MB가 아니라, 28MB → 수 GB로 부풀어 오를 수 있습니다. xlsx는 압축 포맷이라 압축 해제 시 5~10배로 커지고, 그걸 모두 객체로 다시 표현하면 또 한 차례 늘어나기 때문입니다.

하지만 예상과달리 oom이 발생하지 않았습니다. 이는 POI라이브러리가 자체 안전장치로 100MB 이상 배열할당을 거부합니다.
(POI 5.2.1에서 추가)
POI 라이브러리가 이런 가드를 만든 이유는 xlsx 파일은 zip으로 압축되어 있어서 28MB 파일을 압축 해제 시 수 GB로 부풀 수 있습니다. 그렇게 되면 악의적인 zip bomb 공격이 가능해집니다. 만약, DOM 파서가 그걸 그대로 메모리에 올린다면, 서버는 OOM이 발생하게 됩니다. 그래서 POI는 이만큼 이상은 위험하니 거부 한도를 내장하였습니다. 물론, IOUtils.setByteArrayMaxOverride()을 통해 강제로 늘릴 수 있수 있지만, 그 길로 가면 진짜 OOM이 발생할 수 있다고 합니다.
ExecutorService를 통한 병렬 처리
ExecutorService를 통한 병렬 처리도 검토해보았습니다. Spring Batch를 사용하면 chunk 단위 처리와 함께 병렬 처리도 구성할 수 있습니다. 하지만 그 전에 순수 Java 관점에서도 데이터를 병렬로 처리할 수 있는지 확인해보고 싶었습니다. 현재 서버에서는 InputStream을 통해 엑셀 파일을 읽고 있습니다. 그렇다면 InputStream으로 읽어온 데이터를 일정 단위로 나눈 뒤, ExecutorService를 이용해 병렬로 처리하면 성능이 개선될 수 있을까요?

이번에는 이 방식을 직접 테스트해보며, 서버 내부에서 병렬 처리를 적용했을 때 처리 시간이 어떻게 달라지는지 확인해보겠습니다.

예상 밖의 결과였습니다. Spring Batch를 사용하지 않았음에도 처리 시간이 약 268.2초로 측정되었고, 지금까지 테스트한 방식 중 가장 빠른 결과였습니다.
아무래도 데이터를 일정 단위로 나눈 뒤 ExecutorService를 통해 여러 스레드에서 병렬로 처리하면서 전체 처리 시간이 줄어든 것으로 보입니다. 즉, 하나의 스레드가 모든 데이터를 순차적으로 처리하는 방식보다, 여러 스레드가 작업을 나누어 처리한 것이 성능 개선에 영향을 준 것으로 판단하였습니다.
그렇다면, 스레드가 2~20까지 테스트한 결과는 어떨까요?

테스트 결과, 스레드 수가 6개가 되는 시점부터 처리 시간이 급격하게 단축되는 것을 확인할 수 있었습니다. 이는 일정 수준까지는 병렬 처리 효과가 분명히 존재한다는 의미로 볼 수 있습니다.
하지만 스레드 수가 10개를 넘어가면서부터는 처리 시간이 안정적으로 감소하지 않고, 오히려 증가와 감소를 반복하는 모습을 보였습니다. 즉, 스레드 수를 늘린다고 해서 무조건 성능이 좋아지는 것은 아니었습니다.
특히 스레드 풀 크기를 16개로 설정했을 때는 알 수 없는 이유로 작업이 중단되었습니다. 원인을 확인해보니 DB 연결이 끊어진 것이 문제였습니다. 여러 스레드가 동시에 insert를 수행하면서 Oracle 쪽 세션이 종료되었고, 그 결과 chunk 작업에서 예외가 발생하였습니다.

이후 Future.get()을 통해 예외가 전파되면서 서버에서는 500 에러가 발생하였고, 최종적으로 curl 응답 파싱 실패로 기록되었습니다.
이를 통해 병렬 처리에는 적정 수준이 존재한다는 것을 확인할 수 있었습니다. 스레드 수를 늘리면 일정 구간까지는 처리량이 증가하지만, DB 커넥션 수, Oracle 세션 제한, insert 부하 등을 넘어서면 오히려 실패 가능성이 높아질 수 있습니다. 이는 ExecutorService 자체의 문제가 아니라, HikariCP 커넥션 풀에서 병목이 발생했던 것이 원인이었습니다. 여러 스레드가 동시에 DB insert를 수행하려고 했지만, 실제로 사용할 수 있는 DB 커넥션 수에는 제한이 있었기 때문입니다.
그렇다면 이를 어떻게 해결할 수 있을까요?
가장 간단한 방법은 HikariCP 커넥션 풀 크기를 스레드 풀 크기에 맞춰 늘리는 것입니다. 예를 들어 작업 스레드가 16개라면, DB 커넥션도 16개 이상 사용할 수 있도록 설정하는 방식입니다.

하지만 이 방법은 권장하기 어렵습니다. 커넥션 풀 크기를 늘리면 애플리케이션 입장에서는 대기 시간이 줄어들 수 있지만, 그만큼 DB가 동시에 처리해야 하는 요청 수가 증가합니다. 결국 병목이 애플리케이션에서 DB로 이동할 뿐이며, DB 세션 수나 insert 부하를 초과하면 오히려 장애 가능성이 높아질 수 있습니다.
따라서 커넥션 풀을 무작정 늘리기보다는, DB가 안정적으로 감당할 수 있는 수준에서 스레드 수와 커넥션 수를 함께 조정하는 것이 더 적절하다고 판단하였습니다.
하지만, 이 글에서는 테스트를 위해 HikariCP 커넥션 풀을 20으로 올려보겠습니다.

이번에는 스레드 2개를 기준으로 얼만큼 성능이 향상 되었는지도 파악해보겠습니다.

결과를 확인해보니 가장 안정적이면서 성능이 우수했던 구간은 스레드 수가 8개일 때였습니다. 가장 빠른 처리 시간은 스레드 12개에서 측정되었지만, 스레드 10개 구간에서 오히려 성능이 감소하는 현상이 나타났기 때문에 안정적인 구간이라고 보기는 어려웠습니다.
결국 단순히 스레드 수를 늘리는 것이 성능 향상으로 이어지는 것은 아니었으며, 적절한 스레드 수를 찾는 과정이 중요하다는 것을 확인할 수 있었습니다.
이번 테스트를 통해 ExecutorService를 활용한 병렬 처리 방법과, 병렬 처리 과정에서 발생할 수 있는 커넥션 풀 병목에 대해 함께 학습할 수 있었습니다. 또한 성능 최적화는 단순히 스레드를 늘리는 것이 아니라, 애플리케이션과 DB가 감당할 수 있는 균형점을 찾는 과정이라는 점도 확인할 수 있었습니다.
프로토콜 관점
현재 업로드는 HTTP Multipart 기반입니다. 하지만 네트워크 장애로 업로드가 실패하면, 서버에 수신 상태가 남지 않아 처음부터 다시 올려야 합니다. 이때 재개 가능한 업로드 프로토콜인 tus를 적용하면, 끊긴 지점부터 이어서 전송할 수 있습니다.
tus
tus - resumable file uploads
tus is the open protocol standard for resumable and reliable file uploads across the web, facilitating efficient and seamless file transfer experiences.
tus.io
tus는 생각보다 다양한 기능을 제공하기 때문에 모든 내용을 다루기에는 범위가 너무 넓습니다. 따라서 이번 글에서는 핵심 동작 원리와 간단한 구현 방법만 살펴보겠습니다.
구현은 가장 간단한 CDN 방식을 이용하여 진행하겠습니다.

tus프로토콜을 이용하면 서버에 얼마나 적재중인지 확인할 수 있습니다.

하지만, db에 적재가 되어지는것이 아니기 때문에 서버 메모리에 저장이 되어지는것으로 추정이 되어집니다.

확인해보니 성능 측면에서는 기대했던 만큼의 차이를 확인할 수는 없었습니다. 다만 tus 프로토콜을 사용하는 가장 큰 이유는 성능이 아니라 전송 재개(Resumable Upload) 에 있습니다. 네트워크 문제가 발생하더라도 처음부터 다시 업로드하는 것이 아니라, 중단된 지점부터 업로드를 이어갈 수 있다는 점이 가장 큰 특징입니다.
실제로 적용해보기 위해 테스트를 진행하였지만 생각보다 쉽지는 않았습니다. 인텔리제이를 재시작하거나 개발 환경을 다시 구성하는 과정에서 업로드 상태가 초기화되는 문제가 있었고, 기대했던 형태로 동작을 확인하지는 못하였습니다.

따라서 이번 글에서는 tus의 핵심 개념과 동작 원리 정도만 소개하고, 상세한 구현 내용은 추후 별도로 다뤄보도록 하겠습니다.
DB에서 데이터를 쪼개보자.

드디어 마지막 단계입니다. 지금까지 프론트와 서버에서 대용량 엑셀 데이터를 어떻게 분할하고 처리할 수 있는지 살펴보았습니다.
그렇다면 DB에서 데이터를 분할하는 것은 어떤 의미가 있을까요?
사실 DB에서 데이터를 분할한다고 해서 파일 업로드 속도가 빨라지거나, 서버가 데이터를 읽는 속도가 개선되는 것은 아닙니다. 즉, 전송 과정의 최적화와는 거리가 있습니다.
그럼에도 불구하고 DB에서 데이터를 분할하는 이유는 무엇일까요?
그 이유는 처리량보다는 안정성에 있습니다. 트랜잭션 범위를 줄이고, DB 부하를 분산하며, 실패 시 재처리 범위를 최소화할 수 있기 때문입니다. 그렇다면 DB에서는 어떤 방식으로 데이터를 분할할 수 있을까요?
프로시저
DB에서 데이터를 분할하는 방법으로는 여러 가지가 있지만, 이번 글에서는 프로시저를 중심으로 살펴보겠습니다. 프로시저를 사용하면 서버가 임시 테이블에 데이터를 적재한 뒤, DB 내부에서 일정 단위로 데이터를 검증하고 실제 테이블에 반영할 수 있습니다.

역시나 처리량은 줄지 않았군요. 프로시저 쿼리를 보면 다음과 같습니다.

이 프로시저는 user_row_tab 형태의 여러 사용자 데이터를 입력받아 users 테이블에 한 번에 저장하는 역할을 합니다. 여기서 핵심은 FORALL입니다. 일반적인 FOR LOOP는 한 건씩 SQL을 실행하지만, FORALL은 PL/SQL 컬렉션에 담긴 데이터를 SQL 엔진에 한 번에 전달하여 대량 INSERT를 수행할 수 있습니다. 즉, 서버에서 100만 건을 한 건씩 DB에 전달하는 것이 아니라, 일정 단위로 묶은 데이터를 프로시저에 전달하고 DB 내부에서 일괄 처리하는 방식입니다. 이를 통해 애플리케이션과 DB 사이의 반복 호출을 줄일 수 있고, 대량 데이터 적재 시 발생하는 오버헤드를 줄일 수 있습니다.
다만 이 구조는 단순 INSERT에 가깝습니다. 실제 운영 환경에서는 여기서 한 단계 더 발전시킬 수 있습니다. 예를 들어 중복 데이터 검증, 필수값 누락 검증, 실패 데이터 별도 저장, 처리 결과 건수 반환, 예외 발생 시 로그 테이블 적재 등을 추가할 수 있습니다.
결국 프로시저 방식의 장점은 DB 내부에서 대량 데이터를 빠르게 반영할 수 있다는 점이고, 발전 방향은 단순 저장을 넘어 검증, 실패 관리, 결과 추적이 가능한 적재 프로세스로 만드는 것이라고 볼 수 있습니다.
다만 프로시저가 제공하는 장점 중 상당수는 서버에서도 대체할 수 있습니다. 데이터 검증, 실패 처리, 배치 Insert, 트랜잭션 제어는 Java와 Spring 기반에서도 충분히 구현할 수 있습니다. 따라서 프로시저는 반드시 필요한 선택이라기보다는, DB 내부에서 처리하는 것이 더 적합한 환경에서 선택할 수 있는 방식이라고 볼 수 있습니다.
간단하게 DB는 이 정도만 알아보도록 하겠습니다. DB파트는 DB에서도 데이터 분할이 가능한지 체크하기 위함이었습니다.
마무리
원래 목적은 프론트, 서버, DB에서 엑셀 row를 어떻게 분할하여 처리할 수 있는지 간단하게 정리하는 것이었습니다. 하지만 생각보다 다양한 방법들을 새롭게 알게 되었습니다. 예를 들어 프론트에서 데이터를 row 단위로 분할하여 전송하는 방법, 파일을 바이트 단위로 분할하여 전송하는 방법, tus 프로토콜을 이용한 전송 재개 방식, 그리고 ExecutorService를 활용한 병렬 처리 방식 등이 있었습니다. 이번에 테스트한 방법들이 모두 대용량 엑셀 업로드에 적합한 것은 아니었습니다. 엑셀 업로드는 결국 파일을 읽고 row 단위로 처리해야 하기 때문에, 바이트 단위 분할과 같은 방식은 목적에 비해 다소 우회적인 접근이라고 느껴졌습니다. 하지만 이런 방식도 존재한다는 것을 알게 되었고, 언젠가 다른 문제를 해결할 때 활용할 수 있을 것이라 생각합니다.
또한, 이번 글에서 소개한 방법들이 전부는 아닙니다. 조금만 더 깊게 들어가도 훨씬 다양한 기술과 아키텍처를 확인할 수 있었습니다. 다만 모든 내용을 담기에는 범위가 너무 넓고, 실제 엑셀 업로드 문제를 해결하는 데 필요한 수준을 넘어선다고 판단하여 제외하였습니다.
결과적으로, 이번 글을 통해 느낀 점은 단순히 엑셀 업로드 기능 하나를 구현하는 것이 아니라, 데이터를 어디에서 분할할 것인지에 따라 전송 방식, 처리량, 안정성, 운영 방식까지 달라질 수 있다는 점이었습니다. 앞으로도 새로운 요구사항을 마주하게 된다면 구현에만 집중하기보다는 어떤 위치에서 문제를 해결하는 것이 가장 적절한지 먼저 고민해보려고 합니다.
'개발 > 이력관리' 카테고리의 다른 글
| 외부 API 호출은 어떤 라이브러리를 선택해야 할까 (0) | 2026.05.21 |
|---|---|
| 마이바티스 매퍼 방식은 어떤것을 사용해야 할까? (0) | 2026.05.16 |
| 그레들DSL은 어떤것을 사용해야 할까? (0) | 2026.05.10 |
| java에서 println()을 사용하면 안되는 이유 (0) | 2026.05.01 |