2022.09.16/Web Application Server

Jimin·2022년 9월 17일
0

비트캠프

목록 보기
43/60
post-thumbnail
  • board-app 프로젝트 수행
      1. Web Application Server 구조로 전환하기 - 웹 기술 도입(계속)
      1. 명령처리 부분에 Command 패턴을 적용하기
      1. Command 객체의 사용법 통일하기 - 인터페이스 활용
      1. 애노테이션과 리플렉션 API를 활용하여 커맨드 객체 자동 생성하기

URL 인코딩 (percent encoding)

URL을 표현할 때 다음에 해당하는 문자는 %00 형태로 변환해야한다.

1. 예약문자(reserved characters)

! * ' ( ) ; @ & = + $ , / ? # [ ] %

2. 비예약 문자

인코딩이 필요하지 않다. → URL에 그대로 사용 가능하다.
대문자(A-Z) 소문자(a-z) 숫자(0-9) - _ . ~

3. 그외 문자

%00 형태로 인코딩해야한다. (웹브라우저와 서버가 자동으로 처리한다.)
⇒ 개발자가 신경 쓸 필요가 없다.
↔ 직접 웹 서버를 만든다면 URL 디코딩을 직접 처리해주어야 한다.

가 → %EA%B0%80
16진수 정수 값 → ASCII 문자코드로 변환


Command Design Pattern

게시판에 대한 새 요청 처리를 추가할 때, BoardHandler에 새 메서드 코드를 변경해야한다.
→ 게시글에 대해 새로운 요청 처리를 추가할 때마다 기존코드를 손대야한다.
⇒ 버그가 추가될 가능성이 높아진다.
그래서 사용하는 것이 Command Design Pattern이다.

Command Design Pattern: 메서드를 객체화!

앞으로 계속 새로운 요청, 새로운 명령이 추가될 가능성이 높은 경우, 기존 코드를 손대지 않고 해결하는 방법!


053. 명령처리 부분에 Command 패턴을 적용하기

요청 처리를 담당하는 메서드를 별도의 클래스로 분리

1단계 - BoardHandler의 메서드를 객체화한다.

  • com.bitcamp.board.handler.BoardListHandler 클래스 정의
  • com.bitcamp.board.handler.BoardDetailHandler 클래스 정의
  • com.bitcamp.board.handler.BoardUpdateHandler 클래스 정의
  • com.bitcamp.board.handler.BoardDetailHandler 클래스 정의
  • com.bitcamp.board.handler.BoardFormHandler 클래스 정의
  • com.bitcamp.board.handler.BoardAddHandler 클래스 정의
  • com.bitcamp.board.handler.BoardHandler 클래스 정의
  • com.bitcamp.board.MiniWebServer 클래스 정의

