๐ค ์คํธ๋ฆฌ๋ฐ์ด๋ ๋ฌด์์ธ๊ฐ?
์ผ์์ํ ๋น์ ๋ก ์ดํดํ๊ธฐ
๐ฐ ์๋๊ผญ์ง vs ๐ชฃ ๋ฌผํต
๊ธฐ์กด ๋ฐฉ์ (๋ฌผํต):
1. ํฐ ๋ฌผํต์ ๋ฌผ์ ๊ฐ๋ ์ฑ์
2. ๋ฌผํต์ด ๋ค ์ฐฐ ๋๊น์ง ๊ธฐ๋ค๋ฆผ
3. ๋ฌผํต์ ๋ค๊ณ ์ด๋ (๋ฌด๊ฑฐ์!)
4. ํ ๋ฒ์ ๋ชจ๋ ๋ฌผ์ ์ฌ์ฉ
์คํธ๋ฆฌ๋ฐ (์๋๊ผญ์ง):
1. ์๋๊ผญ์ง๋ฅผ ํ์ด๋์
2. ๋ฌผ์ด ์กฐ๊ธ์ฉ ๊ณ์ ๋์ด
3. ํ์ํ ๋งํผ๋ง ๋ฐ์์ ๋ฐ๋ก ์ฌ์ฉ
4. ์ฌ์ฉํ ๋ฌผ์ ๋ฐ๋ก ๋ฒ๋ฆผ (๊ฐ๋ฒผ์!)
๐พ ๋ฉ๋ชจ๋ฆฌ ๊ด์ ์์ ๋ณด๋ ์คํธ๋ฆฌ๋ฐ
๊ธฐ์กด ๋ฐฉ์ (์ ์ฒด ๋ก๋)
// ๐ชฃ ๋ฌผํต ๋ฐฉ์: ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
String ๊ฑฐ๋ํJSON = "{ 50๋ง๊ฐ ๋ฐ์ดํฐ... }"; // 200MB ๋ฉ๋ชจ๋ฆฌ ์ ์ !
// ๋ฉ๋ชจ๋ฆฌ ์ํฉ:
// โโโโโโโโโโโโโโโโโโโโโโโโ (200MB ์ฌ์ฉ ์ค)
// ์ฌ์ฉ์: "์์ง ํ์ฑ๋ ์ ํ๋๋ฐ ๋ฒ์จ 200MB๋ ์จ์!"
์คํธ๋ฆฌ๋ฐ ๋ฐฉ์ (์กฐ๊ธ์ฉ ์ฒ๋ฆฌ)
// ๐ฐ ์๋๊ผญ์ง ๋ฐฉ์: 8KB์ฉ ๋ฐ์์ ๋ฐ๋ก ์ฒ๋ฆฌ
while (hasMoreData) {
byte[] ์์์ฒญํฌ = readNextChunk(); // 8KB๋ง ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ
process(์์์ฒญํฌ); // ๋ฐ๋ก ์ฒ๋ฆฌ
์์์ฒญํฌ = null; // ์ฆ์ ๋ฉ๋ชจ๋ฆฌ ํด์ !
}
// ๋ฉ๋ชจ๋ฆฌ ์ํฉ:
// โ (8KB๋ง ์ฌ์ฉ) → ์ฒ๋ฆฌ → ํด์ → โ (๋ค์ 8KB) → ์ฒ๋ฆฌ → ํด์ ...
// ์ฌ์ฉ์: "๊ณ์ 8KB๋ง ์ฌ์ฉํ๋ค์!"
๐ ์ค์ ์ฝ๋๋ก ๋ณด๋ ์ฐจ์ด์
1. ๊ธฐ์กด ๋ฐฉ์ (Non-Streaming)
// โ ํ ๋ฒ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋
public AJAX_RES ๊ธฐ์กด๋ฐฉ์(String url) {
// 1๋จ๊ณ: ์ ์ฒด JSON์ String์ผ๋ก ๋ฐ์ (200MB!)
String ์ ์ฒดJSON = webClient
.get()
.uri(url)
.retrieve()
.bodyToMono(String.class) // ← ์ฌ๊ธฐ์ 200MB ๋ฉ๋ชจ๋ฆฌ ์ ์
.block();
// 2๋จ๊ณ: ์ ์ฒด JSON์ ํ์ฑ (400MB ์ถ๊ฐ!)
AJAX_RES result = objectMapper.readValue(์ ์ฒดJSON, AJAX_RES.class);
// ์ด ๋ฉ๋ชจ๋ฆฌ: 200MB + 400MB = 600MB ์ฌ์ฉ ์ค!
return result;
}
2. ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์ (Streaming)
// โ
์กฐ๊ธ์ฉ ๋ฐ์์ ๋ฐ๋ก ์ฒ๋ฆฌ
public AJAX_RES ์คํธ๋ฆฌ๋ฐ๋ฐฉ์(String url) {
return webClient
.get()
.uri(url)
.retrieve()
.bodyToFlux(DataBuffer.class) // ← 8KB ์ฒญํฌ๋ก ๋ฐ์
.collectList() // ์ฒญํฌ๋ค๋ง ๋ชจ์ (String ๋ณํ ์ํจ)
.map(์ฒญํฌ๋ค -> {
// ์ฒญํฌ๋ค์ ์คํธ๋ฆผ์ผ๋ก ๋ฐ๋ก ํ์ฑ
InputStream stream = ์ฒญํฌ๋ค์์คํธ๋ฆผ์ผ๋ก๋ณํ();
AJAX_RES result = objectMapper.readValue(stream, AJAX_RES.class);
์ฒญํฌ๋ค์์ฆ์ํด์ (); // ๋ฉ๋ชจ๋ฆฌ ํด์ !
return result;
})
.block();
// ์ด ๋ฉ๋ชจ๋ฆฌ: 400MB๋ง ์ฌ์ฉ! (200MB ์ ์ฝ)
}
๐ DataBuffer์ ์ฒญํฌ ๊ฐ๋
DataBuffer๋?
// DataBuffer = ์์ ๋ฐ์ดํฐ ์กฐ๊ฐ (๋ณดํต 8KB)
DataBuffer chunk1 = { "id": 1, "name": "ํ๊ธธ๋" } // 8KB
DataBuffer chunk2 = { "id": 2, "name": "๊น์ฒ ์" } // 8KB
DataBuffer chunk3 = { "id": 3, "name": "์ด์ํฌ" } // 8KB
// ...
// DataBuffer chunk25000 = ๋ง์ง๋ง ์กฐ๊ฐ // 8KB
// ์ ์ฒด ๋ฐ์ดํฐ = chunk1 + chunk2 + ... + chunk25000 = 200MB
์ฒญํฌ ์ฒ๋ฆฌ ๊ณผ์
// 1. ์ฒญํฌ๋ค์ ํ๋์ฉ ๋ฐ์
Flux<DataBuffer> ์ฒญํฌ์คํธ๋ฆผ = webClient...bodyToFlux(DataBuffer.class);
// 2. ์ฒญํฌ๋ค์ ๋ฆฌ์คํธ๋ก ๋ชจ์ (์์ง String ๋ณํ ์ํจ!)
List<DataBuffer> ์ฒญํฌ๋ฆฌ์คํธ = ์ฒญํฌ์คํธ๋ฆผ.collectList().block();
// 3. ์ฒญํฌ๋ค์ InputStream์ผ๋ก ์ฐ๊ฒฐ
InputStream ์ฐ๊ฒฐ๋์คํธ๋ฆผ = new SequenceInputStream(
์ฒญํฌ1์InputStream,
์ฒญํฌ2์InputStream,
์ฒญํฌ3์InputStream,
// ...
);
// 4. Jackson ํ์๊ฐ ์คํธ๋ฆผ์์ ์ง์ ์ฝ์ด์ ๊ฐ์ฒด ์์ฑ
AJAX_RES result = objectMapper.readValue(์ฐ๊ฒฐ๋์คํธ๋ฆผ, AJAX_RES.class);
// 5. ์ฌ์ฉํ ์ฒญํฌ๋ค ์ฆ์ ํด์
์ฒญํฌ๋ฆฌ์คํธ.forEach(DataBufferUtils::release); // ๋ฉ๋ชจ๋ฆฌ ํด์ !
๐ง ์คํธ๋ฆฌ๋ฐ์ ํต์ฌ ์์ด๋์ด
"ํ์ํ ๋๋ง, ์กฐ๊ธ์ฉ, ๋ฐ๋ก ๋ฒ๋ฆฌ๊ธฐ"
// ๐ฏ ํต์ฌ ์๋ฆฌ
while (๋ฐ์ดํฐ๊ฐ_๋_์๋์?) {
์์์กฐ๊ฐ = ๋ค์_8KB_๋ฐ๊ธฐ(); // 1. ์กฐ๊ธ๋ง ๋ฐ๊ธฐ
์ฒ๋ฆฌ๊ฒฐ๊ณผ = ์ฆ์_์ฒ๋ฆฌ(์์์กฐ๊ฐ); // 2. ๋ฐ๋ก ์ฒ๋ฆฌ
์์์กฐ๊ฐ = null; // 3. ์ฆ์ ๋ฒ๋ฆฌ๊ธฐ
// ๋ฉ๋ชจ๋ฆฌ๋ ๊ณ์ 8KB๋ง ์ฌ์ฉ!
}
์ ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ์ฝ๋ ๊น?
// ๊ธฐ์กด ๋ฐฉ์: ๋์์ ๋ฉ๋ชจ๋ฆฌ์ ์กด์ฌ
String ์๋ณธJSON = "200MB ๋ฐ์ดํฐ"; // 200MB ์ ์
AJAX_RES ํ์ฑ๊ฒฐ๊ณผ = parse(์๋ณธJSON); // 400MB ์ถ๊ฐ ์ ์
// ์ด 600MB ๋์ ์ฌ์ฉ!
// ์คํธ๋ฆฌ๋ฐ: ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌ
// ์์ 1: ์ฒญํฌ1 ์ฒ๋ฆฌ (8KB ์ฌ์ฉ)
// ์์ 2: ์ฒญํฌ1 ํด์ , ์ฒญํฌ2 ์ฒ๋ฆฌ (8KB ์ฌ์ฉ)
// ์์ 3: ์ฒญํฌ2 ํด์ , ์ฒญํฌ3 ์ฒ๋ฆฌ (8KB ์ฌ์ฉ)
// ...
// ์ต์ข
: ํ์ฑ๊ฒฐ๊ณผ๋ง ๋จ์ (400MB ์ฌ์ฉ)
// ์ด 400MB๋ง ์ฌ์ฉ! (200MB ์ ์ฝ)
๐ง ์ค์ ๊ตฌํ์์ ์ค์ํ ๋ถ๋ถ๋ค
1. DataBuffer ํด์ (๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง)
// โ ๏ธ ์ํ: DataBuffer๋ฅผ ํด์ ํ์ง ์์ผ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์!
List<DataBuffer> buffers = ...;
// ์ฌ์ฉ ํ ๋ฐ๋์ ํด์ ํด์ผ ํจ!
// โ
์์ : ๋ฐ๋์ ํด์ ํ๊ธฐ
try {
// ๋ฐ์ดํฐ ์ฒ๋ฆฌ...
} finally {
buffers.forEach(DataBufferUtils::release); // ํ์!
}
2. SequenceInputStream ์ฌ์ฉ
// ์ฌ๋ฌ ์ฒญํฌ๋ฅผ ํ๋์ ์คํธ๋ฆผ์ผ๋ก ์ฐ๊ฒฐ
List<InputStream> ๊ฐ๋ณ์คํธ๋ฆผ๋ค = buffers.stream()
.map(buffer -> new ByteArrayInputStream(buffer์์_๋ฐ์ดํธ_์ถ์ถ()))
.collect(toList());
// ๋ง๋ฒ! ์ฌ๋ฌ ์คํธ๋ฆผ์ ํ๋๋ก ์ฐ๊ฒฐ
InputStream ํ๋๋ก_์ฐ๊ฒฐ๋_์คํธ๋ฆผ = new SequenceInputStream(
Collections.enumeration(๊ฐ๋ณ์คํธ๋ฆผ๋ค)
);
// Jackson์ด ํ๋์ ์ฐ์๋ JSON์ผ๋ก ์ธ์!
AJAX_RES result = objectMapper.readValue(ํ๋๋ก_์ฐ๊ฒฐ๋_์คํธ๋ฆผ, AJAX_RES.class);
3. Jackson ์คํธ๋ฆฌ๋ฐ ํ์
// ์ผ๋ฐ ํ์: String ์ ์ฒด๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ ํ ํ์ฑ
AJAX_RES result = objectMapper.readValue(๊ฑฐ๋ํ_JSON_๋ฌธ์์ด, AJAX_RES.class);
// ์คํธ๋ฆฌ๋ฐ ํ์: ์คํธ๋ฆผ์์ ์กฐ๊ธ์ฉ ์ฝ์ด์ ํ์ฑ
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(inputStream);
AJAX_RES result = objectMapper.readValue(parser, AJAX_RES.class);
// JSON ๋ฌธ์์ด์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํ์ง ์์!
๐ญ ์คํธ๋ฆฌ๋ฐ vs Non-Streaming ๋น๊ต
์ธก๋ฉด Non-Streaming Streaming
| ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ | ๐ ๋ง์ (600MB) | ๐ ์ ์ (400MB) |
| ์ฒ๋ฆฌ ๋ฐฉ์ | ๐ชฃ ํ ๋ฒ์ ๋ชจ๋ | ๐ฐ ์กฐ๊ธ์ฉ ๊ณ์ |
| ์ฝ๋ ๋ณต์ก๋ | ๐ ๊ฐ๋จ | ๐ ๋ณต์ก |
| ์๋ฌ ์ฒ๋ฆฌ | ๐ฏ ๋ช ํ | ๐ ๋ณต์ก |
| ๋๋ฒ๊น | ๐ ์ฌ์ | ๐ต๏ธ ์ด๋ ค์ |
| ์ฑ๋ฅ | โก ๋น ๋ฆ (๋ฉ๋ชจ๋ฆฌ ์ถฉ๋ถ ์) | ๐ข ์ ๋น |
๐ฏ ์ธ์ ์คํธ๋ฆฌ๋ฐ์ ์จ์ผ ํ ๊น?
โ ์คํธ๋ฆฌ๋ฐ์ ์จ์ผ ํ๋ ๊ฒฝ์ฐ
if (๋ฐ์ดํฐ_ํฌ๊ธฐ > ์ฌ์ฉ๊ฐ๋ฅ_๋ฉ๋ชจ๋ฆฌ * 0.5) {
return "์คํธ๋ฆฌ๋ฐ ์ฌ์ฉํ์ธ์!";
}
if (OutOfMemoryError๊ฐ_์์ฃผ_๋ฐ์) {
return "์คํธ๋ฆฌ๋ฐ์ด ๋ต์
๋๋ค!";
}
โ ์คํธ๋ฆฌ๋ฐ์ ํผํด์ผ ํ๋ ๊ฒฝ์ฐ
if (๋ฐ์ดํฐ_ํฌ๊ธฐ < 10MB) {
return "๊ตณ์ด ๋ณต์กํ๊ฒ ํ์ง ๋ง์ธ์";
}
if (๊ฐ๋ฐ_์๋๊ฐ_์ค์) {
return "์ผ๋จ ๊ฐ๋จํ๊ฒ ํ๊ณ ๋์ค์ ์ต์ ํ";
}
๐ ๋ง๋ฌด๋ฆฌ: ์คํธ๋ฆฌ๋ฐ์ ๋ณธ์ง
์คํธ๋ฆฌ๋ฐ = "ํฐ ๊ฒ์ ์๊ฒ ๋๋์ด ์์ฐจ ์ฒ๋ฆฌ"
๐ฌ ์ํ ์คํธ๋ฆฌ๋ฐ (Netflix)
- 2์๊ฐ ์ํ๋ฅผ ํต์งธ๋ก ๋ค์ด๋ก๋ โ
- 10์ด์ฉ ๋ฐ์์ ๋ฐ๋ก ์ฌ์ โ
๐ ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ (์ฐ๋ฆฌ ์ฝ๋)
- 200MB JSON์ ํต์งธ๋ก ๋ก๋ โ
- 8KB์ฉ ๋ฐ์์ ๋ฐ๋ก ํ์ฑ โ
ํต์ฌ์ "๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ ๊ฒ ์ฐ๋ฉด์๋ ๊ฐ์ ๊ฒฐ๊ณผ๋ฅผ ์ป๊ธฐ"์ ๋๋ค!๋ณต์กํด ๋ณด์ด์ง๋ง, ๊ฒฐ๊ตญ์ "์กฐ๊ธ์ฉ ๋ฐ์์ ๋ฐ๋ก ์ฒ๋ฆฌํ๊ณ ๋ฒ๋ฆฌ๊ธฐ"๋ผ๋ ๋จ์ํ ์์ด๋์ด์์!
Spring WebFlux ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ์์ ๊ฐ์ด๋
๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ ์ ๋ต
์์ฑ์ผ: 2025๋ 9์ 26์ผ
์์ฑ์: Backend Developer
์ฃผ์ : Spring WebFlux, ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ, ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
๐ ๋ค์ด๊ฐ๋ฉฐ
์ต๊ทผ ํ๋ก์ ํธ์์ 50๋ง๊ฑด์ ์ถ์
์ด๋ฒคํธ ๋ก๊ทธ๋ฅผ Excel๋ก ๋ค์ด๋ก๋ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ฉด์, ์ฌ๊ฐํ ๋ฉ๋ชจ๋ฆฌ ๋ฌธ์ ์ ์ง๋ฉดํ์ต๋๋ค. ๊ธฐ์กด ๋ฐฉ์์ผ๋ก๋ 1.2GB์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉฐ, OutOfMemoryError๊ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํ์ต๋๋ค.
์ด ๊ธ์์๋ Spring WebFlux์ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ๋ฅผ ํตํด ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ๊ณผ, ๊ทธ ํ๊ณ์ ์ ๋ํด ์ค๋ฌด ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์ ๋ฆฌํด๋ณด๊ฒ ์ต๋๋ค.
๐ ๋ฌธ์ ์ํฉ ๋ถ์
๊ธฐ์กด ์ฝ๋์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ ํจํด
// ๊ธฐ์กด ๋ฐฉ์: ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋
@Service
public class AccessEventLogService {
public byte[] excelDownload(ExternalAccessEventLogTableReqDTO reqDTO) throws Exception {
// 1๋จ๊ณ: API ํธ์ถ๋ก JSON ๋ฐ์ดํฐ ์์ (200MB)
String raw = webClient
.post()
.uri("/api/access-events")
.bodyValue(reqDTO)
.retrieve()
.bodyToMono(String.class) // ← ์ ์ฒด JSON์ String์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ๋ก๋
.block(Duration.ofSeconds(30));
// 2๋จ๊ณ: JSON → Java ๊ฐ์ฒด ๋ณํ (400MB ์ถ๊ฐ)
AJAX_RES result = objectMapper.readValue(raw, AJAX_RES.class);
// 3๋จ๊ณ: Excel ํ์ผ ์์ฑ (์ถ๊ฐ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ)
return createExcelFile(result.getData());
}
}
๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ถ์
| ๊ตฌ์ฑ ์์ | ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ | ์ค๋ช |
|---|---|---|
| JSON ์๋ณธ ๋ฌธ์์ด | 200MB | String raw ๋ณ์ |
| ํ์ฑ๋ Java ๊ฐ์ฒด | 400MB | 50๋ง๊ฐ AccessEventLogDTO |
| JVM ์ค๋ฒํค๋ | 600MB | String ์ค๋ฒํค๋, ๊ฐ์ฒด ํค๋, Collection ๋ฑ |
| ์ด ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ | 1.2GB | ํผํฌ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ |
๐ง JVM ๋ฉ๋ชจ๋ฆฌ ์ค๋ฒํค๋ ์ดํดํ๊ธฐ
Java String์ ์ค์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋
// ๋จ์ํด ๋ณด์ด๋ String ํ๋
String eventDate = "2025-09-26 10:30:00"; // 19 characters
// ์ค์ JVM ๋ฉ๋ชจ๋ฆฌ์์๋:
// 1. String ๊ฐ์ฒด ํค๋: 16 bytes
// 2. char[] ๋ฐฐ์ด ํค๋: 16 bytes
// 3. char[] ๋ฐ์ดํฐ: 19 × 2 = 38 bytes (UTF-16 ์ธ์ฝ๋ฉ)
// 4. ๋ฉ๋ชจ๋ฆฌ ์ ๋ ฌ ํจ๋ฉ: 2 bytes
// ์ด: 72 bytes (์๋ณธ ๋ฐ์ดํฐ์ 3.8๋ฐฐ!)
๊ฐ์ฒด ์ค๋ฒํค๋ ๊ณ์ฐ
// 50๋ง๊ฐ์ AccessEventLogDTO ๊ฐ์ฒด
public class AccessEventLogDTO {
private String eventDate; // 8 bytes (์ฐธ์กฐ) + String ์ค๋ฒํค๋
private String deviceId; // 8 bytes (์ฐธ์กฐ) + String ์ค๋ฒํค๋
private String deviceName; // 8 bytes (์ฐธ์กฐ) + String ์ค๋ฒํค๋
// ... ์ด 12๊ฐ ํ๋
// ๊ฐ ๊ฐ์ฒด๋น ์ค๋ฒํค๋:
// - Object ํค๋: 16 bytes
// - ํ๋ ์ฐธ์กฐ: 12 × 8 = 96 bytes
// - ๋ฉ๋ชจ๋ฆฌ ์ ๋ ฌ: 16 bytes
// ์ด: 128 bytes per object
}
// 500,000 ๊ฐ์ฒด × 128 bytes = 64MB (๊ฐ์ฒด ํค๋๋ง)
// + String ์ค๋ฒํค๋: 300MB
// + Collection ์ค๋ฒํค๋: 50MB
// = ์ด 414MB์ ์์ ์ค๋ฒํค๋!
๐ Spring WebFlux ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ๋์
1๋จ๊ณ: ๊ธฐ๋ณธ ์คํธ๋ฆฌ๋ฐ (๊ฐ์ง ์คํธ๋ฆฌ๋ฐ)
// โ ์๋ชป๋ ์ ๊ทผ: ๊ฒฐ๊ตญ ์ ์ฒด ๋ฌธ์์ด์ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋
public String fetchDataWithFakeStreaming(DiApiReqDTO reqDTO) {
return webClient
.post()
.uri("/api/access-events")
.bodyValue(reqDTO)
.retrieve()
.bodyToFlux(DataBuffer.class) // ์ฒญํฌ๋ก ๋ฐ์ง๋ง...
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return new String(bytes, StandardCharsets.UTF_8);
})
.collect(Collectors.joining()) // ← ๊ฒฐ๊ตญ ์ฌ๊ธฐ์ ์ ์ฒด ํฉ์น๊ธฐ!
.block();
}
๋ฌธ์ ์ : collect(Collectors.joining())์์ ๊ฒฐ๊ตญ ์ ์ฒด ๋ฌธ์์ด์ ๋ฉ๋ชจ๋ฆฌ์ ๋ก๋ํ๋ฏ๋ก ๊ธฐ์กด๊ณผ ๋์ผํ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋
2๋จ๊ณ: ์ง์ง ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
// โ
์ฌ๋ฐ๋ฅธ ์ ๊ทผ: JSON ์คํธ๋ฆฌ๋ฐ ํ์ ์ฌ์ฉ
public AJAX_RES fetchDataWithRealStreaming(DiApiReqDTO reqDTO) {
return webClient
.post()
.uri("/api/access-events")
.bodyValue(reqDTO)
.retrieve()
.bodyToFlux(DataBuffer.class)
.collectList() // DataBuffer ์ฒญํฌ๋ค๋ง ์์ง
.map(dataBuffers -> {
try {
// ์ฒญํฌ๋ค์ InputStream์ผ๋ก ๋ณํ
List<InputStream> inputStreams = dataBuffers.stream()
.map(db -> {
byte[] bytes = new byte[db.readableByteCount()];
db.read(bytes);
return new ByteArrayInputStream(bytes);
})
.collect(Collectors.toList());
InputStream combinedStream = new SequenceInputStream(
Collections.enumeration(inputStreams)
);
// Jackson ์คํธ๋ฆฌ๋ฐ ํ์๋ก ์ง์ ํ์ฑ
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(combinedStream);
AJAX_RES result = objectMapper.readValue(parser, AJAX_RES.class);
// ์ฆ์ ๋ฒํผ ํด์
dataBuffers.forEach(DataBufferUtils::release);
return result;
} catch (Exception e) {
throw new RuntimeException("JSON ์คํธ๋ฆฌ๋ฐ ํ์ฑ ์คํจ", e);
}
})
.block(Duration.ofSeconds(30));
}
3๋จ๊ณ: ๋ฉ๋ชจ๋ฆฌ ํด์ ์ต์ ํ
// โ
์ฆ์ ๋ฉ๋ชจ๋ฆฌ ํด์ ํจํด
public AJAX_RES optimizedDataFetch(DiApiReqDTO reqDTO) {
log.info("๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (์์): {:.2f} GB", getUsedMemoryGB());
AJAX_RES result = fetchDataWithRealStreaming(reqDTO);
log.info("๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (ํ์ฑ ํ): {:.2f} GB", getUsedMemoryGB());
// ๊ฐ์ ๊ฐ๋น์ง ์ปฌ๋ ์
(๋์ฉ๋ ์ฒ๋ฆฌ ์์๋ง ๊ถ์ฅ)
System.gc();
Thread.sleep(100); // GC ์๋ฃ ๋๊ธฐ
log.info("๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (GC ํ): {:.2f} GB", getUsedMemoryGB());
return result;
}
private double getUsedMemoryGB() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
return usedMemory / (1024.0 * 1024.0 * 1024.0);
}
๐ ์ฑ๋ฅ ๋น๊ต ๋ฐ ์ธก์ ๊ฒฐ๊ณผ
๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋น๊ต
| ์ฒ๋ฆฌ ๋ฐฉ์ | JSON ๋ฌธ์์ด | ํ์ฑ๋ ๊ฐ์ฒด | JVM ์ค๋ฒํค๋ | ์ด ๋ฉ๋ชจ๋ฆฌ |
|---|---|---|---|---|
| ๊ธฐ์กด ๋ฐฉ์ | 200MB | 400MB | 600MB | 1.2GB |
| ๊ฐ์ง ์คํธ๋ฆฌ๋ฐ | 200MB | 400MB | 600MB | 1.2GB |
| ์ง์ง ์คํธ๋ฆฌ๋ฐ | 0MB | 400MB | 300MB | 700MB |
| ํ์ด์ง ์ฒ๋ฆฌ | 0MB | 8MB | 42MB | 50MB |
์ค์ ์ธก์ ๋ก๊ทธ
2025-09-26 10:30:00 INFO ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (์์): 0.15 GB
2025-09-26 10:30:05 INFO ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (ํ์ฑ ํ): 0.68 GB
2025-09-26 10:30:06 INFO ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ (GC ํ): 0.65 GB
โ ๏ธ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ์ ํ๊ณ์
1. ๊ทผ๋ณธ์ ํ๊ณ: ๊ฐ์ฒด ์์ฑ์ ๋ถ๊ฐํผ
// 50๋ง๊ฐ์ AccessEventLogDTO ๊ฐ์ฒด๋ ์ด๋ค ๋ฐฉ์์ ์จ๋ ์์ฑํด์ผ ํจ
List<AccessEventLogDTO> eventLogs = new ArrayList<>(); // 50๋ง๊ฐ ๊ฐ์ฒด + ์ค๋ฒํค๋
// ๊ฐ ๊ฐ์ฒด์ String ํ๋๋ค๋ ๋์ผํ๊ฒ ์์ฑ
eventLog.setEventDate("2025-09-26 10:30:00"); // ์ฌ์ ํ 72 bytes ์ค๋ฒํค๋
2. ๋ณต์ก์ฑ ์ฆ๊ฐ
- ์ฝ๋ ๋ณต์ก๋: Jackson ์คํธ๋ฆฌ๋ฐ API ํ์ต ํ์
- ๋๋ฒ๊น ์ด๋ ค์: ์คํธ๋ฆผ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ์ถ์ ๋ณต์ก
- ์ ์ง๋ณด์ ๋น์ฉ: ์ผ๋ฐ์ ์ธ Spring Boot ํจํด๊ณผ ๋ค๋ฆ
3. ์ ํ์ ํจ๊ณผ
์คํธ๋ฆฌ๋ฐ์ผ๋ก ์ ์ฝ๋๋ ๋ฉ๋ชจ๋ฆฌ: JSON ์๋ณธ ๋ฌธ์์ด 200MB
์ฌ์ ํ ์ฌ์ฉ๋๋ ๋ฉ๋ชจ๋ฆฌ: ํ์ฑ๋ ๊ฐ์ฒด + ์ค๋ฒํค๋ 500MB
์ด ์ ์ฝ ํจ๊ณผ: 1.2GB → 700GB (์ฝ 42% ์ ์ฝ)
๐ฏ ์ค๋ฌด์์์ ์ ํ ๊ธฐ์ค
์คํธ๋ฆฌ๋ฐ์ ๋์ ํด์ผ ํ๋ ๊ฒฝ์ฐ
// โ
์คํธ๋ฆฌ๋ฐ์ด ํจ๊ณผ์ ์ธ ๊ฒฝ์ฐ
if (expectedDataSize > availableMemory * 0.7) {
// ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ์ด ์์๋๋ ๊ฒฝ์ฐ
return useStreamingApproach();
}
if (isFrequentLargeDataProcessing()) {
// ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ๊ฐ ๋น๋ฒํ ๊ฒฝ์ฐ
return useStreamingApproach();
}
์คํธ๋ฆฌ๋ฐ์ ํผํด์ผ ํ๋ ๊ฒฝ์ฐ
// โ ์คํธ๋ฆฌ๋ฐ์ด ๊ณผ๋ํ ๊ฒฝ์ฐ
if (dataSize < maxHeapSize * 0.3) {
// ๋ฉ๋ชจ๋ฆฌ ์ฌ์ ๊ฐ ์ถฉ๋ถํ ๊ฒฝ์ฐ
return useSimpleApproach();
}
if (isDevelopmentPhase()) {
// ๊ฐ๋ฐ ๋จ๊ณ์์๋ ๋จ์ํจ์ด ์ฐ์
return useSimpleApproach();
}
๐ก ๋์์ ํด๊ฒฐ์ฑ ๋ค
1. JVM ํ ๋ฉ๋ชจ๋ฆฌ ์ฆ์ค
# ๊ธฐ์กด: 4GB
java -Xmx4g -jar application.jar
# ์ฆ์ค: 8GB
java -Xmx8g -jar application.jar
์ฅ์ : ์ฝ๋ ๋ณ๊ฒฝ ์์, ์ฆ์ ์ ์ฉ ๊ฐ๋ฅ
๋จ์ : ์๋ฒ ๋ฆฌ์์ค ๋น์ฉ ์ฆ๊ฐ
2. ๋ฐ์ดํฐ ์ ํ ์ ์ฑ
// ์ฌ์ฉ์์๊ฒ ๋ช
ํํ ์ ํ ์๋ด
@GetMapping("/excel-download")
public ResponseEntity<?> downloadExcel(@RequestParam int maxRecords) {
if (maxRecords > 100000) {
return ResponseEntity.badRequest()
.body("์ต๋ 10๋ง๊ฑด๊น์ง๋ง ๋ค์ด๋ก๋ ๊ฐ๋ฅํฉ๋๋ค.");
}
// ์ฒ๋ฆฌ ๋ก์ง...
}
3. ํ์ด์ง + ๋ค์ค ํ์ผ ์์ฑ
// 10๋ง๊ฑด์ฉ ๋๋์ด ์ฌ๋ฌ Excel ํ์ผ ์์ฑ
public List<String> createMultipleExcelFiles(RequestDTO reqDTO) {
List<String> fileUrls = new ArrayList<>();
int pageSize = 100000;
for (int page = 0; page < getTotalPages(reqDTO, pageSize); page++) {
String fileUrl = createExcelForPage(reqDTO, page, pageSize);
fileUrls.add(fileUrl);
}
return fileUrls; // ๋ค์ด๋ก๋ ๋งํฌ ๋ชฉ๋ก ๋ฐํ
}
๐ง ์ค์ ๊ตฌํ ์ฝ๋
์์ฑ๋ ์คํธ๋ฆฌ๋ฐ ์๋น์ค
@Service
@Slf4j
public class StreamingApiService {
private final WebClient webClient;
private final ObjectMapper objectMapper;
public StreamingApiService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(50 * 1024 * 1024)) // 50MB๋ก ์ ํ
.build();
this.objectMapper = new ObjectMapper();
}
public AJAX_RES fetchLargeDataWithStreaming(DiApiReqDTO reqDTO) {
log.info("๋์ฉ๋ ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ์์ - URL: {}", reqDTO.getUrl());
Instant startTime = Instant.now();
double initialMemory = getUsedMemoryGB();
try {
AJAX_RES result = webClient
.post()
.uri(reqDTO.getUrl())
.headers(h -> setAuthHeaders(h, reqDTO))
.bodyValue(reqDTO.getRequest())
.retrieve()
.bodyToFlux(DataBuffer.class)
.collectList()
.map(this::parseJsonStream)
.doOnSuccess(res -> forceGarbageCollection())
.block(Duration.ofSeconds(60));
// ์ฑ๋ฅ ๋ฉํธ๋ฆญ ๋ก๊น
logPerformanceMetrics(startTime, initialMemory, reqDTO.getUrl());
return result;
} catch (Exception e) {
log.error("์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {}", e.getMessage(), e);
throw new RuntimeException("๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์คํจ", e);
}
}
private AJAX_RES parseJsonStream(List<DataBuffer> dataBuffers) {
try (InputStream combinedStream = createCombinedInputStream(dataBuffers)) {
JsonFactory factory = new JsonFactory();
JsonParser parser = factory.createParser(combinedStream);
return objectMapper.readValue(parser, AJAX_RES.class);
} catch (IOException e) {
throw new RuntimeException("JSON ์คํธ๋ฆฌ๋ฐ ํ์ฑ ์คํจ", e);
} finally {
// DataBuffer ํด์
dataBuffers.forEach(DataBufferUtils::release);
}
}
private InputStream createCombinedInputStream(List<DataBuffer> dataBuffers) {
List<InputStream> inputStreams = dataBuffers.stream()
.map(db -> {
byte[] bytes = new byte[db.readableByteCount()];
db.read(bytes);
return new ByteArrayInputStream(bytes);
})
.collect(Collectors.toList());
return new SequenceInputStream(Collections.enumeration(inputStreams));
}
private void forceGarbageCollection() {
System.gc();
try {
Thread.sleep(100); // GC ์๋ฃ ๋๊ธฐ
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void logPerformanceMetrics(Instant startTime, double initialMemory, String url) {
Duration processingTime = Duration.between(startTime, Instant.now());
double finalMemory = getUsedMemoryGB();
double memoryIncrease = finalMemory - initialMemory;
log.info("์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ์๋ฃ - URL: {}, ์ฒ๋ฆฌ์๊ฐ: {}ms, ๋ฉ๋ชจ๋ฆฌ ์ฆ๊ฐ: {:.2f}GB",
url, processingTime.toMillis(), memoryIncrease);
}
private double getUsedMemoryGB() {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
return usedMemory / (1024.0 * 1024.0 * 1024.0);
}
}
๐ ๊ฒฐ๋ก ๋ฐ ๊ถ์ฅ์ฌํญ
ํต์ฌ ์ธ์ฌ์ดํธ
- ์คํธ๋ฆฌ๋ฐ์ ์์ด์์ด ์๋๋ค: JSON ์๋ณธ ๋ฌธ์์ด๋ง ์ ์ฝ๋ ๋ฟ, ํ์ฑ๋ ๊ฐ์ฒด์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ๋์ผ
- JVM ์ค๋ฒํค๋๋ ์์๋ณด๋ค ํฌ๋ค: ์ค์ ๋ฐ์ดํฐ์ 2-3๋ฐฐ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ
- ๋ณต์ก์ฑ vs ํจ๊ณผ ํธ๋ ์ด๋์คํ: 500MB ์ ์ฝ์ ์ํด ๋ณต์กํ ์ฝ๋ ์์ฑ์ด ๊ฐ์น ์๋๊ฐ?
์ค๋ฌด ์ ์ฉ ๊ฐ์ด๋๋ผ์ธ
graph TD
A[๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํ์] --> B{๋ฐ์ดํฐ ํฌ๊ธฐ ์์ธก}
B -->|< 100MB| C[๊ธฐ์กด ๋ฐฉ์ ์ ์ง]
B -->|100MB - 500MB| D{๋ฉ๋ชจ๋ฆฌ ์ฌ์ ๋ ํ์ธ}
B -->|> 500MB| E[ํ์ด์ง ์ฒ๋ฆฌ ํ์]
D -->|์ฌ์ ์์| F[JVM ํ ์ฆ์ค]
D -->|์ฌ์ ์์| G[์คํธ๋ฆฌ๋ฐ ๋์
]
C --> H[๋จ์ํจ ์ฐ์ ]
F --> I[๋น์ฉ vs ๊ฐ๋ฐ์๊ฐ]
G --> J[๋ณต์ก์ฑ ๊ฐ์]
E --> K[๊ทผ๋ณธ์ ํด๊ฒฐ]
์ต์ข ๊ถ์ฅ์ฌํญ
- 10๋ง๊ฑด ์ดํ: ๊ธฐ์กด ๋ฐฉ์ ์ ์ง
- 10-50๋ง๊ฑด: JVM ํ ๋ฉ๋ชจ๋ฆฌ ์ฆ์ค (4GB → 8GB)
- 50๋ง๊ฑด ์ด์: ํ์ด์ง ์ฒ๋ฆฌ + ๋ค์ค ํ์ผ ๋ค์ด๋ก๋
- ์คํธ๋ฆฌ๋ฐ ๋์ : ๋ฉ๋ชจ๋ฆฌ๊ฐ ์ ๋์ ์ผ๋ก ๋ถ์กฑํ๊ณ , ๋ณต์ก์ฑ์ ๊ฐ์ํ ์ ์๋ ๊ฒฝ์ฐ๋ง
๐ฌ ํผ๋๋ฐฑ: ์ด ๊ธ์ ๋ํ ์๊ฒฌ์ด๋ ์ง๋ฌธ์ด ์์ผ์๋ฉด ์ธ์ ๋ ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์!
'๊ฐ๋ฐ ์ผ์ง ๐ฉโ๐ป' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| package.json vs package-lock.json (0) | 2025.10.21 |
|---|---|
| ๋ธ๋ผ์ฐ์ ๊ฐ ๋ฉ์ท๋ค: DOM Node ๋ฉ๋ชจ๋ฆฌ ๋์ ํด๊ฒฐ๊ธฐ (0) | 2025.10.16 |
| spring boot) 50๋ง๊ฑด์ ์กฐํ, ์ฒ๋ฆฌํ๋ ๋ก์ง์์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ 1G ์ด์ ์ฌ์ฉ.. ํด๊ฒฐ๋ฒ ์ ๋ฆฌ (0) | 2025.09.26 |
| Auto Logout ์์คํ ์ฐ๊ตฌ๋ ธํธ (0) | 2025.09.22 |
| Alarm Popup ์์คํ ์ฐ๊ตฌ๋ ธํธ (0) | 2025.09.22 |