멀티스레스 채팅 서버 구현하기 | Java GUI 활용

Bluewave·2025년 1월 1일
post-thumbnail

🍰 개념 정리

프로세스

실행 중인 프로그램 단위
앱 or 프로그램 = 프로세스

특징

  • 운영체제가 프로그램을 실행하면 프로세스가 만들어짐
  • 각 프로세스는 독립된 메모리 공간을 가짐
  • 프로세스 간에는 서로 정보를 주고받기 위해 특별한 방법이 필요함

예시

in 스마트폰

  • 유튜브 앱 실행 = 하나의 프로세스
  • 워드 파일 열기 = 또 다른 프로세스

스레드

프로세스 내에서 실행되는 작업의 최소 단위
하나의 프로세스는 여러 개의 스레드를 가질 수 있음

특징

  • 스레드는 공유된 메모리 공간을 사용
  • 스레드 간에는 직접 데이터를 공유하며, 속도가 빠름
  • 한 스레드에서 문제가 발생하면 같은 프로세스의 다른 스레드에도 영향을 줄 수 있음

예시

유튜브에서 동영상을 재생하는 스레드 + 댓글을 표시하는 스레드 + 알림을 받는 스레드

구분프로세스스레드
독립성서로 독립적임같은 프로세스 안에서 상호작용 가능
메모리 사용각각 독립적인 메모리 공간 사용메모리 공간 공유
생성 비용생성 및 관리 비용 높음생성 및 관리 비용 낮음
문제 발생 시한 프로세스에 문제 발생해도 다른 프로세스는 영향 없음한 스레드에 문제가 생기면 같은 프로세스 전체에 영향

프로세스와 스레드 비교

  • 프로세스는 프로그램이 제대로 실행되도록 환경 제공
  • 스레드는 멀티태스킹을 통해 사용자 경험 향상

네트워크

두 개 이상의 컴퓨터를 연결하여 데이터를 주고받는 구조
이를 효율적이고 표준화된 방식으로 처리하기 위해 OSI 7계층 모델이 만들어짐

OSI 7계층

네트워크 통신을 단계별로 나눈 모델
각 계층은 서로 다른 역할 수행

계층 번호계층 이름역할
7응용 계층 (Application)사용자 인터페이스 제공 (웹 브라우저, 이메일 등)
6표현 계층 (Presentation)데이터 형식 변환, 암호화/복호화, 압축 (예: JPEG, PNG)
5세션 계층 (Session)연결 관리 (세션 생성, 유지, 종료)
4전송 계층 (Transport)신뢰성 있는 데이터 전송 (TCP, UDP)
3네트워크 계층 (Network)데이터를 목적지까지 라우팅 (IP 주소 기반)
2데이터 링크 계층 (Data Link)에러 감지 및 수정, 데이터 프레임 전달 (MAC 주소 사용)
1물리 계층 (Physical)하드웨어 전송 기술 (케이블, 신호, 전송 속도 등)

소켓 통신

프로세스 간 통신을 가능하게 하는 인터페이스

소켓이란?

네트워크에서 데이터를 주고받기 위한 종단점(엔드포인트)
클라이언트-서버 모델에서 주로 사용

작동 원리

  1. 서버는 소켓을 열어 클라이언트의 요청을 기다림
  2. 클라이언트는 서버에 연결 요청을 보냄
  3. 연결이 완료되면 데이터를 송수신
  4. 작업이 끝나면 소켓 닫음

예시

  • 웹 브라우저(클라이언트)가 웹 서버에 데이터를 요청할 때 소켓 사용

+) OSI 계층을 이해하면 네트워크 문제를 계층별로 분석 가능
+) 소켓 프로그래밍은 네트워크 기반 앱(채팅, 파일 전송 등)의 핵심!


🍰 실습하기

1. 실시간 멀티 채팅 서버, 클라이언트 프로그램 구현

in Java

서버 코드

import java.io.*;
import java.net.*;
import java.util.*;

public class ChatServer {
    private static final int PORT = 12345;
    private static Set<PrintWriter> clientWriters = new HashSet<>();