2단계 - MemberHandler의 메서드를 객체화한다.

  • com.bitcamp.board.handler.MemberListHandler 클래스 정의

  • com.bitcamp.board.handler.MemberDetailHandler 클래스 정의

  • com.bitcamp.board.handler.MemberUpdateHandler 클래스 정의

  • com.bitcamp.board.handler.MemberDetailHandler 클래스 정의

  • com.bitcamp.board.handler.MemberFormHandler 클래스 정의

  • com.bitcamp.board.handler.MemberAddHandler 클래스 정의

  • com.bitcamp.board.handler.MemberHandler 클래스 정의

  • com.bitcamp.board.MiniWebServer 클래스 정의

  • MiniWebServer class

	...

		  } else if (path.equals("/board/list")) {
            boardListHandler.list(paramMap, printWriter);

          } else if (path.equals("/board/detail")) {
            boardDetailHandler.detail(paramMap, printWriter);

          } else if (path.equals("/board/update")) {
            boardUpdateHandler.update(paramMap, printWriter);

          } else if (path.equals("/board/delete")) {
            boardDeleteHandler.delete(paramMap, printWriter);

          } else if (path.equals("/board/form")) {
            boardFormHandler.form(paramMap, printWriter);

          } else if (path.equals("/board/add")) {
            boardAddHandler.add(paramMap, printWriter);

          } else if (path.equals("/member/list")) {
            memberListHandler.list(paramMap, printWriter);

          } else if (path.equals("/member/detail")) {
            memberDetailHandler.detail(paramMap, printWriter);

          } else if (path.equals("/member/update")) {
            memberUpdateHandler.update(paramMap, printWriter);

          } else if (path.equals("/member/delete")) {
            memberDeleteHandler.delete(paramMap, printWriter);

          } else if (path.equals("/member/form")) {
            memberFormHandler.form(paramMap, printWriter);

          } else if (path.equals("/member/add")) {
            memberAddHandler.add(paramMap, printWriter);

          } else {
            errorHandler.error(paramMap, printWriter);
            
            ...

054. Command 객체의 사용법 통일하기 - 인터페이스 활용

  • 인터페이스를 사용하여 커맨드 객체의 사용 규칙을 정의하는 방법
  • 맵을 활용하여 커맨드 객체를 관리하는 방법

1단계 - Command 객체의 사용 규칙을 정의한다.

  • com.bitcamp.servlet.Servlet 인터페이스 정의

  • Servlet interface

// 요청을 처리하는 객체의 사용법을 정의한다.

public interface Servlet {

  // HTTP Client의 요청이 들어올 때 마다 해당 요청을 처리하는 객체에 대해 호출되는 메서드이다.
  void service(Map<String,String> paramMap, PrintWriter out) throws Exception ;
}

2단계 - 기존의 Command 객체를 Servlet 구현체로 변경한다.

  • com.bitcamp.board.handler.BoardListHandler 클래스 변경
  • com.bitcamp.board.handler.BoardDetailHandler 클래스 변경
  • com.bitcamp.board.handler.BoardUpdateHandler 클래스 변경
  • com.bitcamp.board.handler.BoardDetailHandler 클래스 변경
  • com.bitcamp.board.handler.BoardFormHandler 클래스 변경
  • com.bitcamp.board.handler.BoardAddHandler 클래스 변경
  • com.bitcamp.board.handler.BoardHandler 클래스 변경
  • com.bitcamp.board.MiniWebServer 클래스 변경
  • com.bitcamp.board.handler.MemberListHandler 클래스 변경
  • com.bitcamp.board.handler.MemberDetailHandler 클래스 변경
  • com.bitcamp.board.handler.MemberUpdateHandler 클래스 변경
  • com.bitcamp.board.handler.MemberDetailHandler 클래스 변경
  • com.bitcamp.board.handler.MemberFormHandler 클래스 변경
  • com.bitcamp.board.handler.MemberAddHandler 클래스 변경
  • com.bitcamp.board.handler.MemberHandler 클래스 변경
  • com.bitcamp.board.handler.ErrorHandler 클래스 변경
  • com.bitcamp.board.handler.WelcomeHandler 클래스 변경
  • com.bitcamp.board.MiniWebServer 클래스 변경

모든 Command 객체 class들이 모두 Servlet interface를 구현할 수 있도록 하고, Servlet의 규칙을 따라 메소드의 이름을 service()로 수정해준다.

  • BoardAddHandler class
public class BoardAddHandler implements Servlet{

  private BoardDao boardDao;

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

  @Override
  public void service(Map<String, String> paramMap, PrintWriter out)  throws Exception {
    out.println("<!DOCTYPE html>");
    out.println("<html>");
    out.println("<head>");
	
  • MiniWebServer class
		...
        
   		  } else if (path.equals("/board/list")) {
            boardListHandler.service(paramMap, printWriter);

          } else if (path.equals("/board/detail")) {
            boardDetailHandler.service(paramMap, printWriter);

          } else if (path.equals("/board/update")) {
            boardUpdateHandler.service(paramMap, printWriter);

          } else if (path.equals("/board/delete")) {
            boardDeleteHandler.service(paramMap, printWriter);

          } else if (path.equals("/board/form")) {
            boardFormHandler.service(paramMap, printWriter);

          } else if (path.equals("/board/add")) {
            boardAddHandler.service(paramMap, printWriter);

          }  // board
          else if (path.equals("/member/list")) {
            memberListHandler.service(paramMap, printWriter);

          } else if (path.equals("/member/detail")) {
            memberDetailHandler.service(paramMap, printWriter);

          } else if (path.equals("/member/update")) {
            memberUpdateHandler.service(paramMap, printWriter);

          } else if (path.equals("/member/delete")) {
            memberDeleteHandler.service(paramMap, printWriter);

          } else if (path.equals("/member/form")) {
            memberFormHandler.service(paramMap, printWriter);

          } else if (path.equals("/member/add")) {
            memberAddHandler.service(paramMap, printWriter);

          } else {
            errorHandler.service(paramMap, printWriter);
          }
          
          ...

3단계 - Servlet Map 이용하기

Servlet interface를 구현한 모든 Command 객체 클래스들을 모두 Map에 넣어주어 코드를 줄인다.

  • MiniWebServer class
public class MiniWebServer {

  public static void main(String[] args) throws Exception {
    BoardDao boardDao = new MariaDBBoardDao(con);
    MemberDao memberDao = new MariaDBMemberDao(con);

    // 서블릿 객체를 보관할 맵을 준비
    Map<String, Servlet> servletMap = new HashMap<>();
    servletMap.put("/", new WelcomeHandler());
    servletMap.put("/board/list", new BoardListHandler(boardDao));
    servletMap.put("/board/form", new BoardFormHandler());
    servletMap.put("/board/delete", new BoardDeleteHandler(boardDao));
    servletMap.put("/board/detail", new BoardDetailHandler(boardDao));
    servletMap.put("/board/update", new BoardUpdateHandler(boardDao));
    servletMap.put("/board/add", new MemberAddHandler(memberDao));
    servletMap.put("/member/list", new MemberListHandler(memberDao));
    servletMap.put("/member/form", new MemberFormHandler());
    servletMap.put("/member/delete", new MemberDeleteHandler(memberDao));
    servletMap.put("/member/detail", new MemberDetailHandler(memberDao));
    servletMap.put("/member/update", new MemberUpdateHandler(memberDao));
    servletMap.put("/member/add", new MemberAddHandler(memberDao));

    ErrorHandler errorHandler = new ErrorHandler();
    
    ...
    
          Servlet servlet = servletMap.get(path);
          
          if (servlet != null) {
            servlet.service(paramMap, printWriter);
          } else {
            errorHandler.service(paramMap, printWriter);
          }
          
    ...

055. 애노테이션과 리플렉션 API를 활용하여 커맨드 객체 자동생성하기

  • 애노테이션을 정의하고 활용하는 방법
  • Reflections API를 활용하는 방법
    • 객체 생성을 자동화하는 방법

1단계 - 요청을 처리하는 객체의 path를 설정할 어노테이션을 정의한다.

  • com.bitcamp.servlet.WebServlet 애노테이션 정의

  • WebServlet annotation

@Target(value=ElementType.TYPE) // annotation을 붙일 수 있는 범위를 설정한다.
@Retention(value=RetentionPolicy.RUNTIME) // annotation 값을 추출할 때를 지정 -> 프로그램이 실행 중일 때 추출하겠다.

// 다음 annotation은 HTTP 요청을 처리하는 객체에 대해 경로를 설정할 때 사용한다.
public @interface WebServlet {
  String value(); // annotation의 필수 속성을 설정
}

2단계 - annotation으로 Command 객체의 경로를 지정한다.

  • com.bitcamp.board.handler.BoardListHandler 클래스 변경
  • com.bitcamp.board.handler.BoardDetailHandler 클래스 변경
  • com.bitcamp.board.handler.BoardUpdateHandler 클래스 변경
  • com.bitcamp.board.handler.BoardDeleteHandler 클래스 변경
  • com.bitcamp.board.handler.BoardFormHandler 클래스 변경
  • com.bitcamp.board.handler.BoardAddHandler 클래스 변경
  • com.bitcamp.board.handler.MemberListHandler 클래스 변경
  • com.bitcamp.board.handler.MemberDetailHandler 클래스 변경
  • com.bitcamp.board.handler.MemberUpdateHandler 클래스 변경
  • com.bitcamp.board.handler.MemberDeleteHandler 클래스 변경
  • com.bitcamp.board.handler.MemberFormHandler 클래스 변경
  • com.bitcamp.board.handler.MemberAddHandler 클래스 변경
  • com.bitcamp.board.handler.WelcomeHandler 클래스 변경
  • BoardAddHandler class
WebServlet(value="/board/add")
public class BoardAddHandler implements Servlet{

  private BoardDao boardDao;
  
  ...
  

3단계 - @WebServlet 애노테이션을 부은 클래스를 찾아 인스턴스를 생성한다.

  • Reflections 라이브러리를 프로젝트에 추가한다.
    implementation 'org.reflections:reflections:0.10.2'

    • search.maven.org에서 reflections로 검색한다.
    • build.gradle에 의존 라이브러리를 추가한다.
    • gradle.eclipse를 실행하여 이클립스 IDE 설정 파일을 갱신한다.
    • 이클립스 IDE에서 프로젝트를 refresh한다.
  • @WebServlet annotaion이 붙은 클래스를 찾는다.

  • MiniWebServer class

public class MiniWebServer {

  public static void main2(String[] args) throws Exception {
    // 클래스를 찾아주는 도구를 준비
    Reflections reflections = new Reflections("com.bitcamp.board");

    /*
    // 지정된 패키지에서 @WebServlet 애노테이션이 붙은 클래스를 모두 찾는다.
    // 검색필터 1) WebServlet 애노테이션이 붙어 있는 클래스의 이름들을 모두 찾아라!
    QueryFunction<Store,String> 검색필터1 = TypesAnnotated.with(WebServlet.class);

    // 검색필터 2) 찾은 클래스 이름을 가지고 클래스를 Method Area 영역에 로딩하여
    //             Class 객체 목록을 리턴하라!
    QueryFunction<Store,Class<?>> 검색필터2 = 검색필터1.asClass();

    // 위의 두 검색 조건으로 클래스를 찾는다.
    Set<Class<?>> 서블릿클래스들 = reflections.get(검색필터2);

    for (Class<?> 서블릿클래스정보 : 서블릿클래스들) {
      System.out.println(서블릿클래스정보.getName());
    }
     */

    Set<Class<?>> servlets = reflections.get(TypesAnnotated.with(WebServlet.class).asClass());
    for (Class<?> servlet : servlets) {
      WebServlet anno = servlet.getAnnotation(WebServlet.class);
      System.out.printf("%s ---> %s\n", anno.value(), servlet.getName());
    }

  }

  public static void main(String[] args) throws Exception {
    Connection con = DriverManager.getConnection(
        "jdbc:mariadb://localhost:3306/studydb","study","1111");

    BoardDao boardDao = new MariaDBBoardDao(con);
    MemberDao memberDao = new MariaDBMemberDao(con);

    // 서블릿 객체를 보관할 맵을 준비
    Map<String,Servlet> servletMap = new HashMap<>();

    // WebServlet 애노테이션이 붙은 클래스를 찾아 객체를 생성한 후 맵에 저장한다.
    // 맵에 저장할 때 사용할 key는 WebServlet 애노테이션에 설정된 값이다.
    //
    Reflections reflections = new Reflections("com.bitcamp.board");
    Set<Class<?>> servlets = reflections.get(TypesAnnotated.with(WebServlet.class).asClass());
    for (Class<?> servlet : servlets) {
      // 서블릿 클래스의 붙은 WebServlet 애노테이션으로부터 path 를 꺼낸다.
      String servletPath = servlet.getAnnotation(WebServlet.class).value();

      // 생성자의 파라미터의 타입을 알아내, 해당 객체를 주입한다.
      Constructor<?> constructor = servlet.getConstructors()[0];
      Parameter[] params = constructor.getParameters();

      if (params.length == 0) { // 생성자의 파라미터가 없다면 
        servletMap.put(servletPath, (Servlet) constructor.newInstance());

      } else if (params[0].getType() == BoardDao.class) {
        servletMap.put(servletPath, (Servlet) constructor.newInstance(boardDao));

      } else if (params[0].getType() == MemberDao.class) {
        servletMap.put(servletPath, (Servlet) constructor.newInstance(memberDao));
      } 
    }

    ErrorHandler errorHandler = new ErrorHandler();

    class MyHttpHandler implements HttpHandler {
      @Override
      public void handle(HttpExchange exchange) throws IOException {
        System.out.println("클라이언트가 요청함!");

        URI requestUri = exchange.getRequestURI();
        String path = requestUri.getPath();
        // String query = requestUri.getQuery(); // 디코딩을 제대로 수행하지 못한다!
        String query = requestUri.getRawQuery(); // 디코딩 없이 query string을 그대로 리턴 받기!
        byte[] bytes = null;

        try (StringWriter stringWriter = new StringWriter();
            PrintWriter printWriter = new PrintWriter(stringWriter)) {

          Map<String,String> paramMap = new HashMap<>();
          if (query != null && query.length() > 0) { // 예) no=1&title=aaaa&content=bbb
            String[] entries = query.split("&");
            for (String entry : entries) { // 예) no=1
              String[] kv = entry.split("=");
              // 웹브라우저가 보낸 파라미터 값은 저장하기 전에 URL 디코딩 한다.
              paramMap.put(kv[0], URLDecoder.decode(kv[1], "UTF-8"));
            }
          }
          System.out.println(paramMap);

          Servlet servlet = servletMap.get(path);

          if (servlet != null) {
            servlet.service(paramMap, printWriter);
          } else {
            errorHandler.service(paramMap, printWriter);
          }

          bytes = stringWriter.toString().getBytes("UTF-8");

        } catch (Exception e) {
          bytes = "요청 처리 중 오류 발생!".getBytes("UTF-8");
          e.printStackTrace(); // 서버 콘솔 창에 오류에 대한 자세한 내용을 출력한다.
        }

        // 보내는 콘텐트의 MIME 타입이 무엇인지 응답 헤더에 추가한다.
        Headers responseHeaders = exchange.getResponseHeaders();
        responseHeaders.add("Content-Type", "text/html; charset=UTF-8");

        exchange.sendResponseHeaders(200, bytes.length);

        OutputStream out = exchange.getResponseBody();
        out.write(bytes);
        out.close();
      }
    }

    HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
    server.createContext("/", new MyHttpHandler()); 
    server.setExecutor(null); 
    server.start();

    System.out.println("서버 시작!");
  }

}
profile
https://github.com/Dingadung

0개의 댓글