실습 프로젝트 저장소
실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함
jwp-basic (6.1)
https://github.com/slipp/jwp-basic/tree/step0-getting-started
web-application-server
https://github.com/slipp/web-application-server
*참고.
https://dev-ljw1126.tistory.com/402
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/
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] 8장 Ajax를 활용해 새로고침 없이 데이터 갱신하기 (0) | 2023.11.17 |
---|---|
[Next Step] 7장 DB를 활용해 데이터를 영구적으로 저장하기 (0) | 2023.11.17 |
[Next Step] 5장 웹 서버 리팩토링, 서블릿 컨테이너와 서블릿의 관계 (0) | 2023.11.16 |
[Next Step] 3~4장 HTTP 웹서버 구현을 통해 HTTP 이해하기(No Framework) (0) | 2023.11.15 |
[Next Step] 12.8 웹서버 도입을 통한 서비스 운영(p458) 정리 (0) | 2023.11.10 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!