Java

2024.05.23 Java 유용한 클래스 1:1 양방향 통신(채팅 기본 기능 구현)(리팩토링)

정훈5 2024. 5. 23. 09:20

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

package ch05;

import java.io.BufferedReader;
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("===== 서버 실행 =====");
		
		// 서버측 소켓을 만들기 위한 준비물
		// 서버 소켓, 포트 번호
		
		try(
				ServerSocket serverSocket = new ServerSocket(5000);
				) {
			// 클라이언트 대기 타다가 --> 연결 요청이 오면 -- 소켓 객체를 생성하는 메서드 (클라이언트와 연결된 상태)
		Socket socket = serverSocket.accept();
		System.out.println("------ client connected ------");
		
		// 클라이언트 통신을 위한 스트림을 설정해야 한다. (대상 소켓을 얻었기 때문이다.)
		BufferedReader readerStream = 
				new BufferedReader(new InputStreamReader(socket.getInputStream()));
		// Output 스트림
		PrintWriter writerStream = new PrintWriter(socket.getOutputStream(), true);
		
		// 키보드 스트림 준비
		BufferedReader keyBufferedReader =
				new BufferedReader(new InputStreamReader(System.in));
		
		// 스레드를 시작 합니다.
		startReadThread(readerStream);
		startWriteThread(writerStream, keyBufferedReader);
		
		System.out.println("main 스레드 작업 완료 . . . ");
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	
	} // end of main
	
	
	// 클라이언트로 부터 데이터를 읽는 스레드(Thread) 분리
	// 소켓 <--- 스트림을 얻어야 한다. 데이터를 읽는 객체는 누구인가? <-- 문자.
	private static void startReadThread(BufferedReader bufferedReader) {
		
		Thread readThread = new Thread(() -> {
			try { // run 메서드 안쪽
				
				String clientMessage;
				while( (clientMessage = bufferedReader.readLine()) != null ) {
					// 서버측 콘솔에 클라이언트가 보낸 문자 데이터 출력
					System.out.println("클라이언트에서 온 MSG : " + clientMessage);
				}
				
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		});
		readThread.start(); // 스레드 실행 -> run() 메서드 진행
		// 메인 스레드 대기 처리 --> join() --> 고민 --> 2번에 반복 될듯
		
	}
	
	// 서버측에서 --> 클라이언트로 데이터를 보내는 기능
	private static void startWriteThread(PrintWriter printWriter,
			BufferedReader keyboardReader) {
		
		Thread writeThread = new Thread(() -> {
			try {
				String serverMessage;
				while( (serverMessage = keyboardReader.readLine()) != null )  {
					printWriter.println(serverMessage);
					printWriter.flush();
				}
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		});
		writeThread.start();
		// 메인 스레드 대기
		waitForThreadToEnd(writeThread);
	}
	
	// 워커 스레드가 종료될때까지 기다리는 메서드
	private static void waitForThreadToEnd(Thread thread) {
		try {
			thread.join();
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	
	
	
	
	
	
	
	
	

} // end of class

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

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

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;
	}
	
	// 실행에 흐름이 필요하다. (순서가 중요)
	public final void run() { // 메서드에 final 쓰면 자식 메서드에서 @Override 사용 못함
		// 1. 서버 셋팅 - 포트 번호 할당
		try {
			setupServer();
			connection();
			setupStream();
			startService(); // 내부적으로 while 동작 
			
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			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 = createdReadThread();
		// while  --->
		Thread writeThread = createWriteThread();
		
		readThread.start();
		writeThread.start();
	}
	
	// 캡슐화 개념 외부에서 어떻게 코드 작성했는지 모른다.
	private Thread createdReadThread() {
		return new Thread(() -> {
			
			try {
				String message;
				while( (message = readerStream.readLine()) != null ) {
					// 서버측 콘솔에 출력
					System.out.println("client 측 message: " + message);
				}
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
			
		});
	}
	
	private Thread createWriteThread() {
		return new Thread(() -> {
			try {
				String message;
				// 서버측 키보드에서 데이터를 한줄 라인으로 읽음
				while( (message = keyboardReader.readLine()) != null ) {
					// 클라이언트와 연결된 소켓에다가 데이터를 보냄 
					writerStream.println(message);
					writerStream.flush();
				}
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		});
	}
	
	// 캡슐화 - 소켓 자원 종료
	private void cleanup() {
		try {
			
			if(socket != null) {
				socket.close();
			}
			
			if(serverSocket != null) {
				serverSocket.close();
			}
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}

} // end of class

구현 클래스 - 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 on 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());
			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("client에서 온 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) {
			e.printStackTrace();
		} 
	}
	
} // end of class

 

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

 

public abstract class AbstractClient {

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

	public final void run() {
		try {
			connectToServer();
			setupStreams();
			startCommunication();
		} catch (IOException | InterruptedException e) {
			e.printStackTrace();
		} finally {
			cleanup();
		}
	}

	protected abstract void connectToServer() throws IOException;

	private void setupStreams() throws IOException {
		writerStream = new PrintWriter(socket.getOutputStream(), true);
		readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		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();
		}
	}
}

 

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

public class MultiThreadedClient 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("#### 클라이언트 실행 ####");
		MultiThreadedClient client = new MultiThreadedClient();
		client.run();
	}
}