Java/JAVA 3 - 유용한 클래스

1:1 양방향 통신(채팅 기본 기능 구현) - 24

CNOW 2024. 5. 22. 22:24
💡 멀티 스레드 활용
      양방향 통신을 지속적으로 수행하기 위해 서버와 클라이언트 모두에서 키보드 입력을
      받아 상대방에게 데이터를 보내고 받을 수 있도록 스레드와 while문을 활용하여 코드를 작성해봅시다.

 

서버 측 코드

  • ServerSocket 을 생성하고 클라이언트의 연결을 기다립니다.
  • BufferedReader 를 사용하여 클라이언트로부터 메시지를 읽고, PrintWriter를 사용하여 클라이언트에게 메시지를 보냅니다.
  • 키보드 입력을 받기 위해 BufferedReader를 사용합니다.
  • 클라이언트로부터 데이터를 읽는 스레드와 키보드 입력을 클라이언트로 보내는 스레드를 각각 실행합니다.

 

package ch04;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiThreadServer {

	public static void main(String[] args) {

		System.out.println("=== 서버 실행 ===");

		ServerSocket serverSocket = null;
		Socket socket = null;

		try {
			serverSocket = new ServerSocket(5001);
			socket = serverSocket.accept();
			System.out.println("포트번호 - 5001 할당 완료");

			// 클라이언트로 데이터를 받을 입력 스트림 필요
			BufferedReader socketReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

			// 클라이언트에게 데이터를 보낼 출력 스트림 필요
			PrintWriter sockeWriter = new PrintWriter(socket.getOutputStream(), true);

			// 서버측 - 키보드 입력을 받기 위한 입력 스트림 필요
			BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

			// 멀티 스레딩 개념의 확장
			// 클라이언트로 부터 데이터를 읽는 스레드
			Thread readThread = new Thread(() -> {
				try {
					String clientMessage;
					while ((clientMessage = socketReader.readLine()) != null) {
						System.out.println("서버측 콘솔 : " + clientMessage);
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
			});

			// 클라이언트에게 데이터를 보내는 스레드 생성
			Thread writThread = new Thread(() -> {
				try {
					String serverMessage;
					while ((serverMessage = keyboardReader.readLine()) != null) {
						// 1. 먼저 키보드를 통해서 데이터를 읽고
						// 2. 출력 스트림을 활용해서 데이터를 보내야 한다.
						sockeWriter.println(serverMessage);
					}
				} catch (Exception e2) {

				}
			});

			// 스레드 동작 -> start() 호출
			readThread.start();
			writThread.start();

			// Thread join() 메서드는 하나의 스레드가 종료될때 까지 기다리도록 하는
			// 기능을 제공한다.
			readThread.join();
			writThread.join();

			System.out.println("--- 서버 프로그램 종료 ---");

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				socket.close();
				serverSocket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	} // end of main

} // end of class

 

Thread의 join() 메서드 - 역할과 기능

  1. 스레드 동기화:
      join() 메서드를 사용하여 여러 스레드가 순서대로 종료되도록 할 수 있습니다. 메인 스레드는 join() 메서드를 호출한 스레드가 작업을 마칠 때까지 기다립니다.

  2. 프로그램 흐름 제어:
      join() 메서드를 통해 스레드가 완료되기 전까지 메인 스레드가 종료되지 않도록 보장할 수 있습니다. 이는 프로그램이 모든 작업을 완료하기 전에 종료되는 것을 방지합니다.

  3. 정확한 종료 시점:
      join() 메서드를 사용하면 특정 스레드가 완료되기 전까지 다른 작업을 진행하지 않도록 제어할 수 있습니다. 이를 통해 정확한 종료 시점을 확인할 수 있습니다.

 

클라이언트 측 코드

  • Socket 을 생성하여 서버에 연결합니다.
  • BufferedReader를 사용하여 서버로부터 메시지를 읽고, PrintWriter를 사용하여 서버에게 메시지를 보냅니다.
  • 키보드 입력을 받기 위해 BufferedReader를 사용합니다.
  • 서버로부터 데이터를 읽는 스레드와 키보드 입력을 서버로 보내는 스레드를 각각 실행합니다.
package ch04;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class MultiThreadClient {

	public static void main(String[] args) {

		System.out.println("### 클라이언트 실행 ###");

		try {
			Socket socket = new Socket("192.168.0.48", 5000);
			System.out.println("*** connected to the Server  ***");

			PrintWriter socketWriter = new PrintWriter(socket.getOutputStream(), true);
			BufferedReader socketReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

			// 서버로 부터 데이터를 읽는 스레드
			Thread readThread = new Thread(() -> {
				// while <----
				try {
					String serverMeString;
					while ((serverMeString = socketReader.readLine()) != null) {
						System.out.println("서버에서 온 MSG. " + serverMeString);
					}
				} catch (Exception e) {
					// TODO: handle exception
				}
			});

			// 서버에게 데이터를 보내는 스레드
			Thread writeThread = new Thread(() -> {
				try {
					String clienMessage;
					while ((clienMessage = keyboardReader.readLine()) != null) {
						// 1. 키보드에서 데이터를 응용프로그래 ㅁ안으로 입력 받아서
						// 2. 서버측 소켓과 연결 되어 있는 출력 스트림 통해 데이터를 보낸다.
						socketWriter.println(clienMessage);
					}
				} catch (Exception e2) {
					e2.printStackTrace();
				}
			});

			readThread.start();
			writeThread.start();

			readThread.join();
			writeThread.join();

			System.out.println(" 클라이언트 측 프로그램 종료 ");

		} catch (Exception e) {

		}

	} // end of main
} // end of class

 

서버측 코드 리팩토링 1단계 - 함수로 분리해보기

package ch05;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class MultiThreadServer {

	public static void main(String[] args) {
		
		System.out.println("====== 서버 실행 ======");
		
		// 서버측 소켓을 만들기 위한 준비물
		// 서버소켓, 포트번호
		
		try (ServerSocket serverSocket = new ServerSocket(5000)){
			
			Socket socket = serverSocket.accept(); // 클라이언트 대기 --> 연결 요청 --소켓 객체를 생성(클라이언트와 연결된 상태)
			System.out.println("------ client connected ------");
			
			// 클라이언트와 통신을 위한 스트림을 설정 (대상 소켓을 얻었다.)
			BufferedReader readerStream = 
					new BufferedReader(new InputStreamReader(socket.getInputStream()));
			
			PrintWriter writerStream = 
					new PrintWriter(socket.getOutputStream(), true);
			
			// 키보드 스트림 준비
			BufferedReader keyboardReader = 
					new BufferedReader(new InputStreamReader(System.in));
			
			// 스레드를 시작합니다.
			startReadThread(readerStream);
			startWriterThread(writerStream, keyboardReader);

		
			
			System.out.println("main 스레드 작업 완료...");
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	} // end of main
		////////////////////////////////////////////////////////////////////

	// 클라이언트로 부터 데이터를 읽는 스레드 분리
	// 소켓 <--- 스트림을 얻어야 한다. 데이터를 읽는 객체는 뭐지???? <--- 문자,
	private static void startReadThread(BufferedReader bufferedReader) {

		Thread readThread = new Thread(() -> {
			try {
				String clientMessage;
				while ((clientMessage = bufferedReader.readLine()) != null) {
					// 서버측 콘솔에 클라이언트가 보낸 문자 데이터 출력
					System.out.println("클라이언트에서 온 MSG : " + clientMessage);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
		readThread.start(); // 스레드 실행 -> run() 메서드 진행
		
	}

	// 서버측에서 --> 클라이언트로 데이터를 보내는 기능
	private static void startWriterThread(PrintWriter printWriter, 
											BufferedReader keybordReader) {
		Thread writeThread = new Thread(() -> {
			try {
				String serverMessage;
				while ((serverMessage = keybordReader.readLine()) != null) {
					printWriter.println(serverMessage);
					printWriter.flush();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
		writeThread.start();
		
	}

	// 워커 스레드가 종료될 때까지 가다리는 메서드
	private static void waitForThread(Thread thread) {
		try {
			thread.join();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

} // end of class

waitForThreadToEnd(writeThread); ← 제거 대상 or 리팩토링 대상

 

서버측 코드 리팩토링 2단계 - 상속 활용

package ch05;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

// 상속의 활용
public abstract class AbstractServer {
	
	private ServerSocket serverSocket;
	private Socket socket;
	private BufferedReader readerStream;
	private PrintWriter writerStream;
	private BufferedReader keyboardReader;

	// set 메서드
	// 메서드 의존 주입(멤버 변수의 참조 변수 할당) 
	protected void setServerSocket(ServerSocket serverSocket) {
		this.serverSocket = serverSocket;
	}
	
	protected void setSocket(Socket socket) {
		this.socket = socket;
	}
	
	// get 메서드
	protected ServerSocket getServerSocket() {
		return this.serverSocket;
	}
	
	// 실행의 흐름이 필요하다.(순서가 중요)
	// 메서드에서 final을 쓰면 자식 클래스에서 오버라이딩 불가
	public final void run() {
		// 1. 서버 셋팅 = 포트번호 할당
		try {
			setupServer();
			connection();
			setupStream();
			startService(); // 내부적으로 while 동작
			
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("cleanup() 호출 확인");
			cleanup();
		}
	}
	
	// 1. 포트번호 할당(구현 클래스에서 직접 설계)
	protected abstract void setupServer () throws IOException;
	
	// 2. 클라이언트 연결 대기 실행 (구현 클래스)
	protected abstract void connection() throws IOException;
	
	// 3. 스트림 초기화 (연결된 소켓에서 스트림을 뽑아야 함) - 여기서 함
	private void setupStream() throws IOException {
		readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		writerStream = new PrintWriter(socket.getOutputStream(), true);
		keyboardReader = new BufferedReader(new InputStreamReader(System.in));
	}
	// 4. 서비스 시작
	private void startService() {
		// while <--
		Thread readThread = createReadThread();
		// while -->
		Thread wrThread = createWriteThread();
		
		readThread.start();
		wrThread.start();
		
		try {
			readThread.join();
			wrThread.join();
			// main 스레드 잠깐 기다려 
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	// 캡슐화 
	private Thread createReadThread() {
		return new Thread(() -> {
			try {
				String msg;
				// scnnner.nextLine();  <--- 무한 대기 (사용자가 콘솔에 값 입력 까지 대기) 
				// 코드 ....  
				while((msg = readerStream.readLine()) != null) {
					// 서버측 콘솔에 출력
					System.out.println("client 측 msg : " + msg);
					
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}
	
	private Thread createWriteThread() {
		return new Thread(() -> {
			try {
				String msg;
				// 서버측 키보드에서 데이터를 한줄라인으로 읽음
				while((msg = keyboardReader.readLine()) != null) {
				// 클라이언트와 연결된 소켓에다가 데이터를 보냄 
				writerStream.println(msg);
				writerStream.flush();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}
	
	// 캡슐화 - 소켓 자원 종료
	private void cleanup() {
		try {
			if(socket != null) {
				socket.close();
			}
			if(serverSocket != null) {
				serverSocket.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

 

구현 클래스 - AbstractServer 상속 활용

package ch05;

import java.io.IOException;
import java.net.ServerSocket;

public class MyThreadServer extends AbstractServer {

	@Override
	protected void setupServer() throws IOException {
		// 추상 클래스 --> 부모 -- 자식(부모 기능의 확장 또는 사용)
		// 서버측 소켓 통신 -- 준비물 : 소버 소켓
		super.setServerSocket(new ServerSocket(5000));
		System.out.println(">>> Server started port 5000 <<< ");
	}

	@Override
	protected void connection() throws IOException {
		// 서버 소켓.accept() 호출이다!!
		super.setSocket(super.getServerSocket().accept());
	}
	
	public static void main(String[] args) {
		MyThreadServer myThreadServer = new MyThreadServer();
		myThreadServer.run();
	}

}

복잡한 애플리케이션에서는 추상 클래스와 구현 클래스를 분리하는 것이 유용할 수 있지만, 간단한 경우에는 단일 클래스 설계가 더 적합할 수 있습니다. 상황에 따라 적절한 설계를 선택하는 것이 중요합니다.

 

도전 과제 - 클라이언트 측 코드 리팩토링

1단계, 2단계로 진행해 보기

 

풀이 - 클라이언트 측 코드 리팩토링 1단계

package ch05;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

// 1단계 - 함수로 분리 해서 리팩토링 진행
public class MultiThreadClient {

	// 메인 함수
	public static void main(String[] args) {
		System.out.println("### 클라이언트 실행 ###");

		try (Socket socket = new Socket("localhost", 5000)) {
			System.out.println("*** connected to the Server  ***");
			
			// 서버와 통신을 위한 스트림 초기화
			BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
			BufferedReader keyboardReader = new BufferedReader(new InputStreamReader(System.in));

			StartReadThread(bufferedReader);
			StartWriteThread(printWriter, keyboardReader);
			// 메인 스레드 기다려 어디에 있지??? 가독성이 떨어짐
			// startWriteThread() <--- 내부에 있음
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // end of main
	

	// 1. 클라이언트로부터 데이터를 읽는 스레드 시작 메서드 생성
	private static void StartReadThread(BufferedReader reader) {
		Thread readThread = new Thread(() -> {
			try {
				String msg;
				while ((msg = reader.readLine()) != null) {
					System.out.println("server에서 온 MSG. " + msg);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
		readThread.start();
	}

	// 2. 키보드에서 입력을 받아 클라이언트 측으로 데이터를 전송하는 스레드
	private static void StartWriteThread(PrintWriter writer, BufferedReader keyboardReader) {

		Thread writeThread = new Thread(() -> {
			try {
				String msg;
				while ((msg = keyboardReader.readLine()) != null) {
					// 전송
					writer.println(msg);
					writer.flush();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
		writeThread.start();
		
		try {
			// 메인 쓰레드야 기다려!
			writeThread.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
} // end of class

 

 

풀이 - 클라이언트 측 코드 리팩토링 2단계 (상속활용)

package ch05;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

// 2단계 - 상속 활용
public abstract class AbstractClient {

	private Socket socket;
	private BufferedReader readerStream;
	private PrintWriter writerStream;
	private BufferedReader keyboardReader;

	public final void run() {
		try {
			connectToServer();
			setupStream();
			startCommunication();

		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		} finally {
			cleanup();
		}
	}

	protected abstract void connectToServer() throws IOException;

	private void setupStream() throws IOException {
		readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		writerStream = new PrintWriter(socket.getOutputStream(), true);
		keyboardReader = new BufferedReader(new InputStreamReader(System.in));
	}

	private void startCommunication() throws InterruptedException {
		Thread readThread = createReadThread();
		Thread writeThread = createWriteThread();

		readThread.start();
		writeThread.start();

		readThread.join();
		writeThread.join();
	}

	// 캡슐화
	private Thread createReadThread() {
		return new Thread(() -> {
			try {
				String serverMessage;
				while ((serverMessage = readerStream.readLine()) != null) {
					System.out.println("서버에서 온 msg: " + serverMessage);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
	}

	private Thread createWriteThread() {
		return new Thread(() -> {
			try {
				String clientMessage;
				while ((clientMessage = keyboardReader.readLine()) != null) {
					writerStream.println(clientMessage);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
	}

	protected void setSocket(Socket socket) {
		this.socket = socket;
	}

	private void cleanup() {
		try {
			if (socket != null) {
				socket.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

 

package ch05;

import java.io.IOException;
import java.net.Socket;

// 2 - 1 상속을 활용한 구현 클래스 설계하기
public class MyClient extends AbstractClient {

	@Override
	protected void connectToServer() throws IOException {
		setSocket(new Socket("localhost", 5000));
		System.out.println("*** Connected to the server ***");
	}

	public static void main(String[] args) {
		System.out.println("#### 클라이언트 실행 ####");
		MyClient client = new MyClient();
		client.run();

	}
}

 

 

 

도전 과제 - 자유 (스윙을 활용한 서버소켓 실행 및 로그 관리)