๋ค์ด๊ฐ๋ฉฐ
์๋
ํ์ธ์,
์ด๋ ๋ , ์ฌ๋ด QA ์์คํ
์ ์ด๋ฐ ์๋ฌ๊ฐ ์ฌ๋ผ์ต๋๋ค.
"100๋ง ๊ฑด์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ก๋ํ๋ ๋ฐ ๋ก๋ฉ๋ฐ๊ฐ ๋๋์ง ์์์"
๊ฒฐ๋ก ์ ์ผ๋ก ์์ธ์, ์๋ฒ์์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ฐ ๋๋ฌด ๋ง์ ์๊ฐ์ด ๊ฑธ๋ฆฐ๋ค๋ ๊ฒ๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด SAX(Simple API for XML) ๋ฐฉ์์ ์คํธ๋ฆฌ๋ฐ ํ์๋ฅผ ์ฌ์ฉํ๋ ๋ฑ,
๊ทธ ๊ณผ์ ์์ ๊ฒช์๋ ๋ค์ํ ๋ฌธ์ ๋ค๊ณผ ๋ฉํฐ์ค๋ ๋ฉ์ ํตํ ์ถ๊ฐ ์ฑ๋ฅ ๊ฐ์ ๋์ ๋ฐ ๊ฒฐ๊ณผ๊น์ง
82% ๊ฐ๋์ ์ฑ๋ฅ์ ๊ฐ์ ํ๊ธฐ๊น์ง ์์๋ ๊ณผ์ ์ ๋ค๋ค๋ณด๋ ค ํฉ๋๋ค.
์ ๊ฒฐ๋ก
| ๊ตฌ๋ถ | XSSFWorkbook (DOM) | XSSFReader (SAX) | ๋น๊ณ |
|---|---|---|---|
| ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ | ๋งค์ฐ ๋์ (ํ์ผ ํฌ๊ธฐ์ ๋น๋ก) | ๋งค์ฐ ๋ฎ์ (ํ์ผ ํฌ๊ธฐ์ ๋ฌด๊ด) | 100๋ง ๊ฑด ๊ธฐ์ค 15GB -> 4GB |
| ์ฒ๋ฆฌ ์๋(๋์ฉ๋) | ๋งค์ฐ ๋๋ฆผ | ๋งค์ฐ ๋น ๋ฆ | 100๋ง ๊ฑด ๊ธฐ์ค 60์ด+ -> 11์ด |
| ๊ตฌํ ๋์ด๋ | ์ฌ์ (์ง๊ด์ ) | ๋ณต์กํจ (์ด๋ฒคํธ ๊ธฐ๋ฐ) | ์ํ ๊ด๋ฆฌ๊ฐ ํต์ฌ |
| ํต์ฌ ์ฌ์ฉ์ฒ | ์์ฉ๋ ํ์ผ, ํ์ผ ์์ ํ์ ์ | ๋์ฉ๋ ํ์ผ ์ฝ๊ธฐ ์ ์ฉ | ์ด๋ฒ ์ด์์ ์์ |
1. ๋ฌธ์ ์ํฉ ๋ถ์
๋จผ์ , ์ด์๋ฅผ ์ฌํํ๊ธฐ ์ํด QA์์ ์
๋ ฅํ๋ ํ์ผ์ ์ ๊ณต๋ฐ์,
100๋ง ๊ฑด์ ์์
ํ์ผ์ ์
๋ก๋ํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ง์ผ๋ดค์ต๋๋ค.

ํ์ง๋ง, ๊ฐ๋ฐํ๊ฒฝ์์๋ 1.4๋ถ๋ง์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ฑธ ํ์ธํ์ต๋๋ค.
์ด? ์ด ์ ๋๋ฉด ๊ด์ฐฎ์ ๊ฒ ์๋๊ฐ?
๋ผ๋ ์๊ฐ์ด ๋ค ๋ ์ฆ์, VisualVM์ ํตํด ๋ฉ๋ชจ๋ฆฌ ์ํ๋ฅผ ํ์ธ ํด ๋ณด๊ธฐ๋ก ํ์ต๋๋ค.

