Home > Troubleshooting > πŸ” [Troubleshooting] πŸš€ WebFlux 없이 순수 Java NIO둜 κ΅¬ν˜„μ‹œ ꡬ쑰와 주의점

πŸ” [Troubleshooting] πŸš€ WebFlux 없이 순수 Java NIO둜 κ΅¬ν˜„μ‹œ ꡬ쑰와 주의점
Troubleshooting Backend Development

🎯 μ‹œμž‘ν•˜λ©°

β€œSpring WebFlux μ“°λ©΄ λ˜λŠ”λ° μ™œ ꡳ이 순수 NIOλ‘œβ€¦?”

λˆ„κ΅°κ°€λŠ” μ΄λ ‡κ²Œ 생각할 수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ λ•Œλ‘œλŠ” ν”„λ ˆμž„μ›Œν¬ λ°‘λ°”λ‹₯이 μ–΄λ–»κ²Œ λŒμ•„κ°€λŠ”μ§€ 직접 κ΅¬ν˜„ν•΄λ³΄λŠ” 것이 졜고의 ν•™μŠ΅μ΄μ£ . 마치 μžλ™μ°¨ μš΄μ „λ§Œ ν•˜λ‹€κ°€ 엔진을 직접 λœ―μ–΄λ³΄λŠ” κ²ƒμ²˜λŸΌμš”.

이 글은 IRC μ„œλ²„λ₯Ό 순수 Java NIO둜 κ΅¬ν˜„ν•˜λ©΄μ„œ λ§ˆμ£Όν•œ ν˜„μ‹€μ μΈ λ¬Έμ œλ“€κ³Ό ν•΄κ²° 방법을 λ‹΄μ•˜μŠ΅λ‹ˆλ‹€.


πŸ“¦ 1. μ˜μ‘΄μ„±: λ†€λžκ²Œλ„ 아무것도 ν•„μš” μ—†μŠ΅λ‹ˆλ‹€

// build.gradle에 μΆ”κ°€ν•  것이 μ—†μŠ΅λ‹ˆλ‹€!

λ§žμŠ΅λ‹ˆλ‹€. 단 ν•œ μ€„μ˜ μ˜μ‘΄μ„±λ„ μΆ”κ°€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

Java NIO(java.nio.*)λŠ” JDK에 κΈ°λ³Έ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. JDK 1.4λΆ€ν„° 제곡된 였래된(?) 친ꡬ죠. μ™ΈλΆ€ 라이브러리 없이 순수 Java μ½”λ“œλ§ŒμœΌλ‘œ κ³ μ„±λŠ₯ λ„€νŠΈμ›Œν¬ μ„œλ²„λ₯Ό λ§Œλ“€ 수 μžˆλ‹€λŠ” 것, λ†€λžμ§€ μ•Šλ‚˜μš”?


πŸ—οΈ 2. μ•„ν‚€ν…μ²˜: Spring Boot와 NIO의 μœ„ν—˜ν•œ 동거

μ—¬κΈ°μ„œλΆ€ν„° μ§„μ§œ 고민이 μ‹œμž‘λ©λ‹ˆλ‹€.

⚠️ 핡심 문제: 두 개의 라이프사이클

Spring Boot μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ‹œμž‘ν•˜κ³ , λΉˆμ„ μ΄ˆκΈ°ν™”ν•˜κ³ , μ›Ή μ„œλ²„λ₯Ό λ„μš°κ³ β€¦ 그리고 λ‚˜μ„œ 메인 μŠ€λ ˆλ“œλŠ” ν•  일을 λλƒ…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ NIO μ„œλ²„λŠ” while(true) λ¬΄ν•œ λ£¨ν”„λ‘œ 계속 이벀트λ₯Ό κ°μ‹œν•΄μ•Ό ν•©λ‹ˆλ‹€.

λ§Œμ•½ 메인 μŠ€λ ˆλ“œμ—μ„œ NIO 루프λ₯Ό 돌리면?
β†’ Spring Bootκ°€ 먹톡이 λ©λ‹ˆλ‹€. μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ‹œμž‘μ‘°μ°¨ μ•ˆ 되죠.

