실습 프로젝트 저장소
실습의 경우 처음에 fork 받았는데, 깃 허브 잔디가 심어지지 않아 기술 블로그 참고(링크)하여 저장소 설정을 변경하도록 함
jwp-basic
https://github.com/slipp/jwp-basic/tree/step7-self-check
자체 점검 요구사항(필수)
① 로컬 개발 환경에 톰캣 서버를 시작하면 서블릿 컨테이너의 초기화 과정을 설명하라 (아래 참고)
② 로컬 개발환경에서 톰캣 서버를 시작한 후 http://localhost:8080으로 접근해서 질문 목록이 보이기까지 소스코드의 호출 순서 및 흐름을 설명하라 (아래 참고)
③ 질문하기 기능을 구현한다 ( 2023-10-27 완료 )
- 질문하기를 성공한 후 질문 목록 페이지("/")로 이동해야 한다.
- 질문 추가 로직은 QuestionDao 클래스의 insert()메서드를 활용한다
④ 로그인한 사용자만 질문이 가능하도록 수정한다 ( 2023-10-27 완료 )
또한 질문할 때 글쓴이를 입력하지 않고 로그인한 사용자 정보를 가져와 글쓴이 이름으로 사용하도록 구현한다.
⑤ 질문 상세보기 화면에서 답변 목록을 정적인 HTML이 아니라 DB에 저장되 어있는 답변을 출력하도록 구현한다 ( 2023-10-27 완료 )
단 <% %>와 같이 스크립틀릿을 사용하 지않고, JSTL과 EL만으로 구현해야 한다
⑥ 한글이 깨지는 문제를 해결하기 위해 ServletFilter를 활용해 문제를 해결할 수 있다. ( 2023-10-27 완료 )
core.web.filter.CharacterEncodingFilter 클래스 생성하고 애노테이션 설정을 통해 한글 문제를 해결한다.
⑦ next.web.qna 패키지의 ShowController는 멀티스레드 상황에서 문제가 발생할 가능성이 있는 코드이다. 멀티스레드 상황에서 문제가 발생하지 않도록 수정한다. 그리고 멀티스레드에서 문제가 되는 이유를 작성하라 ( 2023-10-27 완료 , 아래 참고)
⑧ 답변을 추가하는 시점에 질문의 댓글 수도 1증가하도록 로직을 구현한다 ( 2023-10-27 완료 )
⑨ 이 Q&A 서비스는 모바일에서도 서비스할 계획이라 API를 추가해야 한다. ( 2023-10-27 완료 )
API는 JSON 형식으로 제공할 계획이다. /api/qna/list URL로 접근했을 때 질문 목록을 JSON데이터로 조회할 수 있도록 구현한다.
⑩ 상세보기 화면의 답변 목록에서 답변을 삭제해야 한다. 답변 삭제 또한 화면을 깜빡이지 않고 구현이 가능하도록 AJAX로 구현한다. ( 2023-10-27 완료 )
⑪ 질문 수정이 가능해야 한다. 질문수정은 글쓴이와 로그인 사용자가 같은 경우에만 수정이 가능하다. ( 2023-10-28 완료 )
⑫ 컨트롤러에서 접근하는 DAO 클래스에서 DB 접근 로직을 구현할 때 사용하는 JdbcTemplate은 인스턴스를 여러개 생성할 필요없다. ( 2023-10-27 완료 )
인스턴스를 하나만 생성하도록 구현한다. (싱글톤 패턴)
⑬ 질문 삭제 기능을 구현한다. 질문 삭제가 가능한 경우는 다음과 같다. ( 2023-10-28 완료 )
설명이 필요한 ①, ②, ⑦ 제외한 나머지 구현 요구사항은 아래 깃 저장소에 처리 완료
https://github.com/ljw1126/my-jwp-basic/tree/chapter9_self_check
요구사항 1. 로컬 개발 환경에 톰캣 서버를 시작하면 서블릿 컨테이너의 초기화 과정을 설명하라
나의 답안
① 톰캣 초기화 진행
② 서블릿 컨테이너 초기화 진행 (DispatcherServlet은 서블릿이지, 서블릿 컨테이너가 아니다)
③ StandardContext 클래스에서 ServletContextListener 와 Filter 초기화 진행
④ 서블릿 init() 실행
⑤ dispactherServlet의 onStartUp = 1이므로 서블릿 초기화시 서블릿 인스턴스가 생성된다
⑥ 서블릿 인스턴스 생성시 RequestMapping 초기화가 실행되고, URL과 Controller Mapping 정보를 초기화한다
책 답안
① 서블릿 컨테이너는 웹 애플리케이션의 상태를 관리하는 ServletContext 생성
② ServletContext 초기화되면서 초기화 이벤트 발생
③ 등록된 ServletContextListener의 콜백 메소드 contextInitialized가 호출된다
참고. StandardContext 클래스의 listenerStart() 메소드에서 예시인 ContextLoaderListener 클래스 호출함을 확인
④ 서블릿 컨테이너는 클라이언트로부터 최초 요청시, 서블릿 인스턴스 (실습에선 DispatcherServlet)을 생성자 호출한다. 현재 loadOnStartup 속성이 1로 설정되어 있기 때문에 서블릿 컨테이너 시작하는 시점에 인스턴스를 생성한다
참고.
StandardContext 클래스의 startInternal() 메서드 하단에 loadOnStartup() 메서드를 실행한다
loadOnStartup() 메서드에서 children은 Container 안에 있는 Servlet들을 말하며 loadOnStartup 속성값이 0이상인 경우 리스트에 담아 Wrapper.load() 메서드를 실행한다
StandWrapper 클래스에 load() 이후 GenericServlet의 init()을 호출하여 DispatcherServlet init() 호출됨
⑤ DispatcherServlet 인스턴스의 init() 메소드를 호출해 초기화 작업을 진행
⑥ init() 메소드 안에서 RequestMapping 객체를 생성한다
⑦ RequestMapping 인스턴스의 initMapping 메소드 호출하여 요청 URL 과 Controller 인스턴스를 매핑한다
요구사항 2. 첫 화면에 접근했을 때 사용자 요청부터 응답까지 흐름을 설명하라
나의 답안
① 사용자 요청시 CharacterEncodingFilter와 ResourceFilter를 먼저 거침
- ResourceFilter에서 처리가능한 Resource인 경우 DispatcherServlet까지 가지 않고 forward 처리되고, 아닌 경우 넘어감
② DispatcherServlet에서 service() 메소드가 실행되고, 요청 URL에 맞는 Controller를 찾아 작업을 위임한다
③ Controller는 execute() 메소드를 실행하여 결과 처리 후 ModelAndView 타입 객체를 반환한다
④ View 인터페이스 render() 메소드에 ModelAndView 객체와 HttpServletRequest, HttpServletResponse를 전달하면 View 구현체에 따라 JspView 또는 JsonView 결과를 클라이언트에게 응답한다
책 답안
① 클라이언트 접근 요청을 처리할 서블릿에 접근하기 전에 먼저 Filter의 doFilter() 메소드가 실행된다.
ResourceFilter의 경우 해당 요청이 정적 자원(CSS, JS, 이미지) 요청이 아니기 때문에 서블릿으로 요청을 위임한다
② 요청 처리는 "/"으로 매핑 되어 있는 DispatcherServlet이므로 이 서블릿의 service() 메소드가 실행된다
③ service() 메소드는 요청받은 URL을 분석해 해당 Controller 객체를 RequestMapping에서 가져온다.
요청 URL이 "/" 일 때 HomeController가 반환된다
④ service() 메소드는 HomController의 execute() 메소드에게 작업을 위임한다.
이때 execute() 메소드의 반환값은 ModelAndView이다
⑤ service() 메소드는 반환 받은 ModelAndView의 모델 데이터를 뷰의 render() 메소드에 전달한다. 이 요청에서 View는 JspView 이다. JspView는 render() 메소드로 전달된 모델 데이터를 home.jsp에 전달해 HTML을 생성하고, 응답함으로써 작업을 끝낸다
요구사항 7. 멀티스레드 상황에서 문제가 발생하지 않도록 수정한다. 그리고 멀티스레드에서 문제가 되는 이유를 작성하라
아래의 예시 코드를 살펴보자
public class ShowController extends AbstractController {
private QuestionDao questionDao = QuestionDao.getInstance();
private AnswerDao answerDao = AnswerDao.getInstance();
private Question question; //*
private List<Answer> answers; //*
@Override
public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
long questionId = Long.parseLong(request.getParameter("questionId"));
question = questionDao.findById(questionId);
answers = answerDao.findAllByQuestionId(questionId);
return jspView("/qna/show.jsp")
.addAttribute("question", question)
.addAttribute("answerList", answers);
}
}
멀티 스레드 환경에서 여러 명의 사용자가 인스턴스 하나를 재사용하고 있다고 가정하자
한 명의 사용자 요청이 발생한 경우 별다른 특이사항 없이 동작한다. 하지만 스레드1의 요청이 끝나지 않은 상태에서 스레드2의 요청이 발생한다면 어떻게 될까
스레드1 경우 1번 글에 대한 내용을 요청하고 있는데, 스레드2가 끼어 들면서 2번 글에 대한 참조 주소를 가르키게 되고
그 결과 스레드 1은 원치않은 결과를 응답으로 받게 된다.
새로 고침을 하게 될 경우 정상적으로 스레드 1이 원하는 결과를 확인 가능하다. 하지만 결제와 같은 서비스에서 위와 같은 상황이 발생한다면 막대한 피해가 발생할 수 있다. 따라서 멀티스레드 웹 애플리케이션을 개발할 때 자신이 구현하고 있는 코드가 어떻게 동작하는지를 정확히 이해하고 프로그래밍하는 것은 안전한 애플리케이션을 개발하는데 있어 정말 중요하다(p315)
위의 코드를 개선하면 다음과 같다
public class ShowController extends AbstractController {
private QuestionDao questionDao = QuestionDao.getInstance();
private AnswerDao answerDao = AnswerDao.getInstance();
@Override
public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
long questionId = Long.parseLong(request.getParameter("questionId"));
Question question = questionDao.findById(questionId);
List<Answer> answers = answerDao.findAllByQuestionId(questionId);
return jspView("/qna/show.jsp")
.addAttribute("question", question)
.addAttribute("answerList", answers);
}
}
지역 변수로 변경하여 (클래스 필드)상태값을 가지지 않도록 하여 인스턴스간의 영향을 없앨수 있었다
'독서 > 📚' 카테고리의 다른 글
[Next Step] 11장 의존관계 주입(DI)을 통합 테스트 하기 쉬운 코드 만들기 (0) | 2023.11.21 |
---|---|
[Next Step] 10장 새로운 MVC 프레임워크 구현을 통한 점진적 개선 (2) | 2023.11.20 |
[Next Step] 8장 Ajax를 활용해 새로고침 없이 데이터 갱신하기 (0) | 2023.11.17 |
[Next Step] 7장 DB를 활용해 데이터를 영구적으로 저장하기 (0) | 2023.11.17 |
[Next Step] 6장 서블릿/JSP를 활용해 동적인 웹 애플리케이션 개발하기 (0) | 2023.11.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!