자바 웹 프로그래밍 Next-Step - 박재성 저자
책으로 스터디를 하며 진행했던 내용들을 기록하고 있습니다.
3주차에 진행했던 Chapter 06의 목표는 다음과 같습니다.
모든 코드들은 다음 저장소에서 확인할 수 있습니다.
https://github.com/Java-web-programming-Next-Step/next-step-web-programming/tree/HiiWee/6
프로젝트명: jwp-basic-gradle
추가로 세션 구현에 해당되는 6.2 ~ 6.3절은 web-application-server-gradle에서 진행했습니다.
문제
과정
bootstrap.min.css
파일은 응답 됐지만, style.css
파일은 응답되지 않음해결
Internet Explorer 9버전에서 HTML5를 지원
하게 도와주는 구문이므로 Chrome 브라우저를 사용하는 본인에게는 필요가 없음, 또한 해당 구문으로 인해 다음 link 태그가 읽히지 않고 있었다.org.apache.jasper.JasperException: The absolute uri: [http://java.sun.com/jsp/jstl/core](http://java.sun.com/jsp/jstl/core) cannot be resolved in either web.xml or the jar files deployed with this application
WEB-INF/lib/
위치에 jstl-1.2.jar 파일을 추가함IDE에서 embedded tomcat을 직접 실행할 때 발생하는 에러 해결
문제
View 파일 은닉을 위해 WEB-INF/views/ 경로로 모든 jsp파일을 옮겼을때 CSS가 적용이 안되는 이슈가 발생
response header에서 Content-Type 헤더가 text/html로 전송되어서 적용이 안되는줄 알았으나 css용 서블릿을 통해 Content-Type을 text/css로 변경해도 적용이 되지 않는다.
원인
해결
https://github.com/slipp/jwp-basic
위 프로젝트의 step0-getting-started
브랜치에서 파일을 가져옴
/user/create
로 POST 요청이 입력되고 메모리 DB에 회원가입 정보가 저장된다. 이후 /user/list
로 redirect하고 회원가입된 사용자의 정보가 브라우저에 출력된다./user/list —GET?userId=${value}
→ /user/update -FORWARD
→ update.jsp -POST
→ /user/update
기존의 존재하는 user를 삭제하고, updatedUser를 저장하는 방식으로 구현
service 계층에서 진행됨, 실패하게 되면 HttpSession에 로그인 시도에 관한 boolean 값을 저장하고 메인페이지로 리다이렉트 된다. 로그인 실패의 경우는 다음과 같다.
GET /user/login
Navigation bar에서 로그아웃을 클릭하면 GET /user/logout 요청된다. 단순히 HttpSession을 날려주고, 메인 페이지로 리다이렉트한다.
현재 사용자 목록은 모든 사람이 보고, 수정할 수 있다. 이를 로그인한 사용자가 자기 자신의 정보만 변경할 수 있도록 수정해보자
각 요청마다 서로 다른 세션이 사용된다. 세션은 사용자 한 명을 위한 저장소이며 서로다른 사용자가 접속되면 서로 다른 세션이 생성되어야 한다.
위의 세션에 담을 데이터를 저장할 저장소와 해당 세션의 아이디를 가진다. 그 외에는 요구사항을 충족한다.
각 사용자마다의 세션을 저장하기 위해 정적인 세션 저장소이다. 모든 사용자는 서로다른 UUID값을 통해 구분된다.
getSession()을 통해 세션을 반환하는데 만약 존재하지 않는 key라면 새로운 세션을 만들어 반환한다.
사실 실제 세션은 웹애플리케이션이 실행되자마자 생성되진 않는다.
하지만 편의를 위해 requestDispatch가 되기 직전에 해당 작업을 통해 현재 사용자의 요청에 대한 SessionId를 생성한다. 이 방법은 하나의 문제점을 가지고 있는데 이는 뒤에서 설명하겠다.
위에서 쿠키에 심게되면 SESSION_ID_NAME을 Key값으로 각 세션을 구분하는 임의의 변수 UUID가 value값으로 들어가 외부에서 쿠키를 직접 보아도 실제 데이터를 캡슐화 할 수 있다.
실제 데이터는 UUID를 key값으로 이용해 서버 내부에서 접근할 수 있다.
LoginController (POST /user/login)
모든 로그인에 대한 조건을 만족하게되면 세션을 통해 현재 user에 대한 정보를 저장한다.
UserListController (GET /user/list)
현재 세션의 UUID 값을 통해 세션에 저장되어있는 user 객체를 불러와 로그인을 검증한다. 단순히 null 체크로 검증하고 있지만, 실제 로직은 이보다 더 철저하게 검증하는편이 좋다.
현재 세션이 가지고 있는 문제점을 말하기전에 우선 세션에 대한 UUID 값이 생성되는 시기를 알아야한다. 세션은 다음과 같은 순서로 생성된다.
정상적인 흐름
위의 순서는 검증이 전부 통과했을때 이루어진다. 여기서는 GET이후 Set-Cookie가 웹브라우저가 전달되고 POST 요청시 웹 브라우저는 Cookie 값을 전달하기에 request header에서 MYJSESSIONID 값을 파싱할 수 있다.
**만약 GET을 통해 로그인한다면?
문제가 발생한다.**
문제를 가진 흐름
다음은 문제가 되는 부분을 실제 코드로 작성한 것이다.
(GET /user/login?userId=id&password=password)
해결방안?
가장 문제가 되는 부분은 FrontController에서 생성된 세션의 UUID 값을 request가 즉시 알 수 없다는 부분이다. 이런 부분은 Request와 Response가 서로 알 수 있도록 의존성을 주면 해결할 수 있을것 같다. 하지만 더 좋은 방법이 있지 않을까 싶습니다 여러분은 어떻게 생각하시나용?
서블릿간 초기화에서 우선순위가 필요한 경우 0에 가까운 숫자일수록 높은 우선순위로 먼저 초기화된다.
DispatcherServlet은 모든 서블릿에 대한 요청을 받아야 하므로 항상 먼저 초기화 돼야 하므로 속성을 1로 지정한다.
더하여 서블릿은 클라이언트의 요청이 최초로 발생하는 시점에 서블릿 인스턴스가 생성되는데 해당 설정을 이용하면 서블릿 컨테이너가 시작하는 시점에 서블릿 인스턴스 생성과 초기화가 진행된다.
@WebFilter(urlPatterns = {"/css/*", "/js/*", "/fonts/*", "/images/*", "/favicon.ico"})
public class ResourceFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(ResourceFilter.class);
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
request.getRequestDispatcher(httpServletRequest.getRequestURI()).forward(request, response);
}
@Override
public void destroy() {
}
}
- 박재성님의 코드는 서블릿 컨텐스트에서 default라는 이름을 가지는 디스패처를 가져온다 그러면 default의 이름을 가진 디스패처의 역할을
getNamedDispatcher
메소드를 디버깅해 알아보면 다음과 같다.findChild 메소드
위의 메소드는 name 매개변수에 전달된
default
라는 이름을 가지고 children이라는 저장소에서 값을 찾는다.children에 저장되어 있는 인스턴스들
children에 저장된 인스턴스를 살펴보면 우리가 만들었던 서블릿 뿐만 아니라 default와 jsp라는 서블릿이 보인다. 아마 jsp는 View 파일을 처리할때 사용되는 서블릿인것 같고 default의 역할은 아직도 구분이 되지 않는다.“default”라는 이름을 가진 org…
DefaultServlet
해당 default는 DefaultServlet이라는 클래스명을 가진 서블릿으로 구성되어 있다.DefaultServlet의 역할
첫 줄 해석: 대부분의 웹 응용 프로그램에 대한 기본 리소스 서비스 서블릿으로, HTML 페이지 및 이미지와 같은 정적 리소스를 제공하는 데 사용됩니다.DefaultServlet의 반환
즉, “default”라는 이름을 가진 DefaultServlet은 톰캣에서 정적 리소스를 처리하는 서블릿이다. 따라서 해당 서블릿을 이용해 정적 리소스를 처리하면 간단하게 처리할 수 있다.결과
결과적으로 다음과 같이 코드를 구성하면 간단하게 정적 리소스를 필터링할 수 있다.
(urlPatterns를 이용해 경로를 지정한 부분은 새로운 정적 리소스의 추가가 없을것 같아 사용함)@WebFilter(urlPatterns = {"/css/*", "/js/*", "/fonts/*", "/images/*", > "/favicon.ico"}) public class ResourceFilter implements Filter { @Override public void init(final FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(final ServletRequest request, final >ServletResponse response, final FilterChain chain) throws IOException, ServletException { request.getServletContext().getNamedDispatcher("default").forward(request, response); } @Override public void destroy() { } }
public class RequestMapping {
private static final Logger log = LoggerFactory.getLogger(RequestMapping.class);
private static final Map<String, Controller> handlerMapping = new HashMap<>();
static {
handlerMapping.put("/", new HomeController());
handlerMapping.put("/user/create", new CreateUserController());
handlerMapping.put("/user/list", new ListUserController());
handlerMapping.put("/user/login", new LoginUserController());
handlerMapping.put("/user/logout", new LogoutUserController());
handlerMapping.put("/user/profile", new ProfileController());
handlerMapping.put("/user/update", new UpdateUserFormController());
}
public Controller getHandlerMapping(final String requestURI) {
log.debug("requestURI={}", requestURI);
return handlerMapping.get(requestURI);
}
}
사용자의 요청 → 필터 → DispatcherServlet에서 RequestMapping 이용해 핸들러 찾음 → handler(DB 조회까지) → DispatcherServlet은 결과값을 이용해 응답 결정
디스패처 서블릿은 모든 url에 대한 요청을 처리한다. RequestMapping은 핸들러들의 매핑을 부가적으로 관리하여 DispatcherServlet에게 매핑 핸들러를 넘겨준다.
이들을 사용하면서 가장 크게 얻을 수 있는 장점은, 요청에 대한 일괄적인 처리와 응답에 대한 중복적인 코드가 한 곳으로 응집화 된다는 부분이다.
실제로 각 컨트롤러들의 코드들의 중복을 제거할 수 있다.
response.sendRedirect("/user/login");
, request.getRequestDispatcher("/WEB-INF/views/user/profile.jsp").forward(request, response);
return "redirect:/user/login";
, return "user/profile";
사용자에게 view를 직접적으로 요청할 수 있도록 요청할 수 있게 해야할까?