by Raffi Krikorian, 역 한빛리포터 2기 신동섭 자바가 프로그래밍 언어로 유명해진 이유는 입력/출력과 네트워킹 동작을 행함에 있어 자바가 지니고 있는 편리성 때문이다. 자바는 C#과 같은 맥락을 가지고 있으며 복잡함에 대해 신경쓰지 않아도 되는 라이브러리를 제공한다. 이 시리즈 중 앞의 두 기사에서는 자바 프로그래머가 간단한 C# 프로그램을 구축하기 위해 알아야 하는 언어 구조의 다른점을 중점적으로 다루었다. 이번 기사는 이러한 라이브러리들의 공통적 사용패턴을 따라 I/O와 네트워킹을 다루는 몇몇 C# 네임스페이스에 중점을 두고 작성할 예정이다.
스트림의 이해 자바와 C#에서 스트림이란 보통 콘솔로부터 콘솔까지, 파일시스템에서 파일시스템까지, 네트워크에서 네트워크까지 바이트를 읽고 쓰는 것을 포함한다. 두 언어 모두에서 스트림 패러다임은 하나의 프로그램을 바이트 그룹으로 동작하거나 이동할 필요가 있을 때 더 일반적으로 사용된다. 자바는 두 개의 추상 클래스인
java.io.InputStream과
java.io.OutputStream을 제공한다. 이 클래스는 프로그램이 이러한 두 스트림 서브 타입으로부터 읽고 쓸 수 있게 인증될 필요가 있는 구현되지 않은 메소드를 포함하고 있다. 반면, C#은 이러한 두 개의 클래스를
System.IO.Stream 하나로 통합한다. 하나는 읽기를 하고 하나는 쓰기를 하는 객체 두 개 대신에 C# 스트림 객체는
CanRead와
CanWrite 프로퍼티를 그 능력을 검증받을 필요가 있다.
동기 I/O 동기 I/O는 두 언어에 있어서 문법적으로 아주 비슷하다.
C# System.IO.Stream와 함께 자바
java.io.InputStream와
java.io.OutputStream은 바이트 배열로 동작하는 메소드와 함께 한 번에 1 바이트를 동작하는 메소드를 가진다. (C#은 전체 배열에 대한 운영 문법이 필요하다. C#은 대신에 offset/length 쌍으로 배열을 사용하는 방법만 알고있다.)
기능 |
자바 |
C# |
1 바이트를 읽어라 |
java.io.InputStream에 있는 read() 메소드 |
System.IO.Stream에 있는 ReadByte() 메소드 |
전체 바이트 배열을 읽어라 |
java.io.InputStream에 있는 read( byte[] b ) 메소드 |
그러한 구문적 메소드 없음 - offset/count 쌍을 가진 읽기 메소드를 사용할 것 |
바이트 배열의 일부분으로 읽어라. |
java.io.InputStream에 있는 read( byte[] b, int off, int len ) 메소드 |
System.IO.Stream에 있는 Read( byte[] buffer, int offset, int length ) 메소드 |
1 바이트를 작성해라. |
java.io.OutputStream에 있는 write( int b ) 메소드 |
System.IO.Stream에 있는 WriteByte( byte value ) 메소드 |
전체 바이트 arrayjava.io.OutputStream을 작성해라 |
write( byte[] b ) 메소드 |
특정 메소드 없음 - offset/count 쌍을 요청하는 메소드를 사용할 것 |
바이트 배열의 일부분을 작성해라 |
java.io.OutputStream에 있는 write( byte[] b, int off, int len ) 메소드 |
System.IO.Stream에 있는 Write( byte[] buffer, int offset, int length ) 메소드 |
[표 1] 자바와 C#에서 동기 I/O 메소드 동시에 스트림하도록 일하기 원할 때 사용하는 메소드들 자바 프로그래머들을 위한 조언 중 하나는
IOException을 포착하는 것을 잊지 말라는 것이다. 자바와는 달리 C# 컴파일러는 컴파일 할 때 예외(exceptions)를 강요하지 않기 때문이다.
비동기 I/O 자바는 I/O 동작을 공식적으로 비동기로 수행하는 방법이 부족하다. 스트림에 발생하는 읽기 또는 쓰기 야기시킨 후 그 결과를 나중에 점검하는 '내장' 방법이 없다. 자바에서 가장 근접한 시뮬레이션은 동기 메소드 주위에 java.lang.Thread를 생성하는 것이며 콜백을 실행하거나 부작용을 일으키는 스레드를 가지고 있거나 I/O 동작상태에 있을 수도 있다. C#은 그 라이브러리에 내장된 비동기 I/O 메소드를 가지고 있다. 예를 들어 자바에서 비동기
read(byte[] b) 호출을 실행하기 위해 콜백 또는 이후에 점검될 수 있는 상태 모두에서 할 수 있는 구현은 아래와 같다.
// variables to hold the side effects of the read
int read; // to hold the result of the read
IOException exception; // to hold a possible exception
Object wait ...
// some value to block on until the end of the = call
// a wrap around a read on the InputStream variable "is"
( new Thread( new Runnable() {
public void run() {
try {
read is.read();
} catch( IOException error ) {
exception error;
} finally {
synchronized( wait ) {
// wake up all other threads waiting on this
// read
wait.notifyAll();
}
// call a callback method
callback();
}
}
} ) ).start();
|
이것은 '읽기'와 '예외'에 각각 저장된 것에 읽기 값 또는 읽을 때 포착되는 예외를 발생시킬 것이다. 다른 스레드는 변수 'wait'를 수반하거나 비동기 읽기가 완성될 때를 알기 위해 'callback' 메소드를 구현할 수도 있다. 이것을 정리함에 있어 C#은 위에서 언급한 모든 기능을 포함하는
BeginRead와
EndRead 메소드를 제공한다.
AsyncCallback과 상태 객체인 두 개 이상의 변수를 가지는 것을 제외하고
BeginRead와
Read의 서명은 비슷하며
BeginRead의 서명은 나중에 비동기 읽기의 과정을 점검하는데 사용하는
IasyncResult 객체를 환원한다.
BeginRead의 표준사용은 아래와 같다.
IAsyncResult iar sbs.BeginRead( buffer, 0, 1, new AsyncCallback( = callback ), null );
|
아래와 같이 보이는
callback 메소드
public void callback( IAsyncResult iar )
|
실제로 몇 바이트를 읽었는지 알아 보기위해 EndRead 메소드 호출은
IAsyncResult 오브젝트로 호출될 수 있다.
EndRead를 호출하는 것은
BeginRead이 완성될때까지 블록킹 할 것이라는 것을 경고받는다. 블록킹 없는 읽기상태를 발견하기 위해
IasyncResult 환원에 대한
IsCompleted 프로퍼티가 리턴되는지 점검해라. 또한
buffer 변수의 내용이 비동기식 읽기가 완성될 때까지 보장되지 않는다는 것에 주목해야 한다.
스트림 구현 자바와 C# 스트림은 여러분이 자바 스트림에 대해 알고있는 것처럼 C# 스트림 구현이 그렇게 어렵지 않기 때문에 거의 비슷하다고 볼 수 있다. 이 두 가지를 구현하는데 있어 주요한 차이점은 적절한 읽기, 쓰기 메소드가 구현되어야 하는 것 외에도 C# 스트림 클래스는 읽기 또는 쓰기 두 가지 모두가 가능하기 때문에 성능 프로퍼티는 스트림이 할 수 있는 것을 정확하게 반영해야 한다는 것이다.
기능 |
자바 |
C# |
읽기 |
java.io.InputStream에 있는 read() 메소드 |
System.IO.Stream에 있는 ReadByte() 메소드 |
검색 |
java.io.InputStream에 있는 메소드를 생락하고 파일 내로 향하는 필요한 연산을 수행하고, markSupported, mark 및 스트림으로 되돌아가는 reset 메소드를 구현 |
이 스트림이 Seek 메소드를 검색하고 구현할 수 있는지 프로그램을 보고하기 위해 System.IO.Stream에 있는 CanSeek 프로퍼티를 사용해라. |
쓰기 |
java.io.OutputStream에 있는 write 메소드 구현 (다시 한 번, 최적화 하기위해 OutputStream에 있는 다른 write 메소드들이 제작될 수 있음) |
CanWrite라고 명명된 System.IO.Stream 프로퍼티로부터 참값을 환원해라. 그리고 최소한 Write, WriteByte, BeginWrite/EndWrite 메소드 중에 하나를 써라. |
[표 2] 자바와 C#에서 스트림 구현하기 대응하는 스트림 클래스에서 구현될 필요가 있는 메소드 리스트 C# 스트림 클래스는 기능 구현을 위한 메소드에 관련된 많은 옵션을 제공한다. 모든 메소드의 기본 구현이 다른 메소드를 사용할 수 있는 것처럼
Read와
Write(두 가지 모두 바이트 배열, 오프셋과 길이를 차지함)를 오버라이딩 하는 것은 일반적으로 충분하다. 단순하게 읽기/쓰기 메소드 중 최소한 하나만이라도 오버라이딩 하는 것은 전체 스트림에 요구되는 기능을 추가해 줄 것이다.
ReadByte와
WriteByte의 기본 구현은 롱 값을 바이트 배열로 바꿀 것이다. 반면에 비동기
BeginRead와
BeginWrite 메소드 기본 구현은 분할된 스레드로
Read와
Write를 실행할 것이다.
읽기와 쓰기 이 기사의 대부분은 C#에서
System.IO.Stream 클래스와 관련된 사항에 많은 시간을 할애했다. 그렇지만 가끔씩
System.IO.TextReader와
System.IO.TextWriter에 대한 사항에 대해서도 생각해 볼 필요가 있다. 이 두 클래스는 다른 클래스 타입이 쓰기를 다루고 있는 동안 또다른 클래스 타입은 읽기를 하는 자바 I/O 모델과 아주 흡사하다. 그런 점에서 C# 스트림 객체는 바이트를 동시에 읽고 쓰는 방법에 대한 지식을 캡슐화 하고
TextReader와
TextWriter 클래스는 읽기와 쓰기 특성을 각각 캡슐화한다. 위의 두 가지로부터 나온 가장 일반적으로 사용되는 클래스는
System.IO.StreamReader와
System.IO.StreamWriter 클래스로서 이들 두 클래스는
Stream 객체를 취할 수 있으며 선택사항으로 바이트 스트림을 문자 스트림(C#은 기본으로 UTF-8 인코더/디코더를 사용)으로 바꾸는 방법을 명세하는
System.Text.Encoding 객체를 취할 수 있다. 만약 스트림과 유사한 기능으로 접근하는 것이 요구되고, 바이트로 동작하는 것 대신 문자 사용을 위해 프로그래밍을 하고 있다면 스트림 클래스의 미묘한 차이를 다루는 것보다
TextReader와
TextWriter 클래스의 서브 클래스를 구현하는 것이 더 쉬울 수도 있다. 만약 스트림이 적절히 구현되었다고 할지라도, 여러분은 커스텀 스트림을 랩핑(Wrapping)하기 위해
StreamReader와
StreamWriter 클래스를 사용해야 한다.
파일시스템 I/O 자바에서 디스크 작동 실행은 아주 간단하다. 그것은
java.io.FileInputStream이나
java.io.FileOutputStream 둘 중 하나를 사용하고
java.io.File 객체를 조작하는 것과 관련된 것이다. 이 전에서부터 많이 언급해왔던 것처럼 C#은 자바와 거의 비슷하지만 미묘한 차이가 있다. 자바에서처럼, C# 파일 객체는 파일 시스템 하에서 확고한 관계를 형성하지 않는다. 존재하지 않는 파일을 위해 파일 객체를 생성할 수 있으며 존재하는 파일을 위해 파일 객체를 생성해 파일을 열려고 할 때까지 C# 프로그램도 모르게 CLR하에서 그 파일을 이동시킬 수도 있다. 자바와 달리 파일 객체는 파일시스템으로 스트림을 환원해줄
AppendText 또는
CreateText와 같은 정적 메소드를 가지는 것만큼 훨씬 더 중요한 역할을 수행할 수 있다. 자바에서는
FileInputStream을 위한 생성자는 똑같은 기능을 얻기 위해 사용되어야 한다. 자바에서 쓰기를 위한 새로운 파일을 생성하기 위해서는
FileInputStream을 사용해야 한다.
FileOutputStream fos new FileOutputStream( "brand-new-file.txt" =);
fos.write( ... )
|
그렇지만 C#에서는
Stream s File.Create( "brand-new-file.txt" );
|
또는
StreamWriter sw File.CreateText( "brand-new-file.txt" );
|
을 새 파일을 얻기 위해
Stream 또는
StreamWriter 을 허락한다. (
FileOutputStream의 생성자 중 하나로 'append' 불린을 설정함에 따라 자바에서는
appending이 수행됨) C#은
OpenWrite와
OpenText라고 이름 붙여진 정적 메소드를 가지는 반면 자바는
java.io.FileInputStream를 사용하는 파일로부터 읽기를 허락한다. 마지막으로 C#은
Open 메소드에 더욱 세심한 컨트롤을 제공하며 이 메소드는 파일 퍼미션과 접근 내용을 설정할 수 있는 능력을 보여준다.
기능 |
자바 |
C# |
쓰기 위한 새로운 파일 생성 |
java.io.FileOutputStream 사용 |
정적 File.Create 메소드나 정적 CreateText 사용 또는 인스턴스 CreateText 메소드 사용 |
기존 파일에 쓰기 |
java.io.FileOutputStream 사용 |
정적 또는 인스턴스 OpenWrite 메소드 사용 |
파일에 텍스트 추가하기 |
java.io.FileOutputStream 사용 그러나 append 매개변수를 차지하는 생성자를 사용해야 함. |
정적 또는 인스턴스 AppendText 메소드 사용 |
파일로부터 텍스트 제거하기 |
java.io.FileInputStream 사용 |
정적 또는 인스턴스 OpenRead, OpenText 메소드 사용 |
[표 3] 읽기와 쓰기를 위한 파일 조작 자바와 C# 모두에서 파일로부터 쓰거나 읽기 위해 사용하는 메소드 호기심을 위해 언급할 가치가 충분한 C#이 가져온 또다른 향상점은
File.Copy 메소드의 추가라고 말할 수 있다. 파일 시스템 I/O를 사용했던 대부분의 자바 프로그래머들은 파일을 적절하게 옮기는 능력이 부족했다.
Java.io.File에는 파일이름을 다시 정의할 수 있는
renameTo 메소드를 포함하고 있지만 파일시스템 경계(디스크, 네트워크 등등)에서는 실행되지 않는다. 대개의 경우 프로그래머는 자신의 이동 명령어를 구현해야만 했다. 이동 명령어는
java.io.FileInputStream과
java.io.FileOutputStream 모두를 사용해 파일을 복사하고 원본 파일을 지운다.
File.Move 명령어 역시 볼륨(Volumes)과 파일시스템 경계에서는 실행되지 않는다고 하더라도 Copy 메소드의 C# 산물은 작은 파일들을 옮긴다. C# 파일-시스템 구현은 자바 모델이 대처해야만 하는 범용 플렛폼을 다룰 필요는 없다.
Java.io.File.pathSeparator나
java.io.File.separator과 일치하는 변수는 없다. 불행하게도 이것은 또한
java.io.File 생성자중 호의적인
public File( File parent, String child )이 존재하지 않는다는 것을 의미한다. 그 대신 C# 프로그래머는
File parent ...
File child new File( parent.FullName + "\" + childName );
|
와 함께 새로운
System.IO.File 객체를 생성하는 문제가 남는다.
네트워크 이해하기 두 가지 프로그래밍 언어(C#, Java) 모두는 기본레벨 소켓 구현에 있어 몇 가지 추상적 계층을 제공한다(당연히 자바의
java.net.Socket 클래스는 C#의
System.Net.Sockets.Socket 클래스보다 더 추상적임).
Tier |
자바 |
C# |
응답/요청 |
java.net.URL 및 java.net.URLConnection |
System.Net.WebRequest |
프로토콜 |
TCP/IP를 위한 java.net.Socket 및 java.net.ServerSocket; UDP를 위한 java.net.DatagramSocket 및 java.net.MulticastSocket |
TCP/IP를 위한 System.Net.Sockets.TCPListener 및 System.Net.Sockets.TCPClient ; System.Net.Sockets.UDPClient |
원래 소켓 |
없음 |
System.Net.Sockets.Socket |
[표 4] 자바와 C#에서 네트워크 아키텍처 계층 자바와 C# 모두 인터페이스의 다른 점에 영향을 주도록 허락하는 네트워크를 위한 서로 다른 추상 층을 갖고 있다. 응답/요구 계층은 한쪽 끝에서 연결을 초기화하고 스트림으로 바이트를 보낸다. 그리고 응답으로 바이트 집합을 기다리는 동안 블록킹하는 HTTP 형태의 요구를 위해 사용될 수 있다. 좀더 유연한 스트림 작동을 위해 프로토콜 계층은 아주 유용하다(우리는 아래에서 TCP/IP 동작을 커버할 것이다). 대부분 자바 프로그래머들은 네트워크 작동을 최적화하지 않는다면 정확한 소켓 콘트롤을 요구하지는 않는다. C#은 만약 필요하다면 원래의 버클리 소켓을 제어할 수 있는 능력을 제공한다.
응답/요구 계층 이 계층은 모든 네트워킹을 제거하고 앞뒤로 데이터를 이동할 수 있도록 스트림 같은 인터페이스를 제공한다. 자바는 HTTP URL을 가질것이고 아래와 같은 코드를 실행함으로써 간단히 GET을 실행할 것이다.
URL url new URL( "http://to.post.to.com" );
URLConnection urlConnection url.openConnection();
InputStream input urlConnection.getInputStream();
... read stuff from input ...
input.close();
|
C#은 System.Net.WebRequest 객체로 이 코드를 모방한다:
WebRequest request WebRequestFactory.Create( = "http://to.post.to.com" );
Stream input request.GetResponse().GetResponseStream();
... read stuff from input ...
input.Close();
|
이러한 두 가지 구현은 기본적인 소켓 생성과 HTTP protocol 요구를 숨길 것이며 프로그래머가 데이터를 싣고 받는데 사용할 수 있는 스트림을 제공할 것이다. C# 스트림 클래스처럼,
WebRequest 클래스는 읽기 위한
WebResponse 객체 또는 쓰기 위한 요구 스트림을 비동기적으로 얻기 위한 메소드를 가진다.
프로토콜 계층 System.Net.Sockets.TCPClient 클래스는
java.net.Socket에 친숙한 자바 프로그래머들에게 더 친숙해 보여야 한다. 왜냐하면 이 두 가지는 거의 똑같기 때문이다. 사용되는 리턴 스트림 대신에 프로그래머가 소켓 구현을 다룰 필요가 없는 것처럼 둘 다 아주 비슷한 API 및 비슷한 기능을 공유한다. 간단한 텔넷 클라이언트 구현은 간단히 아래의 코드를 사용함으로써 자바에서 조합될 수 있다:
Socket telnet new Socket( "telnet.host.com", 23 );
OutputStream output telnet.getOutputStream();
InputStream input telnet.getInputStream();
|
그리고 위의 두 스트림도
telnet.host.com에 텔넷에 결합하는데 사용될 수 있다. C#으로도 거의 같은 형태로 똑같은 프로그램을 쓸 수 있다.
TCPClient telnet new TCPClient( "telnet.host.com", 23 );
Stream telnetStream telnet.GetStream();
StreamWriter output new StreamWriter( telnetStream );
StreamReader input new StreamReader( telnetStream );
|
또한, 자바로 들어오는 소켓을 설정하고 아래 코드를 사용하여 받는 것처럼 TCP/IP 연결을 받는 것은 두 언어에 있어 아주 비슷하다.
ServerSocket server new ServerSocket( 23 );
Socket accept server.accept();
|
반면에 C#은 아래와 같은 코드를 사용한다.
TCPListener server new TCPListener( 23 );
server.Start();
Socket accept server.Accept();
|
두 언어에서 인정되는 각각의 소켓은 따로 다루어져야 한다. 자바에서 선호하는 방법(java 1.4까지)은 받는 개개의 소켓을 위한 스레드를 생성하는 것이었다. 똑같은 것이 C# 소켓에서도 사용될 수 있다. 그러나 소켓 클래스는 'select' 메소드로 이벤트 구동 인터페이스를 사용하는 능력을 제공한다. (이벤트 구동 모델에서 소켓 프로그래밍은 이 기사의 범주를 벗어나는 것이므로 다루지 않겠다.)
Raw 소켓 계층 이제 우리는 대부분의 자바 프로그래머에게 익숙하지 않은 영역으로의 모험을 시작할 것이다. 자바만 사용하는 프로그래머들은 버클리 소켓 구현에 대해 알 필요가 없었다. 왜냐하면 버클리 소켓 구현은
java.net.Socket 및
java.net.DatagramSocket 클 래스에 의해 추출되기 때문이었다. 이 버클리 소켓 클래스를 적절하게 조작함으로써 눈에 익은 자바 기능의 스트림을 성취할 수 있다. 지금까지 우리는 자바(I/O와 네트워킹을 수행할 수 있는 능력)로부터 가장 강력한 추상개념을 포함하는 C# 레퍼토리에 대해 살펴보았다. 다음 기사에서는 병렬 동작을 허용하는 멀티스레딩을 다룰 것이다.
Raffi Krikorian은 메사추세스 주 캠브리지에서 컨설턴트로 활동하고 있으며 대용량 분산 P2P 시스템, JXTA와 C#에 능통한 전문가 이다.