프로그래밍 공부/Java

자바 NIO 기반 입출력 및 네트워킹 1

U&MeBlue 2019. 7. 26. 14:15

들어가며

이 글은 "이것이 자바다"(신용권, 한빛미디어) 도서를 읽고 공부한 것들을 정리한 내용입니다. 잘못된 내용이 있을 수 있으며, 관련 내용을 지적해주신다면 정말 감사하게 듣겠습니다!!

1.0 NIO 소개

NIO(New Input / Output)은 자바 4에서부터 소개된 API이며, 자바 7에서는 조금 더 버전 업이 된 NIO.2 API가 소개되었다.

  • IO와 NIO의 차이점

    IO NIO
    블로킹 여부 블로킹만 지원 블로킹, 넌블로킹 모두 지원
    비동기 여부 비동기 지원하지 않음 비동기 지원
    버퍼 사용 사용하지 않음 사용
    입출력 방식 스트림 방식 채널 방식
  • 채널 방식 입출력
    IO에서는 스트림 방식의 입출력을 사용한다. 스트림은 한쪽 방향으로만 흐르는 특성이 있기 때문에, 한 파일에 대해 읽고 쓰기 위해서는 입력 스트림과 출력 스트림을 모두 생성해야 한다.
    NIO에서는 채널 방식의 입출력을 사용한다. 채널 방식은 양방향 통신이 가능하기 때문에 한 파일에 대해 읽고 쓰기 위해서 하나의 채널만 생성하면 된다.

  • 버퍼 사용
    IO에서는 스트림을 통해서 바이트 데이터를 읽을 때, 한 바이트를 바로 바로 읽고 처리한다. 읽은 데이터를 따로 저장하지 않는다면, 전에 읽은 데이터를 다시 참조할 수 없다. 한 바이트씩 입출력을 하기 때문에 입출력 성능이 떨어질 수 있다. 이를 보완하기 위해서 BufferedInputStream과 같은 보조 스트림을 사용하기도 한다.
    NIO에서는 기본적으로 버퍼를 사용해서 입출력을 해야한다. 버퍼를 사용하기 때문에 여러 바이트씩 처리가 가능하여 IO방식보다 성능이 좋다. 또한 버퍼에 임시로 데이터를 저장해두기 때문에 버퍼에 담긴 데이터를 여러번 반복해서 참조할 수 있다. 또한 버퍼 내에서 위치를 이동하며 필요한 부분만 읽고 쓸 수 있다.

  • 블로킹, 넌블로킹 지원
    IO에서는 ServerSocket, Socket 객체를 이용해서 네트워크에 연결하거나, 입출력을 할 때, 요청이 있을 때 까지 블로킹된다. 그 때문에 UI를 처리하는 코드나 다른 작업을 계속해서 처리해야 하는 thread에서는 Socket관련 메소드를 호출해서는 안된다.
    NIO에서는 블로킹, 넌블로킹 모두 지원한다. IO 블로킹과의 차이점은 블로킹 된 스레드를 인터럽트 함으로써 블로킹에서 빠져나오게 할 수 있다는 점이다. 넌블로킹 방식의 핵심은 입출력 준비가 완료된 채널만 입출력을 진행하도록 하는 것이다. 이와 같이 동작할 수 있도록 하는 것이 Selector이며 일종의 멀티플렉서 역할을 한다.

  • NIO, IO 선택
    IO에서는 연결을 수락하는 스레드, 클라이언트로부터 데이터를 입력받는 스레드가 각각 존재해야 하기 때문에 많은 수의 클라이언트가 접속을 하게 되면 일부 클라이언트는 입출력을 하지 못하게 될 수 있다. 그러나 NIO는 넌블로킹방식, 비동기방식을 통해서 많은 수의 클라이언트가 동시에 접속할 수 있도록한다.
    NIO에서는 입출력관련 작업을 스레드풀의 작업큐에 할당한다. 대용량의 입출력이 필요한 작업이라면 큐에 남아있는 다른 작업이 처리되기까지 딜레이가 발생할 수 있다.
    많은 수의 클라이언트와 연결해야 하고, 입출력하는 데이터가 크지 않은 경우, NIO를 선택한다. 적은 수의 클라이언트와 연결하고, 대용량 데이터를 처리하는 경우에는 IO를 선택한다.

1.1 TCP 넌블로킹 채널

블로킹 채널에서는 네트워킹과 관련하여 accept(), connect(), read(), write() 메소드를 호출하면 요청이 완료될 때까지 블로킹 되었다. 그래서 SeverSocketchannel과 연결된 SocketChannel마다 스레드가 필요하게 되었다. 만약 서버와 연결 요청을 하는 클라이언트가 많아지게 되면 스레드가 급격히 증가하여 서버의 성능을 저하시키는 문제가 발생하였다. 이를 해결하기 위해서 스레드풀을 사용하였다.
넌블로킹 채널에서는 accept(), connect(), read(), write() 메소드에서 블로킹이 발생하지 않는다. 예를 들어 accept() 메소드는 서버에 대한 요청이 없으면 바로 null을 리턴하게 된다. 즉, 연결 요청이나 데이터 입출력 요청이 있을 경우에는 평소와 똑같이 행동하고, 그렇지 않은 경우에는 대기 하지 않고 바로 메소드를 종료하는 것이다.
이러한 넌블로킹 채널을 활용하기 위해서는 이벤트 리스너 역할을 하는 Selecter가 필요하게 된다. 네트워킹 채널들을 셀렉터에 등록한 뒤, 채널에 요청이 들어오면 채널은 셀렉터에 통보한다. 셀렉터는 입출력 준비가 완료된 채널들을 Set의 형태로 반환하고, 작업스레드가 순차적으로 처리하게 된다.

스레드가 하나일 필요는 없고, 스레드 풀을 이용할 수도 있다. 스레드가 블로킹되지 않기 때문에 많은 양의 작업을 적은 수의 스레드로 고속 처리할 수 있게 된다.

1.2 TCP 비동기 채널

NIO에서 비동기 채널은 accept(), connect(), read(), write() 메소드 호출시 블로킹 되지 않고 바로 리턴된다. 이 점은 넌블로킹 방식과 유사하다. 차이점은 이 같은 메소드가 결과값을 반환하지 않는다는 것이다. 대신 작업이 완료되면 작업스레드가 콜백 메소드를 호출하게 된다. 따라서 메소드 실행 완료시 처리해야 하는 코드들을 이 콜백 메소드에 작성하면 된다.