mini-pms 34g
PMS 코드를 C/S로 분리
Server
01) json 데이터 포맷을 다룰 Gson 라이브러리를 추가한다.
- build.gradle 파일에 gson 라이브러리 정보를 추가한다.
- $ gradle eclipse 를 실행하여 라이브러리를 프로젝트에 추가한다.
- 이클립스 IDE에서 프로젝트를 refresh 한다.
plugins {
id 'java'
id 'application'
id 'eclipse'
}
repositories {
jcenter()
dependencies {
//json
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.guava:guava:29.0-jre'
testImplementation 'junit:junit:4.13'
}
application {
mainClassName = 'com.eomcs.pms.ServerApp'
}
02) 기존 애플리케이션에서 관련된 패키지 및 클래스를 가져온다.
- serverApp을 코드에 맞춰 고친다.
public class ServerApp {
// 옵저버에 관련된 코드를 가져온다.
//옵저버와 공유할 맵 객체
Map<String,Object> context = new Hashtable<>();
// 옵저버를 보관할 컬렉션 객체
List<ApplicationContextListener> listeners = new ArrayList<>();
// 옵저버를 등록하는 메서드
public void addApplicationContextListener(ApplicationContextListener listener) {
listeners.add(listener);
}
// 옵저버를 제거하는 메서드
public void removeApplicationContextListener(ApplicationContextListener listener) {
listeners.remove(listener);
}
// service() 실행 전에 옵저버에게 통지한다.
private void notifyApplicationContextListenerOnServiceStarted() {
for (ApplicationContextListener listener : listeners) {
listener.contextInitialized(context);
}
}
// service() 실행 후에 옵저버에게 통지한다.
private void notifyApplicationContextListenerOnServiceStopped() {
for (ApplicationContextListener listener : listeners) {
// 서비스가 종료되었으니 마무리 작업하라고,
// 마무리 작업에 관심있는 각 옵저버에게 통지한다.
// => 옵저버에게 맵 객체를 넘겨준다.
// => 옵저버는 작업 결과를 파라미터로 넘겨준 맵 객체에 담아 줄 것이다.
listener.contextDestroyed(context);
}
}
// 지정된 포트를 파라미터로 받는다.
private void service(int port) {
// 서비스가 시작되기 전에 옵저버에게 통지하는 코드를 추가한다.
notifyApplicationContextListenerOnServiceStarted();
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중");
while (true) {
Socket clientSocket = serverSocket.accept();
// 람다 문법 적용
new Thread(() -> handleClient(clientSocket)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
// 서비스가 끝났을 때 옵저버에게 통지하는 코드를 추가한다.
notifyApplicationContextListenerOnServiceStopped();
}
public static void main(String[] args) {
ServerApp serverApp = new ServerApp();
// 옵저버 등록
serverApp.addApplicationContextListener(new AppInitListener());
serverApp.addApplicationContextListener(new DataHandlerListener());
// 지정된 포트를 파라미터로 받는다.
serverApp.service(8888);
}
private static void handleClient(Socket clientSocket) {
InetAddress address = clientSocket.getInetAddress();
System.out.printf("클라이언트(%s)가 연결되었습니다.\n",
address.getHostAddress());
try (Socket socket = clientSocket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());
) {
while (true) {
String request = in.readLine();
sendResponse(out, request);
if (request.equalsIgnoreCase("quit")) {
break;
}
}
} catch (Exception e) {
System.out.println("클라이언트와의 통신 오류");
}
System.out.printf("클라이언트(%s)와의 연결을 끊었습니다.\n",
address.getHostAddress());
}
private static void sendResponse(PrintWriter out, String message) {
out.println(message);
out.println();
out.flush();
}
}
03) 클라이언트의 stop 명령어를 처리한다.
- ServerApp 변경
public class ServerApp {
// 클라이언트가 "stop" 명령을 보내면 이 값이 true로 변경된다.
// - 이 값이 true 이면 다음 클라이언트가 접속할 때 서버를 종료한다.
static boolean stop = false;
// 다른 클래스에서도 사용해야 하기 때문에 context를 static으로 바꾼다.
static Map<String,Object> context = new Hashtable<>();
List<ApplicationContextListener> listeners = new ArrayList<>();
public void addApplicationContextListener(ApplicationContextListener listener) {
listeners.add(listener);
}
public void removeApplicationContextListener(ApplicationContextListener listener) {
listeners.remove(listener);
}
private void notifyApplicationContextListenerOnServiceStarted() {
for (ApplicationContextListener listener : listeners) {
listener.contextInitialized(context);
}
}
private void notifyApplicationContextListenerOnServiceStopped() {
for (ApplicationContextListener listener : listeners) {
listener.contextDestroyed(context);
}
}
private void service(int port) {
notifyApplicationContextListenerOnServiceStarted();
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("서버 실행 중");
while (true) {
Socket clientSocket = serverSocket.accept();
new Thread(() -> handleClient(clientSocket)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
notifyApplicationContextListenerOnServiceStopped();
}
public static void main(String[] args) {
ServerApp serverApp = new ServerApp();
serverApp.service(8888);
}
private static void handleClient(Socket clientSocket) {
InetAddress address = clientSocket.getInetAddress();
System.out.printf("클라이언트(%s)가 연결되었습니다.\n",
address.getHostAddress());
try (Socket socket = clientSocket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());
) {
while (true) {
String request = in.readLine();
// stop 명령어를 처리하도록 코드를 변경한다.
if (request.equalsIgnoreCase("quit")) {
out.println("안녕");
out.println();
out.flush();
break;
} else if (request.equalsIgnoreCase("stop")) {
stop = true;
out.println("서버를 종료하는 중입니다.");
out.println();
out.flush();
break;
}
Command command = (Command) context.get(request);
if (command != null) {
command.execute();
} else {
out.println("해당 명령을 처리할 수 없습니다.");
}
// 응답의 끝을 알리는 빈 문자열을 보낸다.
out.println();
out.flush();
}
} catch (Exception e) {
System.out.println("클라이언트와의 통신 오류");
}
System.out.printf("클라이언트(%s)와의 연결을 끊었습니다.\n",
address.getHostAddress());
}
private static void sendResponse(PrintWriter out, String message) {
out.println(message);
out.println();
out.flush();
}
}
04) 파일에서 JSON 데이터를 로딩하고 파일로 저장하는 옵저버를 등록한다.
- ServerApp 변경
- AppInitListener 를 등록한다.
- DataHandlerListener 를 등록한다.
public class ServerApp {
.
.
.
public static void main(String[] args) {
ServerApp serverApp = new ServerApp();
// AppInitListener와 DataHandlerlistener를 등록한다.
serverApp.addApplicationContextListener(new AppInitListener());
serverApp.addApplicationContextListener(new DataHandlerListener());
serverApp.service(8888);
}
.
.
.
}
05) 클라이언트의 요청을 처리하는 Command 객체를 준비한다.
- 05-01) RequestMappingListener 생성
- DataHandlerListener 가 준비한 데이터를 가지고 Command 객체를 생성한다.
// 클라이언트 요청을 처리할 커맨드 객체를 준비한다.
public class RequestMappingListener implements ApplicationContextListener {
@SuppressWarnings("unchecked")
@Override
public void contextInitialized(Map<String,Object> context) {
// 옵저버가 작업한 결과를 맵에서 꺼낸다.
List<Board> boardList = (List<Board>) context.get("boardList");
List<Member> memberList = (List<Member>) context.get("memberList");
List<Project> projectList = (List<Project>) context.get("projectList");
List<Task> taskList = (List<Task>) context.get("taskList");
context.put("/board/add", new BoardAddCommand(boardList));
context.put("/board/list", new BoardListCommand(boardList));
context.put("/board/detail", new BoardDetailCommand(boardList));
context.put("/board/update", new BoardUpdateCommand(boardList));
context.put("/board/delete", new BoardDeleteCommand(boardList));
MemberListCommand memberListCommand = new MemberListCommand(memberList);
context.put("/member/add", new MemberAddCommand(memberList));
context.put("/member/list", memberListCommand);
context.put("/member/detail", new MemberDetailCommand(memberList));
context.put("/member/update", new MemberUpdateCommand(memberList));
context.put("/member/delete", new MemberDeleteCommand(memberList));
context.put("/project/add", new ProjectAddCommand(projectList, memberListCommand));
context.put("/project/list", new ProjectListCommand(projectList));
context.put("/project/detail", new ProjectDetailCommand(projectList));
context.put("/project/update", new ProjectUpdateCommand(projectList, memberListCommand));
context.put("/project/delete", new ProjectDeleteCommand(projectList));
context.put("/task/add", new TaskAddCommand(taskList, memberListCommand));
context.put("/task/list", new TaskListCommand(taskList));
context.put("/task/detail", new TaskDetailCommand(taskList));
context.put("/task/update", new TaskUpdateCommand(taskList, memberListCommand));
context.put("/task/delete", new TaskDeleteCommand(taskList));
context.put("/hello", new HelloCommand());
}
@Override
public void contextDestroyed(Map<String,Object> context) {
}
}
- 05-02) ServerApp 에 RequestMappingListener 를 등록한다.
public class ServerApp {
.
.
.
public static void main(String[] args) {
ServerApp serverApp = new ServerApp();
serverApp.addApplicationContextListener(new AppInitListener());
serverApp.addApplicationContextListener(new DataHandlerListener());
// RequestMappingListener를 등록한다.
serverApp.addApplicationContextListener(new RequestMappingListener());
serverApp.service(8888);
}
.
.
.
}
06) 클라이언트 명령이 들어오면 커맨드 객체를 찾아 실행하고 클라이언트에게 입력 값을 요구할 수 있도록 프로토콜을 변경한다.
- 06-01) Command 객체가 클라이언트에게 응답할 수 있도록 출력 스트림 객체를 넘겨주고 입력 값을 읽을 수 있도록 파라미터에 입력 스트림을 추가한다.
- Command 클래스 변경
// 사용자의 명령을 처리하는 객체에 대해 호출할 메서드 규칙을 정의 한다.
public interface Command {
// 클라이언트에게 응답할 때 사용할 출력 스트림을 파리미터로 받는다.
// 클라이언트가 보낸 데이터를 읽을 때 사용할 입력 스트림을 파라미터로 받는다.
void execute(PrintWriter out, BufferedReader in);
}
- 06-02) Command 인터페이스 변경에 따라 execute() 메서드의 코드를 수정한다.
- ServerApp 클래스 변경
- XxxListCommand 클래스들 변경
public class ServerApp {
.
.
.
private static void handleClient(Socket clientSocket) {
InetAddress address = clientSocket.getInetAddress();
System.out.printf("클라이언트(%s)가 연결되었습니다.\n",
address.getHostAddress());
try (Socket socket = clientSocket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream());
) {
while (true) {
String request = in.readLine();
if (request.equalsIgnoreCase("quit")) {
out.println("안녕");
out.println();
out.flush();
break;
} else if (request.equalsIgnoreCase("stop")) {
stop = true;
out.println("서버를 종료하는 중입니다.");
out.println();
out.flush();
break;
}
Command command = (Command) context.get(request);
if (command != null) {
// execute에 파라미터를 받는다.
command.execute(out, in);
} else {
out.println("해당 명령을 처리할 수 없습니다.");
}
out.println();
out.flush();
}
} catch (Exception e) {
System.out.println("클라이언트와의 통신 오류");
}
System.out.printf("클라이언트(%s)와의 연결을 끊었습니다.\n",
address.getHostAddress());
}
}
- 06-03) Prompt 클래스 변경
- 파라미터로 받은 출력 스트림으로 프롬프트 제목을 출력하고 파라미터로 받은 입력 스트림에서 값을 읽어 리턴하는 메서드를 추가한다.
public class Prompt {
static Scanner keyboardScan = new Scanner(System.in);
// 다른 패키지에서 메서드를 호출할 수 있도록 사용 범위를 public 으로 공개한다.
public static String inputString(String title) {
System.out.print(title);
return keyboardScan.nextLine();
}
public static String inputString(
String title,
PrintWriter out,
BufferedReader in) throws Exception {
// 클라이언트로 출력할 때는 제목 다음에 "!{}!" 문자열을 보내
// 클라이언트가 사용자로부터 값을 입력받아 다시 서버에 보내도록 요청한다.
out.print(title); // 클라이언트가 출력할 프롬프트 제목
out.println("!{}!"); // 클라이언트에게 값을 보내라는 요청
out.flush(); // 주의! 출력하면 버퍼에 쌓이기 때문에 flush()를 호출해서 서버로 보낸다.
return in.readLine(); // 클라이언트가 보낸 값을 읽기
}
public static int inputInt(String title) {
return Integer.parseInt(inputString(title));
}
public static int inputInt(
String title,
PrintWriter out,
BufferedReader in) throws Exception {
return Integer.parseInt(inputString(title, out, in));
}
public static Date inputDate(String title) {
return Date.valueOf(inputString(title));
}
public static Date inputDate(
String title,
PrintWriter out,
BufferedReader in) throws Exception {
return Date.valueOf(inputString(title, out ,in));
}
// 프롬프트의 사용이 모두 끝났으면
// 이 메서드를 호출하여 System.in 입력 스트림 자원을 해제하도록 한다.
public static void close() {
keyboardScan.close();
}
}
- 06-04) Xxx[Add, Detail, Update, Delete]Command 구현체 변경
- Command 인터페이스 변경에 따라 execute() 메서드의 코드를 수정한다.
- System.out.println 은 out.println으로 변경
- 그 외의 prompt를 사용하는 등의 것은 out, in을 추가한다.
- 예외가 발생하는 경우에는 try-catch 를 사용한다.
- Command 인터페이스 변경에 따라 execute() 메서드의 코드를 수정한다.
Client
01) 서버에 stop 명령을 보내면 클라이언트를 종료하게 만든다.
public class ClientApp {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("프로그램 사용법");
System.out.println("java -cp ... ClientApp 서버주소 포트번호");
System.exit(0);
}
// 클라이언트가 서버에 stop 명령을 보내면 다음 변수를 true로 변경한다.
boolean stop = false;
try (Socket socket = new Socket("localhost", 8888);
PrintWriter out = new PrintWriter(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
while (true) {
String input = Prompt.inputString("명령> ");
out.println(input);
out.flush();
receiveResponse(in);
if (input.equalsIgnoreCase("quit")) {
break;
// 클라이언트가 서버에 stop 명령을 보내면 다음 변수를 true로 변경한다.
} else if (input.equalsIgnoreCase("stop")) {
stop = true;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (stop) {
// 서버를 멈추기 위해 그냥 접속했다가 끊는다.
try (Socket socket = new Socket(args[0], Integer.parseInt(args[1]))) {
// 아무것도 안한다.
// 서버가 stop 하게 만들기 위해서 접속했다가 끊을 뿐이다.
} catch (Exception e) {
// 아무것도 안한다.
}
}
}
private static void receiveResponse(BufferedReader in) throws Exception {
while (true) {
String response = in.readLine();
if (response.length() == 0) {
break;
}
System.out.println(response);
}
}
}
02) 서버가 입력 값을 요구하면 사용자로부터 입력 값을 받아 보낸다.
public class ClientApp {
public static void main(String[] args) {
if (args.length != 2) {
System.out.println("프로그램 사용법");
System.out.println("java -cp ... ClientApp 서버주소 포트번호");
System.exit(0);
}
boolean stop = false;
try (Socket socket = new Socket("localhost", 8888);
PrintWriter out = new PrintWriter(socket.getOutputStream());
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
while (true) {
String input = Prompt.inputString("명령> ");
out.println(input);
out.flush();
// 사용자로부터 값을 입력받는 파라미터를 만든다.
receiveResponse(out, in);
if (input.equalsIgnoreCase("quit")) {
break;
} else if (input.equalsIgnoreCase("stop")) {
stop = true;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (stop) {
try (Socket socket = new Socket(args[0], Integer.parseInt(args[1]))) {
} catch (Exception e) {
}
}
}
// 사용자로부터 값을 입력받는 파라미터를 만든다.
private static void receiveResponse(PrintWriter out, BufferedReader in) throws Exception {
while (true) {
String response = in.readLine();
if (response.length() == 0) {
break;
} else if (response.equals("!{}!")) {
// 사용자로부터 값을 입력받아서 서버에 보낸다.
out.println(Prompt.inputString(""));
out.flush();
} else {
System.out.println(response);
}
}
}
}