mini-pms 34g

8 minute read

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 를 사용한다.

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);
      }
    }
  }
}

Categories:

Updated: