Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

차곡차곡 쌓아가는 개발일기

[JAVA] IO와 NIO에 대해 알아보자 본문

Java

[JAVA] IO와 NIO에 대해 알아보자

KimTaeO 2024. 10. 11. 11:34

먼저 Java에서의 I/O는 외부의 정보를 필요로 할 때 사용된다. 외부라 함은 파일 그리고 네트워크를 의미한다.

Java I/O API는 여러 프로토콜로 데이터를 주고받거나 파일을 읽고 쓸 수 있다. 이 API는 데이터베이스에 접근할 수 있게도 해주며, 수많은 API들의 꼭대기에 구축되어 있는 게 바로 Java I/O API이다.

I/O나 NIO 둘 다 사용되는 목적은 같다. 이제 왜 이걸 사용하는지 알았으니 둘의 차이점에 대해 알아보자

I/O 와 NIO의 차이점

I/O는 Stream이라는 데이터가 한 방향으로만 흐르는 통로를 사용해 외부에서 데이터를 가져오고 내보낸다. NIO는 Channel이라는 양방향으로 데이터가 흐를 수 있는 통로를 사용해 외부에서 데이터를 가져오고 내보낸다.

I/O 는 Stream을 통해 데이터를 읽는다고 하였다. 이같은 방식은 한번에 데이터를 읽을 때 1바이트 (문자열을 읽을 때에는 2바이트) 밖에 읽지 못한다. 또한 Stream 단어 그대로 흐름이라는 뜻을 가지고 있기 때문에 읽은 데이터를 저장하지 않고 다음 데이터를 읽었다면 이전 데이터를 다시 읽지 못한다. NIO는 채널을 사용하여 입출력을 동시에 수행할 수 있으며, 한번에 데이터를 읽어와 버퍼에 저장하게 된다. 따라서 원하는 위치에 있는 데이터에 접근하는데 제약이 없다.


I/O 는 동기 & Blocking 방식으로 동작하지만 NIO는 이들을 지원하는 동시에 비동기 & Non-Blocking 방식도 지원한다.

Blocking 방식은 데이터가 송수신 되기 전까지 해당 스레드가 대기 상태로 존재해야 한다. 따라서 동시에 요청을 받는 경우에는 동시 요청 수만큼의 스레드가 필요하다. Non-Blocking 방식은 데이터가 송수신 되지 않더라도 블로킹 되지 않도록 해서 하나의 스레드가 여러 입출력을 수행할 수 있도록 해주는 작업이다.

NIO의 동작 방식

Non-Blocking은 Blocking에 비해 복잡하기 때문에 간단히 데이터를 처리하는 흐름을 알아보도록 하자. SocketChannel을 통해 클라이언트 소켓과의 연결을 만들고 이를 Selector에 등록한다. 이때 Selector가 감지할 채널에서 발생하는 이벤트도 함께 지정해 주어야 한다. Selector에 등록된 채널의 정보를 가지고 있는 객체를 SelectionKey라 한다. Set에 SelectionKey를 저장하며, select()를 사용해서 등록된 채널에 대한 이벤트를 감시한다. 여러 채널에서 발생한 이벤트들을 돌아가면서 처리하는 것이다.

NIO 서버의 동작 방식의 이해를 돕기 위해 코드를 첨부하였다

        try(
                // 자바의 NIO 컴푸넌트 중 하나인 Selector는 자신에게 등록된 채널에 변경 사항이 발생했는지 검사하고 변경 사항이 발생한 채널에 대한 접근을 가능하게 해준다
                Selector selector = Selector.open();

                // 블로킹 소켓과 대응되는 논블로킹 서버 소켓 채널을 생성한다 블로킹 소켓과는 다르게 채널을 생성한 후 포트를 바인딩한다
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()
        ) {

            if(serverSocketChannel.isOpen() && selector.isOpen()) { // 생성한 Selector와 Channel이 생성이 잘 되었는지 확인한다
                // 소켓 채널의 모드는 기본이 블로킹이기 때문에 메서드를 호출해 논블로킹 모드로 변경해주어야 한다
                serverSocketChannel.configureBlocking(false);

                // 클라이언트의 연결을 받을 포트를 지정하고 채널에 할당한다
                serverSocketChannel.bind(new InetSocketAddress(8888));

                // ServerSocketChannel 객체를 Selector에 등록한다. Selector가 감지할 이벤트는 연결 요청에 해당하는 SelectionKey.OP_ACCEPT이다
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                System.out.println("접속 대기중");

                while(true) {
                    // Selector에 등록된 채널에서 변경 사항이 발생했는지 검사한다. Selector에 아무런 I/O 이벤트가 발생하지 않는다면 스레드는 이 부분에서 블로킹된다. 만약에 블로킹을 피하고 싶다면 selectNow() 메서드를 사용하자.
                    selector.select();
                    // Selector에 등록된 채널 중에서 I/O 이벤트가 발생한 채널들의 목록을 조회한다
                    Iterator<SelectionKey> keys = selector.selectedKeys().iterator();

                    while(keys.hasNext()) {
                        SelectionKey key = (SelectionKey) keys.next();
                        // 조회된 I/O 이벤트들 중 동일한 이벤트가 감지되는 것을 방지하기 위해서 조회된 목록에서 제거한다
                        keys.remove();

                        if(!key.isValid()) {
                            continue;
                        }

                        // 발생한 이벤트의 종류가 연결 요청인지 확인하고 맞다면 연결 요청을 처리하는 메서드를 호출한다
                        if(key.isAcceptable()) {
                            acceptOP(key, selector);
                        }
                        // 발생한 이벤트의 종류가 데이터 수신 요청인지 확인하고 맞다면 데이터 수신을 처리하는 메서드를 호출한다
                        else if (key.isReadable()) {
                            readOP(key);
                        }
                        // 발생한 이벤트의 종류가 데이터 송신인지 확인하고 맞다면 데이터를 송신하는 메서드를 호출한다
                        else if (key.isWritable()) {
                            writeOP(key);
                        } else {
                            System.err.println("서버 소켓 생성 실패.");
                        }
                    }
                }
            }
        } catch(IOException ex) {
            System.err.println(ex);
        }

언제 I/O를 사용하고 NIO를 사용해야 할까

위에서 알아봤듯이 그저 소켓을 accept() 하고 Stream을 열어 데이터를 주고받는 I/O와 달리 NIO는 많은 연결을 처리하는데에는 적절하지만 여러 채널의 이벤트들을 핸들링하기 때문에 한 채널의 이벤트를 처리하는 도중엔 다른 이벤트를 처리하지 못한다. 이러한 점에 비추어 보편적으로 I/O는 상대적으로 적은 연결과 많은 데이터 처리에 유리하다.

코드 출처 : 자바 네트워크 소녀 Netty 2장 중