2022.08.19/자바정리/Proxy/Stateful/Stateless/Connection-Oriented/Connectionless

Jimin·2022년 8월 20일
0

비트캠프

목록 보기
25/60
post-thumbnail
  • board-app 프로젝트 수행
      1. Proxy 패턴을 이용한 네트워킹 코드 캡슐화
      1. Client/Server 공통 코드를 라이브러리 프로젝트로 분리하기
      1. 통신 방식을 Stateful에서 Stateless로 변경하기
  • Proxy 패턴의 이해와 활용법
  • Connection-Oriented 통신과 Connectionless 통신 비교
  • Stateful 통신 방식과 Stateless 통신 방식 비교

Client/Server 리팩토링

  • 순차적으로 요청 처리
  • ServerApp
ServerSocket ss = ...;
while(true){
	Socket s = ss.accept(); <-- 계속 받음
    .
    .
    .
}
  • ClientApp
Socekt s = new Socket(IP, Port); <- 소켓 생성 즉시 server에서 ss.accpet()
.
.
.
s.close();

Proxy 패턴을 활용한 네트워킹 코드를 캡슐화

캡슐화: 복잡한 코드가 안보이도록 클래스 안에 메서드로 감춘다.

해당 기능이 필요한 경우 간단히 메서드 호출로 처리한다.

Proxy 패턴의 이해와 활용법



  • Proxy 패턴 적용 이전과 이후 구조

    즉, Client인 BoardHandler 클래스에서는 통신에 관련된 정보(코드)가 없어도 되도록,
    통신 관련 모든 코드를 전부 BoardDaoProxy 클래스에서 담당한다.

board-app project

board-app-server

1단계 - BoardDao 대행자 클래스를 만든다.

  • board.dao.BoardDaoProxy 클래스 추가

    • BoardHandler 클래스에서 통신과 관련된 모든 것을 이 클래스로 옮긴다.(대리인을 만드는 것)
    • 즉, BoardDaoProxy 클래스는 BoardDao 클래스와 통신을 담당할 대행 객체이다.
    • Server 개발자가 Client 개발자는 통신과 관련된 정보를 몰라도 되도록, 고유 개발에만 집중할 수 있도록 만들어주는 부분이다.
  • board.dao.BoardDaoProxyTest 클래스 추가

    • Server 개발자는 Proxy 클래스를 만든 뒤 잘 돌아가는지 테스트를 한 뒤 Client 개발자에게 배포해주어야 한다.
  • BoardDaoProxy class

package com.bitcamp.board.dao;

// BoardDao와 통신을 담당할 대행 객체
// 모든 out은 서버로 보내는 것이다.

public class BoardDaoProxy {
  String dataName;
  DataInputStream in;
  DataOutputStream out;

  public BoardDaoProxy(String dataName, DataInputStream in, DataOutputStream out) {
    this.dataName = dataName;
    this.in = in;
    this.out = out;
  }

  public boolean insert(Board board)throws Exception {
    out.writeUTF(dataName);
    out.writeUTF("insert");
    out.writeUTF(new Gson().toJson(board)); // json을 서버로 보내기
    // 서버로부터 요청했던 데이터 읽어오기
    return in.readUTF().equals("success");
  }

  public boolean update(Board board) throws Exception{
    out.writeUTF(dataName);
    out.writeUTF("update");
    out.writeUTF(new Gson().toJson(board));
    return in.readUTF().equals("success");
  }

  public Board findByNo(int boardNo)  throws Exception{
    out.writeUTF(dataName);
    out.writeUTF("findByNo");
    out.writeInt(boardNo);
    if(in.readUTF().equals("fail")) {
      return null;
    }
    return  new Gson().fromJson( in.readUTF(), Board.class);
  }

  public boolean delete(int boardNo) throws Exception{
    out.writeUTF(dataName);
    out.writeUTF("delete");
    out.writeInt(boardNo);
    return in.readUTF().equals("success");
  }

  public Board[] findAll() throws Exception{
    out.writeUTF(dataName);
    out.writeUTF("findAll");

    if(in.readUTF().equals("fail")) {
      return null;
    }

    return new Gson().fromJson( in.readUTF(), Board[].class);
  }

}
  • BoardDaoProxyTest class
package com.bitcamp.board.dao;

