
N:N 채팅방 프로젝트
본 프로젝트는 Java의 기본 블로킹 소켓(Blocking Socket)을 사용하여 N:N(다대다) 채팅을 구현한 예제입니다. 1) 멀티스레딩 환경에서 발생하는 공유 자원 문제 해결, 2) 커스텀 패킷 프로토콜 정의 및 3) I/O 모킹을 통한 단위 테스트에 중점을 두었습니다.
- GitHub repo : proLmpa/ChatApp
✨ 주요 기능 (Features)
- 사용자 이름 등록: 클라이언트는 서버 접속 시 반드시 고유한 사용자 이름을 등록해야 합니다.
- 실시간 Pub-Sub 채팅: 한 명의 클라이언트가 메시지를 전송하면, 접속 중인 모든 사용자에게 실시간으로 메시지가 전송됩니다.
- 접속/단절 알림: 새로운 사용자의 접속 및 기존 사용자의 단절 상황을 모든 클라이언트에게 알립니다.
- 세션 통계 전송: 클라이언트가 접속을 종료하면, 해당 사용자가 세션 동안 주고받은 메시지 개수가 모든 접속자에게 통계 정보와 함께 전송됩니다.
🏗️ 프로젝트 아키텍처 및 기술 스택
본 프로젝트는 Gradle의 멀티 프로젝트 구조를 따르며, 모듈 간의 역할을 명확히 분리합니다.
| 모듈 이름 | 역할 | 포함된 주요 로직 |
| root | 빌드 설정 및 의존성 관리 | Gradle 설정 (build.gradle.kts) |
| Server | 클라이언트 연결 관리 및 메시지 브로드캐스팅 | ServerSocket, ClientHandler, 공유 맵 (clients) |
| Client | 서버 접속 및 사용자 I/O 처리 | Socket, InputHandler, ClientApp |
| Share | 공통 사용 객체 및 프로토콜 정의 | Packet, PacketType, Protocol (패킷 생성/파싱 유틸리티) |
기술 스택:
- 언어: Kotlin/JVM
- I/O: java.net.ServerSocket, java.net.Socket (Java Blocking I/O)
- 빌드: Gradle
- 테스트: JUnit 5, Mockito (I/O Mocking)
📡 패킷 프로토콜 정의
안정적인 통신을 위해 Header + Body 형태의 커스텀 프로토콜을 사용합니다. 모든 통신은 byte[] 배열 형태로 이루어지며, 길이는 가변적입니다.

패킷 구조 다이어그램
| 구분 | 시작 위치 | 길이 (Bytes) | 데이터 타입 | 설명 |
| Header 1 - Total Length | 0 | 4 | Int | Header(8) + Body의 실제 데이터 포함 전체 패킷 길이 |
| Header 2 - Packet Type | 4 | 4 | Int | 패킷의 목적 정의 (1: 이름 등록, 2: 채팅, 3: 서버 알림 등) |
| Body | 8 | N | ByteArray | 실제 전송될 데이터 (UTF-8 인코딩 문자열) |
패킷 타입 (PacketType) 예시
| 패킷 타입 | 패킷 타입 값 | 설명 |
| REGISTER_NAME | 1 | 클라이언트가 서버에 이름을 등록할 때 사용 |
| CHAT_MESSAGE | 2 | 사용자 간의 실제 채팅 메시지 |
| SERVER_INFO | 3 | 서버가 알림이나 성공 메시지를 전송할 때 사용 |
| DISCONNECT_INFO | 4 | 클라이언트 단절 시 통계 정보를 전송할 때 사용 |
| DISCONNECT_REQUEST | 5 | 클라이언트의 연결 단절 요청 |
🔑 구현 특징 및 하이라이트
1. 동시성 제어 (Concurrency Control)
멀티스레드 환경에서 공유 자원 접근 문제를 해결하기 위해 java.util.concurrent.locks.ReentrantLock, AtomicInteger을 사용하여 동기화를 구현했습니다.
- Server 모듈의 클라이언트 목록 (clients map) 접근 시
- 공유 변수인 클라이언트 메시지 통계 (sentCount, receivedCount) 접근 시
2. Mocking을 통한 단위 테스트
스레드 및 실제 네트워크 I/O 의존성을 제거하고 핵심 로직만 검증하기 위해 Mockito를 활용했습니다.
- ClientTest.kt: Client의 패킷 생성/파싱 및 전송 로직 검증.
- ClientHandlerTest.kt: Server의 핸들러 로직(이름 등록, 연결 해제, 통계 처리) 검증 시 Socket, InputStream, OutputStream을 Mocking하여 사용.
3. 통계 관리 및 전달
각 ClientHandler는 연결된 클라이언트의 sentCount와 receivedCount를 관리하며, 연결 해제(DISCONNECT_INFO) 시 이 통계 정보를 패킷에 담아 모든 사용자에게 브로드캐스팅합니다.
🚀 사용 방법 (Getting Started)
1. 빌드 요구 사항
- Java 17+
- Gradle (Wrapper 포함)
2. 프로젝트 빌드
루트 디렉토리에서 다음 명령을 실행하여 모든 서브 프로젝트(Client, Server)의 JAR 파일을 빌드합니다.
./gradlew clean build
3. 서버 실행
빌드가 성공하면 Server/build/libs 디렉토리에서 실행 가능한 JAR 파일을 찾을 수 있습니다.
java -jar Server/build/libs/Server-1.0-SNAPSHOT.jar
(서버는 기본 포트,8080에서 대기합니다.)
4. 클라이언트 실행
다른 터미널 창을 열어 Client JAR 파일을 실행합니다. 여러 개의 클라이언트를 실행하여 테스트할 수 있습니다.
java -jar Client/build/libs/Client-1.0-SNAPSHOT.jar
(클라이언트 실행 후 이름 등록 프롬프트가 나타납니다.)
참고 자료
- [Back-end] kotlin으로 간단한 소켓을 만들어보기
- JAVA IO - 바이트기반스트림 :ByteArrayInputStream과 ByteArrayOutputStream
- proLmpa/ChatApp: PubSub 패턴으로 다수의 사용자가 동시에 채팅할 수 있는 앱 프로그램입니다.
Powered By. Gemini
'Backend' 카테고리의 다른 글
| [Backend] ByteArray class 이해하기 (1) | 2025.10.16 |
|---|---|
| [Backend] 서버-클라이언트 연결 (ServerSocket & Socket) (0) | 2025.10.16 |
| [Backend] Pub-Sub 패턴: 경쟁적 소비 모델 vs. Broadcast (0) | 2025.10.10 |
| [Backend] volatile 키워드, 언제 쓰고 왜 쓰는가? (2) | 2025.10.04 |
| [Backend] TCP/IP 4계층 & 1024 이하 Known ports (0) | 2025.09.26 |