Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 62일차(13주차4일) - Java(Client, Server 아키텍처), myapp-32-1~2 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Java

[비트캠프] 62일차(13주차4일) - Java(Client, Server 아키텍처), myapp-32-1~2

끈기JK 2023. 2. 2. 17:33

 

문자 처리할 때 아래 사용하니 용어 참고한다.

regex

lex yacc

 

 

 

com.eomcs.basic.ex11

 

 

Java 17 API 의 Regex > Pattern 에 나와있다.

package com.eomcs.basic.ex11;

import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Exam0100 {

  public static void main(String[] args) {

    // 1) 패턴 정의
    //    Pattern pattern = Pattern.compile("...", Pattern.CASE_INSENSITIVE);
    //    Pattern pattern = Pattern.compile("\\*", Pattern.CASE_INSENSITIVE);
    //    Pattern pattern = Pattern.compile("[0123456789]", Pattern.CASE_INSENSITIVE);
    //    Pattern pattern = Pattern.compile("[0-9]+", Pattern.CASE_INSENSITIVE);
    Pattern pattern = Pattern.compile("[0-9]+|[\\+\\-\\*/]", Pattern.CASE_INSENSITIVE);
    //    Pattern pattern = Pattern.compile("\\d+|\\D", Pattern.CASE_INSENSITIVE);

    // 2) 패턴에 따라 콘텐트를 검사할 도구 준비
    Matcher matcher = pattern.matcher("123   +   2* 98-24/19");

    // 3) 패턴의 결과를 담을 컬렉션 준비
    ArrayList<String> items = new ArrayList<>();

    // 4) 패턴 검사
    while (matcher.find()) {
      // 5) 패턴과 일치하는 항목을 추출하여 컬렉션에 담기
      items.add(matcher.group());
    }

    System.out.println("-----------------------------");

    for (String item : items) {
      System.out.println(item);
    }
  }

}

 

 

 

Client / Server 프로젝트 분리

 

myapp 을 app-client 와 app-server 로 분리한다. 공통 코드(vo 객체)는 app-common 으로 추출하고 포함하도록 한다.

 

app-common 의 build.gradle (build script file) 에 다음 코드 추가한다.

plugins {
  id 'java-library'
}

 

app-client, app-server 의 build.gradle 파일에 plugins 를 다음과 같이 설정하고 dependencies 에 아래 추가한다.

plugins {
    id 'application'
}

dependencies {
    implementation project(':app-common')
}

 

myapp > settings.gradle 을 이렇게 수정한다.

rootProject.name = 'myapp'
// include('app')
include('app-client')
include('app-server')

이후 myapp 에서 $ gradle eclipse 하면 위 두군데를 빌드한다.

 

 

 

데이터처리 아키텍처

 

Client 가 BoardDao 를 ① call 한다. BoardDao 에서 Server 측의 ServerApp 으로 ② 요청 한다. ③ 응답을 하면 BoardDao 가 ④ return 한다.

 

BoardDao ← 기존의 BoardDao 를 네트워킹을 통해 데이터를 처리하는 방식으로 변경해야 한다.

 

 

 

강결합의 문제점

 

BoardHandler 가 BoardDao 를 직접 지정해 사용하는 강결합 관계이다.

BoardDao 에서 네트워크가 필요해 기능 변경한다. BoardDao 가 사용하는 List 와 파일 I/O 를 ServerApp 으로 별도의 Application 으로 분리한다. 그러면 BoardDao 에서 데이터 처리 작업 요청하고 처리 결과를 받는 통신을 할 수 있다.

이때 BoardDao 변경에 맞춰 Handler도 변경해야 한다. 왜? Handler 에서 DAO를 생성했기 때문이다. → 강결합 되어 있다.

 

 

 

강결합의 문제점 해결책?

 

low coupling 구조로 변경

① 의존 객체를 직접 생성하는 대신 주입 받기

     Dependency Injection (DI)

② 인터페이스를 통해 관계를 맺는다.

     직접 클래스를 언급하지 않기

BoardHandler 에서 생성자 아규먼트로 BoardDao 를 주입 받는다. 그러면 객체 생성방식이 변경되더라도 핸들러에게 영향을 끼치지 않는다.

 

BoardDao boardDao = new BoardDao(in, out);  ← 외부에서 의존 객체 생성

BoardHandler boardHandler = new BoardHandler(boardDao);  ← 의존 객체 주입

 

 

low coupling 구조로 변경

① 의존 객체를 직접 생성하는 대신 주입 받기

     Dependency Injection (DI)

② 인터페이스를 통해 관계를 맺는다.

     직접 클래스를 언급하지 않기

BoardDao 를 interface 로 변경하고 NetworkBoardDao 로 교체한다면?

