๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ‘ฉ‍๐Ÿ’ป

Spring WebFlux ์ŠคํŠธ๋ฆฌ๋ฐ ์ฒ˜๋ฆฌ ์™„์ „ ๊ฐ€์ด๋“œ

by chuyj15 2025. 9. 26.
728x90
๋ฐ˜์‘ํ˜•
SMALL

๐Ÿค” ์ŠคํŠธ๋ฆฌ๋ฐ์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

์ผ์ƒ์ƒํ™œ ๋น„์œ ๋กœ ์ดํ•ดํ•˜๊ธฐ


๐Ÿšฐ ์ˆ˜๋„๊ผญ์ง€ 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);
    }
}

๐Ÿ“š ๊ฒฐ๋ก  ๋ฐ ๊ถŒ์žฅ์‚ฌํ•ญ

ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ

  1. ์ŠคํŠธ๋ฆฌ๋ฐ์€ ์€์ด์•Œ์ด ์•„๋‹ˆ๋‹ค: JSON ์›๋ณธ ๋ฌธ์ž์—ด๋งŒ ์ ˆ์•ฝ๋  ๋ฟ, ํŒŒ์‹ฑ๋œ ๊ฐ์ฒด์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์€ ๋™์ผ
  2. JVM ์˜ค๋ฒ„ํ—ค๋“œ๋Š” ์˜ˆ์ƒ๋ณด๋‹ค ํฌ๋‹ค: ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ 2-3๋ฐฐ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ
  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[๊ทผ๋ณธ์  ํ•ด๊ฒฐ]

์ตœ์ข… ๊ถŒ์žฅ์‚ฌํ•ญ

  1. 10๋งŒ๊ฑด ์ดํ•˜: ๊ธฐ์กด ๋ฐฉ์‹ ์œ ์ง€
  2. 10-50๋งŒ๊ฑด: JVM ํž™ ๋ฉ”๋ชจ๋ฆฌ ์ฆ์„ค (4GB → 8GB)
  3. 50๋งŒ๊ฑด ์ด์ƒ: ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ + ๋‹ค์ค‘ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ
  4. ์ŠคํŠธ๋ฆฌ๋ฐ ๋„์ž…: ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ ˆ๋Œ€์ ์œผ๋กœ ๋ถ€์กฑํ•˜๊ณ , ๋ณต์žก์„ฑ์„ ๊ฐ์ˆ˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ

๐Ÿ’ฌ ํ”ผ๋“œ๋ฐฑ: ์ด ๊ธ€์— ๋Œ€ํ•œ ์˜๊ฒฌ์ด๋‚˜ ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ์–ธ์ œ๋“  ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”!

728x90
๋ฐ˜์‘ํ˜•
LIST