2022.09.13/Application Server

Jimin·2022년 9월 13일
0

비트캠프

목록 보기
40/60
post-thumbnail
  • board-app 프로젝트 수행
      1. Application Server 구조로 전환하기
        → 애플리케이션을 서버에서 실행하는 방법

board-app project

Application Server 구조로 변환

app-server

1단계 - app-client 프로젝트를 복사하여 app-server 프로젝트로 만든다.

  • build.gradle 변경
    • 이클립스 프로젝트 이름을 'board-app-server'로 변경
    • application 메인 클래스를 ServerApp으로 변경

2단계 - 메인 클래스의 이름을 'ServerApp'으로 변경한다.

  • com.bitcamp.board.ServerApp 클래스 이름 변경

3단계 - 클라이언트와 통신 프로토콜을 정의한다.

[client] <-------------------> [server]
	|---- 접속 ------------------->>|
    |<<------ 환영 메시지 ------------|
    |---- 사용자 입력 전송 ---------->>|
    |<<------ 응답 메시지 ------------|
    |          요청/응답 반복	       |
    |---- "quit" ---------------->>|
    |<<------ 연결 끊기  ------------|
요청 메시지: // client -> server
	한 덩어리의 문자열
    
응답 메시지: // server -> client
	한 덩어리의 문자열

4단계 - 통신 프로토콜에 따라 클라이언트의 요청을 처리한다.

  • com.bitcamp.board.ServerApp 클래스 변경
  1. 클라이언트 접속 시 환영 메시지 전송
  2. 여러 클라이언트를 순차적으로 접속 처리
  3. 스레드를 이용하여 여러 클라이언트를 동시 접속 처리
  4. 클라이언트가 보낸 요청 값을 받아서 그래도 돌려둔다.
  5. 요청/응답을 무한 반복한다.
  6. quit 명령을 보내면 연결 끊기
  7. 환영 메시지 후에 메인 메뉴를 응답한다.
  8. 사용자가 선택한 메뉴 번호의 유효성을 검증한다.
  9. 메인 메뉴 선택에 따라 핸들러를 실행하여 클라이언트에게 하위 메뉴를 출력한다.
    • Handler 인터페이스 변경
    • AbstractHandler 추상 클래스의 execute() 변경
  10. Breadcrumb 기능을 객체로 분리한다.
    • BreadCrumb 클래스를 정의한다.

ServerApp class

public class ServerApp {
  //메인 메뉴 목록 준비
  static String[] menus = {"게시판","회원"};

  public static void main(String[] args) {
    try(ServerSocket serverSocket = new ServerSocket(8888)){
      System.out.println("서버 실행중 ...");

      // 핸들러를 담을 컬렉션을 준비한다.
      ArrayList<Handler> handlers = new ArrayList<>();
      handlers.add(new BoardHandler(null));
      handlers.add(new MemberHandler(null));

      while(true) {
        Socket socket = serverSocket.accept();

        new Thread(() -> { // Runnable interface 익명 클래스
          // 스레드를 시작하는 순간 별도의 실행 흐름에서 병행으로 실행된다.
          try(
              DataOutputStream out = new DataOutputStream(socket.getOutputStream()); // 실제 client에게 출력하기 위해서는 이 out을 이용해야한다.
              DataInputStream in = new DataInputStream(socket.getInputStream())
              )
          {
            System.out.println("클라이언트 접속!");

            // client가 접속한 순간, 접속한 클라이언트의 이동 경로를 보관할 breadcrumb 객체 준비 -> 스레드마다 각각 생성해야한다.
            BreadCrumb breadcrumb =new BreadCrumb(); // 현재 스레드 보관소에 저장된다.
            breadcrumb.put("메인");


            boolean first = true;
            String errorMessage = null;

            while(true) {
              // 접속 후 환영 메시지와 메인 메뉴를 출력한다.
              try(
                  StringWriter strOut = new StringWriter(); // 여기에 buffer로 다 쌓여있다가 밑에 toString()으로 다 쏟아낸다.
                  PrintWriter tempOut = new PrintWriter(strOut)
                  ) // try()
              {
                if(first) { // 최초 접속이면 환영 메시지도 출력한다.
                  welcome(tempOut);
                  first = false;                
                }

                if(errorMessage != null) {
                  tempOut.println(errorMessage);
                  errorMessage = null;
                }

                // breadcrumb 메뉴출력
                tempOut.println(breadcrumb.toString());
                // main menu 출력 (게시판, 회원)
                printMainMenus(tempOut);
                out.writeUTF(strOut.toString());  // client로 전송
              } //try(){}

              // 클라이언트가 보낸 요청을 읽는다.
              String request = in.readUTF();
              if(request.equals("quit")) break;

              try {
                int mainMenuNo = Integer.parseInt(request);
                if (mainMenuNo >= 1 && mainMenuNo <= menus.length) { // 메뉴 번호가 유효한 경우
                  // 핸들러에 들어가기 전에 breadcrumb 메뉴에 하위 메뉴 이름을 추가한다.
                  breadcrumb.put(menus[mainMenuNo-1]);
                  // 메뉴 번호로 Handler  객체를 찾아 실행한다.
                  handlers.get(mainMenuNo-1).execute(in, out); // client로 전송할 정보들을 tempOut으로 담으라고 파라미터로 전달해준다.

                  // 다시 메인 메뉴로 돌아 왔다면, breadcrumb 메뉴에서 한 단계 위로 올라간다.
                  breadcrumb.pickUp();
                }else {
                  throw new Exception("해당 번호의 메뉴가 없습니다!");
                }
              }catch(Exception e) {
                errorMessage = String.format("실행 오류: %s\n", e.getMessage());
              } // try{} - catch{}
            } // while()

            System.out.println("클라이언트와 접속 종료!");
          } catch (Exception e) {
            System.out.println("클라이언트와 통신하는 중 오류 발생!");
            e.printStackTrace();
          } // Socket.accept() try(){}
        }).start(); // Thread()
      } // while()

      //      System.out.println("서버 종료!");

    }catch (Exception e) {
      System.out.println("서버 실행 중 오류 발생!");
      e.printStackTrace();
    } // ServerSocket try(){}

  } // main()