public class BoardDaoProxyTest {

  public static void main(String[] args) throws Exception {
    try (Socket socket = new Socket("127.0.0.1", 8888);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream())) {

      BoardDaoProxy boardDao = new BoardDaoProxy("board", in, out);

      // 테스트1) 목록 가져오기
      Board[] boards = boardDao.findAll();
      for (Board b : boards) {
        System.out.println(b);
      }
      System.out.println("---------------------------------");

      // 테스트2) 상세 데이터 가져오기
      Board board = boardDao.findByNo(3);
      System.out.println(board);
      System.out.println("---------------------------------");

      // 테스트3) 데이터 등록하기
      board = new Board();
      board.title = "xxxx";
      board.content = "xxxxxxxxxx";
      board.viewCount = 11;
      board.createdDate = System.currentTimeMillis();
      board.writer = "hong";
      board.password = "1111";

      System.out.println(boardDao.insert(board));
      System.out.println("---------------------------------");

      // 데이터 등록 확인
      boards = boardDao.findAll();
      for (Board b : boards) {
        System.out.println(b);
      }
      System.out.println("---------------------------------");

      // 테스트4) 데이터 변경하기
      board = boardDao.findByNo(3);
      board.title = "okokok";
      board.content = "nononono";
      System.out.println(boardDao.update(board));
      System.out.println("---------------------------------");

      // 데이터 변경 확인
      board = boardDao.findByNo(3);
      System.out.println(board);
      System.out.println("---------------------------------");

      // 테스트5) 데이터 삭제하기
      System.out.println(boardDao.delete(3));
      System.out.println("---------------------------------");

      // 데이터 삭제 확인
      boards = boardDao.findAll();
      for (Board b : boards) {
        System.out.println(b);
      }
      System.out.println("---------------------------------");

      out.writeUTF("exit");
    }
  }

}

2단계 - MemberDao 대행자 클래스를 만든다.

  • board.dao.MemberDaoProxy 클래스 추가
  • board.dao.MemberDaoProxyTest 클래스 추가

board-app-client

1단계 - BoardDaoProxy 클래스를 적용한다.

  • board.dao.BoardDaoProxy 클래스 추가

    • 서버 개발자가 만들어준 클래스를 board-app-client 폴더에 추가한다.
  • board.handler.BoardHandler 클래스를 사용한다.

    • 직접 통신하는 대신에 BoardDaoProxy 클래스를 사용한다.
    • 통신과 관련된 코드는 전부 안보이게 된다.
  • BoardHandler class

*
 * 게시글 메뉴 처리 클래스
 */
package com.bitcamp.board.handler;

public class BoardHandler extends AbstractHandler {

  private BoardDaoProxy boardDao;

  public BoardHandler(String dataName, DataInputStream in, DataOutputStream out) {

    // 수퍼 클래스의 생성자를 호출할 때 메뉴 목록을 전달한다.
    super(new String[] {"목록", "상세보기", "등록", "삭제", "변경"});

    boardDao = new BoardDaoProxy(dataName, in, out);
  }

