선장님과 함께하는 자바 스터디입니다.
자바 스터디 Github
github.com/whiteship/live-study
나의 Github
github.com/cmg1411/whiteShip_live_study
- 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
- InputStream과 OutputStream
- Byte와 Character 스트림
- 표준 스트림 (System.in, System.out, System.err)
- 파일 읽고 쓰기
NIO
- 새로운 IO (New IO) 라는 의미로 java 4 버전에 추가된 java.nio 패키지를 말한다.
- 이후 버전이 올라가며 지속적으로 개선되어 왔다. 아래는 java.nio 패키지 안의 패키지들이다.
서비스 제공자 클래스
내부적으로 ServiceLoader 를 이용하여 다른 드라이버를 사용할 수 있게 해준다.
(본인 ServiceLoader 관련 글 alkhwa-113.tistory.com/entry/ServiceLoader)
예를 들어, java.nio.files.spi.FileSystemProvider 는 아래와 같은 메서드를 가지고 있다.
private static List<FileSystemProvider> loadInstalledProviders() {
List<FileSystemProvider> list = new ArrayList<FileSystemProvider>();
ServiceLoader<FileSystemProvider> sl = ServiceLoader
.load(FileSystemProvider.class, ClassLoader.getSystemClassLoader());
// ServiceConfigurationError may be throw here
for (FileSystemProvider provider: sl) {
String scheme = provider.getScheme();
// add to list if the provider is not "file" and isn't a duplicate
if (!scheme.equalsIgnoreCase("file")) {
boolean found = false;
for (FileSystemProvider p: list) {
if (p.getScheme().equalsIgnoreCase(scheme)) {
found = true;
break;
}
}
if (!found) {
list.add(provider);
}
}
}
return list;
}
만약 Dropbox 와 같은 서비스를 개발한다고 할 떄 새로운 파일 시스템을 정의 한다고 치자. 그럼 새로운 FileSystem 클래스 구조가 필요할 것이고, 이 정보를 읽어와서 새로운 메서드를 제공할 수 있게 하는 것이 서비스 제공자 클래스이다.
새로운 파일 시스템을 만드는 개발자는 FileSystemProvider 클래스를 상속받아서 개발을 하면 된다.
(사실 잘 모르겠다. ㅎ 영어공부를 해야하나..)
아래는 Oracle 에서 사용자 정의 FileSystemProvider 구현 튜토리얼
docs.oracle.com/javase/8/docs/technotes/guides/io/fsp/filesystemprovider.html
아래는 Ftp 에서의 새로운 FileSystem 을 구현한 예제
IO vs NIO
새로운 단어가 많이 등장했다.
하나하나 알아보자.
스트림 vs 채널
- java.io 는 스트림 기반이라 했고, 스트림의 특성은 단방향이라 했다. 따라서 입력 스트림, 출력 스트림을 따로 만들었었다.
- java.nio 는 채널 기반이다.
- 채널의 특징은 양방향이다. 따라서 입출력 채널이 나워져있지 않다.
- 양방향 : 하나의 채널로 입출력이 가능.
non - buffer vs buffer
- buffer : 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리의 영역
- 버퍼를 사용하면 입출력 데이터를 버퍼에 모아놓고 한꺼번에 보내는 일이 가능하기 때문에, 입력단과 출력단 사이의 데이터 이동 횟수를 줄일 수 있고, 전체 시간이 더 빨라진다.
- 여기서는 바이트의 입출력을 버퍼에 임시 저장해놨다가 한꺼번에 입출력하는 용도로 쓰인다.
- NIO - buffer
- 기본적으로 버퍼를 사용한다.
- 출력시 버퍼에 쌓다가 버퍼에 쌓인 데이터를 출력 채널로 보낸다.
- 입력시 데이터를 버퍼에 쌓는다.
- 버퍼 내에서 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓸 수 있다.
- IO - non buffer
- 버퍼를 사용하지 않는다.
- 출력 스트림이 1 바이트를 쓰면 입력 스트림이 1 바이트를 읽는다.
- 버퍼를 사용하지 않기 때문에 읽은 데이터를 즉시 처리한다.
- 따라서 입력된 데이터의 위치를 이동하거나 하는 작업은 불가능하다.
- 이런 단점을 보완하기 위해 보조스트림 bufferedInputStream 등을 사용한다.
non - blocking vs blocking
- [블로킹] 은 프로그램 제어권에 대한 단어이다.
- 블로킹
- 호출자에서 메서드를 호출 했을때 메서드가 끝날 때 까지 프로그램의 제어권을 호출자에게 반납하지 않는 것을 말한다. 따라서 작업하는 메서드 이외에는 호출하는 쪽의 동작이 멈추고 메서드의 실행이 끝날 때 까지 기다린다.
- 따라서 하나의 호출마다 하나의 스레드가 필요하게 된다.
- 논블로킹
- 호출받은 메서드가 프로그램의 제어권은 바로 반납하고, 호출자 쪽은 호출자 대로, 메서드는 메서드대로 멀티스레드 같은 방식으로 동작하는 것을 말한다.
- 따라서 요청을 하는 스레드는 하나만 있으면 된다.
- IO 는 블로킹 : 만약 입력 스트림의 read() 를 실행하면 데이터 입력이 완료될때 까지 IO 스레드는 동작을 멈추고 대기한다. 이를 블로킹이라 하며 블로킹을 바져나오는 방법은 스트림을 close() 하는 방법뿐이다.
- NIO : 블로킹과 논블로킹을 모두 지원. 논블로킹의 예로는 thread는 channel에 데이터를 쓰도록 요청할 수 있다. 하지만 write 동작이 끝날때까지 기다리지 않는다. 따라서 스레드는 준비가 완료된 채널들을 바로바로 처리할 수 있는데, 이 역할을 하는 것이 Selector 이다.
번외) 동기(Synchronous) vs 비동기(Asynchronous)
- 이 두 개념은 위의 블로킹 / 넌블로킹과 매우 헷갈린다. 나도 정확하게는 모르지만 겉핥기식으로 기록을 남긴다.
- 블로킹 / 넌블로킹이 [제어권] 에 대한 개념이었다면, 동기와 비동기는 [시간] 에 대한 개념이다.
- 동기라는 것은 같은 시간에 모든 정보가 같게 유지되는것을 말하며 그를 위해서는 요청과 동시에 결과가 반환되어야 한다.
- 그에 반해 비동기는 요청과 결과의 반환이 동시에 이뤄지지 않는다. 즉 요청한 자리에서 결과가 반환되지 않는다는 말이다.
블로킹&동기 : java
논블로킹&비동기 : javascript
언제 IO 언제 NIO?
- NIO 가 new 라고, 양방향이 되는 채널이라고, 버퍼를 사용한다고, 블로킹/넌블로킹을 모두 지원한다고 무조건적으로 좋은 것은 아니다.
- NIO 의 특징들을 되짚어보면, 다음과 같은 사실을 알 수 있다.
- 넌블로킹 방식으로 처리할 수 있기 때문에, 다수의 클라이언트 연결이나 멀티 파일의 처리가 필요할 시에 스레드를 과도하게 만들 필요가 없다.
- 버퍼를 사용하기 때문에 입출력 성능이 좋다.
- 하지만 입출력 처리 자체가 오래 걸리는 경우에는 하나의 스레드에서 처리하기 때문에 뒤에서 대기하는 작업이 많아질 수 있다.
- 따라서 동시 요청이 많고, 입출력 처리작업의 시간이 적은 경우 적절하다.
- 그에반해, IO 의 특징을 되짚어보자.
- 대용량 데이터라 입출력 시간이 긴 경우에, 멀티 스레딩을 이용하여 처리할 수 있고 버퍼를 사용하지 않으므로 하나의 작업에 대해서는 성능이 낫다. (대용량 데이터의 경우 NIO 에서 버퍼의 크기가 문제가 될 수 있다.)
- 따라서 데이터가 대용량이고 연결 클라이언트 수가 적은 경우 IO 가 더 적절할 수 있다.
파일, 디렉토리
io 의 File 클래스와 같이 nio 에도 파일의 속성 정보를 제공하는 클래스와 인터페이스들이 있다. io 보다 더 다양한 기능을 제공한다.
java.nio.file 패키지와 java.nio.file.attribute 패키지에서 제공한다.
java.nio.file.Path
- java.io.File 클래스에 대응되는 인터페이스.
- 파일의 경로를 지정하기 위해 사용.
경로로 Path 객체 생성하기
- java.nio.file.Paths 클래스의 get() 정적 메서드를 사용.
Path path = Paths.get(String first, String...more);
Path path = Paths.get(URI uri);
- 경로를 매개변수의 인자로 넘기게 되는데, 절대경로 상대경로 모두 사용 가능하다.
- String first 와 가변인자로 구성되어 있는 부분은, 상위 디렉토리와 하위 디렉토리를 나눠서 인자로 넘길 수 있다는 뜻이다. 아래 모두 가능하다.
Path path = Paths.get("C:/Temp/dir/file.txt");
Path path = Paths.get("C:/Temp/dir", "file.txt");
Path path = Paths.get("C:", "Temp", "dir", "file.txt");
- Path 인터페이스에는 파일 경로에서 얻을 수 있는 여러가지 메서드를 제공한다.
docs.oracle.com/javase/8/docs/api/
java.nio.file.FileSystem
- 운영체제의 파일 시스템을 얻을 수 있다.
- FileSystem 인터페이스를 이용한다.
- FileSystem 타입의 객체는 FileSystems 의 정적 메서드 getDefault() 를 사용한다.
FileSystem fileSystem = FileSystems.getDefault();
- FileSystem 인터페이스에는 다음과 같은 메서드들이 있다.
java.nio.file.FileStore
- 드라이버를 표현한 클래스이다.
FileSystem, FileStore 예제
public class FileSystemEx {
public static void main(String[] args) throws IOException {
FileSystem fileSystem = FileSystems.getDefault();
Iterable<FileStore> fileStores = fileSystem.getFileStores();
for (FileStore fileStore : fileStores) {
System.out.println("드라이버 명 : " + fileStore.name());
System.out.println("파일시스템 : " + fileStore.type());
System.out.println("전체 공간 : " + fileStore.getTotalSpace());
System.out.println("사용중인 공간 : " + (fileStore.getTotalSpace() - fileStore.getUnallocatedSpace()));
System.out.println("할당되지 않은 공간 : " + fileStore.getUnallocatedSpace());
System.out.println("사용가능한 공간 : " + fileStore.getUsableSpace());
}
}
}
출력으로 드라이버별 정보를 얻을 수 있을 것이다.
java.nio.file.Files
- 파일과 디렉토리의 생성 및 삭제, 속성을 읽는 메서드를 제공.
- 속성 - 숨김상태인지, 디렉토리인지, 크기가 얼마인지, 소유자가 누구인지
- 생성자가 private 인 유틸클래스이다.
- 따라서 모든 메서드는 정적(static) 메서드로 제공된다.
Files 의 메서드
docs.oracle.com/javase/8/docs/api/
Files 클래스 메서드 예제
public static void main(String[] args) throws IOException {
Path path = Paths.get("파일 경로/abc.txt");
System.out.println("디렉토리 여부 : " + Files.isDirectory(path));
System.out.println("파일 여부 : " + Files.isRegularFile(path));
System.out.println("마지막 수정 시간 : " + Files.getLastModifiedTime(path));
System.out.println("파일 크기 : " + Files.size(path));
System.out.println("소유자 : " + Files.getOwner(path).getName());
System.out.println("숨김 파일 여부 : " + Files.isHidden(path));
}
java.nio.files.WatchService
- 디렉토리 내부에서 파일 생성, 삭제, 수정 등의 내용 변화를 감시하는데 사용
- 예를 들어, 에디터에서 파일을 편집하고 있을 때, 다른 곳에서 파일을 변경하면 파일 내용이 변했으니 다시 불러올 것인지를 묻는 경우가 있다.
- WatchService 객체를 만들기 위해서는 FileSystem의 정적 메서드를 호출한다.
WatchService watchService = FileSystem.getDefault().newWatchService();
버퍼
- NIO 에서는 입출력 성능을 향상시키기 위해 버퍼를 기본으로 사용한다.
- 버퍼는 읽고 쓰기가 가능한 메모리 배열이다.
Buffer 종류
어떤 데이터 타입이 저장되느냐 - 데이터 타입에 따른 분류
- Buffer 는 추상클래스로 많은 하위타입들을 가진다.
- 다음은 Buffer 의 계층도이다.
어떤 메모리(위치) 를 사용하느냐 - 넌다이렉트 / 다이렉트 버퍼
- non-direct 버퍼 : JVM 이 관리하는 힙 메모리 공간을 이용하는 버퍼, 운영체제 - JVM 을 거치므로 논다이렉트
- direct 버퍼 : 운영체제가 관리하는 메모리 공간을 이용하는 버퍼, 운영체제가 다이렉트.
- 논 다이렉트 버퍼는 힙영역에서 생성되므로 빠르게 생성된다.
- 반면, 다이렉트 버퍼는 운영체제에서 생성되므로 native 함수 (C언어) 를 실행하고 여러가지 처리를 하므로 느리다.
- 논 다이렉트 버퍼는 JVM 이 할당한 제한된 힙 메모리 영역을 사용하므로 크기가 작다.
- 다이렉트 버퍼는 운영체제의 메모리를 사용하므로 큰 영역을 할당할 수 있다.
- 넌 다이렉트 버퍼는 동작할 떄, OS 에 임시 다이렉트 버퍼를 생성하고 거기에 넌 다이렉트 버퍼의 내용을 복사하여 임시 다이렉트 버퍼가 IO 기능을 하기 때문에 입출력 성능이 낮다. (느리다)
private static final int MEGA_BYTE = 14336;
public static void main(String[] args) {
ByteBuffer directBuffer = ByteBuffer.allocateDirect(MEGA_BYTE * 1024 * 1024);
System.out.println(" 다이렉트 버퍼 생성 완료");
ByteBuffer nonByteBuffer = ByteBuffer.allocate(MEGA_BYTE*1024*1024);
System.out.println("넌다이렉트 버서 생성 성공");
}
사실 이론 자체는 위와 같지만 요즘은 워낙에 컴퓨터가 좋아져서 그런지 내 컴퓨터에서는 다이렉트, 넌다이렉트 버퍼 크기 제한이 거의 같은것 같았다.
버퍼의 생성
- 버퍼의 생성은 Buffer 추상 클래스를 확장한 각 타입 버퍼에 정의된 각각의 정적 메서드들을 사용하면 된다.
- 아래의 메서드들이 각각의 타입 버퍼 클래스에 정의되어 있다. (추상 클래스 buffer 에 추상메서드로 정의된 것이 아니라 각각 정의되어 있다.)
- non-direct 버퍼 : allocate(), wrap()
- direct 버퍼 : allocateDirect()
allocate()
각 리턴타입인 클래스에 정적 메서드로 allocate() 가 정의되어 있다.
CharBuffer charBuffer = CharBuffer.allocate(100);
// 100개의 char 형 데이터를 저장할 수 있는 버퍼 생성
wrap()
이미 생성된 자바 배열을 buffer 로 래핑하여 버퍼 객체를 생성한다.
자바 배열은 힙 영역에 생성되므로 wrap() 로 반환되는 버퍼도 넌다이렉트 버퍼이다.
int[] intArray = new int[100];
IntBuffer intBuffer = IntBuffer.wrap(intArray);
// int 형 데이터 100개를 저장할 수 있는 넌다이렉트 버퍼 생성
IntBuffer intBuffer = IntBuffer.wrap(intArray, 0, 50);
// intArray 100개 인덱스중 0 ~ 50만 넌다이렉트 버퍼 생성에 사용
allocateDirect()
이 메서드는 각 타입 버퍼 클래스에 정의된 것이 아니고, ByteBuffer 클래스에만 있다.
따라서 byte 타입이 아닌 다이렉트 버퍼를 만들고 싶다면
ByteBuffer 를 먼저 만들고, ByteBuffer 클래스에서 제공하는 asCharBuffer(), asIntBuffer() 같은 변환 메서드로 변환해야 한다.
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);
// 100 개의 byte 값을 저장할 수 있는 다이렉트 바이트 버퍼
CharBuffer charBuffer = ByteBuffer.allocateDirect(100).asCharBuffer();
// 100 개의 byte 값을 저장할 수 있는 다이렉트 바이트 버퍼를 먼저 만들고
// CharBuffer 로 변환
// char 형은 2Bytes 이므로 50 개의 char 데이터를 저장할 수 있는 다이렉트 버퍼 생성
IntBuffer intBuffer = ByteBuffer.allocateDirect(100).asIntBuffer();
// 100 개의 byte 값을 저장할 수 있는 다이렉트 바이트 버퍼를 먼저 만들고
// IntBuffer 로 변환
// int 형은 4Bytes 이므로 25 개의 int 데이터를 저장할 수 있는 다이렉트 버퍼 생성
버퍼의 위치 속성
- position : 현재 쓰거나 읽는 위치 인덱스값. 처음은 0부터 시작.
- limit : 버퍼에서 읽거나 쓸 수 있는 한계 인덱스값. 최초 버퍼를 만들었을 때에는 capacity 와 같은 값을 가진다. 읽기를 시작할 때 flip() 메서드로 limit을 position 의 위치로 이동하고, position 은 0으로 바꾼다. 이후 position 을 limit 까지 순회하며 읽는다.
- capacity : 버퍼의 최대 갯수를 말한다. 고정된 값이다.
- mark : reset() 을 실행했을 때 돌아올 인덱스를 가진다. mark() 로 지정한다.
- rewind() : limit 은 변하지 않고, position 을 다시 0으로 돌려서 읽은 값을 다시 읽을 수 있다.
- clear() : 데이터는 삭제되지 않고, position 은 0 으로, capacity와 limit 은 최댓값으로 초기화된다.
각 위치 속성은 아래의 크기 순서를 무조건 만족한다.
0 <= mark <= position <= limit <= capacity
Buffer 클래스의 메서드
- 버퍼 추상 클래스에서 제공하는 메서드와 각 타입 버퍼의 메서드가 따로 있다.
공통 메서드
- 이 메서드들은 모든 버퍼 클래스의 조상인 추상 클래스 Buffer 에 있다.
- 따라서 모든 버퍼 클래스들이 사용할 수 있다.
docs.oracle.com/javase/8/docs/api/
데이터를 읽고 저장하는 메서드
- 이 메서드들은 각 타입 버퍼 클래스에만 있다.
- 버퍼에 데이터를 저장하는 put(), 버퍼에서 데이터를 읽어오는 get() 이 있다.
- 이 메서드들은 다양하게 오버로딩되어 있는데, 절대적인 메서드와 상대적인 메서드로 나눌 수 있다.
- 절대적 메서드 : 매개변수로 int 값인 인덱스를 넘겨서 position 에 상관없이 메서드를 읽거나 쓴다.
- get(int idx), put(int idx)
- 상대적 메서드 : 버퍼 내의 현재 위치 position 에서 데이터를 읽거나 쓴다.
- get(), get(byte[] dst), putDouble(doueble value)
버퍼 관련 예외 종류
- BufferOverflowException : position 이 limit 과 같은 상태에서 put() 으로 데이터를 쓰려할 떄 발생
- BufferUnderflowException : position 이 limit 과 같을 때 get() 으로 데이터를 읽으려 할 때 발생
- InvaildMarkException : mark 를 지정하지 않은 상태에서 reset() 을 호출했을 때 발생
- ReadOnlyBufferException : 읽기 전용 버퍼에서 put() 으로 쓰기 시도시 발생
NIO 파일 입출력 - 파일 채널
- java.nio.channels.FileChannel 클래스를 이용하면 파일 읽기와 쓰기가 가능하다.
- 동기화 처리가 되어있기 때문에 Thread-safe 하다.
FileChannel 생성과 닫기.
- java.io 의 FileInputStream, FIleOutputStream 의 getChannel() 을 이용할 수 도 있다.
- FileChannel 클래스의 정적 메서드 open() 를 이용하면된다.
public static FileChannel open(Path path, OpenOption... options) { ... }
OpenOption 인터페이스를 상속한 StandardOpenOption enum의 상수들을 나열해서 인자로 넘기면 FileChannel 객체의 옵션을 부여할 수 있다.
다음은 java.nio.file 패키지의 StandardOpenOption enum 이다.
package java.nio.file;
public enum StandardOpenOption implements OpenOption {
/**
* 읽기 전용으로 열기
*/
READ,
/**
* 쓰기 전용으로 열기
*/
WRITE,
/**
* 파일 끝에 데이터를 추가 (WRITE, CREATE 와 함께 사용)
*/
APPEND,
/**
* 파일을 0 바이트로 잘라냄 (WRITE 와 함께 사용)
*/
TRUNCATE_EXISTING,
/**
* 파일이 없다면 새 파일로 생성.
*/
CREATE,
/**
* 새 파일을 만든다. 이미 있으면 예외를 발생시킨다.
*/
CREATE_NEW,
/**
* 파일 채널을 닫을때 파일을 삭제 (임시 파일용)
*/
DELETE_ON_CLOSE,
/**
* Sparse 파일
*/
SPARSE,
/**
* 파일의 내용과 메타 데이터의 모든수정사항을 동기화해야 함.
*/
SYNC,
/**
* 파일 내용의 모든 수정사항을 동기화해야 함
*/
DSYNC;
}
예를 들어 파일을 생성하고 쓰기 전용으로 만들고 싶으면 다음과 같이 하면 된다.
public class CreatFileChannel {
public static void main(String[] args) {
try(FileChannel fileChannel = FileChannel.open(
Paths.get("/Users/tomas/Documents/gitLocalRepository/white_ship_study/src/main/java/com/whiteship/white_ship_study/week13/abcd.txt"),
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
)) {
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileChannel 도 사용이 끝나면 close() 로 닫아주어야 하기 때문에 try-with-resource 로 쓰면 좋다.
객체를 생성하는 부분을 메서드로 빼면 더 깔끔해질 수 있을 것이다.
FileChannel 파일 읽기와 쓰기
- FileChannel 의 read(), write() 를 사용할 수 있따.
- write() 는 매개값으로 쓸 ByteBuffer 객체를 주면 된다.
- wirte() 의 리턴값은 ByteBuffer 의 데이터 바이트로, 작성된 바이트이다.
int bytesCount = fileChannel.write(ByteBuffer 객체);
- read() 는 매개값으로 크기를 설정한 ByteBuffer 객체를 주면 된다.
- read() 의 리턴값은 읽어온 데이터의 바이트값이다.
- 더이상 읽어올 데이터가 없으면 -1 을 반환한다.
int byteCount = fileChannel.read(ByteBuffer 객체);
파일 비동기 채널 - AsynchronousFileChannel
- FileChannel 의 read() write() 는 파일 입출력동안 블로킹이 된다. 따라서 다른 작업을 하기 위해서는 별도의 스레드를 만들어서 작업해야 한다.
- 작업의 수가 많으면 스레드의 수가 많아져 문제가 될 수 있어서 자바에서는 비동기 파일 채널을 따로 제공한다.
- AsynchronousFileChannel 클래스.
- 스레드 풀을 만들어서, 블로킹이 필요한 read(), write() 작업은 스레드 풀이 담당한다. write(), read() 메서드는 스레드풀에게 작업을 맡기고 바로 리턴한다.
- 스레드 풀을 사용하면 과도한 스레드의 생성을 방지할 수 있다.
- 스레드풀의 작업 스레드가 파일 입출력을 완료하게 되면 Callback() 가 자동으로 호출된다.
write(), read() 메서드를 호출하자 마자 return 을 하여 제어권을 넘겨주므로 논 블로킹이라 할 수 있다.
TCP 논블로킹 채널
- read(), write(), accept() 같은 메서드는 블로킹이 되는 메서드들이다.
- 따라서 ServerSocketChannel 과 연결된 SocketChannel 당 하나의 스레드가 할당되어야 하고, 과도한 스레드의 생성위험이 생긴다.
- 따라서 블로킹 채널에서는 ExecuteorService 의 스레드 풀 방식을 사용한다.
- 이와는 다른 방식으로 논블로킹 채널을 이용할 수 있다.
Selector
- java.nio.channels 의 클래스
- 논 블로킹 방식은 read(), write(), accept() 에 블로킹이 없다.
- 클라이언트의 연결 요청이 없으면 accept() 는 바로 null 을 반환하고, 클라이언트가 데이터를 보내지 않으면 read() 는 즉시 0을 반환한다.
- 소켓 통신에서는 클라이언트의 연결을 받아들이기 위해 다음과 같은 코드를 작성한다.
while(true) {
SocketChannel socketChannel = serverChannel.accept();
...
}
- 만약 사용자의 요청이 없다면, accept() 에서 null 을 반환하게 되고, while 문이 계~속 돌게 된다.
- 이는 CPU 자원이 과도하게 소비되는 문제점을 가진다.
- 따라서 Selector 라는게 등장하였다.
- 넌 블로킹 채널에 Selector 를 등록해 놓으면 클라이언트의 연결 요청이 들어오거나 데이터가 들어올 경우 채널이 Selector 에게 이를 알린다.
- Selector 는 작업 스레드가 요청이 들어온 채널의 read() 나 accept() 를 바로 실행할 수 있게 한다.
논블로킹, Selector 작동 순서
- 채널은 Selector에 자신을 등록할 떄 작업 유형을 키(SelectorKey) 로 생성하고, Selector의 관심키셋에 저장시킨다.
- 클라이언트가 처리 요청을 하면
- Selector는 관심키셋에 등록된 키 중에서 작업 처리가 준비된 키를 선택된 키셋에 별도로 저장한다.
- 작업 스레드는 선택된 키셋에 있는 키를 하나씩 꺼내어 키와 연관된 채널 작업을 처리한다.
- 선택된 키셋에 있는 모든 키를 처리하면 선택된 키셋은 비워지고, Selector 는 다시 관심 키셋에서 작업 처리 준비가 완료된 키들을 선택된 키셋으로 옮긴다.
참고
이것이 자바다 - 신용권
'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글
람다식(feat. 익명 구현 클래스 vs 람다식) (0) | 2021.03.07 |
---|---|
제네릭 (0) | 2021.02.24 |
I/O (0) | 2021.02.17 |
ServiceLoader (0) | 2021.02.14 |
Annotation (0) | 2021.02.13 |