
소켓 프로그래밍을 공부하면서 겪은 실수들과, 그 과정에서 이해하게 된 핵심 개념들을 정리합니다.
소켓 채팅 프로그램은 서버(Server)와 클라이언트(Client) 두 프로세스로 구성됩니다.
BufferedReader / BufferedWriter를 통해 메시지를 주고받습니다.package scoketPractice;
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Server {
public static void main(String[] args) {
String clientMsg = null, serverMsg = null;
System.out.println("클라이언트로 부터 받을 준비");
try (
ServerSocket serverSocket = new ServerSocket(1100);
Socket socket = serverSocket.accept();
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter =
new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Scanner sc = new Scanner(System.in);
) {
System.out.println("클라이언트가 연결 되었습니다.");
while (true) {
clientMsg = bufferedReader.readLine();
System.out.println("[클라이언트]: " + clientMsg);
System.out.println("클라이언트로 보낼 답변을 입력하시오...");
serverMsg = sc.nextLine();
bufferedWriter.write(serverMsg + "\n");
bufferedWriter.flush();
System.out.println("[서버]: " + serverMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
package scoketPractice;
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
String clientMsg = null, serverMsg = null;
int port = 1100;
String ip = "192.168.215.105";
System.out.println("서버에 연결 준비");
try (
Socket socket = new Socket(ip, port);
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter =
new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Scanner sc = new Scanner(System.in);
) {
System.out.println("서버에 연결 완료");
while (true) {
System.out.println("서버에 보낼 메세지를 입력하세요");
clientMsg = sc.nextLine();
bufferedWriter.write(clientMsg + "\n");
bufferedWriter.flush();
System.out.println("[클라이언트]: " + clientMsg);
serverMsg = bufferedReader.readLine();
System.out.println("[서버]: " + serverMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
"\n" 없이 readLine()이 동작하지 않는 이유처음에 아래처럼 작성했을 때 수신 측에서 메시지가 오지 않는 문제가 발생했습니다.
// ❌ 문제 코드
bufferedWriter.write(serverMsg);
bufferedWriter.flush();
수정 후:
// ✅ 올바른 코드
bufferedWriter.write(serverMsg + "\n");
bufferedWriter.flush();
"\n"이 필요한가?BufferedReader.readLine()은 줄 구분자(line terminator)를 만날 때까지 스트림을 블로킹(blocking) 상태로 읽습니다.
Java 공식 문서(BufferedReader.readLine())에 따르면:
"Reads a line of text. A line is considered to be terminated by any one of a line feed (
'\n'), a carriage return ('\r'), a carriage return followed immediately by a line feed, or by reaching the end-of-file (EOF)."
즉, readLine()은 다음 세 가지 중 하나가 나타날 때까지 기다립니다.
| 종류 | 표기 | 설명 |
|---|---|---|
| Line Feed | \n | Unix / Linux / macOS |
| Carriage Return | \r | 구형 Mac |
| CRLF | \r\n | Windows |
"\n"을 붙이지 않으면 readLine()이 "아직 줄이 끝나지 않았다"고 판단해 계속 대기하게 되고, 결국 수신 측이 영원히 블로킹 상태에 빠집니다.
💡 실무 팁: 실제 프로덕션 코드에서는 명시적 줄 구분자 대신
PrintWriter의println()메서드를 활용하거나, JSON/프로토콜 버퍼 같은 구조화된 데이터 포맷과 데이터 길이 헤더를 조합하는 방식이 더 안전합니다.
IOException은 왜 반드시 처리해야 하나?처음에 예외 처리를 생략했다가 컴파일 오류를 만났습니다. 단순히 "컴파일러가 요구하니까" 추가한 것이었는데, 그 이유를 제대로 이해하는 것이 중요합니다.
Java에는 두 가지 예외 계층이 있습니다.
RuntimeException 하위): 컴파일러가 처리를 강제하지 않음. 예) NullPointerException, ArrayIndexOutOfBoundsExceptionException 하위, RuntimeException 제외): 컴파일러가 반드시 처리하도록 강제. 예) IOException, SQLExceptionSocket, ServerSocket, BufferedReader, BufferedWriter 등 I/O 및 네트워크 관련 클래스들은 모두 IOException(Checked Exception)을 던질 수 있습니다.
로컬 파일 I/O와 달리, 네트워크 소켓은 다음과 같은 이유로 언제든 실패할 수 있습니다.
SocketException: Connection reset)실습 환경에서는 항상 서버가 켜져 있어서 체감이 어렵지만, 실제 환경에서는 연결 자체가 보장되지 않습니다. Java가 IOException을 Checked Exception으로 설계한 이유가 바로 이것입니다 — 개발자가 예외 상황을 반드시 인식하고 처리하도록 강제하는 것입니다.
try-with-resources로 자원을 자동으로 닫기소켓, 스트림 등의 자원은 사용 후 반드시 close()를 호출해야 합니다. 그렇지 않으면 파일 디스크립터(File Descriptor)나 포트가 계속 점유되는 자원 누수 문제가 발생합니다.
기존 방식(finally 블록 사용)은 코드가 복잡해지는 단점이 있었습니다.
// ❌ 기존 방식 — finally에서 수동으로 close
Socket socket = null;
try {
socket = new Socket(ip, port);
// ... 로직
} catch (IOException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try { socket.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
try-with-resources (Java 7+)Java 7부터 도입된 try-with-resources 문법을 사용하면, try(...) 괄호 안에 선언된 자원이 블록 종료 시 자동으로 close() 됩니다.
// ✅ try-with-resources 방식
try (
ServerSocket serverSocket = new ServerSocket(1100);
Socket socket = serverSocket.accept();
BufferedReader bufferedReader = new BufferedReader(...);
BufferedWriter bufferedWriter = new BufferedWriter(...);
) {
// ... 로직
} catch (IOException e) {
e.printStackTrace();
}
이것이 가능한 이유는 ServerSocket, Socket, BufferedReader, BufferedWriter 모두 java.lang.AutoCloseable 인터페이스를 구현하고 있기 때문입니다. Java 공식 스펙(JLS §14.20.3)에 따르면, try-with-resources 블록이 종료될 때 자원은 선언된 역순으로 자동 close됩니다.
| 항목 | 기존 방식 | try-with-resources |
|---|---|---|
| 자원 해제 | 수동 (finally에서 close()) | 자동 |
| 코드 가독성 | 복잡 | 간결 |
| 누수 위험 | 있음 (실수 가능) | 없음 |
| Java 버전 | 모든 버전 | Java 7 이상 |
이번 소켓 채팅 실습을 통해 이해한 핵심 내용을 세 줄로 정리하면 다음과 같습니다.
readLine()은 줄 구분자(\n)를 기준으로 동작한다. 송신 측에서 "\n"을 반드시 붙여야 수신 측에서 블로킹 없이 메시지를 읽을 수 있다.IOException은 단순한 컴파일 요구사항이 아니라, 불안정한 네트워크 환경에 대한 방어적 설계이다.try-with-resources를 사용하면 자원 해제를 자동화할 수 있다. AutoCloseable을 구현한 자원은 try(...) 안에 선언하는 것이 Best Practice이다.학습 목적으로 작성된 코드입니다. 실무에서는 멀티스레딩, 타임아웃 설정, 보안(TLS) 등 추가적인 고려가 필요합니다.