λ§Œμ•½ 별도 μŠ€λ ˆλ“œ 없이 μ‹€ν–‰ν•˜λ©΄?
β†’ μ„œλ²„λŠ” 컀λ„₯μ…˜μ„ 받을 수 μ—†μŠ΅λ‹ˆλ‹€.

βœ… ν•΄κ²° 방법: μš°μ•„ν•œ 뢄리

@Component
public class IRCServer {
    private Selector selector;
    private ServerSocketChannel serverSocket;
    
    // Spring이 κ΄€λ¦¬ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈλ‘œ 등둝
}

@Component
public class ServerInitializer implements ApplicationRunner {
    @Autowired
    private IRCServer ircServer;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // Spring Bootκ°€ μ™„μ „νžˆ 뜨고 λ‚˜μ„œ IRC μ„œλ²„ μ‹œμž‘
        ircServer.start();
    }
}

핡심 포인트:

  1. IRCServerλŠ” Spring Component둜 λ§Œλ“€μ–΄ μ˜μ‘΄μ„± μ£Όμž… ν˜œνƒμ„ λ°›μŠ΅λ‹ˆλ‹€
  2. ApplicationRunner둜 μ‹œμž‘ μ‹œμ μ„ μ œμ–΄ν•©λ‹ˆλ‹€
  3. NIO λ£¨ν”„λŠ” λ°˜λ“œμ‹œ 별도 μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰ν•©λ‹ˆλ‹€

βš™οΈ 3. Event Loop: Nettyκ°€ λ‚΄λΆ€μ—μ„œ ν•˜λŠ” κ·Έ 일

직접 κ΅¬ν˜„ν•˜λ©΄ 이런 λͺ¨μŠ΅μ΄ λ©λ‹ˆλ‹€:

public class NioIrcServer implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverSocket;

    public void start() throws Exception {
        // 1. Selector 생성 - 이벀트λ₯Ό κ°μ‹œν•  κ΄€μ œνƒ‘
        selector = Selector.open();
        
        // 2. μ„œλ²„ μ†ŒμΌ“ 생성 및 바인딩
        serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(6667)); // IRC ν‘œμ€€ 포트
        
        // 3. πŸ”‘ Non-blocking λͺ¨λ“œ μ„€μ • (이게 NIO의 핡심!)
        serverSocket.configureBlocking(false);
        
        // 4. Accept 이벀트λ₯Ό Selector에 등둝
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);

        // 5. 별도 μŠ€λ ˆλ“œμ—μ„œ 이벀트 루프 μ‹œμž‘
        new Thread(this, "IRC-NIO-Thread").start();
    }

    @Override
    public void run() {
        while (true) { // λ¬΄ν•œ 루프 - μ„œλ²„κ°€ μ‚΄μ•„μžˆλŠ” ν•œ 계속
            try {
                // ⏰ μ΄λ²€νŠΈκ°€ λ°œμƒν•  λ•ŒκΉŒμ§€ λŒ€κΈ°
                // (blockingμ΄μ§€λ§Œ, μ—¬λŸ¬ 채널을 λ™μ‹œμ— κ°μ‹œν•˜λŠ” 효율적인 λŒ€κΈ°)
                selector.select();

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iter = selectedKeys.iterator();

                while (iter.hasNext()) {
                    SelectionKey key = iter.next();

                    if (key.isAcceptable()) {
                        // πŸ†• μƒˆλ‘œμš΄ ν΄λΌμ΄μ–ΈνŠΈ 접속!
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        // πŸ“¨ ν΄λΌμ΄μ–ΈνŠΈκ°€ 데이터λ₯Ό λ³΄λƒˆμ–΄μš”
                        handleRead(key);
                    }
                    
                    iter.remove(); // μ²˜λ¦¬ν•œ ν‚€λŠ” λ°˜λ“œμ‹œ 제거!
                }
            } catch (Exception e) {
                // μ—λŸ¬ 처리...
            }
        }
    }
}

