프로젝트에서 엑셀 다운로드 API를 개발하다가 문득
어떠한 과정을 통해서 엑셀 다운로드가 이루어지는지 원리가 궁금해졌다
Apache POI는 MS Office 파일 포맷을 Java로 읽고 쓰는 기능을 제공하는 라이브러리다
Apache POI를 사용하는 방법보다는
POI가 라이브버리 내부에서 어떠한 과정을 거쳐서 엑셀 파일을 만드는지,
엑셀파일은 어떻게 HTTP 통신을 통해 클라이언트로 전달되는지,
뜯어보면서 무지성으로 라이브러리만 썼던 시간을 반성해보고자 한다ㅋㅋ
🤔 파일 다운로드 HTTP Response
파일 다운로드 API의 Response의 Header를 살펴보면,
Content-Disposition=attachment;filename=파일이름.확장자
를 확인할 수 있다
💁 Content-Disposition
- Http Response Body에 오는 컨턴츠의 기질을 알려주는 속성
- 파일다운로드 혹은 첨부파일과 관련된 처리를 제어하기 위해 사용되고, 브라우저는 서버로부터 수신한 파일을 어떻게 처리해야할지 지시받는 헤더
주요 값은 아래와 같은데 HTTP Response Body에 따라 다른데
👤 Main Body 즉, 단일 컨텐츠를 담고 있는 Body인 경우
ex. 파일, JSON, HTML 등
이런경우에는 Content-Disposition 헤더는 Body에 포함된 데이터가 어떻게 처리되어야하는지 명시하는 역할을 한다고 한다
- Content-Disposition: inline
- 콘텐츠를 다운로드하지 않고 웹페이지안에서 바로 보여줌
- 예를 들어 이미지
- Content-Disposition: attachment; filename="파일이름.확장자"
- 콘텐츠를 브라우저에서 열지 않고, 사용자의 로컬로 다운로드
👥 Multipart Body, 즉 다중 컨텐츠를 담고 있는 Body의 경우
ex. 여러 파트로 Body가 나누어진 경우, 복수의 데이터 전송
이때의 Http Response Header Content-Type은 multipart/form-data를 가진다
Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWxkTrZu0gW
하위파트가 여러개인 경우, Boundary 값은 아래에서 보겠지만 Body에서 각 Multipart를 구분하는 구분자로 쓰인다
브라우저와 서버는 boundary 값 기준으로 문자열을 파싱하여 각 Multipart를 구분하는 것이다
Http Repsonse Body에서
아래와 같이 각 하위 part는 header와 body를 가진다고 한다
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
johndoe
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
<파일 이진코드 등>
------WebKitFormBoundary7MA4YWxkTrZu0gW--
이때의 Content-Disposition은 Body의 하위파트에서만 의미가 있고
multipart의 각 하위파트가 어떠한 데이터를 담고 있는지 설명하는 역할을 한다
브라우저와 서버는 각 하위파트의 헤더값들을 보면서, 각 데이터가 어떤 필드이고 어떤 유형인지 대응할 수 있다
- Content-Disposition: form-data;
실제 개발한 엑셀 다운로드 API Response의 Header를 살펴봤으니
Body를 한번 살펴보면 아래와 같다
🤔 파일 다운로드 Response Body
PKSn0*6PUC\{X%]8R
qcfgfWdqZCB||**h㻆},^{Va^K<46NXQdž9!P$҆dcDj);ѝPgEM'OʕH7LhRG^'{zސʮB3˙h.hWжFj娄CQՠ똈}ιLU:D%އ,B[ ;˱ {N~XpykOLkNVܿBZ~q ar{OPKzq;j0_8`Q2m4[ILbږ.[K
($}v?IQ.uӂhx>=@pH"~} n*"H8Z^'#7m{O3Guܓ'y|aD l_EYȾvql3MLeh*\3Y0oJ :^}PKz
0D~EȽADҔA? 6lBJ?ߜ0ͯ)@H6V>$;SC
;̢(ragl&eL!y%49`_4GFJWg
GSb
~PK|wؑJ0F_依늆(+w!bCu%&_9!{Gzkj
J24Ҫ5yo3(5XcɦzyC4&pj8@jDXELW߂U%1
%IHJ%{,Pp@&`6߁9YCjb\\ڈ|ޛIS\zU<~woIS$g4g=-9;k\IuHt'nyO'PK;5A ツ.z0Ɣ`,q2oԇNEx5z>W(RK^4{ŀ5yVymXV\.j
8&x
1o0
{a
PsRGj-ۉ6T"u>?o{)
^֪x,zm[7uxڂv$uٕҀKdηXm56H4}J$HOΒZ4I
JFPB4?V+@MA7AC&!N0<&aL:I1_9?b ą}\3`2ACAQL7|6,/>(-W,y.N園h8;*d" gTxl
B?vߒķ&쏄ϥ4_"{%}{^w*\V;_~PKeAO0wtCI!1{Fk ?S#'=~B\ADIawF>c<ICXKLO*EL#e?ȵRңp#F:g%OO! LR6nL4{ea1S4t8$6~hԔse4M{kg5n@j&,gry~PKMk0@}qn/c迟@;${~Γy"#i #
^O7`D=E?1bn8y?$YLE8HZ g/
g^6pUr%좃/I`|Rˤ:f~mFv:ׯp9HBSyݵK~PK;[o0QYqڢ]vu-k6M0w`҆$.R
+I|8-lNlx|Gn?,ǎ;-UWocW"+{+BEUi^D*7T͟bRuWuAkӲX$oU8T\RjHExJwqt>𨹜%e2*yR0霳"VDy<_.&5y`ţR@P8ae&Y-UE4Ѵ|i_kslZ;-bqo;dM$u8>iSW㧼.Tiv~Aa9yvfQwss>Z(`Gu
2$MY3h/x:lkwCA3Ħm-tX# pq1?eg.!v8כ-Ko~
a3(L$GK$7 ಒiܼxK܂dxnQVl pKQNH([S!'alD]bFc7CQ1v+KJz0p!Mb$\Hi1.~"IޥD(u}gtqC' }RǓ>)H4xgu<s:62P
0aG8qHO QG!=p)0e =p }EH_uK#E֬E2iEeRo&7U8C7eRT_CzkTkVPKB\|
q;z|wؑ&xe
;\|
처음에는 인코딩이 깨졌나? 싶었는데 찾아보니
압축된 파일의 이진 데이터였다
위의 이진데이터를 보면 PK로 시작하는 것을 볼 수 있는데
PK는 Zip 파일의 시그니처로 ZIP 파일이 시작됨을 의미한다
위의 API에 Request시에 브라우저는 .xlsx 형식의 엑셀 파일을 다운로드 했는데 왜 압축된 zip 파일의 이진데이터가 Body에 내려오는가??
docx, xlsx, pptx 등 2007년 이후에 나온 오피스 프로그램의 파일들은 확장자만 바뀌었을 뿐
실제로는 zip 파일이라고 한다
실제로 위의 파일들을 zip으로 확장자명을 바꾼후 들어가보면,
xml 파일들과 파일(이미지 등) 구성된 하나의 패키지 일 뿐이라는 것이다
그래서 zip 파일 이진데이터가 Response Body에 담겨 내려오는 것이다
위의 배경 기반 지식을 알고 코드를 한번 보자면
(물론 나는 POI를 까보면서, 위의 원리를 알게되었다..)
📝 Apache POI 엑셀 다운로드 코드 분석하기
SXSSFWorkbook workbook = new SXSSFWorkbook();
// Sheet 생성
SXSSFSheet sheet = workbook.createSheet("Sheet1");
// 헤더 스타일 설정
CellStyle = headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.serBold(true);
headerStyle.setFont(headerFont);
SXSSFRow headerRow = sheet.createRow(0);
SXSSFCell cell = headerRow.createCell(0);
cell.setCellValue("헤더값");
cell.setCellStyle(headerStyle);
SXSSRow row = sheet.createRow(1);
row.createCell(0).setCellValue("값");
위와 같이 SXSSFWorkbook 객체를 통해 엑셀에 들어가는 값들을 세팅하고
String encodedFileName = URLEncoder.encode("파일명.xlsx", StandardCharsets.UTF_8);
// HttpServeletResponse
// MIME 타입 명시화
// Content-Disposition 헤더 명시화
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=파일명.xlsx");
try(ServeletOuputStream outputStream = response.getOutputStream())}
workbook.write(outputStream);
} finally {
workbook.dispose();
}
MIME 타입??
- 클라이언트와 서버에게 전송된 파일의 형식을 알려주기 위한 메커니즘
- 클라이언트와 서버가 파일을 받았을 때, 해야할 기본 동작을 알려주기 위한 메타데이터
- 파일 이름에 붙은 확장자는 브라우저에게는 파일 이름 그이상도 아니고, ContentType을 보고 데이터의 종류를 결정한다
슬래쉬(/)를 기준으로 type과 subtype으로 나뉘고,
- type은 데이터의 일반적 범주
- ex. text, image, application, audio, video 등
- subtype은 데이터의 구체적인 형식을 나타낸다
- ex. plain(일반 텍스트), html, jpeg, png, json, pdf, zip, mpeg, mp4 등
예를 들어 브라우저는
text/html을 받으면, 웹 페이지로 랜더링되고,
application/pdf는 PDF 뷰어에서 열린다
그리고 2007년 오피스 프로그램 파일 OpenXML로 구성된 xlsx 확장자의 파일은
https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types
Common MIME types - HTTP | MDN
This topic lists the most common MIME types with corresponding document types, ordered by their common extensions.
developer.mozilla.org
위의 doc에서 참고할 수 있듯이,
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
값을 Content-Type으로 가져야한다
한층 더 들어가서 SXSSFWorkbook 객체의 write 메서드를 살펴보면
write는 파라미터로, HttpServeletResponse의 OutputStream을 인자로 받는다
public void write(OutputStream stream) throws IOException {
this.flushSheets();
// 임시로 일단 서버에 파일 생성
File tmplFile = TempFile.createTempFile("poi-sxssf-template", ".xlsx");
boolean deleted;
try{
FileOutputStream os = new FileOutputStream(tmplFile);
Throwable var5 = null;
try{
this._wb.write(os); // OpenXML 파일의 골격을 생성
} catch (Throwalbe var67) {
...중략
} finally {
...중략
}
ZipSecureFile zf = new ZipSecureFile(tmplFile);
var5 = null;
try{
ZipFileZipEntrySource source = new ZipFileZipEntrySource(zf);
Throwable var7 = null;
try{
this.injectData(source, stream);
}catch(Throwable var66){
...중략
}finally{
}
}catch(Throwable var70){
...중략
}finally{
...중략
}
} finally{
deleted = tmplFile.delete(); // 서버에 생성한 파일 삭제
}
if(!deleted) {
throw new IOException("Could not delete temporary file after processing: " + tmpFile);
}
this._wb.write(os);
- org.apache.poi > ooxml > POIXMLDocument에 위치
- 파일의 각각의 Part들의 틀을 만들어 주는 것 같다
- OpenXML로 엑셀 파일이 구성되니, ooxml 하위의 POIXMLProperties 등에 위임하여, XML의 속성 값을 만들어준다
- 생성한 내용을 ServeletResponse의 OutputStream 출력 스트림에 이진 데이터로 작성한다
this.injectData(source, stream);
- SXSSFWorkbook의 protected 메서드로 WorkBook에 등록되 엑셀 데이터를 Zip 파일 OutputStream에 작성한다
- ZipArchiveOutputStream을 이용하여 스트림에 입력된 엑셀 데이터를 압축한다
이렇게해서 엑셀(.xlsx) 파일이 서버에서 클라이언트로 전달되는 과정을 살펴보았다
파일에 대해서 알아보니, 공부할게 더 많이 생겼다
다음에는 Servlet의 OutputStream, InputStream과
OS의 JNI를 통해 제공되는 Java.IO의 File OutputStream, InputStream에 대해서도
원리를 한번 공부해보고자한다