문제! NetworkBoardDao를 받을 수 있도록 변경해야 한다.

BoardHandler 의 BoardDao 필드 및 생성자 아규먼트 변경해야 한다.

 

해결책은 인터페이스를 통해 관계를 맺는다.

NetworkBoardDao boardDao = new NetworkBoardDao(in, out);

BoardHandler boardHandler = new BoardHandler(BoardDao);

 

 

BoardHandler 의 BoardDao 필드 및 생성자 아규먼트가 인터페이스로 변경되어 의존 객체를 교체하기 쉽다.

concrete 클래스인 LocalBoardDao 또는 NetworkBoardDao 로 변경 가능하다.

 

 

 

Client / Server Protocol

 

요청: 데이터명, 액션, 파라미터(선택) 으로 한다. 예컨대 "board", "findAll" 각각 요청한다.

응답 : 상태코드(문자열), 데이터(JSON 텍스트) 로 한다.

 

 

 

Server Architecture

 

Server Application + let (작은 조각) = Servlet 이라 한다.

 

ServerApp 으로 요청이 들어올때, "board" 일 경우 BoardServlet 을 호출해 BoardDao 로 List 를 사용한다.

                                                      "student" 일 경우 StudentServlet 을 호출해 StudentDao 로 List 를 사용한다.

                                                      "teacher" 일 경우 TeacherServlet 을 호출해 TeacherDao 로 List 를 사용한다.

 

 

 

RPC → RMI → CORBA → EJB → Web Service → RESTful Service (REST API)

 

RPC (Remote Procedure Call), RMI (Remote Method Invocation)

예전 방식은 BoardHandler 가 BoardDao 를 call 하는 방식이었다.

 

BoardHandler 가 NetworkBoardDao 를 call 한다. BoardServlet 으로 요청하고 BoardServlet 은 BoardDao (Remote Object) 를 call 해 파일을 이용하거나 List 를 사용한다. 결과를 BoardServlet 가 응답한다.

BoardHandler 가 BoardDao 를 바로 call 하는 것처럼 사용한다.

*Remote : 다른 프로세스(실행중인 프로그램)

 

이처럼 중간에서 데이터를 주고 받는 객체를 Object Request Broker (ORB) 라 한다. 특히 Client ORB 는 "Stub" (클라이언트 측 중개인(ORB)) 라 하고 Server ORB 는 "Skeleton" (서버측 중개인) 이라 한다.

 

Common ORB Architecture 를 줄여서 "CORBA" 라 한다.

 

RMI : Client 와 Server 가 동일 언어여야 한다.

CORBA : Stub 을 Server 언어에 맞게 자동으로 만들어 준다.

EJB : 2000년 초반에 은행권에서 대세 기술이었다.

Web Service : 통신에 HTTP 기술을 사용한다.

RESTful Service : HTTP 로 통신한다.

 

 

 

app-common 추출

 

app-client 와 app-server 의 공통 코드를 추출하기 위해 복사해서 이름을 app-common 으로 설정하고 vo 패키지 외에 모두 삭제한다.

 

 

server 측 외에 csv, json 파일 삭제한다.

 

myapp-server, myapp-client 속성을 보면 myapp-common 이 포함되어 있어야 한다.

Classpath 에 myapp-common 포함되어 있어야 한다.

 

 

### 32-1. DAO 객체를 교체하기 쉽게 만들기
- 인터페이스와 의존 객체 주입 
- Handler에서 DAO 객체를 교체하기 쉽게 만들기
### 32-2. 네트워킹을 이용한 파일 공유: client/server app. 아키텍처로 전환 
- 네트워크를 통해 파일을 공유하고 데이터 입출력을 처리하는 방법   
- 데이터를 파일에 저장하고 꺼내는 기능을 별도의 애플리케이션으로 분리한다.   
- 기존의 프로그램은 네트워크를 통해 파일 서버에 접속하여 데이터 입출력을 처리한다.

 

 

myapp-client

 

public class ClientApp {

  public static void main(String[] args) {
    new ClientApp().execute("localhost", 8888);
  }