이게 λ°”λ‘œ Netty, Reactor, Vert.x 같은 λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ΄ λ‚΄λΆ€μ—μ„œ ν•˜λŠ” μΌμž…λ‹ˆλ‹€.
μš°λ¦¬κ°€ 직접 κ΅¬ν˜„ν•˜λ©΄μ„œ κ·Έλ“€μ˜ κ³ λ§ˆμ›€μ„ λΌˆμ €λ¦¬κ²Œ 느끼게 λ©λ‹ˆλ‹€β€¦ πŸ˜…


πŸ”₯ 4. ν˜„μ‹€μ˜ λ²½: 직접 κ΅¬ν˜„ν•˜λ©΄ λ§ˆμ£Όν•˜λŠ” λ¬Έμ œλ“€

β€œμ΄λ‘ μƒμœΌλ‘œλŠ” 간단해 λ³΄μ΄λŠ”λ°?”라고 μƒκ°ν–ˆλ‹€λ©΄, μ΄μ œλΆ€ν„°κ°€ μ§„μ§œμž…λ‹ˆλ‹€.

1️⃣ TCP νŒ¨ν‚· νŒŒνŽΈν™”: μ•…λͺ… 높은 Half-Packet 문제

상황 μž¬ν˜„:

ν΄λΌμ΄μ–ΈνŠΈ: "PRIVMSG #channel :Hello World\r\n" 전솑

μ„œλ²„ μˆ˜μ‹  1μ°¨: "PRIVMSG #cha"
μ„œλ²„ μˆ˜μ‹  2μ°¨: "nnel :Hello World\r\n"

ν˜Ήμ€ λ°˜λŒ€λ‘œ:

ν΄λΌμ΄μ–ΈνŠΈ: "NICK user1\r\n" + "USER user1 0 * :Real Name\r\n" 전솑

μ„œλ²„ μˆ˜μ‹  1μ°¨: "NICK user1\r\nUSER user1 0 * :Real Name\r\n"

이게 무슨 μΌμΈκ°€μš”?

TCPλŠ” 슀트림 기반 ν”„λ‘œν† μ½œμž…λ‹ˆλ‹€. λ©”μ‹œμ§€ 경계λ₯Ό 보μž₯ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

  • Fragmentation: ν•œ λ©”μ‹œμ§€κ°€ μ—¬λŸ¬ νŒ¨ν‚·μœΌλ‘œ μͺΌκ°œμ Έμ„œ 도착
  • Sticky Packets: μ—¬λŸ¬ λ©”μ‹œμ§€κ°€ ν•œ νŒ¨ν‚·μ— λΆ™μ–΄μ„œ 도착

ν•΄κ²° 방법: 직접 λ§Œλ“œλŠ” Frame Decoder

public class MessageFrameDecoder {
    private ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    public List<String> decode(ByteBuffer incoming) {
        List<String> messages = new ArrayList<>();
        
        // κΈ°μ‘΄ 버퍼에 μƒˆ 데이터 μΆ”κ°€
        buffer.put(incoming);
        buffer.flip();
        
        // \r\n을 μ°Ύμ•„ λ©”μ‹œμ§€ μΆ”μΆœ
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            if (b == '\n') {
                // μ™„μ „ν•œ λ©”μ‹œμ§€ 발견!
                messages.add(extractMessage());
            }
        }
        
        buffer.compact(); // 남은 λ°μ΄ν„°λŠ” λ‹€μŒ λ²ˆμ„ μœ„ν•΄ 보관
        return messages;
    }
}

이 뢀뢄이 κ°€μž₯ 버그가 많이 λ°œμƒν•˜λŠ” μ§€μ μž…λ‹ˆλ‹€. Netty의 DelimiterBasedFrameDecoderκ°€ μ–Όλ§ˆλ‚˜ κ³ λ§ˆμš΄μ§€ μ•Œκ²Œ λ©λ‹ˆλ‹€.

2️⃣ ByteBuffer: κΉŒλ‹€λ‘œμš΄ 친ꡬ