  static void welcome(PrintWriter out) throws Exception { 
    out.println("[게시판 애플리케이션]");
    out.println();
    out.println( "환영합니다!");
    out.println();
  }

  static void printMainMenus(PrintWriter out) {
    // 메뉴 목록 출력
    for (int i = 0; i < menus.length; i++) {
      out.printf("  %d: %s\n", i + 1, menus[i]);
    }

    // 메뉴 번호 입력을 요구하는 문장 출력
    out.printf("메뉴를 선택하세요[1..%d](quit: 종료) ", menus.length);
  }

}

Handler interface

// 사용자 요청을 다룰 객체의 사용법을 정의한다.
//
public interface Handler {
  // Handler가 클라이언트와 통신할 수 있도록 입출력 스트림을 파라미터로 전달한다.
  void execute(DataInputStream in, DataOutputStream out) throws Exception;
}

execute() method in AbstractHandler class

public void execute(DataInputStream in, DataOutputStream out) throws Exception {
    // 현재 스레드를 위해 보관된 BreadCrumb 객체를 꺼낸다.
    BreadCrumb breadCrumb = BreadCrumb.getBreadCrumbOfCurrentThread();

    // 핸들러의 메뉴를 클라이언트에게 보낸다.
    try(StringWriter strOut = new StringWriter();
        PrintWriter tempOut = new PrintWriter(strOut);
        ){
      tempOut.println(breadCrumb.toString());
      printMenus(tempOut);
      out.writeUTF(strOut.toString());
    }

    while (true) {
      // 클라이언트가 보낸 요청을 읽는다.
      String request = in.readUTF();
      if(request.equals("0")) break;

      // 클라이언트에게 출력
      try(StringWriter strOut = new StringWriter();
          PrintWriter tempOut = new PrintWriter(strOut);
          ){
        tempOut.println("해당 메뉴를 준비 중 입니다.");

        printBlankLine(tempOut);
        tempOut.println(breadCrumb.toString());
        printMenus(tempOut);
        out.writeUTF(strOut.toString());
      }

BreadCrumb class

public class BreadCrumb {

  public Stack<String> menuStack = new Stack<>(); // client마다 현재 있는 목록 위치가 다르므로 각각 관리해주어야 하므로 static이 아니라 instance로 생성해준다.

  // Thread마다 BreadCrumb 객체를 따로 관리해주는 관리자를 준비한다.
  // -> 현재 스레드의 이름으로 저장하고 꺼낸다.  -> 스레드를 구분해서 관리하고 싶을 때 사용한다.
  static ThreadLocal<BreadCrumb> localManager = new ThreadLocal<>();

  public static BreadCrumb getBreadCrumbOfCurrentThread() {
    // 스레드 로컬 관리자를 통해 현재 스레드 보관소에 저장되어 있는
    // BreadCrumb 객체를 달라고 요청한다.
    return localManager.get(); // 현재 스레드의 이름으로 꺼내기 
  }

  public BreadCrumb() {
    // 스레드 로컬 관리자에게 현재 스레드 전용 보관소에
    // BreadCrumb 객체를 보관해 달라고 요청한다.
    localManager.set(this);  // 현재 스레드의 이름으로 저장
  }

  public void put(String menu) {
    menuStack.push(menu);
  }

  public void pickUp() {
    menuStack.pop(); 
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    for(String title: menuStack) {
      if(!builder.isEmpty()) {
        builder.append(" > ");
      }
      builder.append(title);
    }
    return  builder.toString();
  }// toString()
}

app-client

1단계 - 통신 프로토콜에 따라 서버와 통신한다.

  • com.bitcamp.board.ClientApp 클래스 변경

ClientApp class

public class ClientApp {

  public static void main(String[] args) {
    System.out.println("[게시글 관리 클라이언트]");
    try(
        Socket socket = new Socket("localhost", 8888);
        DataInputStream in = new DataInputStream(socket.getInputStream()); // Decorator pattern을 사용하면 기능 변경이 쉽다.
        DataOutputStream out = new DataOutputStream(socket.getOutputStream())
        ){
      String response = null;

      while(true) {
        response = in.readUTF();
        System.out.println(response);

        // 사용자의 입력값 서버에 전송
        String input = Prompt.inputString("> ");
        out.writeUTF(input);



        if(input.equals("quit")) break;
      }//while()
    } /*try(){}*/ catch(Exception e){
      System.out.println("서버와 통신 중 오류 발생");
    }

}

StringWriter의 활용

profile
https://github.com/Dingadung

1개의 댓글

comment-user-thumbnail
2022년 9월 13일

우왕 동글이다

답글 달기