  void execute(String ip, int port) {
    try (Socket socket = new Socket(ip, port);
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        DataInputStream in = new DataInputStream(socket.getInputStream())) {

      NetworkBoardDao boardDao = new NetworkBoardDao(in, out);
      NetworkStudentDao studentDao = new NetworkStudentDao(in, out);
      NetworkTeacherDao teacherDao = new NetworkTeacherDao(in, out);

      StudentHandler studentHandler = new StudentHandler("학생", studentDao);
      TeacherHandler teacherHandler = new TeacherHandler("강사", teacherDao);
      BoardHandler boardHandler = new BoardHandler("게시판", boardDao);

      loop: while (true) {
        System.out.println("1. 학생관리");
        System.out.println("2. 강사관리");
        System.out.println("3. 게시판");
        System.out.println("9. 종료");

        int menuNo;
        try {
          menuNo = Prompt.inputInt("메뉴> ");
        } catch (Exception e) {
          System.out.println("메뉴 번호가 옳지 않습니다!");
          continue;
        }

        try {
          switch (menuNo) {
            case 1:
              studentHandler.service();
              break;
            case 2:
              teacherHandler.service();
              break;
            case 3:
              boardHandler.service();
              break;
            case 9:
              out.writeUTF("quit");
              break loop; // loop 라벨이 붙은 while 문을 나간다.
            default:
              System.out.println("잘못된 메뉴 번호 입니다.");
          }
        } catch (Exception e) {
          System.out.printf("명령 실행 중 오류 발생! - %s : %s\n",
              e.getMessage(),
              e.getClass().getSimpleName());
        }
      }

      System.out.println("안녕히 가세요!");

      Prompt.close();

    } catch (Exception e) {
      System.out.println("네트워킹 오류!");
      e.printStackTrace();
    }
  }
}

 

BoardDao 를 인터페이스로 변경한다.

public interface BoardDao {
  void insert(Board board);
  Board[] findAll();
  Board findByNo(int no);
  void update(Board b);
  boolean delete(Board b);
}

StudentDao, TeacherDao 도 인터페이스로 바꾼다.

public interface StudentDao {
  void insert(Student s);
  Student[] findAll();
  Student findByNo(int no);
  void update(Student s);
  boolean delete(Student s);
}
public interface TeacherDao {
  void insert(Teacher t);
  Teacher[] findAll();
  Teacher findByNo(int no);
  void update(Teacher t);
  boolean delete(Teacher t);
}

기존 BoardDao 였던 파일은 LocalBoardDao 로 이름을 바꾼다. StudentDao, TeacherDao 도 동일하게 바꾼다.

 

 

NetworkBoardDao 생성한다. NetworkTeacherDao, NetworkStudentDao 는 변수 이름만 다를 뿐 동일하게 동작한다.

public class NetworkBoardDao implements BoardDao {

  List<Board> list;
  int lastNo;
  DataInputStream in;
  DataOutputStream out;

  public NetworkBoardDao(DataInputStream in, DataOutputStream out) {
    this.in = in;
    this.out = out;
  }

  @Override
  public void insert(Board b) {
    fetch("board", "insert", b);
  }

  @Override
  public Board[] findAll() {
    return new Gson().fromJson(fetch("board", "findAll"), Board[].class);
  }

  @Override
  public Board findByNo(int no) {
    return new Gson().fromJson(fetch("board", "findByNo", no), Board.class);
  }

  @Override
  public void update(Board b) {
    fetch("board", "update", b);
  }

  @Override
  public boolean delete(Board b) {
    fetch("board", "delete", b);
    return true;
  }

