오픈 챌린지 문제
서버-클라이언트 1:1 채팅 예제는 클라이언트에서부터 시작하여 클라이언트와 서버가 순서대로 번갈아 가면서 메시지를 주고 받았다. 이 예제를 참고하여 순서에 상관없이 자유롭게 서버와 클라이언트가 메시지를 주고 받을 수 있도록 스레드를 이용하여 스윙 프로그램으로 재작성하라.
| 요소 | 제약 조건 및 규칙 |
|---|---|
| 통신 방식 | 순서에 상관 없이(스레드를 이용한 비동기 수신/발신 구현) 자유롭게 메시지를 주고 받는다. |
| GUI 요소 | 상대방으로부터 받은 메시지 출력: JTextArea상대방에게 보낼 메시지 입력: JTextField |
| 메시지 전송 | JTextField 클래스에 입력 후 <Enter> 키를 입력하면 즉시 전송한다. |
| 접속 및 실행 | ChatServer 객체를 먼저 실행 후, ChatClient 객체를 실행하여 접속한다. |
| 프로그램 종료 | 어느 한쪽에서 "bye"를 입력하거나 창을 닫으면 상대방에게 종료를 알리고 프로그램이 종료된다. |
- 요구 사항: 서버와 클라이언트는 JTextArea를 이용하여 상대방으로부터 받은 메시지를 출력하고, JTextField 창을 이용하여 상대방에게 보낼 메시지를 사용자로부터 입력받고
<Enter>키를 입력하면 상대방에게 바로 전송하도록 하라. 본문에 예제와 달리 어느 한쪽이 접속을 끊으면 프로그램이 종료되도록 한다.
| 클래스 | 역할 | 주요 기능 및 스레드 활용 |
|---|---|---|
| ChatServer | 서버 프로그램(메인) | 1. ServerSocket (9999 포트 번호)을 열고 클라이언트 연결을 대기한다. 2. 메시지 발신은 JTextField 클래스의 ActionListener 객체에서 처리한다.3. 메시지 수신은 ReceiverThread 객체를 별도로 생성하여 처리한다.4. 창 닫기 "bye" 입력 시 안전하게 자원을 해제하고 종료한다. |
| ChatClient | 클라이언트 프로그램(메인) | 1. 서버(localhost:9999)에 Socket 객체를 통해 접속한다.2. 메시지 발신은 JTextField 클래스의 ActionListener 객체에서 처리한다.3. 메시지 수신은 ReceiverThread 객체를 별도로 생성하여 처리한다.4. 창 닫기 "bye" 입력 시 안전하게 자원을 해제하고 종료한다. |
| ReceiverThread | 수신 전용 스레드(내부 클래스) | while (true) 무한 루프를 통해 in.readLine() 메소드를 실행하여, 수신을 전담하여 송신과 수신이 블로킹 없이 동시에 가능하게 한다. |
제시 코드는 다음과 같다.
- ServerEx.java
package ch15n3;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class ServerEx {
public static void main(String[] args) {
BufferedReader in = null;
BufferedWriter out = null;
ServerSocket listener = null;
Socket socket = null;
Scanner scanner = new Scanner(System.in); // 키보드에서 읽을 scanner 객체를 생성한다.
try {
listener = new ServerSocket(9999); // 서버 소켓 생성
System.out.println("연결을 기다리고 있습니다...");
socket = listener.accept(); // 클라이언트로부터 연결 요청을 대기한다.
System.out.println("연결되었습니다.");
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while (true) {
String inputMessage = in.readLine(); // 클라이언트로부터 한 행을 읽는다.
if (inputMessage.equalsIgnoreCase("bye")) {
System.out.println("클라이언트에서 bye로 연결을 종료합니다.");
break;
}
System.out.println("클라이언트: " + inputMessage);
System.out.print("보내기>>"); // 프롬프트
String outputMessage = scanner.nextLine(); // 키보드에서 한 행을 읽는다.
out.write(outputMessage + "\n"); // 키보드에서 읽은 문자열을 전송한다.
out.flush(); // out 객체의 스트림 버퍼에 있는 모든 문자열을 전송한다.
}
}
catch (IOException e) {
System.out.println(e.getMessage());
}
finally {
try {
scanner.close(); // scanner 객체를 닫는다.
socket.close(); // 통신용 소켓을 닫는다.
listener.close(); // 서버 소켓을 닫는다.
}
catch (IOException e) {
System.out.println("클라이언트와 채팅 중 오류가 발생하였습니다.");
}
}
}
}
- ClientEx.java
package ch15n3;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;
public class ClientEx {
public static void main(String[] args) {
BufferedReader in = null;
BufferedWriter out = null;
Socket socket = null;
Scanner scanner = new Scanner(System.in); // 키보드에서 읽은 scanner 객체를 생성한다.
try {
socket = new Socket("localhost", 9999); // 클라이언트 소켓을 생성하고 서버에 연결한다.
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while (true) {
System.out.print("보내기>>"); // 프롬프트
String outputMessage = scanner.nextLine(); // 키보드에서 한 행을 읽는다.
if (outputMessage.equalsIgnoreCase("bye")) {
out.write(outputMessage + "\n");
out.flush();
break; // 사용자가 "bye"를 입력한 경우 서버로 전송 후 실행을 종료한다.
}
out.write(outputMessage + "\n"); // 키보드에서 읽은 문자열을 전송한다.
out.flush(); // out 객체의 스트림 버퍼에 있는 모든 문자열을 전송한다.
String inputMessage = in.readLine(); // 서버로부터 한 행을 수신한다.
System.out.println("서버: " + inputMessage); // 서버로부터 받은 메시지를 화면에 출력한다.
}
}
catch (IOException e) {
System.out.println(e.getMessage());
}
finally {
try {
scanner.close();
if (socket != null) {
socket.close(); // 클라이언트 소켓을 닫는다.
}
}
catch (IOException e) {
System.out.println("서버와 채팅 중 오류가 발생하였습니다.");
}
}
}
}
오픈 챌린지 풀이
사용자 요청에 따라, 서버와 클라이언트가 스레드를 이용하여 순서에 상관없이 메시지를 자유롭게 주고 받을 수 있는 GUI 기반 1:1 채팅 프로그램을 설계했다. 자세한 설계 내용은 다음과 같다.
- ChatServer 객체
package ch15nC;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
// GUI 기반 서버 채팅 프로그램
public class ChatServer extends JFrame implements ActionListener {
private JTextArea displayArea;
private JTextField inputField;
private Socket socket;
private ServerSocket listener;
private BufferedReader in;
private BufferedWriter out;
public ChatServer() {
super("서버 채팅 프로그램");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 직접 종료 처리
// 1. UI 구성
displayArea = new JTextArea(10, 30);
displayArea.setEditable(false);
displayArea.setFont(new Font("맑은 고딕", Font.PLAIN, 14));
JScrollPane scrollPane = new JScrollPane(displayArea);
inputField = new JTextField(30);
inputField.addActionListener(this); // Enter 키 입력 시 액션 처리
add(scrollPane, BorderLayout.CENTER);
add(inputField, BorderLayout.SOUTH);
setSize(400, 350);
setVisible(true);
// 윈도우 닫기 이벤트 처리(리소스 정리 및 종료)
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
try {
// 창을 닫을 때 "bye" 메시지를 클라이언트에게 전송
sendMessage("bye");
closeResources();
} catch (IOException ex) {
// 무시
}
System.exit(0);
}
});
// 서버 네트워크 시작
new Thread(() -> startServer()).start();
}
// 메시지 전송 처리
@Override
public void actionPerformed(ActionEvent e) {
String message = inputField.getText();
if (message.isEmpty()) return;
try {
sendMessage(message);
inputField.setText(""); // 전송 후 입력창 초기화
if (message.equalsIgnoreCase("bye")) {
closeResources();
System.exit(0);
}
} catch (IOException ex) {
displayArea.append(">> 메시지 전송 오류: " + ex.getMessage() + "\n");
tryCloseAndExit();
}
}
private void sendMessage(String message) throws IOException {
if (out != null) {
out.write("서버: " + message + "\n");
out.flush();
displayArea.append("나: " + message + "\n");
}
}
private void startServer() {
try {
listener = new ServerSocket(9999); // 서버 소켓 생성
displayArea.append(">> 서버 시작. 클라이언트 연결을 기다립니다...\n");
socket = listener.accept(); // 클라이언트 연결 대기
displayArea.append(">> 클라이언트와 연결되었습니다.\n");
// 스트림 설정
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 수신 전용 스레드 시작
new ReceiverThread().start();
} catch (IOException e) {
displayArea.append(">> 서버 실행 오류: " + e.getMessage() + "\n");
tryCloseAndExit();
}
}
// 통신 자원 정리
private void closeResources() {
try {
if (socket != null) socket.close();
if (listener != null) listener.close();
if (in != null) in.close();
if (out != null) out.close();
} catch (IOException e) {
System.err.println("자원 해제 오류: " + e.getMessage());
}
}
// 오류 발생 시 종료 시도
private void tryCloseAndExit() {
closeResources();
System.exit(0);
}
// 클라이언트로부터 메시지를 수신하는 전용 스레드
class ReceiverThread extends Thread {
@Override
public void run() {
String inputMessage;
try {
while (true) {
inputMessage = in.readLine(); // 클라이언트로부터 메시지 수신을 대기한다.
if (inputMessage == null || inputMessage.equalsIgnoreCase("클라이언트: bye")) {
displayArea.append(">> 상대방이 'bye'를 입력하여 연결을 종료합니다.\n");
break;
}
displayArea.append(inputMessage + "\n");
}
} catch (IOException e) {
// readLine() 메소드에서 오류가 발생하면 연결 끊김으로 판단한다.
displayArea.append(">> 클라이언트 연결이 끊어졌습니다: " + e.getMessage() + "\n");
} finally {
tryCloseAndExit();
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ChatServer::new);
}
}
- ChatClient 객체
package ch15nC;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.*;
import java.net.Socket;
// GUI 기반 클라이언트 채팅 프로그램
public class ChatClient extends JFrame implements ActionListener {
private JTextArea displayArea;
private JTextField inputField;
private Socket socket;
private BufferedReader in;
private BufferedWriter out;
private final String SERVER_IP = "localhost";
private final int PORT = 9999;
public ChatClient() {
super("클라이언트 채팅 프로그램");
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 직접 종료 처리
// 1. UI 구성
displayArea = new JTextArea(10, 30);
displayArea.setEditable(false);
displayArea.setFont(new Font("맑은 고딕", Font.PLAIN, 14));
JScrollPane scrollPane = new JScrollPane(displayArea);
inputField = new JTextField(30);
inputField.addActionListener(this); // Enter 키 입력 시 액션 처리
add(scrollPane, BorderLayout.CENTER);
add(inputField, BorderLayout.SOUTH);
setSize(400, 350);
setVisible(true);
// 2. 윈도우 닫기 이벤트 처리(리소스 정리 및 종료)
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
try {
// 창을 닫을 때 "bye" 메시지를 서버에게 전송
sendMessage("bye");
closeResources();
} catch (IOException ex) {
// 무시
}
System.exit(0);
}
});
// 3. 클라이언트 네트워크 시작
new Thread(() -> startClient()).start();
}
// 메시지 전송 처리
@Override
public void actionPerformed(ActionEvent e) {
String message = inputField.getText();
if (message.isEmpty()) return;
try {
sendMessage(message);
inputField.setText(""); // 전송 후 입력창 초기화
if (message.equalsIgnoreCase("bye")) {
closeResources();
System.exit(0);
}
} catch (IOException ex) {
displayArea.append(">> 메시지 전송 오류: " + ex.getMessage() + "\n");
tryCloseAndExit();
}
}
private void sendMessage(String message) throws IOException {
if (out != null) {
out.write("클라이언트: " + message + "\n");
out.flush();
displayArea.append("나: " + message + "\n");
}
}
private void startClient() {
try {
socket = new Socket(SERVER_IP, PORT); // 서버에 연결 시도
displayArea.append(">> 서버에 연결되었습니다.\n");
// 스트림 설정
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
// 수신 전용 스레드 시작
new ReceiverThread().start();
} catch (IOException e) {
displayArea.append(">> 서버 연결 오류: " + e.getMessage() + "\n");
tryCloseAndExit();
}
}
// 통신 자원 정리
private void closeResources() {
try {
if (socket != null) socket.close();
if (in != null) in.close();
if (out != null) out.close();
} catch (IOException e) {
System.err.println("자원 해제 오류: " + e.getMessage());
}
}
// 오류 발생 시 종료 시도
private void tryCloseAndExit() {
closeResources();
System.exit(0);
}
// 서버로부터 메시지를 수신하는 전용 스레드
class ReceiverThread extends Thread {
@Override
public void run() {
String inputMessage;
try {
while (true) {
inputMessage = in.readLine(); // 서버로부터 메시지 수신 대기
if (inputMessage == null || inputMessage.equalsIgnoreCase("서버: bye")) {
displayArea.append(">> 상대방이 'bye'를 입력하여 연결을 종료합니다.\n");
break;
}
displayArea.append(inputMessage + "\n");
}
} catch (IOException e) {
// readLine() 메소드에서 오류가 발생하면 연결 끊김으로 판단
displayArea.append(">> 서버 연결이 끊어졌습니다: " + e.getMessage() + "\n");
} finally {
tryCloseAndExit();
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(ChatClient::new);
}
}
- 실행: ChatServer를 먼저 실행하고, ChatClient를 나중에 실행하여 서버에 접속되어 채팅을 시작할 수 있는지 테스트하고, JTextField에 메세지를 입력하고
<Enter>키를 누르면 전송되는지 점검했다. 마지막으로 어느 한쪽에서"bye"를 입력하거나 창을 닫으면 프로그램이 종료되는 것을 확인했다.