Java/네트워크 통신

[Java] 97. 코드 리팩토링 (1:1 양방향 통신-서버측)

Song hyun 2024. 5. 23. 09:53
728x90
반응형

[Java] 97. 코드 리팩토링 (1:1 양방향 통신)

(1) 1단계. 함수로 분리하기

(2) 2단계. 상속 활용하기 (부모-추상 클래스 작성)

(3) 3단계. 상속 활용하기 (자식 클래스 작성)

 

*먼저 보고 오면 좋은 글

https://whatsthatsound.tistory.com/192

 

[Java] 96. 1:1 양방향 통신(채팅 기본 기능 구현)

[Java] 96. 1:1 양방향 통신(채팅 기본 기능 구현)1. 멀티 스레드의 개념2. 시나리오 코드 작성 (1): 서버 측 구현3. 시나리오 코드 작성 (2): 클라이언트 측 구현 1. 멀티 스레드의 개념* 멀티 스레

whatsthatsound.tistory.com

 

 


(1) 1단계. 함수로 분리하기

(1) 클라이언트로부터 데이터를 읽는 Thread 분리

 

-원래는 main Thread 내부에 readThread/writeThread가 존재했다. 이번에는 리팩토링을 위해, 각각 read/writeThread를 호출하는 startReadThread()/startWriteThread() 메서드를 main 바깥에 생성해보자. 

-해당 메서드 내부에는 Thread의 실행(.start())과 제어(waitForThreadToEnd())가 이루어진다.

// 클라이언트로부터 데이터를 읽는 스레드 분리
	// 소켓 <--- 스트림을 얻어야 한다.
	/// 데이터를 읽는 객체는 뭐지??? <--- 문자.
	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) {

			}

		});
		readThread.start(); // 스레드 실행 -> run() 메서드 실행/
		waitForThreadToEnd(readThread);
		// 메인 스레드 대기 처리 --> join() --> 고민 --> 2번의 반복 될 듯

	}

 

(2) 클라이언트에게 데이터를 보내는 Thread 분리

// 서버 측에서 --> 클라이언트로 데이터를 보내는 기능
	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) {
				e.printStackTrace();
			}

		});
		writeThread.start();
		waitForThreadToEnd(writeThread);
		// 메인 스레드 대기
	}

 

 

(3) 워커 Thread가 종료될 때까지 기다리는 메서드(join())

-앞서 이야기했듯이, 한 스레드가 실행될 동안 다른 스레드가 먼저 종료된다면, socket 역시 close되어 통신이 불가능해진다. 이를 위해 .join()을 호출한다.

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

 

(4) main 함수 작성

-main 쓰레드 내부에 있던 Thread 들을 main 클래스 바깥에 메서드 형태로 만들게 되었다. (method-thread)

그래서 main 함수가 더욱 간단해졌다.

 

-ServerSocket/socket(serverSocket.accept()) 생성

-클라이언트와의 통신/키보드 입력을 위한 readerStream/writerStream/keyboardReader 생성

-위에서 생성한 스트림을 활용해 startReadThread()/startWriteThread() 실행

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);
			startWriteThread(writerStream, keyboardReader);

			System.out.println("main 스레드 작업 완료 ...");

		} catch (Exception e) {
			e.printStackTrace();
		}

	} // end of main

 


(2) 2단계. 상속 활용하기 (부모-추상 클래스 작성) : AbstractServer 클래스

-이번에는 추상 클래스의 상속을 통해, 더욱 간단하게 리팩토링해보자.

우선 AbstractServer 클래스에 핵심적인 멤버변수/ 메서드를 만들어보자.


(1) 멤버변수로 serverSocket / socket / readerStream(BufferedReader) / writerStream(PrintWriter) / keyboardReader(BufferedReader)를 선언한다.

(2) 메서드 의존 주입을 통해 serverSocket과 socket을 초기화한다. (=setter)

(3) serverSocket을 가져올 수 있도록 getter 메서드를 만든다. (=getServerSocket)

 

(4) run() 메서드

-setupServer() // 포트번호 할당

-connection() // 클라이언트 연결 대기

-setupStream() // 스트림 초기화

-startService() // 서비스 시작

메서드들을 호출한다.

 

*try-catch-finally문을 사용해 cleanup() 메서드를 호출한다.

 

(5) setupServer() 메서드: 추상 메서드로, 자식 클래스에서 포트 번호를 할당하게 한다.

(6) connection() 메서드: 추상 메서드로,  클라이언트 연결을 기다리게 한다. (.accpet())

(7) setupStream() 메서드: 앞서 선언된 입출력 스트림들을 초기화시킨다. 

(8) startService() 메서드: readThread/writeThread들을 start/join시킨다.

 

(9) createRead/WriteThread 쓰레드: 입/출력한 데이터를 출/입력한다.

(10) cleanup() 메서드: 자원들을 종료시킨다.

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();
		} catch (Exception e) {
			
		} finally {
			cleanup();
		}

	}
	
	// 1. 포트 번호 할당 (구현 클래스에서 직접 설계)
	protected abstract void setupServer() throws IOException; // 구현부 x = 추상 메서드
	
	// 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 writeThread = createWriteThread();
		
		readThread.start();
		writeThread.start();
		
		try {
			readThread.join();
			writeThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	// 캡슐화 
	private Thread createReadThread() {
		return new Thread(() -> {
			try {
				String msg;
				// scanner.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();
		}
	}
	
}

 

 


(3) 3단계. 상속 활용하기 (자식 클래스 작성)

: MyThreadServer 클래스

 

(1) 앞서 작성한 추상 클래스 - AbstractServer 클래스를 상속(extends) 한다.

(2) setupServer() 메서드: 부모 클래스로부터 serverSocket을 받아와, 메세지를 출력한다.

(3) connection() 메서드: setupServer()로부터 받아온 serverSocket에 .accept()를 걸어 socket을 초기화한다.

(4) main 쓰레드: MyThreadServer 객체를 생성한 뒤 .run()한다.

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 {
		// 서버 소켓.accpet(); 호출이다!!!
		super.setSocket(super.getServerSocket().accept());
		
	}
	
	public static void main(String[] args) {
		MyThreadServer myThreadServer = new MyThreadServer();
		myThreadServer.run();
	}

}
728x90
반응형