  public void save(String filename) {
    try (FileWriter out = new FileWriter(filename)) {

      out.write(new Gson().toJson(list));

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void load(String filename) {
    if (list.size() > 0) { // 중복 로딩 방지!
      return;
    }

    try (BufferedReader in = new BufferedReader(new FileReader(filename))) {

      // 1) JSON 데이터를 어떤 타입의 객체로 변환할 것인지 그 타입 정보를 준비한다.
      TypeToken<List<Board>> collectionType = new TypeToken<>() {};

      // 2) 입력 스트림에서 JSON 데이터를 읽고, 지정한 타입의 객체로 변환하여 리턴한다.
      list = new Gson().fromJson(in, collectionType);

      if (list.size() > 0) {
        lastNo = list.get(list.size() - 1).getNo();
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public String fetch(String dataName, String action, Object... data) throws DaoException {
    try {
      out.writeUTF(dataName);
      out.writeUTF(action);

      if (data.length > 0) {
        out.writeUTF(new Gson().toJson(data[0]));
      }

      // 응답
      String status = in.readUTF();
      if (status.equals("400")) {
        throw new DaoException("클라이언트 요청 오류!");
      } else if (status.equals("500")) {
        throw new DaoException("서버 실행 오류!");
      }
      return in.readUTF();

    } catch (DaoException e) {
      throw e;
    } catch (Exception e) {
      throw new DaoException("오류 발생!", e);
    }
  }
}

 

Handler 들의 생성자에 BoardDao 객체 주입받도록 한다.

public class BoardHandler {

  private BoardDao boardDao;
  private String title;

  public BoardHandler(String title, BoardDao boardDao) {
    this.title = title;
    this.boardDao = boardDao;
  }
public class TeacherHandler {

  private TeacherDao teacherDao;
  private String title;

  public TeacherHandler(String title, TeacherDao teacherDao) {
    this.title = title;
    this.teacherDao = teacherDao;
  }
public class StudentHandler {

  private StudentDao memberDao;
  private String title;

  public StudentHandler(String title, StudentDao memberDao) {
    this.title = title;
    this.memberDao = memberDao;
  }

 

 

 

myapp-server

 

public class ServerApp {

  public static void main(String[] args) {
    new ServerApp().service(8888);
    System.out.println("서버 종료!");
  }

  void service(int port) {
    System.out.println("서버 실행 중...");

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

      BoardDao boardDao = new BoardDao(new LinkedList<Board>());
      boardDao.load("board.json");

      StudentDao studentDao = new StudentDao(new ArrayList<Student>());
      studentDao.load("student.json");

      TeacherDao teacherDao = new TeacherDao(new ArrayList<Teacher>());
      teacherDao.load("teacher.json");

      StudentServlet studentServlet = new StudentServlet(studentDao);
      TeacherServlet teacherServlet = new TeacherServlet(teacherDao);
      BoardServlet boardServlet = new BoardServlet(boardDao);

      while (true) {
        String dataName = in.readUTF();
        switch (dataName) {
          case "board":
            boardServlet.service(in, out);
            boardDao.save("board.json");
            break;
          case "student":
            studentServlet.service(in, out);
            studentDao.save("student.json");
            break;
          case "teacher":
            teacherServlet.service(in, out);
            teacherDao.save("teacher.json");
            break;
          case "quit":
            return;
        }
      }

    } catch (Exception e) {
      System.out.println("서버 오류 발생!");
      e.printStackTrace();
    }
  }
}

 

server 측 Handler 는 Servlet 으로 이름 변경하고 다음과 같이 수정한다.

student, teacher 도 변수명만 다를 뿐 동일하게 동작한다.

public class BoardServlet {

  private BoardDao boardDao;

  public BoardServlet(BoardDao boardDao) {
    this.boardDao = boardDao;
  }

  private void onInsert(DataInputStream in, DataOutputStream out) throws Exception {

    // 클라이언트가 보낸 JSON 데이터를 읽어서 Board 객체로 만든다.
    Board b = new Gson().fromJson(in.readUTF(), Board.class);
    this.boardDao.insert(b);
    out.writeUTF("200");
    out.writeUTF("success");
  }

  private void onFindAll(DataInputStream in, DataOutputStream out) throws Exception {
    out.writeUTF("200");
    out.writeUTF(new Gson().toJson(this.boardDao.findAll()));
  }

  private void onFindByNo(DataInputStream in, DataOutputStream out) throws Exception {
    int boardNo = new Gson().fromJson(in.readUTF(), int.class);

    Board b = this.boardDao.findByNo(boardNo);

    if (b == null) {
      out.writeUTF("400");
      return;
    }
    out.writeUTF("200");
    out.writeUTF(new Gson().toJson(b));
  }

  private void onUpdate(DataInputStream in, DataOutputStream out) throws Exception {
    Board board = new Gson().fromJson(in.readUTF(), Board.class);

    Board old = this.boardDao.findByNo(board.getNo());

    if (old == null) {
      out.writeUTF("400");
      return;
    }

    this.boardDao.update(board);
    out.writeUTF("200");
    out.writeUTF("success");
  }

  private void onDelete(DataInputStream in, DataOutputStream out) throws Exception {
    Board board = new Gson().fromJson(in.readUTF(), Board.class);

    Board b = this.boardDao.findByNo(board.getNo());
    if (b == null) {
      out.writeUTF("400");
      return;
    }

    this.boardDao.delete(b);
    out.writeUTF("200");
    out.writeUTF("success");
  }

  public void service(DataInputStream in, DataOutputStream out) throws Exception {
    try {
      // 클라이언트가 요구하는 액션을 읽는다.
      String action = in.readUTF();

      switch (action) {
        case "insert": this.onInsert(in, out); break;
        case "findAll": this.onFindAll(in, out); break;
        case "findByNo": this.onFindByNo(in, out); break;
        case "update": this.onUpdate(in, out); break;
        case "delete": this.onDelete(in, out); break;
        default:
          System.out.println("잘못된 메뉴 번호 입니다.");
      }
    } catch (Exception e) {
      out.writeUTF("500");
    }
  }
}

 

 

 


 

 

조언

 

*초급자가 처음부터 끝까지 다 짤 수 없다. 따라 치면서 방법을 익히고 점차 할 수 있는 능력을 넓혀서 나중에는 스스로 할 수 있게 된다.

*네카라쿠배에서 네트워킹 관련 나온 질문 : stateful 방식과 stateless 방식 차이. 상담은 stateful, 114는 stateless

 

 

 


 

과제

 

StudentDao, TeacherDao 네트워킹 버전으로 변경