[Java] 97. 코드 리팩토링 (1:1 양방향 통신)
(1) 1단계. 함수로 분리하기
(2) 2단계. 상속 활용하기 (부모-추상 클래스 작성)
(3) 3단계. 상속 활용하기 (자식 클래스 작성)
*먼저 보고 오면 좋은 글
https://whatsthatsound.tistory.com/192
(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();
}
}
'Java > 네트워크 통신' 카테고리의 다른 글
[Java] 99. 1:N 양방향 통신 (0) | 2024.05.24 |
---|---|
[Java] 98. 코드 리팩토링(1:1 양방향 통신-클라이언트 측) (0) | 2024.05.23 |
[Java] 96. 1:1 양방향 통신(채팅 기본 기능 구현) (0) | 2024.05.22 |
[Java] 95. 1:1 양방향 통신 (서버 측) (0) | 2024.05.22 |
[Java] 94. 1:1 단방향 통신 (Client) (0) | 2024.05.22 |