ByteBufferλŠ” λ‹¨μˆœν•΄ λ³΄μ΄μ§€λ§Œ μ‹€μˆ˜ν•˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€:

// ❌ ν”ν•œ μ‹€μˆ˜
buffer.put(data);
// flip() 없이 λ°”λ‘œ 읽으면? β†’ μ“°λ ˆκΈ° κ°’!
buffer.get(); 

// βœ… μ˜¬λ°”λ₯Έ μ‚¬μš©
buffer.put(data);    // Write λͺ¨λ“œ
buffer.flip();       // Read λͺ¨λ“œλ‘œ μ „ν™˜
buffer.get();        // 이제 읽을 수 있음
buffer.compact();    // 남은 데이터 정리

position, limit, capacity의 삼각관계λ₯Ό μ™„λ²½νžˆ 이해해야 ν•©λ‹ˆλ‹€.

3️⃣ λ¦¬μ†ŒμŠ€ λˆ„μˆ˜: 쑰용히 λ‹€κ°€μ˜€λŠ” μž¬μ•™

try {
    readData(key);
} catch (IOException e) {
    // ❌ μ΄λ ‡κ²Œλ§Œ ν•˜λ©΄?
    e.printStackTrace();
    // μ†ŒμΌ“μ€ μ—΄λ¦° μ±„λ‘œ, SelectionKeyλŠ” λ“±λ‘λœ μ±„λ‘œ...
    // μ‹œκ°„μ΄ μ§€λ‚˜λ©΄ "Too many open files" μ—λŸ¬ λ°œμƒ!
}

μ˜¬λ°”λ₯Έ 처리:

try {
    readData(key);
} catch (IOException e) {
    // βœ… λ¦¬μ†ŒμŠ€ 정리
    key.cancel();                    // Selectorμ—μ„œ 제거
    key.channel().close();           // μ†ŒμΌ“ λ‹«κΈ°
    clientMap.remove(key.channel()); // μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μƒνƒœ 정리
    log.info("Client disconnected: {}", e.getMessage());
}

πŸŽ“ 마치며: κ·Έλž˜λ„ ν•œ λ²ˆμ―€μ€ ν•΄λ³Ό λ§Œν•œ μ—¬μ •

순수 NIO κ΅¬ν˜„μ€ λΆ„λͺ… 고된 μž‘μ—…μž…λ‹ˆλ‹€. Nettyλ‚˜ WebFluxλ₯Ό μ“°λ©΄ 이 λͺ¨λ“  κ±Έ ν”„λ ˆμž„μ›Œν¬κ°€ ν•΄κ²°ν•΄μ£Όλ‹ˆκΉŒμš”.

ν•˜μ§€λ§Œ 이 κ²½ν—˜μ„ 톡해 μ–»λŠ” 것:

  • βœ… Non-blocking I/O의 μ§„μ§œ μž‘λ™ 원리 이해
  • βœ… ν”„λ ˆμž„μ›Œν¬κ°€ ν•΄κ²°ν•˜λŠ” λ¬Έμ œλ“€μ„ ν”ΌλΆ€λ‘œ 체감
  • βœ… μ„±λŠ₯ νŠœλ‹ μ‹œ μ–΄λ””λ₯Ό 봐야 ν•˜λŠ”μ§€ 감각 μŠ΅λ“
  • βœ… λ©΄μ ‘μ—μ„œ β€œReactor νŒ¨ν„΄β€μ„ μžμ‹  있게 μ„€λͺ… κ°€λŠ₯!

ν”„λ ˆμž„μ›Œν¬λŠ” λ§ˆλ²•μ΄ μ•„λ‹ˆλΌ λˆ„κ΅°κ°€μ˜ λ…Έλ ₯μž…λ‹ˆλ‹€. κ·Έ λ…Έλ ₯을 μ΄ν•΄ν•˜λŠ” κ°œλ°œμžκ°€ λ˜λŠ” 것, 그것이 이 μ—¬μ •μ˜ μ§„μ§œ κ°€μΉ˜μž…λ‹ˆλ‹€.