ํ
์คํธ ๊ฒฐ๊ณผ, ๋ฉ๋ชจ๋ฆฌ ๋์๋ ์์ง๋ง 15GB์ ๊ฐ๊น์ด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๋ชจ์ต์ ๋ณผ ์ ์์์ต๋๋ค.
"์, QAํ์์ ํ
์คํธํ ๋ ์ฌ์ฉํ๋ VM์ ์ฑ๋ฅ์ด ๋ฎ์ผ๋ฉด, ์๋ฒ๊ฐ ์ฃฝ์ ์๋ ์๊ฒ ๊ตฌ๋..."
๊ฒฐ๊ตญ, ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ก์๋จน๋ ๋ถ๋ถ์ ์ฐพ์๋ณด๊ธฐ๋ก ํ์ต๋๋ค.
2. ๋ฌธ์ ์์ธ, XSSFWorkbook
๋ฌธ์ ๋ก ์์ฌ ๋ ์ฝ๋๋ Apache POI์ XSSFWorkbook ๊ฐ์ฒด์ ์์์ต๋๋ค.
getRow(), getCell() ๋ฑ ์ฌ์ฉ๋ฒ์ด ๋งค์ฐ ์ง๊ด์ ์ด๋ผ ๋ง์ ๊ณณ์์ ์ฌ์ฉ๋๋ ๋ฐฉ์์ด์ฃ .
try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) {
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
// ... ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ List<Map>์ ์ถ๊ฐํ๋ ๋ก์ง ...
}
}
์ด ์ฝ๋์์, 100๋ง ํ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง ์์ ํ์ผ์ ๋ก๋ํ๊ณ ์์ต๋๋ค.
XSSFWorkbook์ DOM(Document Object Model) ํ์๋ก, DOM ํ์๋ ํ์ผ์ ์ฒ๋ฆฌํ๊ธฐ ์ ์,
ํ์ผ์ ๋ชจ๋ ๋ด์ฉ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ๋ถ ๋ก๋ํ์ฌ ํธ๋ฆฌ ๊ตฌ์กฐ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ญ๋๋ค.
๋ฌธ์ ๋ 100๋ง ํ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง ํ์ผ์ด 30MB์๊ณ , ์ด๋ฌํ ํฐ ํ์ผ์ ๊ฐ์ฒด๋ก ๋ณํํ๋ฉด Heap ์ฌ์ฉ๋์ ์์ฒญ๋๊ฒ ๋ง์์ง๋๋ค.
๊ฒฐ๊ตญ OutOfMemoryError๊ฐ ๋ฐ์ํ๊ฑฐ๋, ์ฌ๊ฐํ GC ์ง์ฐ์ ๋ฐ์์ํค๊ฒ ๋ ๊ฒ๋๋ค.
3. ์ฒซ๋ฒ์งธ ํด๊ฒฐ ์๋ (Simple API for XML ํ์ฉ)
SAX(Simple API for XML)๋?
SAX๋ DOM๊ณผ ๋ฌ๋ฆฌ,
ํ์ผ์ ํ ๋ฒ์ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํ์ง ์๊ณ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์์ฐจ ํ์ฑํ๋ ๋ฐฉ์์
๋๋ค.
์ผ์ข
์ ์ธํฐํ๋ฆฌํฐ์ฒ๋ผ, ํ์ค ํ์ค ์ฝ์ด๋๊ฐ๋๋ค.
์ฆ, <row>, <c> ๋ฑ์ XML ํ๊ทธ๋ฅผ ๋ง๋ ๋๋ง๋ค ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ
ํ์ํ ๋ฐ์ดํฐ๋ง ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
- ์์
์ ๋ด๋ถ์ ์ผ๋ก XML๋ก ๊ตฌ์ฑ๋์ด ์์ (
.xlsx= ZIP + XML) XSSFReader๋ฅผ ์ด์ฉํ๋ฉด SAX ๋ฐฉ์์ผ๋ก ์์ ๋ฐ์ดํฐ๋ฅผ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
์ด๋ฌํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง ํน์ ์ฝ๋๋ฅผ ์คํ์ํฌ ์ ์์ผ๋ฉด,
ํ์ผ ํฌ๊ธฐ์ ๋ฌด๊ดํ๊ฒ ๊ฑฐ์ ์ผ์ ํ ๋ฉ๋ชจ๋ฆฌ๋ง ์ฌ์ฉํ๊ฒ ๋๊ธฐ์ ๋์ฉ๋ ํ์ผ ์ฒ๋ฆฌ์ ์ด์ ์ด ์์๊ฑฐ๋ผ ์๊ฐํ์ต๋๋ค.
1๋จ๊ณ: ๋ฐ์ดํฐ ํ์ ์ฒ๋ฆฌ
SAX๋ ๋ชจ๋ ์
์ ๋จ์ ๋ฌธ์์ด๋ก ์ฝ๊ธฐ ๋๋ฌธ์,
t, s ์์ฑ๊ฐ์ ์ง์ ํ๋ณํด ํ์
์ ๊ตฌ๋ถํด์ผ ํฉ๋๋ค.
์์
์์ ์ฌ์ฉ์ ์ง์ ํ์ ๊ฐ์ ๊ฒ์ด๋ผ ์ดํดํ๋ฉด ๋ฉ๋๋ค.
- ๋ฌธ์์ด(
t="s") โSharedStringsTable๋ก๋ถํฐ ์ค์ ๊ฐ ํ์ธ - ๋ ์งํ โ
StylesTable+DateUtil.isADateFormat()์กฐํฉ์ผ๋ก ํ์ธ
// XML ํ์ผ ์ฝ๊ธฐ
XSSFReader r = new XSSFReader(OPCPackage...);
SharedStringsTable sst = r.getSharedStringsTable();
...
// XML ์ฝ๊ธฐ ์์ ๋ถ๋ถ
@Override
public void startElement(... Attributes attributes ...) throws SAXException {
...
isStringCell = "s".equals(attributes.getValue("t"));
...
}
... // ์
๋ฐ์ดํฐ ์ ์ฅ๋ถ๋ ์๋ต
// XML ์ฝ๊ธฐ ์ข
๋ฃ ๋ถ๋ถ
@Override
public void endElement(...) throws SAXException {
...
if (isStringCell) { cellValue = sst.getItemAt(...).getString(); }
...
}
์ ์ฝ๋์ ๊ฐ์ ํํ๋ก StringCell ์ฌ๋ถ๋ฅผ ํ์ธํ๋ ์ฝ๋๋ฅผ ํจ๊ป ์ฒจ๊ฐํ์ฌ
์ซ์, ๋ ์ง, ํ
์คํธ๋ฅผ ๊ตฌ๋ถํ์ฌ ์ ๋๋ก ์ฝ์ด์ฌ ์ ์๋๋ก ์ค์ ํด ์ฃผ์์ต๋๋ค.
๋ฌผ๋ก , String์ด ์๋ ๊ฒฝ์ฐ๋ ์ฝ๋์์๋ ์ ๋ถ ๋
น์ฌ ์์ต๋๋ค.
2๋จ๊ณ: ๋น ์ (Empty Cell) ์ฒ๋ฆฌ
SAX ํ์ฑ์์ ํฐ ๋ฌธ์ ๊ฐ, ๋น์ด์๋ ์
์ ๋ํด์๋ XML ํ๊ทธ ์์ฒด๊ฐ ์กด์ฌํ์ง ์๋๊ตฐ์.
์๋ฅผ ๋ค์ด, ์ค ์์
ํ์ผ ๋ด์์ C3 ์
์ด ๊ณต๋ฐฑ(๋น์ด์๋) ์ํ์ผ ๋, ์ค์ XML ํ์ผ์์๋ <C1>A</C2> <C4>B</C4> ์ด๋ ๊ฒ ์ฝ์ด์ง๋๋ค.
๋น์ด์๋ ์
์ ๊ฑด๋๋ฐ๊ธฐ ๋๋ฌธ์, ์ด ์์๊ฐ ๋ฐ๋ฆฌ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
๊ทธ๋์, HashMap<์ปฌ๋ผ ์ธ๋ฑ์ค, ๋ฐ์ดํฐ> ํํ๋ก ์์ ์ ์ฅ ํ
ํ์ด ์ข
๋ฃ๋ ๋ List๋ก ๋ณํํ๋ฉด์ ๋น ์ธ๋ฑ์ค์ ""(๋น ๋ฌธ์์ด)์ ์ฑ์ ๋ฃ์์ต๋๋ค.
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if ("row".equals(qName)) {
currentRow = new TreeMap<>(); // ๋ฐ์ดํฐ ์ด๊ธฐํ
} else if ("c".equals(qName)) {
// ์
์ฃผ์(r) ์ถ์ถ โ ์: "C3"
String cellRef = attributes.getValue("r");
currentColIndex = getColumnIndex(cellRef);
isStringCell = "s".equals(attributes.getValue("t")); // ๋ฌธ์์ด ์ฌ๋ถ ํ์ธ
cellValue.setLength(0);
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
cellValue.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
// ...
} else if ("row".equals(qName)) {
// ํ ์ข
๋ฃ ์ โ TreeMap์ List๋ก ๋ณํํ๋ฉฐ ๋น ์นธ ์ฑ์
if (!currentRow.isEmpty()) {
int maxCol = currentRow.lastKey();
List<String> rowData = new ArrayList<>(Collections.nCopies(maxCol + 1, ""));
for (Map.Entry<Integer, String> entry : currentRow.entrySet()) {
rowData.set(entry.getKey(), entry.getValue());
}
rows.add(rowData); // ์ต์ข
๋ฐ์ดํฐ ๋ฆฌ์คํธ์ ์ถ๊ฐ
}
}
}
// ์
์ฃผ์๋ฅผ ์ด ์ธ๋ฑ์ค๋ก ๋ณํ ("A"->0, "B"->1, "AA"->26 ...)
private int getColumnIndex(String cellRef) {
int index = 0;
for (char c : cellRef.replaceAll("\\d", "").toCharArray()) {
index = index * 26 + (c - 'A' + 1);
}
return index - 1;
}
์
์ ์ฃผ์(r ์์ฑ)์์ ์ถ์ถํ ์ปฌ๋ผ ์ธ๋ฑ์ค๋ฅผ Key๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ํ์ด ๋๋๋ ์์ ์, ์ด TreeMap๋ฅผ List๋ก ๋ณํํ๋ฉฐ ๋น์ด์๋ ์ธ๋ฑ์ค์๋ ๋น ๋ฌธ์์ด("")์ ์ฑ์ ๋ฃ์ต๋๋ค.
์ด๋ ๊ฒ ๋๋ฉด, ์ต์ข
List์ ๋ฌด๊ฒฐ์ฑ๋ ์ ์ง๊ฐ ๋๊ณ , ์ํ๋ ๋น ์
๋ ์ป์ด๋ผ ์ ์๊ฒ ๋ฉ๋๋ค.
3๋จ๊ณ: ์ฑ๊ณต์ ์ธ ๊ฒฐ๊ณผ
์ด๋ฐ ์์
์ ํฐ ์ฑ๋ฅ ๊ฐ์ ์ ๋ณด์ฌ์คฌ์ต๋๋ค.
์ฒ์ ์๊ฐํ๋ ๊ฐ์ค์ธ ์ธํฐํ๋ฆฌํฐ ํ์์ผ๋ก ์ฝ์ผ๋ ๋ฉ๋ชจ๋ฆฌ ๋ฝ๋ ์ ๊ฒ ๋ค์ด๊ฐ ๊ฒ์ด ๋ค์ด๋ง์์์ฃ .
๊ทธ ๋ฟ ์๋๋ผ, ์ฝ๋ ์๊ฐ ์์ฒด๋ 11์ด๋ผ๋ ์์ฒญ๋ ์ฑ๋ฅ์ ๋ณด์ฌ์ฃผ๊ฒ ๋ฉ๋๋ค.

๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ํ Heap ๊ธฐ์ค 4GB๋ก, ์์ฒญ๋ ๊ฐ์๋ฅผ ๋ณด์ฌ์คฌ์ต๋๋ค.

4. ๋ณ๋ ฌ ์ฒ๋ฆฌ ๋์
์ฑ๊ธ์ค๋ ๋ SAX ํ์๋ง์ผ๋ก๋ 11์ด๋ผ๋ ๋๋ผ์ด ๊ฒฐ๊ณผ๋ฅผ ์ป์์ง๋ง,
โ10์ด ๋ฏธ๋งโ์ ๋ชฉํ๋ก ๋ฉํฐ์ค๋ ๋(์์ฐ์-์๋น์ ํจํด)๋ฅผ ์๋ํ์ต๋๋ค.
SAX๋ก ์ฝ๋ ์ค๋ ๋(์์ฐ์)์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๊ณตํ๋ ์ค๋ ๋(์๋น์)๋ฅผ ๋ถ๋ฆฌํ๋ฉด ๋ ๋น ๋ฅด์ง ์์๊น?
BlockingQueue<List<String>> queue = new LinkedBlockingQueue<>(1000);
ExecutorService consumers = Executors.newFixedThreadPool(4);
// ์๋น์, ์ค๋ ๋ 4๊ฐ ํ
์คํธ
for (int i = 0; i < 4; i++) {
consumers.submit(() -> {
try {
while (true) {
List<String> row = queue.take();
if (row.isEmpty()) break;
// ๋ก์ง...
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// ์์ฐ์
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("row".equals(qName)) {
queue.put(new ArrayList<>(currentRow.values()));
}
}
// ๋ธ๋กํน ๋ถ๋ถ
for (int i = 0; i < 4; i++) {
queue.put(Collections.emptyList());
}
consumers.shutdown();
ํ์ง๋ง ๊ฒฐ๊ณผ๋ ๋ฐ๋์์ต๋๋ค.
11์ด โ 30.02์ด๋ก ์คํ๋ ค ๋๋ ค์ก์ต๋๋ค.

์์ธ์ ๋ช ํํ์ต๋๋ค.
- ํ ๋จ์ ๋ณํ ์์ ์ ๋งค์ฐ ๊ฐ๋ฒผ์ด ์ฐ์ฐ์ด์๊ณ
- ์ค๋ ๋ ๋๊ธฐํ(
BlockingQueue) ๋ฐ ์ ํ ์ค๋ฒํค๋๊ฐ ๋ ์ปธ๋ ๊ฒ
ํ ํ๋๋ฅผ HashMap์ผ๋ก ๋ณํํ๋ ์์
์์ฒด๊ฐ ๋งค์ฐ ๊ฐ๋ฒผ์ ๋ ํ์, ์ค๋ ๋ ๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐํํ๊ณ (BlockingQueue, ConcurrentSkipListMap),
์ค๋ ๋๋ฅผ ์ ํํ๋ ๋น์ฉ์ด ์ค์ ์์
์๊ฐ๋ณด๋ค ๋ ๋ง์ด ์์๋ ๊ฒ์
๋๋ค.
์ด ๊ฒฝํ์ผ๋ก, ๋ฉํฐ์ค๋ ๋ฉ์ ๋ง๋ฅ์ด ์๋๋ฉฐ, ์์ ์ฑ๊ฒฉ์ ๋ฐ๋ผ ๋ช ํํ ์ฌ์ฉ์ฒ๊ฐ ์๋ค๋ ๊ฒ์ ๋ค์๊ธ ๊นจ๋ซ๊ฒ ๋์๋ค์.
๋ง๋ฌด๋ฆฌ
์ด๋ฒ ์ด์๋ฅผ ํตํด ์์ ํธ๋ค๋ง์ ๋ํด ์กฐ๊ธ ๋ ๋ฐฐ์๋ณผ ์ ์์์ต๋๋ค.
- ๋์ฉ๋ ์์ ์ฒ๋ฆฌ์์๋ SAX๋ฅผ ์ฐ์ ๊ฒํ ํด๋ณผ ๊ฒ, ๋ฉ๋ชจ๋ฆฌ์ ์ด์ต์ด ์๊ธฐ์.
- ์ฑ๋ฅ ์ต์ ํ๋ ๊ฐ์ด ์๋ ์ค ์ธก์ ์ ๊ธฐ๋ฐ์ผ๋ก ํด์ผํ๋ค๋ ๊ฒ,
- ๋ณต์กํ ๋ฉํฐ์ค๋ ๋ฉ์ ๊ฐ๋ฅํ๋ค๋ฉด ๋ฌธ์ ๋ฅผ ๋จ์ํ ํ ๋ค์ ๊ณ ๋ คํ ๊ฒ.
์ฑ๋ฅ ๊ฐ์ ์, ์ธ์ ๋ ์ดํด๊ฐ ๋จผ์ ๋ค.
๊ธด ๊ธ ์ฝ์ด์ฃผ์
์ ๊ฐ์ฌํฉ๋๋ค. ํ๋ฆฐ ๋ถ๋ถ์ด๋ ๋ ๋ณด์ํ ๋งํ ๋ถ๋ถ์ด ์๋ค๋ฉด,
๋๊ธ ๋ถํ๋๋ฆฝ๋๋ค! ๐
๊ฐ์ฌํฉ๋๋ค.