  @Override
  public void service(int menuNo) {
    try {
      switch (menuNo) {
        case 1: this.onList(); break;
        case 2: this.onDetail(); break;
        case 3: this.onInput(); break;
        case 4: this.onDelete(); break;
        case 5: this.onUpdate(); break;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private void onList() throws Exception {
    Board[] boards = boardDao.findAll();

    if (boards == null) {
      System.out.println("목록을 가져오는데 실패했습니다!");
      return;
    }

    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    System.out.println("번호 제목 조회수 작성자 등록일");
    for (Board board : boards) {
      Date date = new Date(board.createdDate);
      String dateStr = formatter.format(date); 
      System.out.printf("%d\t%s\t%d\t%s\t%s\n",
          board.no, board.title, board.viewCount, board.writer, dateStr);
    }
  }

  private void onDetail() throws Exception {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("조회할 게시글 번호? ");
        break;
      } catch (Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    Board board = boardDao.findByNo(boardNo);

    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다!");
      return;
    }

    System.out.printf("번호: %d\n", board.no);
    System.out.printf("제목: %s\n", board.title);
    System.out.printf("내용: %s\n", board.content);
    System.out.printf("조회수: %d\n", board.viewCount);
    System.out.printf("작성자: %s\n", board.writer);
    Date date = new Date(board.createdDate);
    System.out.printf("등록일: %tY-%1$tm-%1$td %1$tH:%1$tM\n", date);
  }

  private void onInput() throws Exception {
    Board board = new Board();

    board.title = Prompt.inputString("제목? ");
    board.content = Prompt.inputString("내용? ");
    board.writer = Prompt.inputString("작성자? ");
    board.password = Prompt.inputString("암호? ");
    board.viewCount = 0;
    board.createdDate = System.currentTimeMillis();

    if (boardDao.insert(board)) {
      System.out.println("게시글을 등록했습니다.");
    } else {
      System.out.println("게시글 등록에 실패했습니다!");
    }
  }

  private void onDelete() throws Exception {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("삭제할 게시글 번호? ");
        break;
      } catch (Exception ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    if (boardDao.delete(boardNo)) {
      System.out.println("삭제하였습니다.");
    } else {
      System.out.println("해당 번호의 게시글이 없습니다!");
    }
  }

  private void onUpdate() throws Exception {
    int boardNo = 0;
    while (true) {
      try {
        boardNo = Prompt.inputInt("변경할 게시글 번호? ");
        break;
      } catch (Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }

    Board board = boardDao.findByNo(boardNo);
    if (board == null) {
      System.out.println("해당 번호의 게시글이 없습니다!");
      return;
    }

    board.title = Prompt.inputString("제목?(" + board.title + ") ");
    board.content = Prompt.inputString(String.format("내용?(%s) ", board.content));

    String input = Prompt.inputString("변경하시겠습니까?(y/n) ");

    if (input.equals("y")) {
      if (boardDao.update(board)) {
        System.out.println("변경했습니다.");
      } else {
        System.out.println("변경 실패입니다!");
      }

    } else {
      System.out.println("변경 취소했습니다.");
    }
  }
}

2단계 - MemberDaoProxy 클래스를 적용한다.

  • com.bitcamp.board.dao.MemberDaoProxy 클래스 추가
    • 서버 개발자가 만들어준 클래스를 추가한다.
  • com.bitcamp.board.handler.MemberHandler 클래스 변경
    • 직접 통신하는 대신에 MemberDaoProxy 클래스를 사용한다.

Client/Server 공통 코드를 라이브러리 프로젝트로 분리하기

  • 서브 프로젝트를 분리하여 공유하는 방법

app-client, app-server pkg

1단계 - 도메인 클래스와 프록시 클래스를 app-common 프로젝트로 옮긴다.

  • Domain class와 Proxy class는 Client와 Server 둘 모두에서 사용되는 공통 클래스이므로 하나의 폴더(Common 폴더)에서 관리한다.
  • com.bitcamp.board.domain.* 패키지 삭제
  • com.bitcamp.board.dao.XxxDaoProxyXxx 클래스 삭제
    Client

    Server

2단계 - app-common 서브 프로젝트를 포함시킨다.

  • build.gradle 파일 변경
  • board-app-client와 board-app-server 폴더의 build.gradle dependencies에 다음의 코드를 추가하여 준다.
implementation project(':app-common') // 폴더 이름

app-common

app-client, app-server pkg

1단계 - 도메인 클래스와 프록시 클래스를 app-common 프로젝트로 옮긴다.

  • com.bitcamp.board.domain.* 패키지 추가
  • com.bitcamp.board.dao.XxxDaoProxyXxx 클래스 추가
    Common

2단계 - app-common 프로젝트 자바 라이브러리 프로젝트로 만든다.

  • build.gradle 파일 변경
    • id 'java-library' 설정
  • board-app-common 폴더
    • plugin 수정
id 'java-library' // app 이 아니라 다른 프로젝트에 의해서 공유 되어짐
  • eclipse project 수정
// Eclipse IDE에서 사용할 프로젝트 정보 설정하기
eclipse {
    project {
        name = "board-app-common" 
        // 프로젝트 이름을 지정하지 않으면 build.gradle 파일이 있는 
        // 폴더 이름을 프로젝트 이름을 사용한다.
    }
    ...
}

통신방식을 Stateful에서 Stateless로 변경하기

Stateful과 Stateless 방식의 차이점 이해

Connection-Oriented 통신과 Connectionless 통신 비교

  1. Connection-Oriented(연결 지향) = TCP(데이터 전송 보장O)
    속도 느림
    • Stateful: 은행업무 ⇒ FTP, POP3, 채팅, 게임 (사용자가 종료할 때까지 연결 지속)
    • Stateless: 114, ARS 인증 ⇒ HTTP
  2. Connectionless = UDP(데이터 전송 보장X)
    속도 빠름
    • 편지(상대편 확인 안하고 무조건 전송), 방송(무조건 다 쏘고, 받을 사람 받는 것)
      ⇒ ping(상대편이 죽었는지 살았는지 확인)

board-app project

app-client

1단계 -ClientApp 에서 서버에 연결하는 방식을 제거한다.

  • com.bitcamp.board.ClientApp 클래스 변경
    • Scoket 객체 생성 코드 제거
    • "exit" Server로 날리는 코드 제거

app-server

1단계 - 한 번 연결에 한 번의 요청만 처리한다.

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

    • while문 삭제
      → 하나의 Client 하고만 연결하지 않게한다.
      → 하나의 Client가 원하는 기능 하나 수행 후 바로 연결을 종료한다.
      ⇒ 여러 Client들이 빠르게 연결하고 연결을 끊을 수 있다.
  • 이전 ServerApp class

public class ServerApp {
  public static void main(String[] args) {
    System.out.println("[게시글 데이터 관리 서버]");

    try (ServerSocket serverSocket = new ServerSocket(8888);) {

      System.out.println("서버 소켓 준비 완료!");

      ...

      while (true) {
        try (Socket socket = serverSocket.accept();
            DataInputStream in = new DataInputStream(socket.getInputStream());
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

          System.out.println("클라이언트와 연결 되었음!");

          while (true) {
            // 클라이언트와 서버 사이에 정해진 규칙(protocol)에 따라 데이터를 주고 받는다.
            String dataName = in.readUTF();

            if (dataName.equals("exit")) {
              break;
            }

            Servlet servlet = servletMap.get(dataName);
            if (servlet != null) {
              servlet.service(in, out);
            } else {
              out.writeUTF("fail");
            }
          } 

          System.out.println("클라이언트와 연결을 끊었음!");
        } // 안쪽 try
      }
    } catch (Exception e) {
      e.printStackTrace();
    } // 바깥 쪽 try 

    System.out.println("서버 종료!");
  }
}
  • 이후 ServerApp class
public class ServerApp {
  public static void main(String[] args) {
    System.out.println("[게시글 데이터 관리 서버]");

    try (ServerSocket serverSocket = new ServerSocket(8888);) {

      System.out.println("서버 소켓 준비 완료!");

      ...

      while (true) {
        try (Socket socket = serverSocket.accept();
            DataInputStream in = new DataInputStream(socket.getInputStream());
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());) {

          System.out.println("클라이언트와 연결 되었음!");

          // 클라이언트와 서버 사이에 정해진 규칙(protocol)에 따라 데이터를 주고 받는다.
          String dataName = in.readUTF();

          if (dataName.equals("exit")) {
            break;
          }

          Servlet servlet = servletMap.get(dataName);
          if (servlet != null) {
            servlet.service(in, out);
          } else {
            out.writeUTF("fail");
          }

          System.out.println("클라이언트와 연결을 끊었음!");
        } // 안쪽 try
      }
    } catch (Exception e) {
      e.printStackTrace();
    } // 바깥 쪽 try 

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

app-common

1단계 - XxxDaoProxy 의 통신 방식을 Stateless 로 변경한다.

  • com.bitcamp.board.dao.XxxDaoProxy 클래스 변경
    • 메소드들 마다 Socket을 생성한다.
    • 즉, 기능별로 Server와 연결을 맺고 끊는다.
  • BoardDaoProxy class
package com.bitcamp.board.dao;

// BoardDao와 통신을 담당할 대행 객체
//
public class BoardDaoProxy {

  ...

  public boolean insert(Board board) throws Exception {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream());) {

      out.writeUTF(dataName);
      out.writeUTF("insert");
      out.writeUTF(new Gson().toJson(board));
      return in.readUTF().equals("success");
    }
  }

  public boolean update(Board board) throws Exception {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream());) {
      out.writeUTF(dataName);
      out.writeUTF("update");
      out.writeUTF(new Gson().toJson(board));
      return in.readUTF().equals("success");
    }
  }

