1. 시작하며: 나의 착각, 실시간 채팅을 하려면 WebSocket이 필요할까?
서버 소켓과 클라이언트 소켓의 구조를 잡고 메시지를 주고받는 테스트를 하던 중, 갑자기 근본적인 의문이 들었습니다.
"내가 타자를 치고 엔터를 쳐야만 그제야 서버의 메시지가 화면에 출력되네? 자연스럽게 실시간으로 서로 메시지를 주고받는 진짜 채팅을 만들려면, TCP 소켓이 아니라 WebSocket(웹소켓) 같은 완전히 다른 방식을 써야 하나?"
하지만 이는 네트워크의 뼈대와 내 코드의 구조를 분리해서 생각하지 못한 완벽한 오해였습니다. 순수 TCP 소켓은 본질적으로 양쪽에서 동시에 데이터를 쏠 수 있는 완벽한 양방향(Full-Duplex) 고속도로입니다. 문제는 통신망이 아니라, 내 코드에 있었습니다.
2. 원인 분석: 무전기(Half-Duplex) 통신을 유발한 동기화 블로킹
기존 코드는 하나의 while 루프 안에서 br.readLine()(내 키보드 입력 대기)과 in.readLine()(상대방 메시지 대기)을 순서대로 처리했습니다.
이러다 보니 내 타자 입력을 기다리는 동안 코드의 흐름이 멈춰버렸고(Blocking), 그 사이에 상대방이 아무리 급한 메시지를 보내도 내 귀는 닫혀있는 상태가 된 것입니다. 양방향 도로를 만들어두고 정작 교대로 한 대씩만 지나가게 통제하는 코드를 짰던 것입니다.
3. 해결책: '듣는 귀'와 '말하는 입'의 완벽한 분리 (멀티스레딩)
진정한 실시간 양방향 통신을 구현하려면, 내가 타자를 치고 있든 말든 상대방의 메시지는 화면에 계속 떠야 합니다. 이를 위해 자바의 Runnable 인터페이스를 활용해 '읽기 작업'과 '쓰기 작업'을 전담할 두 명의 직원을 만들기로 했습니다.
- 👂 ReaderSocket (듣기 전용): 뒤에서 독자적으로 돌아가며 상대방이 보낸 메시지가 도착하는 즉시 화면에 출력합니다.
- 🗣️ WriterSocket (말하기 전용): 사용자의 키보드 입력만 하루 종일 기다렸다가 상대방에게 전송합니다.
4. 실제 구현: 멀티스레드 소켓 통신 전체 코드
설계를 바탕으로 각자의 역할을 완벽히 분리한 최종 코드는 다음과 같습니다. (예외 처리 및 자원 반납 로직 포함)
💻 ReaderSocket & WriterSocket (업무를 전담할 Runnable 객체)
// [듣기 전용 직원]
public class ReaderSocket implements Runnable {
private Socket socket;
public ReaderSocket(Socket socket) { this.socket = socket; }
@Override
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (true) {
String message = in.readLine();
if (message == null) { // 상대방이 연결을 끊으면 null 반환
System.out.println("상대방이 연결을 종료했습니다.");
break;
}
System.out.println(socket.getInetAddress() + " 님이 보낸 메세지: " + message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 통신 종료 시 안전하게 소켓 반납
try { if (socket != null && !socket.isClosed()) socket.close(); }
catch (IOException e) { e.printStackTrace(); }
}
}
}
// [말하기 전용 직원]
public class WriterSocket implements Runnable {
private Socket socket;
public WriterSocket(Socket socket) { this.socket = socket; }
@Override
public void run() {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
while (true) {
System.out.print("메세지 입력(종료는 q): ");
String message = br.readLine();
if (message.equalsIgnoreCase("q")) {
System.out.println("연결을 종료합니다.");
break;
}
out.println(message);
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try { if (socket != null && !socket.isClosed()) socket.close(); }
catch (IOException e) { e.printStackTrace(); }
}
}
}
💻 서버와 클라이언트의 메인 로직 (사장님)
// [서버 측 Main.java]
while (true) {
// 1. 입구에서 손님 대기
Socket socket = serverSocket.accept();
System.out.println(socket.getInetAddress() + "와 연결 성공!");
// 2. 직원을 고용하고 스레드에 태워 업무 지시 (start!)
Thread readerThread = new Thread(new ReaderSocket(socket));
readerThread.start();
Thread writerThread = new Thread(new WriterSocket(socket));
writerThread.start();
}
// [클라이언트 측 ClientSocket.java]
Socket socket = new Socket("192.168.x.x", 8080);
System.out.println("소켓 연결 성공");
// 클라이언트도 동일하게 읽기/쓰기 스레드를 각각 분리하여 실행
Thread readerThread = new Thread(new ReaderSocket(socket));
readerThread.start();
Thread writerThread = new Thread(new WriterSocket(socket));
writerThread.start();
5. 가장 흔한 함정: run() vs start()의 치명적 차이
설계를 완벽하게 마치고 코드를 돌렸는데, 화면이 또 멈춰버리는 현상을 겪었습니다. 분명 스레드를 나눴는데 왜 이런 일이 발생했을까요? 원인은 스레드를 실행하는 단 한 줄의 코드에 있었습니다.
❌
readerSocket.run(); (삽질의 원인)새로운 직원이 일을 하는 것이 아니라, 사장님(메인 스레드)이 직접 그 메서드 안으로 들어가 버립니다. 결국 사장님이 읽기 작업 무한 루프에 갇혀버려 그 밑의 쓰기 스레드는 생성조차 되지 못하고 시스템이 정지합니다.
✅
readerThread.start(); (올바른 방식)반드시
Thread 객체에 태워서 start()를 호출해야 합니다. 그래야 운영체제가 완전히 독립된 새로운 실행 흐름을 만들어 백그라운드에서 직원이 독자적으로 일을 시작하며, 사장님(메인 스레드)은 다음 코드로 정상적으로 넘어갈 수 있습니다.이 작은 차이를 직접 몸으로 부딪히며 겪고 나니, 멀티스레드의 동작 원리와 동시성 제어가 왜 백엔드 아키텍처에서 그토록 중요한지 뼈저리게 느낄 수 있었습니다. 이제 기반 공사를 튼튼히 마쳤으니, 다음 단계인 실제 HTTP 프로토콜 파싱과 응답 로직 구현으로 넘어갈 준비가 되었습니다!
'정리 > WAS' 카테고리의 다른 글
| 순수 Java로 WAS 구현 (6) - 프레임워크의 마법, 리플렉션(Reflection) 완벽 이해 (0) | 2026.05.15 |
|---|---|
| 순수 Java로 WAS 구현 (5) - HTTP 요청 파서(Parser) 구현과 3가지 트러블슈팅 (0) | 2026.05.15 |
| 순수 Java로 WAS 구현 (4) - 파싱(Parsing)을 위한 HTTP 메시지 구조 완벽 분석 (1) | 2026.05.14 |
| 순수 Java로 WAS 구현 (2) - 블로킹(Blocking) 해결을 위한 멀티스레드(Multi-Thread)의 이해 (0) | 2026.05.13 |
| 순수 Java로 WAS 구현 (1) - WAS의 심장, 소켓(Socket) 프로그래밍 (0) | 2026.05.12 |