    public static void main(String[] args) {
        System.out.println("채팅 서버 시작...");
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("클라이언트 연결됨: " + clientSocket);
                new Thread(new ClientHandler(clientSocket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class ClientHandler implements Runnable {
        private Socket socket;
        private PrintWriter out;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try (
                InputStream input = socket.getInputStream();
                OutputStream output = socket.getOutputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            ) {
                out = new PrintWriter(output, true);
                synchronized (clientWriters) {
                    clientWriters.add(out);
                }

                String message;
                while ((message = reader.readLine()) != null) {
                    System.out.println("받은 메시지: " + message);
                    for (PrintWriter writer : clientWriters) {
                        writer.println(message);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    synchronized (clientWriters) {
                        clientWriters.remove(out);
                    }
                }
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. ServerSocket 생성
    • ServerSocket 객체를 생성해 클라이언트의 연결 요청 대기
    • 포트 번호 12345 사용

  2. 클라이언트 연결 수락
    • serverSocket.accept()로 클라이언트 요청 기다림
    • 연결이 수락되면 새로운 Socket 객체가 생성되고, 이를 별도 스레드에서 처리

  3. 멀티스레드로 클라이언트 관리
    - 각 클라이어너트의 요청을 ClientHandeler 클래스에서 처리
    - 클라이언트마다 독립적인 스레드가 생성되므로 동시에 초대 5명 연결 가능

  4. 브로드캐스트 메시지
    • 클라이언트가 보낸 메시지를 모든 클라이언트에게 전달합니다.
    • clientWriters는 연결된 모든 클라이언트의 출력 스트림(PrintWriter)을 저장

주요 코드 설명

ServerSocket 생성 및 대기
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
    while (true) {
        Socket clientSocket = serverSocket.accept();
        new Thread(new ClientHandler(clientSocket)).start();
    }
}

serverSocket.accept(): 클라이언트가 요청을 보낼 때까지 대시
새로운 클라이언트 연결 시, 별도의 스레드에서 ClientHandler로 작업 처리

클라이언트 메시지 처리
String message;
while ((message = reader.readLine()) != null) {
    for (PrintWriter writer : clientWriters) {
        writer.println(message);
    }
}

reader.readLine(): 클라이언트가 보낸 메시지를 읽음
모든 클라이언트에게 메시지 전달(writer.println(message))


클라이언트 코드

import java.io.*;
import java.net.*;

public class ChatClient {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 12345;

    public static void main(String[] args) {
        try (
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        ) {
            System.out.println("서버에 연결됨.");
            
            // 서버로 메시지 전송
            new Thread(() -> {
                try {
                    String message;
                    while ((message = consoleReader.readLine()) != null) {
                        out.println(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();

            // 서버에서 메시지 수신
            String serverMessage;
            while ((serverMessage = in.readLine()) != null) {
                System.out.println("서버: " + serverMessage);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  1. 서버와 연결
    • Socket 객체를 사용해서 서버 연결
    • localhost와 포트 번호 12345 사용
  1. 데이터 송신

    • 사용자 입력을 읽어 서버로 전송
    • PrintWriter를 통해 입력을 서버로 보냄
  2. 데이터 수신

    • 서버에서 전달된 메시지를 읽어 콘솔에 출력하기

중요 코드 설명

서버 연결

Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);

localhost는 현재 PC를 의미
연결 요청이 성공하면, 서버와 통신할 준비가 완료됨

사용자 입력 송신
new Thread(() -> {
    String message;
    while ((message = consoleReader.readLine()) != null) {
        out.println(message);
    }
}).start();

사용자 입력을 BufferedReader로 읽고 서버로 보냄
입력 처리를 별도 스레드로 실행해 입력 지연이 전체 프로그램에 영향을 주지 않도록 설계

서버 메시지 수신
String serverMessage;
while ((serverMessage = in.readLine()) != null) {
    System.out.println("서버: " + serverMessage);
}

서버에서 전달된 메시지를 계속 수신하고 콘솔에 출력


전체 흐름 요약

  • 서버

    • 클라이언트가 연결되면 스레드를 생성하여 관리
    • 클라이언트가 보낸 메시지를 읽어 모든 클라이언트에 전달
  • 클라이언트

    • 서버와 연결 후, 사용자 입력을 서버로 송신
    • 서버로부터 메시지를 수신하여 출력

실행 결과

서버 코드 실행

클라이언트 코드 실행 후 입력

클라이언트 여러 명 접속하기
서버 실행 후 클라이언트를 여러 번 실행 (실행한 횟수만큼 클라이언트 생성)
콘솔 창 여러개 띄워서 각각 클라이언트 지정

+) Display Selected Console로 선택 후 바로 옆 아이콘 Pin Console 눌러주면 고정됨!


이렇게 서버를 통해서 세 클라이언트가 대화하기

닉네임 추가하기 + 출력 형식 다듬기

서버 코드
import java.io.*;
import java.net.*;
import java.util.*;

public class ChatServer {
	private static final int PORT = 12345;
	private static Set<PrintWriter> clientWriters = new HashSet<>();

	public static void main(String[] args) {
		System.out.println("채팅 서버 시작...");
		try (ServerSocket serverSocket = new ServerSocket(PORT)) {
			while (true) {
				Socket clientSocket = serverSocket.accept();
				System.out.println("클라이언트 연결됨: " + clientSocket);
				new Thread(new ClientHandler(clientSocket)).start();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private static class ClientHandler implements Runnable {
		private Socket socket;
		private PrintWriter out;

		public ClientHandler(Socket socket) {
			this.socket = socket;
		}

		@Override
		public void run() {
			try (InputStream input = socket.getInputStream();
					OutputStream output = socket.getOutputStream();
					BufferedReader reader = new BufferedReader(new InputStreamReader(input));) {
				out = new PrintWriter(output, true);
				synchronized (clientWriters) {
					clientWriters.add(out);
				}

				String message;
				while ((message = reader.readLine()) != null) {
				    System.out.println("[서버 로그] " + message); // 서버 로그
				    synchronized (clientWriters) {
				        for (PrintWriter writer : clientWriters) {
				            writer.println(message); // 그대로 클라이언트에 전달
				        }
				    }
				}

			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				if (out != null) {
					synchronized (clientWriters) {
						clientWriters.remove(out);
					}
				}
				try {
					socket.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
클라이언트 코드
import java.io.*;
import java.net.*;

public class ChatClient {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 12345;

    public static void main(String[] args) {
        try (
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        ) {
            System.out.println("서버에 연결됨.");
            
            System.out.print("닉네임을 입력하세요: ");
            String nickname = consoleReader.readLine();
            out.println("[알림] " + nickname + " 님이 입장하셨습니다.");
            
         // 서버로 메시지 전송
            new Thread(() -> {
                try {
                    String message;
                    while ((message = consoleReader.readLine()) != null) {
                        out.println(nickname + ": " + message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();

            // 서버에서 메시지 수신
            String serverMessage;
            while ((serverMessage = in.readLine()) != null) {
                System.out.println(serverMessage);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



2. GUI 환경을 지원하는 채팅 클라이언트 프로그램 구현

with Java Swing

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.Socket;

public class ChatClientGUI {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 11111;

    private JTextArea chatArea;
    private JTextField inputField;
    private PrintWriter out;

    public ChatClientGUI() {
        // GUI 초기화
        JFrame frame = new JFrame("채팅 클라이언트");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 500);

        // 채팅 로그 표시 영역
        chatArea = new JTextArea();
        chatArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(chatArea);
        frame.add(scrollPane, BorderLayout.CENTER);

        // 입력 및 전송 영역
        JPanel inputPanel = new JPanel(new BorderLayout());
        inputField = new JTextField();
        JButton sendButton = new JButton("전송");
        inputPanel.add(inputField, BorderLayout.CENTER);
        inputPanel.add(sendButton, BorderLayout.EAST);
        frame.add(inputPanel, BorderLayout.SOUTH);

        // 메시지 전송 이벤트 처리
        sendButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sendMessage();
            }
        });

        inputField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sendMessage();
            }
        });

        frame.setVisible(true);

        // 서버와 연결
        try {
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 서버로부터 메시지 수신
            new Thread(() -> {
                try {
                    String message;
                    while ((message = in.readLine()) != null) {
                        chatArea.append(message + "\n");
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }).start();
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(frame, "서버에 연결할 수 없습니다.", "오류", JOptionPane.ERROR_MESSAGE);
        }
    }

    // 메시지 전송 메서드
    private void sendMessage() {
        String message = inputField.getText().trim();
        if (!message.isEmpty()) {
            out.println(message); // 서버로 메시지 전송
            inputField.setText(""); // 입력 필드 초기화
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(ChatClientGUI::new);
    }
}
  • JFrame: 전체 채팅 클라이언트 GUI를 구성하는 메인 프레임
  • JTextArea: 채팅 로그 표시 / 서버에서 받은 메시지 추가
  • JTextField: 사용자가 메시지를 입력하는 필드
  • 소켓 통신: 기존 소켓 통신을 유지해서 서버와 클라이언트 간 데이터 주고받음 / 별도의 스레드로 서버에서 오는 메시지를 수신하여 텍스트 영역에 추가

서버 코드는 이전과 동일!

채팅 화면 발전시키기

닉네임 기능 추가, 실제 메신저처럼 화면 업그레이드

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.Socket;

public class ChatClientGUI {
    private static final String SERVER_ADDRESS = "localhost";
    private static final int SERVER_PORT = 12345;

    private JTextArea chatArea;
    private JTextField inputField;
    private PrintWriter out;
    private String nickname;

    public ChatClientGUI() {
        // 닉네임 입력
        nickname = JOptionPane.showInputDialog(null, "닉네임을 입력하세요:", "닉네임 설정", JOptionPane.PLAIN_MESSAGE);
        if (nickname == null || nickname.trim().isEmpty()) {
            nickname = "익명"; // 닉네임이 입력되지 않으면 기본값
        }

        // GUI 초기화
        JFrame frame = new JFrame("채팅 클라이언트 - " + nickname);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 500);

        // 채팅 로그 표시 영역
        chatArea = new JTextArea();
        chatArea.setEditable(false);
        JScrollPane scrollPane = new JScrollPane(chatArea);
        frame.add(scrollPane, BorderLayout.CENTER);

        // 입력 및 전송 영역
        JPanel inputPanel = new JPanel(new BorderLayout());
        inputField = new JTextField();
        JButton sendButton = new JButton("전송");
        inputPanel.add(inputField, BorderLayout.CENTER);
        inputPanel.add(sendButton, BorderLayout.EAST);
        frame.add(inputPanel, BorderLayout.SOUTH);

        // 메시지 전송 이벤트 처리
        sendButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sendMessage();
            }
        });

        inputField.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sendMessage();
            }
        });

        frame.setVisible(true);

        // 서버와 연결
        try {
            Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
            out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 서버로 닉네임 전송
            out.println("[알림] " + nickname + " 님이 입장하셨습니다.");

            // 서버로부터 메시지 수신
            new Thread(() -> {
                try {
                    String message;
                    while ((message = in.readLine()) != null) {
                        chatArea.append(message + "\n");
                        chatArea.setCaretPosition(chatArea.getDocument().getLength()); // 스크롤 자동 이동
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }).start();
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(frame, "서버에 연결할 수 없습니다.", "오류", JOptionPane.ERROR_MESSAGE);
        }
    }

    // 메시지 전송 메서드
    private void sendMessage() {
        String message = inputField.getText().trim();
        if (!message.isEmpty()) {
            out.println(nickname + ": " + message); // 닉네임 포함 메시지 전송
            inputField.setText(""); // 입력 필드 초기화
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(ChatClientGUI::new);
    }
}



요렇게 콘솔창과 GUI 화면 모두 확인 가능합니당
+) 자동 스크롤 기능까지!

profile
Developer's Logbook

0개의 댓글