Java의 Socket API를 사용한 네트워크 프로그래밍에 대한 학습하겠습니다. 기본적인 에코 서버/클라이언트 및 멀티스레드 에코 서버 구현을 통해 Java의 Thread, Socket, 네트워크 Stream의 개념과 사용법을 설명합니다.
소켓은 네트워크 통신의 엔드포인트로, 두 프로그램이 네트워크를 통해 데이터를 주고받을 수 있게 합니다.
스트림은 데이터의 입출력을 다루는 Java의 기본 메커니즘입니다.
스레드는 프로그램 내에서 동시에 실행될 수 있는 작은 실행 단위입니다. 다중 클라이언트 처리가 필요한 서버에서 중요합니다.
public class EchoServer {
public static void main(String[] args) throws IOException {
// 서버 소켓을 포트 9999에서 생성
ServerSocket serverSocket = new ServerSocket(9999);
// 무한 루프로 클라이언트 연결을 대기
while (true) {
// 클라이언트 연결 대기
Socket socket = serverSocket.accept();
// 클라이언트와의 입출력 스트림 생성
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
byte[] buf = new byte[1024]; // 데이터 버퍼
int count = 0;
// 클라이언트로부터 데이터를 읽고 다시 전송
while ((count = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, count);
System.out.write(buf, 0 , count);
}
// 연결 종료 및 소켓 닫기
outputStream.close();
socket.close();
}
}
}
핵심 개념:
ServerSocket(9999)
: 9999 포트에서 서버 소켓 생성serverSocket.accept()
: 클라이언트 연결을 기다리는 블로킹 메서드socket.getInputStream()/getOutputStream()
: 클라이언트와 데이터를 주고받을 스트림 생성inputStream.read(buf)
: 클라이언트로부터 데이터 읽기outputStream.write(buf, 0, count)
: 클라이언트에게 데이터 전송💥한계점: 하나의 클라이언트만 처리 가능. 첫 클라이언트의 연결이, 처리가 끝날 때까지 다른 클라이언트는 대기해야 합니다.
public class EchoClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
// 서버에 연결
socket.connect(new InetSocketAddress("localhost", 9999));
// 서버와의 입출력 스트림 생성
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024]; // 데이터 버퍼
int count = 0;
// 표준 입력에서 데이터를 읽어 서버로 전송하고 응답 받기
while ((count = System.in.read(buf)) != -1) {
outputStream.write(buf, 0, count);
count = inputStream.read(buf);
System.out.write(buf, 0, count);
}
// 연결 종료 및 소켓 닫기
outputStream.close();
socket.close();
}
}
핵심 개념:
new Socket()
: 소켓 객체 생성socket.connect(new InetSocketAddress("localhost", 9999))
: 지정된 주소와 포트로 서버에 연결System.in.read(buf)
: 표준 입력(키보드)에서 데이터 읽기inputStream.read(buf)
: 서버로부터 응답 데이터 읽기public class MultiThreadEchoServer extends Thread {
private Socket socket = null;
// 생성자: 클라이언트 소켓을 받아 초기화
public MultiThreadEchoServer(Socket socket) {
this.socket = socket;
}
// 스레드가 실행되는 메서드
public void run() {
try {
InputStream fromClient = socket.getInputStream();
OutputStream toClient = socket.getOutputStream();
byte[] buf = new byte[1024];
int count = 0;
while ((count = fromClient.read(buf)) != -1) {
toClient.write(buf, 0, count);
System.out.write(buf, 0, count);
}
} catch (IOException e) {
System.out.println(socket + ": 연결종료 (" + e + ")");
} finally {
if (socket != null) {
try {
socket.close();
socket = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws NumberFormatException, IOException {
ServerSocket serverSocket = new ServerSocket(5001);
while (true) {
Socket client = serverSocket.accept();
MultiThreadEchoServer myServer = new MultiThreadEchoServer(client);
myServer.start();
}
}
}
핵심 개념:
extends Thread
: Thread 클래스 상속으로 멀티스레딩 구현myServer.start()
: 스레드 시작, run() 메서드 실행ServerSocket 클래스:
ServerSocket serverSocket = new ServerSocket(포트번호)
Socket clientSocket = serverSocket.accept()
accept()
: 클라이언트 연결 수락(블로킹)close()
: 서버 소켓 닫기Socket 클래스:
Socket socket = new Socket()
또는 new Socket(호스트, 포트)
socket.connect(new InetSocketAddress(호스트, 포트))
getInputStream()
: 소켓에서 데이터를 읽기 위한 스트림 getOutputStream()
: 소켓으로 데이터를 쓰기 위한 스트림close()
: 소켓 닫기InputStream:
read(byte[] b)
: 바이트 배열로 데이터 읽기read(byte[] b, int off, int len)
: 지정된 오프셋과 길이로 데이터 읽기-1
은 더 이상 읽을 데이터가 없음을 의미OutputStream:
write(byte[] b)
: 바이트 배열 전체 쓰기write(byte[] b, int off, int len)
: 지정된 오프셋과 길이로 데이터 쓰기flush()
: 버퍼링된 출력 바이트 강제 쓰기Thread 클래스:
class MyThread extends Thread
class MyRunnable implements Runnable
start()
: 스레드 시작run()
: 스레드 실행 시 실행될 코드 (오버라이드 필요)join()
: 스레드가 종료될 때까지 대기멀티스레드 서버의 이점:
예제 코드에서는 다음과 같은 리소스 관리 패턴을 사용했습니다:
close()
finally
블록에서 리소스 해제 (MultiThreadEchoServer)개선점: 현대 Java에서는 try-with-resources 구문을 사용하여 더 안전하게 리소스를 관리할 수 있습니다.
예제 코드에서는 다음과 같은 예외 처리 패턴을 보여줍니다:
IOException
을 메인 메서드에서 throw개선점: 더 상세한 예외 처리와 로깅이 필요합니다.
현재 구현에서는 클라이언트마다 새 스레드를 생성합니다. 이는 간단하지만 클라이언트가 많아지면 비효율적일 수 있습니다.
개선점: ThreadPool(ExecutorService)을 사용하여 스레드 수를 제한하고 효율적으로 관리할 수 있습니다.
현재 1:1 에코 서버에서 다음과 같이 확장할 수 있습니다:
java.nio
패키지의 Channel과 Selector 활용java.util.concurrent.ExecutorService
사용javax.net.ssl
패키지 활용Java 소켓 프로그래밍의 기본 개념인 Socket, Stream, Thread를 소개하고, 실제 구현 예제를 통해 그 사용법을 보여주었습니다.
제공된 예제 코드는 기본적인 에코 서버/클라이언트와 멀티스레드 서버를 구현하며, 실제 채팅 애플리케이션으로 확장하기 위한 기반을 제공합니다.
Java의 네트워크 프로그래밍은 강력하고 유연하며, 이러한 기본 개념을 이해함으로써 더 복잡한 네트워크 애플리케이션을 개발할 수 있는 토대를 마련할 수 있습니다.
멀티스레드 에코 서버의 데이터 송수신과 예외 처리를 개선하도록 하겠습니다. 주요 개선사항은 다음과 같습니다:
public class MultiThreadEchoServer extends Thread {
private static final int BUFFER_SIZE = 1024;
private final Socket socket;
public MultiThreadEchoServer(Socket socket) {
this.socket = socket;
setName("Client-" + socket.getInetAddress().getHostAddress()); // 스레드 이름 지정
}
public void run() {
System.out.println("[" + getName() + "] 클라이언트 연결됨: " + socket);
try (InputStream fromClient = socket.getInputStream();
OutputStream toClient = socket.getOutputStream()) {
byte[] buf = new byte[BUFFER_SIZE];
int count;
while ((count = fromClient.read(buf)) != -1) {
toClient.write(buf, 0, count);
toClient.flush(); // 버퍼 즉시 전송
// 서버 로그에 수신된 데이터 출력
System.out.printf("[%s] 수신된 데이터 크기: %d bytes%n", getName(), count);
System.out.write(buf, 0, count);
System.out.flush();
}
} catch (IOException e) {
System.out.printf("[%s] 연결 오류: %s%n", getName(), e.getMessage());
} finally {
closeSocket();
}
}
private void closeSocket() {
if (socket != null && !socket.isClosed()) {
try {
socket.close();
System.out.printf("[%s] 소켓 정상 종료%n", getName());
} catch (IOException e) {
System.out.printf("[%s] 소켓 종료 중 오류 발생: %s%n", getName(), e.getMessage());
}
}
}
// ... existing code ...
}
BUFFER_SIZE
를 상수로 정의하여 버퍼 크기를 쉽게 관리할 수 있게 함- try-with-resources를 사용하여 스트림 자원을 자동으로 해제
flush()
를 추가하여 데이터가 즉시 전송되도록 보장- 스레드 이름을 지정하여 로깅 시 어떤 클라이언트의 메시지인지 식별 가능
- 소켓 종료 로직을 별도 메서드로 분리하여 코드 가독성 향상
- 더 자세한 로그 메시지 추가로 디버깅 용이성 향상
이러한 개선사항들은 서버의 안정성과 유지보수성을 높여줄 것입니다.