14. 채널
채널은 일종의 게이트웨이이다. 기존의 파일이나 소켓 등에서 사용하던 스트림이 네이티브 IO 서비스를 이용할 수 있도록 도아주는 메소드를 제공한다. 하지만 채너은 기존의 스트림과 구별되는 몇 가지 큰 차이점이 존재한다. 채널은 데이터를 보내거나 데이터를 받기 위한 타겟으로 ByteBuffer를 사용한다는 점이다. ByteBuffer를 사용함으로써 직간접적으로 여러 가지 다양한 네이티브 서비스를 이용할 수 있기 때문이다.
또 이렇게 채널을 통해 데이터를 주고 받으면, 운영체제 수준의 네이티브 IO 서비스들을 직간접적으로 이용할 수 있다는 점이다. 따라서 기본적으로 채널을 통한 IO 통신은 기존의 스트림보다 좀더 효율적이고 기존에 지원되지 않았던 메모리 맵 파일, 파일 락킹 같은 기능들도 이용할 수 있게 된다.
마지막으로 채널은 스트림과 달리 단방향 뿐만 아니라 양방향 통신도 가능하다. 항상 양방향 통신을 이용할 수 있는 것은 아니지만 소켓 채널의 경우 별다른 설정 없이 양방향 통신이 가능하지만 파일 챈ㄹ의 경우에는 그렇지 않다.
02. 채널의 기본 인터페이스
채널은 버퍼와 달리 인터페이스 기반으로 되어 있으므로 채널이 해야 할 행동에 대한 정의만 하고 그 세부적인 구현은 각각의 하위 구현 클ㄹ스에서 담당하게 되어 있다. 이렇게 설계된 이유는 인터페이스 위주의 설계가 확장성이 뛰어나고 좀더 유엲ㄴ 시스템 구조를 갖게 되는 점도 있지만, 가장 결정적인 이유는 다른 곳에 있다. 그 이유는 각각의 운영체제마다 IO에 관련된 시스템 콜 명령어와 그 처리 루틴이 다르기 때문이다. 따라서 자바 플랫폼이 다르더라도 같은 동작을 해야하기 떄문에 어떤 행동을 해야 하는지에만 초점을 맞춘 채널 인터페이스를 정의하고 그 세부적인 행동에 대한 구현은 각 운영체제에 맞춰 대부분 네이티브 언어를 사용해서 구현하도록 만들 수 밖에 없었던 것이다.
이러한 이유로 자바 플랫폼이면 어디서나 코드를 수정하지 않아도 동일하게 동작한다(write once, run anyway)는 자바의 기본 철학에 위배되지 않게 구현하기 위해 이미 도입되었어야 할 기능들이 이런 구현상의 어려움으로 뒤늦게 나온것이다.
1. Channel, InterruptibleChannel
Channel 인터페이스는 채널의 최상위 인터페이스다.
package java.nio.channel;
import java.io.Closeable;
import java.io.IOException;
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
채널이 현재 열려 있는지의 여부를 확인하기 위한 isOpen() 메소드와 채널이 열려있을 경우, 채널을 닫기 위한 close() 메소드, 이렇게 두개만 정의하고 있다.
InterruptibleChannel 인터페이스는 비동기적으로 채널을 닫거나(close) 인터럽트(Interrupt)할 수 있는 인터페이스다.
public interface InterruptibleChannel extends Channel {
public void close() throws IOException;
}
API
이 인터페이스는 Channel 인터페이스를 상속 받고 Channel 인터페이스의 Close() 메소드를 오버라이딩한다. 이 인터페이스로 인해 기존의 스트림과 달리 명백하게 채널을 닫을 수 있게 되었다.
public class StreamTest {
static FileInputStream in = null;
public static void main(String[] args) throws Exception {
in = new FileInputStream("C:/wwwroot/test.doc");
TestThread t = new TestThread(in);
t.start();
Thread.sleep(2000);
System.out.println(in.available());
t.interrupt();
Thread.sleep(2000);
System.out.println(in.available());
}
static class TestThread extends Thread {
FileInputStream in = null;
public TestThread(FileInputStream o) {
in = o;
}
public void run() {
try {
int v = 0;
while ( ( v = in.read() ) != -1 ) {
System.out.println("Thread start...");
System.out.println(v);
Thread.sleep(1000);
}
in.close();
}catch (Exception e) {
System.out.println("Thread end...");
}
}
}
}
이 코드를 실행하게 되면 스레드와 스트림의 상태 불일치가 일어난다. in.close() 부분을 finally문으로 처리하면 상태 불일치가 발생하지 않겠지만 이러한 현상이 일어날 가능성도 있다. 이러한 상태 불일치를 발생시키지 않고 채널과 스레드의 상태를 명확하게 하기 위한 인터페이스가 바로 InterruptibleChannel이다. 대부분의 세부적인 기능을 제공하는 채널들이 이 인터페이스를 구현하고 있는데, 이 인터페이스를 구현한 채널들은 두 가지 방법으로 채널과 스레드의 상태를 명확하게 유지할 수 있게 해준다.
첫째, 비동기적인 방식으로 채널을 닫을 수 있다. InterruptibleChannel 인터페이스를 구현한 채널 내에서 입출력 작업을 하던 중 블록된 스레드가 있는 경우, 다른 스레드가 이 채널의 close() 메소드를 호출해서 채널을 종료할 수 있다. 이렇게 채널을 종료하면 블록되어 있던 스레드는 AsynchronousCloseException을 받게 된다.
둘째, 앞서와 반대의 경우다. InterruptibleChannel 인터페이스를 구현한 채널 내에서 입출력 작업을 하던 중 블록된 스레드가 있는 경우, 다른 스레드가 블록된 스레드의 interrupt() 메소드를 호출할 수 있다. 이렇게 블록된 스레드의 Interrupt() 메소드를 호출하면 채널이 닫히고 그 후 블록된 스레드는 ClosedByInterruptException을 받게 된다. 또 그 블록된 스레드 상태는 Interrupt로 설정된다.
이처럼 InterruptibleChannel 인터페이스를 구현한 채널들은 스레드 상태를 명확하게 유지할 수 있다.
2. ReadableByteChannel, WritableByteChannel, ByteChannel
ReadableByteChannel은 파라미터로 주어진 ByteBuffer로 채널안의 데이터를 읽어들이는 read()메소드만 제공한다.
package java.nio.channels;
import java.io.IOException;
import java.nio.ByteBuffer;
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException;
}
WritableByteChannel은 앞서와 반대로 ByteBuffer에 저장된 데이터를 채널로 쓰기 위한 write() 메소드만을 제공한다.
package java.nio.channels;
import java.io.IOException;
import java.nio.ByteBuffer;
public interface WritableByteChannel extends Channel {
public int write(ByteBuffer src) throws IOException;
}
ByteChannel은 ReadableByteChannel과 WritableByteChannel을 상속해서 채널로부터 데이터를 읽어들이거나 채널로 데이터를 쓰는 두 가지 동작을 모두 할 수 있는 인터페이스이다.
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
public class SimpleChannelTest {
public static void main(String[] args) throws Exception {
ReadableByteChannel src = Channels.newChannel(System.in);
WritableByteChannel dest = Channels.newChannel(System.out);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while(src.read(buffer) != -1) {
buffer.flip();
while(buffer.hasRemaining()) {
dest.write(buffer);
}
buffer.clear();
}
}
}
입력갑을 단순히 출력해주는 예제.
java.nio.channels.channels 클래스에는 채널에서 사용하는 유틸리티 클래스로 기존의 스트림을 채널로 또는 채널을 스트림으로 손쉽게 변환해주는 여러 유용한 메소드가 있다. newChannel() 메소드를 이용해서 ReadableByteChannel과 WritableByteChannel을 생성했다.
4. ScatteringByteChannel, GatheringByteChannel
Scatter/Gatter는 운영체제가 지원하는 기술이다. NIO의 채널에서는 효율적인 입출력을 위해 운영체제가 지원하는 네이티브 IO 서비스인 Scatter/Gather를 사용할 수 있도록 ScatteringByteChannel, GatheringByteChannel 인터페이스를 제공하고 있다.
Scatter/Gather는 버퍼 여러개를 한번의 IO 작업으로 처리할 수 있게 해주는 강력한 기능이다.
단순히 보면 여러 버퍼를 다루기 위한 반복문을 피하고 코드가 간단해진다는 장점도 있다. 하지만 이 인터페이스가 정말로 강력한 것은 바로 시스템 콜과 커널 영역에서 프로세스 영역으로의 버퍼 복사를 줄여주거나 또는 완전히 없애준다는 점이다.
ByteBuffer[] buf = ....;
for ( int i = 0 ; i < buf.length ; i ++ ) {
// ByteBuffer 배열의 사이즈 만큼 읽기 시스템 콜을 수행한다.
// 만약, 커널과 프로세스 간의 동일한 물리적 메모리를 참조하지 않는다면
// 반복문을 실행하는 횟수만큼 커널 영역에서 프로세스 영역으로
// 버퍼 복사가 이루어진다.
readableByteChannel.read(buf[i]);
}
ByteBuffer[] buf = ...;
// 단 한번의 시스템 콜을 수행한다.
// 만약, 앞선 경우와 같이 동일한 물리적 메모리를 참조하지 않는 경우
// 단 한번의 커널 영역에서 프로세스 영역으로의 버퍼 복사가 이루어진다.
scatteringByteChannel.read(buf);
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ScatteringByteChannel;
public class ScatterTest {
public static void main(String[] args) throws IOException {
FileInputStream fin = new FileInputStream("C:/news.txt");
ScatteringByteChannel channel = fin.getChannel();
ByteBuffer header = ByteBuffer.allocate(100);
ByteBuffer body = ByteBuffer.allocate(200);
ByteBuffer[] buffers = { header, body };
int readCount = (int) channel.read(buffers);
channel.close();
System.out.println("Read Count : " + readCount);
System.out.println("\n//-----------------------------//\n");
header.flip();
body.flip();
byte[] b = new byte[100];
heade.get(b);
System.out.println("Header : " + new String(b));
System.out.println("\n//------------------------------//\n");
byte[] bb = new byte[200];
body.get(bb);
System.out.println("Body : " + new String(bb));
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.GatheringChannel;
public class GatheringTest {
public static void main(String[] args) throws IOException {
FileOutputStream fo = new FileOutputStream("C:/test.txt");
GatheringByteChannel channel = fo.getChannel();
ByteBuffer header = ByteBuffer.allocateDirect(20);
ByteBuffer body = ByteBuffer.allocateDirect(40);
ByteBuffer[] buffers = { header, body };
header.put("Hello ".getBytes());
body.put("World!".getBytes());
header.flip();
body.flip();
channel.write(buffers);
channel.close();
}
}
Scatter/Gather를 적절한 곳에 사용하면 여러 면에서 정말 강력한 기술이 된다. 예를 들어 네트워크 프로그래밍을 하는데 TCP 기반 서버/클라이언트 통신 프로토콜을 만들어 사용할때 프로토콜에 해당하는 부분은 헤더 버퍼에 실제 전달할 내용은 바디 버퍼에 넣어 전달할 수 있다.
03. 파일 채널
- 첫째 ByteChannel 인터페이스를 구현한다. 이 인터페이스를 구현하기 때문에 읽고 쓸수 있는 양방향성을 가질 수 있다. 하지만 항상 그렇진 않다.
- 둘째 AbstractInterruptibleChannel 추상 클래스를 구현하고 있다. 이 클래스는 InterruptibleChannel 인터페이스를 구현한 클래스인데 비동기적인 방식으로 채널을 닫을 수 있게 되며 스레드와 채널 간의 상태 불일치가 발생하지 않도록 보장해준다.
- 셋째 ScatteringByteChannel, GatheringByteChannel 인터페이스를 구현한다는 점이다. 따라서 ByteBuffer 배열을 이용해서 효율적으로 읽기와 쓰기가 가능하다.
파일 채널은 항상 블록킹 모드이며 비블록킹 모드로 설정할 수 없다. 파일채널 객체는 직접 만들 수 없다 파일채널 객체는 이미 열려있는 파일 채널 객체로부터 getChannel() 메소드를 호출해서 생성된다. getChannel() 메소드로 리턴된 파일채널 객체는 파일 객체아 같은 파일에 연결되고 또한 같은 접근 권한을 갖게 된다. 대부분의 채널처럼 파일채널도 가능하면 네이티브 IO 서비스를 사용하기 위해 노력한다. 파일채널 클래스 스스로는 추상 클래스다. 파일채널 클래스의 실제 구현체는 getChannel() 메소드를 통해 얻어지는데 이렇게 얻어진 파일채널의 실제 구현체는 자신이 제공하는 메소드의 일부 또는 전부를 네이티브 코드를 사용해서 만들어 네이티브 IO 서비스를 최대한 이용하게 되어 있다. 파일채널 객체는 스레드에 안전하다. 같은 파일 채널 인스턴스에 대해 여러 스레드들이 동시에 메소드들을 호출해도 동기화 문제가 발생하지 않는다.
package java.nio.channels;
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 1. 기본 속성
public abstract int read (ByteBuffer dst)
public abstract int write (ByteBuffer src)
public abstract long size ()
public abstract long position ()
public abstract void position (long newPosition)
public abstract void truncate (long size)
public abstract void force (boolean metaData)
// 2. 파일 락킹
public final FileLock lock()
public abstract FileLock lock (long position, long size, boolean shared)
public final FileLock tryLock()
public abstract FileLock tryLock (long position, long size, boolean shared)
// 3. 메모리 맵핑
public abstract MappedByteBuffer map (MapMode mode, long position, long size)
public static class MapMode
{
public static final MapMode READ_ONLY
public static final MapMode READ_WRITE
public static final MapMode PRIVATE
}
// 4. 채널 간 직접 전송
public abstract long transferTo (long position, long count, WritableByteChannel target)
public abstract long transferFrom (ReadableByteChannel src, long position, long count)
}
파일 채널은 이 API에서 보듯이 크게 네가지 부분으로 나눠서 살펴볼 수 있다.
출처 : 김성박 송지훈 공저, 『 자바 IO & NIO 네트워크 프로그래밍』, 한빛미디어(2004.9.30), 14장 인용