독서/📚

[Next Step] 6장 서블릿/JSP를 활용해 동적인 웹 애플리케이션 개발하기

leejinwoo1126 2023. 11. 16. 21:01
반응형

 

 


    실습 프로젝트 저장소

    실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함

     

    jwp-basic (6.1)

    https://github.com/slipp/jwp-basic/tree/step0-getting-started

     

    GitHub - slipp/jwp-basic: 자바 웹 프로그래밍 기본 실습

    자바 웹 프로그래밍 기본 실습. Contribute to slipp/jwp-basic development by creating an account on GitHub.

    github.com

     

     

    web-application-server 

    https://github.com/slipp/web-application-server

     

    GitHub - slipp/web-application-server: 웹 애플리케이션 서버 실습을 위한 뼈대

    웹 애플리케이션 서버 실습을 위한 뼈대. Contribute to slipp/web-application-server development by creating an account on GitHub.

    github.com

     


     

    *참고. 

    https://dev-ljw1126.tistory.com/402

     

    [Next Step] 5장 웹 서버 리팩토링, 서블릿 컨테이너와 서블릿의 관계

    실습 프로젝트 저장소 실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함 web-application-server (3 ~ 6장) https://github.com/slipp/we

    dev-ljw1126.tistory.com

     


    6.1 서블릿/JSP로 회원관리 기능 다시 개발하기

    Trouble Shooting

    에러.
    org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar files deployed with this application

     

    → taglibs 라이브러리가 정상적으로 인식되지 않는 것으로 파악

    → pom.xml에 tomcat version을 8.0.53으로 올려서 정상 동작 확인 (4시간 소비, 직접 다운받아 추가해도 안되었음)

    → 참고로 jakarta 의 경우 spring boot 3.0 부터 지원하는 것으로 알고 있음

     

    pom.xml

        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    
            <org.springframework.version>4.2.5.RELEASE</org.springframework.version>
            <tomcat.version>8.0.53</tomcat.version>
        </properties>
        
        <dependencies>
            <!-- ..  -->   
    
            <!-- tomcat -->
            <dependency>
                <groupId>org.apache.tomcat.embed</groupId>
                <artifactId>tomcat-embed-core</artifactId>
                <version>${tomcat.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.tomcat.embed</groupId>
                <artifactId>tomcat-embed-logging-juli</artifactId>
                <version>${tomcat.version}</version>
                <scope>provided</scope>
            </dependency>
            <dependency>
                <groupId>org.apache.tomcat.embed</groupId>
                <artifactId>tomcat-embed-jasper</artifactId>
                <version>${tomcat.version}</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>

    이하 생략


    6.2 ~ 6.3 세션(HttpSession) 구현

    앞 장에서 로그인시 쿠키에 logined=true 설정하여 활용하였다.

    이번에는 직접 HttpSession 구현하고, 이를 활용하여 로그인 여부 판별할 수 있도록 한다.

     

    UUID 테스트

    public class UUIDTest {
        @Test
        void uuid() {
            System.out.println(UUID.randomUUID());
        }
    }

     

    0d7b8a69-1ce8-493b-aa80-0b80802a18bb 와 같은 형태의 임의 값이 생성된다

     

    RequestHandler 클래스

     public class RequestHandler extends Thread {
     	[..]
     
        public void run() {
            try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
                HttpRequest request = new HttpRequest(in);
                HttpResponse response = new HttpResponse(out);
    
                if(request.getCookies().getCookie("JSESSIONID") == null) {
                    response.addHeader("Set-Cookie", "JSESSIONID=" + UUID.randomUUID());
                }
              
                [..]        
       }
    }

     

     

     

    리팩토링

    HttpRequest에서 쿠키 값을 추상화한 HttpCookie와 요청 파라미터를 추상화한 HttpParams 클래스, 그리고 HttpHeader책임 분리하여 사용하도록 한다

    ② 서버의 모든 클라이언트의 세션을 관리할 수 있는 저장소 HttpSessions, 그리고 세션 HttpSession을 구현하도록 한다

     

    HttpCookie 클래스 정의

    public class HttpCookie {
        private Map<String, String> cookies;
    
        public HttpCookie(String cookieValue) {
            cookies = HttpRequestUtils.parseCookies(cookieValue);
        }
    
        public String getCookie(String key) {
            return cookies.get(key);
        }
    }

     

    HttpParams 클래스 정의

    public class HttpParams {
        private static final Logger log = LoggerFactory.getLogger(HttpParams.class);
        private Map<String, String> params = new HashMap<>();
    
        public HttpParams() {}
    
        public void addQueryString(String queryString) { // GET방식
            putParam(queryString);
        }
        
        public String getParameter(String key) {
            return params.get(key);
        }
    
        public void putParam(String data) {
            log.debug("data : {}", data);
    
            if(data == null || data.isEmpty()) return;
    
            params.putAll(HttpRequestUtils.parseQueryString(data));
            log.debug("params: {}", params);
        }
    
        public void addBody(String body) { // POST 방식
            putParam(body);
        }
    }

     

    HttpHeaders 클래스 정의

    public class HttpHeaders {
        private static final Logger log = LoggerFactory.getLogger(HttpHeaders.class);
        private static final String COOKIE = "Cookie";
        private static final String CONTENT_LENGTH = "Content-Length";
        private Map<String, String> headers = new HashMap<>();
        
        public HttpHeaders(BufferedReader br) throws IOException {
            String line = br.readLine();
            while (!"".equals(line)) {
                if(line == null) break;
    
                add(line);
                line = br.readLine();
            }
        }
    
        public void add(String line) {
            log.debug("header : {}", line);
    
            String[] tokens = line.split(":");
            headers.put(tokens[0].trim(), tokens[1].trim());
        }
    
        public String getHeader(String key) {
            return headers.get(key);
        }
    
        public int getIntHeader(String key) {
            String header = getHeader(key);
            return header == null ? 0 : Integer.parseInt(header);
        }
    
        public int getContentLength() {
            return getIntHeader(CONTENT_LENGTH);
        }
    
        public HttpCookie getCookies() {
            return new HttpCookie(getHeader(COOKIE));
        }
    
        public HttpSession getSession() {
            return HttpSessions.getSession(getCookies().getCookie("JSESSIONID"));
        }
    }

     

    HttpSession 클래스 정의(p196)

    public class HttpSession {
        private Map<String, Object> values = new HashMap<>(); // 각 세션별 개별 생성
    
        private String id;
        public HttpSession(String id) {
            this.id = id;
        }
    
        public String getId() {
            return id;
        }
    
        public void setAttribute(String name, Object value) {
            values.put(name, value);
        }
    
        public Object getAttribute(String name) {
            return values.get(name);
        }
    
        public void removeAttribute(String name) {
            values.remove(name);
        }
    
        public void invalidate() {
            HttpSessions.remove(id);
        }
    }

     

    HttpSessions 클래스 정의이때 id값은 JSESSIONID 이다

    public class HttpSessions {
        private static Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
    
        private HttpSessions() {}
    
        public static HttpSession getSession(String id) {
            HttpSession session = sessions.get(id);
    
            if(session == null) {
                session = new HttpSession(id);
                sessions.put(id, session);
                return session;
            }
    
            return session;
        }
    
        public static void remove(String id) {
            sessions.remove(id);
        }
    }

     

    HttpRequest 클래스 리팩토링 (최종)

    public class HttpRequest {
        private static final Logger log = LoggerFactory.getLogger(HttpRequest.class);
    
        private RequestLine requestLine;
        private HttpHeaders headers;
        private HttpParams requestParams = new HttpParams();
    
        public HttpRequest(InputStream inputStream) {
            try {
                BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
                requestLine = new RequestLine(createRequestLine(br));
                requestParams.addQueryString(requestLine.getQueryString());
                headers = new HttpHeaders(br);
                requestParams.addBody(IOUtils.readData(br, headers.getContentLength()));
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    
        private String createRequestLine(BufferedReader br) throws IOException {
            String line = br.readLine();
            if(line == null) {
                throw new IllegalArgumentException();
            }
    
            return line;
        }
    
        public HttpMethod getMethod() {
            return requestLine.getMethod();
        }
    
        public String getPath() {
            return requestLine.getPath();
        }
    
        public String getHeader(String key) {
            return headers.getHeader(key);
        }
    
        public String getParameter(String key) {
            return requestParams.getParameter(key);
        }
    
        public HttpCookie getCookies() {
            return headers.getCookies();
        }
    
        public HttpSession getSession() {
            return headers.getSession();
        }
    }

     

     

    아래와 같이 LoginController 클래스를 수정가능했다

    public class LoginController extends AbstractController {
        private static final Logger log = LoggerFactory.getLogger(LoginController.class);
    
        @Override
        protected void doPost(HttpRequest request, HttpResponse response) {
            User user = DataBase.findUserById(request.getParameter("userId"));
            if(user == null || !user.getPassword().equals(request.getParameter("password"))) {
                response.forward("/user/login_failed.html");
            } else {
                HttpSession session = request.getSession(); //*
                session.setAttribute("user", user);
                response.sendRedirect("/index.html");
            }
        }
    }

    *.getSession() 호출시 HttpSessions에 JSESSIONID에 해당하는 HttpSession 인스턴스가 없으면 신규 생성하여 관리한다

     


    아래의 경우 jwp-basic 프로젝트로 학습 수행한다

     

    6.4 MVC 프레임워크 요구사항

    Hint (p209~210)

    ① 모든 요청을 서블릿 하나(예로 DispatcherServlet)가 받을 수 있도록 URL 매핑한다

    Controller 인터페이스를 추가한다.

    RequestMapping 클래스를 추가해 요청 URL과 컨트롤러 매핑을 설정한다 (Map<String, Controller>)

    ④ 컨트롤러 추가시 회원가입 화면이나 로그인 화면과 같이 특별한 로직을 구현할 필요가 없는 경우에도 매번 컨트롤러를 불필요하게 생성한다. 이와 같이 특별한 로직없이 뷰(JSP)에 대한 이동만을 담당하는 ForwardController를 추가한다. 

    ⑤ DispatcherServlet에서 요청 URL에 해당하는 Controller를 찾아 execute() 메소드를 호출해 실질적인 작업을 위임한다

    ⑥ Controller의 execute() 메소드 반환 값 String을 받아 서블릿에서 JSP로 이동할 때 중복을 제거한다

    → 반환값이 "redirect:"로 시작할 경우 sendRedirect()로 이동하고, 아닌 경우 forward() 방식으로 이동한다.

     

    DispatcherServlet 상단 애노테이션 추가

    @WebServlet(name = "dispatcher", urlPatterns = "/", loadOnStartup = 1)
    public class DispatcherServlet extends HttpServlet {
    	[..]
    }

    *loadOnStartup : 1이상의 경우 서블릿 컨테이너(톰캣) 시작과 동시에 서블릿 초기화 진행됨, 기본 값의 경우 사용자 요청시 초기화 진행

     

    *참고. https://dololak.tistory.com/

     

    코끼리를 냉장고에 넣는 방법

    IT, 프로그래밍, 컴퓨터 활용 정보 등을 위한 블로그

    dololak.tistory.com

     

     

    Controller 인터페이스

    public interface Controller {
        String execute(HttpServletRequest request, HttpServletResponse response) throws Exception;
    }

     

    LoginController 구현

    public class LoginController implements Controller {
    
        private static final Logger log = LoggerFactory.getLogger(LoginController.class);
    
        @Override
        public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
           String userId = request.getParameter("userId");
           String password = request.getParameter("password");
    
           User user = DataBase.findUserById(userId);
    
           if(user == null || !user.matchPassword(password)) {
               request.setAttribute("loginFailed", true);
               return "/user/login.jsp";
           } else {
               HttpSession session = request.getSession();
               session.setAttribute(UserSessionUtils.USER_SESSION_KEY, user);
               return "redirect:/";
           }
        }
    }

     

     

    6.5 MVC 프레임워크 구현

     

    RequestMapping 클래스 정의

    public class RequestMapping {
        private static final Logger log = LoggerFactory.getLogger(RequestMapping.class);
    
        private Map<String, Controller> mapping = new HashMap<>();
    
    
        public RequestMapping() {}
    
        public void initMapping() {
            mapping.put("/", new HomeController());
            mapping.put("/user/form", new ForwardController("/user/form.jsp"));
            mapping.put("/user/loginForm", new ForwardController("/user/login.jsp"));
            mapping.put("/user/list", new ListUserController());
            mapping.put("/user/profile", new ProfileController());
            mapping.put("/user/login", new LoginController());
            mapping.put("/user/logout", new LogoutController());
            mapping.put("/user/create", new CreateUserController());
            mapping.put("/user/updateForm", new UpdateFormUserController());
            mapping.put("/user/update", new UpdateUserController());
    
            log.info("Init Request Mapping!");
        }
    
        public Controller get(String requestUrl) {
            return mapping.get(requestUrl);
        }
    
        public void put(String url, Controller controller) {
            mapping.put(url, controller);
        }
    }

     

    ForwardController 클래스 정의

    public class ForwardController implements Controller {
        private static final Logger log = LoggerFactory.getLogger(ForwardController.class);
    
        private String forwardUrl;
    
        public ForwardController(String forwardUrl) {
            this.forwardUrl = forwardUrl;
            if(forwardUrl == null) {
                throw new NullPointerException("forwardUrl is null. 이동할 URL을 입력하세요");
            }
        }
    
        @Override
        public String execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
            return forwardUrl;
        }
    }

     

    DispatcherServlet 클래스 변경

    -RequestMapping에서 사용자 요청에 맞는 URL Controller를 가져와 요청 처리 위임한다

    -반환 되는 String 결과값에 따라 sendRedirect() 또는 forward() 처리하도록 한다

    @WebServlet(name = "dispatcher", urlPatterns = "/", loadOnStartup = 1)
    public class DispatcherServlet extends HttpServlet {
        private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);
    
        private static final String DEFAULT_REDIRECT_PREFIX = "redirect:";
        private RequestMapping rm;
    
        @Override
        public void init() throws ServletException {
            rm = new RequestMapping();
            rm.initMapping();
        }
    
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String requestUrl = req.getRequestURI();
            log.debug("Method : {}, Request URI : {}", req.getMethod(), requestUrl);
    
            Controller controller = rm.get(requestUrl);
            try {
                String viewName = controller.execute(req, resp);
                move(viewName, req, resp);
            } catch (Exception e) {
                log.error("Exception : {}", e);
                throw new ServletException(e.getMessage());
            }
        }
    
        private void move(String viewName, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
            if(viewName.startsWith(DEFAULT_REDIRECT_PREFIX)) {
                response.sendRedirect(viewName.substring(DEFAULT_REDIRECT_PREFIX.length()));
                return;
            }
    
            RequestDispatcher rd = request.getRequestDispatcher(viewName);
            rd.forward(request, response);
        }
    }

     

     

    이 같은 구조로 MVC 프레임워크를 구현하는 패턴을 프론트 컨트롤러(front controller) 패턴 이라고 한다. 각 컨트롤러의 앞에 모든 요청을 받아 각 컨트롤러에 작업을 위임하는 방식으로 구현되기 때문이다


    6.6 쉘 스크립트를 활용한 배포 자동화 

    https://dev-ljw1126.tistory.com/396

     

    [Next Step] 6.6 쉘 스크립트를 활용한 배포 자동화(p218) 정리

    목차 요구사항 -지금까지 구현한 기능을 개발 서버에 톰캣 서버를 설치한 후 배포한다 -서버가 정상적으로 실행되고 있는지 톰캣 로그 파일( catalina.out )을 통해 모니터링 한다 -쉘 스크립트를 만

    dev-ljw1126.tistory.com

     

    반응형