  public Board findByNo(int boardNo) throws Exception {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream());) {

      out.writeUTF(dataName);
      out.writeUTF("findByNo");
      out.writeInt(boardNo);

      if (in.readUTF().equals("fail")) {
        return null;
      }
      return new Gson().fromJson(in.readUTF(), Board.class);
    }
  }

  public boolean delete(int boardNo) throws Exception {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream());) {

      out.writeUTF(dataName);
      out.writeUTF("delete");
      out.writeInt(boardNo);
      return in.readUTF().equals("success");
    }
  }

  public Board[] findAll() throws Exception {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream());) {
      out.writeUTF(dataName);
      out.writeUTF("findAll");

      if (in.readUTF().equals("fail")) {
        return null;
      }
      return new Gson().fromJson(in.readUTF(), Board[].class);
    }
  }
}

현재 board-app project 구조

게시판 메뉴를 실행하여 목록을 볼 때 작동 순서

  1. ServerApp 클래스 실행
    • Hashtable을 통해 servlet 클래스들의 객체들을 만들어 놓는다.
      key: DataName: "board"와 value: new BoardServlet("board")
    • ServerSocket을 통해 Client에서 요청이 올 때까지 대기한다.
  2. ClientApp 클래스 실행
    • 접속할 네트워크의 IP Address와 Port Number를 준비한다.
    • Handler들을 준비한다.
    • 게시판 메뉴를 실행하기 위해 1을 입력하고 BoardHandler 객체의 execute() 를 호출한다.
  3. BoardHandler 클래스 실행
    • BoardDaoProxy 클래스를 실행한다, dataName: "board" 와 IP, Port를 넘겨준다.
    • 목록 보기 메뉴 실행을 위해 1을 입력하고 onList()를 실행한다.
      • BoardDaoProxy 클래스의 findAll() 메소드를 호출한다.
  4. BoardDaoProxy 클래스 실행
    • socket을 생성해 넘겨 받는 IP, Port를 가지고 Server와 통신 연결한다.
    • DataInputStream inDataOutputStream Out 을 만든다.
    • out을 통해 Server에 dataName: "board""findAll" 명령어를 Gson을 이용해서 전달한다.
  5. ServerApp 클래스
    • accept() 를 통해 Client에서의 socket을 받고, DataInputStream inDataOutputStream Out 을 만든다.
    • Client의 요청인 dataName: "board" 를 입력받아 in 을 통해 dataName에 맞는 servlet 클래스인 BoardServlet의 service() 를 호출한다.
      호출할 때 service() 의 파라미터로 in, out 을 넘겨준다.
  6. BoardServlet 클래스 실행
    • BoardDao 클래스 객체를 만든다. (boardDao의 메소드들을 여기서 사용한다.)
      BoardDao에 dataName을 이용한 json fileName을 넘겨준다.
    • BoardDao클래스의 load() 를 통해 json 파일을 로딩한다.
    • service() 메소드에서 Client의 command인 findAll을 실행시킨다.
      • boardDao의 findAll() 메소드를 이용한다.
      • 성공적으로 Client의 요구를 수행했음을 다시 Gson을 이용하여 Client에게 out으로 요구 성공 여부와 요구한 데이터들을 응답한다.
  7. BoardDao 클래스 실행
    • BoardServlet 클래스에 의해 사용되어 진다.
    • 각각의 메소드들 마다 Gson을 통해 결과를 json 형태로 저장해둔다.
  8. BoardServlet 클래스가 BoardDao 클래스의 메소드를 사용해서 값 받아와서 Client에 응답
  9. Server에서 온 응답 결과 값을 BoardDaoProxy 클래스에서 in 을 통해 json 형태로 받아와서 그 결과 데이터 값을 다시 변환하여 Board객체형태로 BoardHandler에게 리턴해준다.
    +
    그와 동시에 ServerApp 클래스에서는 Client와의 연결을 종료한다.
  10. 최종 응답 결과 데이터 값을 BoardHandler 클래스에서 사용자에게 화면단으로 출력하여 보여준다.
profile
https://github.com/Dingadung

0개의 댓글