HTML내에 자바 코드를 삽입하여 웹 서버에서 동적으로 웹 페이지를 생성하여 웹 브라우저에 돌려주는 서버 사이드 스크립트 언어이다.
JSP 모델 2의 구조
JSP로 작성된 프로그램은 서버로 요청 시 서블릿 파일로 변환되어 JSP 태그를 분해하고 추출하여 다시 순수한 HTML을 변환한다. JSP를 통해 정적인 HTML과 동적으로 생성된 컨텐츠(httprequest params)를 혼합하여 사용할 수 있으며, 사용자가 입력한 컨텐츠에 맞게 동적인 웹 페이지를 생성한다.
JSP는 실행 시 자바 서블릿으로 변환된 후 실행되므로 서블릿과 거의 유사하다고 볼 수 있지만, 서블릿과는 달리HTML 표준에 따라 작성되므로 웹 디자인하기에 편리하다. 서블릿은 자바코드 내에 HTML 코드가 있어서 읽고 쓰기가 불편하여 작업의 효율성이 떨어진다.
특징
1. 스크립트 언어이기 때문에 자바 기능을 그대로 사용할 수 있다
2. Tomcat(WAS)이 만들어 놓은 객체(request, response, session 등)를 사용하고, 수정된 경우 재배포하지 않아도 Tomcat(WAS)이 알아서 처리해준다.
3. 사용자 정의 태그를 사용하여, 보다 효율적으로 웹 사이트를 구성할 수 있다.(JSTL)
모든 데이터 정보를 가공하여 가지고 있는 컴포넌트 상태 변화가 있을 때 컨트롤러와 뷰에 통보한다. (뷰는 최신 결과를 리턴, 컨트롤러는 적용 가능한 명령을 추가,제거,수정)
View
사용자가 보는 시각적인 UI 요소
Controller
Model과 View를 연결 해 주는 역할 Model과 View에 명령을 보낸다.
MVC-1
View와 Controller을 JSP가 담당한다.
JSP에서 MVC가 모두 이루어져 재사용성, 가독성이 떨어져 유지보수성이 낮다.
출처 https://chanhuiseok.github.io/posts/spring-3/
MVC-2
JSP에서 모든 것을 수행하던 MVC1 패턴과 달리 M, V, C의 역할이 분리되어 있다.
오류 발생 혹은 수정 시 M, V, C 중 해당 요소만 수정할 수 있어 유지보수성이 높다.
출처 https://chanhuiseok.github.io/posts/spring-3/
Spring Framework에서의 MVC
MVC가 유기적으로 동작하도록 다양한 요소들이 있다.
구성요소
설명
DispatcherServlet (FrontController)
- 제일 앞 단에서 HTTP Request를 처리 - 요청 시 요청과 매핑되는 컨트롤러를 지정하는 컨트롤러
Controller (Handler)
- HTTP Request를 처리하여 Model을 생성하고, View를 지정 - DispatcherServlet에서 지정된 Controller는 요청을 처리하는 과정에서 필요한 데이터를 뽑아 Model에 저장하며, 요청에 따라 보여줄 View Name 및 View를 반환한다.
ModelAndView
- Controller에 의해 리턴 된 Model과 View가 저장(Wrapping)된 객체 - Model : Key-Value 형태의 데이터 저장 객체 - View(ViewName) : ViewResolver에 리턴 될 View 지정
ViewResolver
ModelAndView 객체를 처리하여 View를 리턴(Model에 저장된 데이터 사용) 사용자가 특정 URL로 들어갔을 때 보여지는 View가 이 곳에서 만들어진다.
q Root, 즉 기준을 의미하는 Question 엔티티의 객체 (질문 제목과 내용을 검색하기 위해 필요)
u1 Question 엔티티와 SiteUser 엔티티를 아우터 조인하여 만든 SiteUser 엔티티의 객체. Question 엔티티와 SiteUser 엔티티는 author 속성으로 연결되어 있기 때문에 q.join("author")와 같이 조인해야 한다. (질문 작성자를 검색하기 위해 필요)
a Question 엔티티와 Answer 엔티티를 아우터 조인하여 만든 Answer 엔티티의 객체. Question 엔티티와 Answer 엔티티는 answerList 속성으로 연결되어 있기 때문에 q.join("answerList")와 같이 조인해야 한다. (답변 내용을 검색하기 위해 필요)
u2 바로 위에서 작성한 a 객체와 다시 한번 SiteUser 엔티티와 아우터 조인하여 만든 SiteUser 엔티티의 객체 (답변 작성자를 검색하기 위해서 필요)
검색어(kw)가 포함되어 있는지를 like로 검색하기 위해 제목, 내용, 질문 작성자, 답변 내용, 답변 작성자 각각에cb.like를 사용하고 최종적으로cb.or로 OR 검색되게 하였다.
또한 그리고 page와 kw를 동시에 GET으로 요청할 수 있는 searchForm을 추가했다.
GET 방식으로 요청해야 하므로 method 속성에 "get"을 설정했다.
kw와 page는 이전에 요청했던 값을 기억하고 있어야 하므로 value에 값을 유지할수 있도록 했다.
그리고 action 속성은 "폼이 전송되는 URL"이므로 질문 목록 URL인 /question/list를 지정했다.
GET 방식을 사용하는 이유
만약 GET이 아닌 POST 방식으로 검색과 페이징을 처리한다면 웹 브라우저에서 "새로고침" 또는 "뒤로가기"를 했을 때 "만료된 페이지입니다."라는 오류를 종종 만나게 될 것이다. 왜냐하면 POST 방식은 동일한 POST 요청이 발생할 경우 중복 요청을 방지하기 위해 "만료된 페이지입니다." 라는 오류를 발생시키기 때문이다.
페이징
그리고 기존 페이징을 처리하는 부분도?page=1처럼 직접 URL을 링크하는 방식에서 값을 읽어 폼에 설정할 수 있도록 다음처럼 변경해야 한다. 왜냐하면 검색어가 있을 경우 검색어와 페이지 번호를 함께 전송해야 하기 때문이다.
그리고 검색버튼을 클릭하는 경우는 새로운 검색에 해당되므로 page에 항상 0을 설정하여 첫 페이지로 요청하도록 했다.
위와 같이 해당 문장만 있는 게시물이 조회된다면 성공이다.
@Query
Specification 대신 직접 쿼리를 작성하여 수행하는 방법에 대해서 알아보자.
QuestionRepository
(... 생략 ...)
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
(... 생략 ...)
@Query("select "
+ "distinct q "
+ "from Question q "
+ "left outer join SiteUser u1 on q.author=u1 "
+ "left outer join Answer a on a.question=q "
+ "left outer join SiteUser u2 on a.author=u2 "
+ "where "
+ " q.subject like %:kw% "
+ " or q.content like %:kw% "
+ " or u1.username like %:kw% "
+ " or a.content like %:kw% "
+ " or u2.username like %:kw% ")
Page<Question> findAllByKeyword(@Param("kw") String kw, Pageable pageable);
}
@Query를 작성할 때에는 반드시 테이블 기준이 아닌 엔티티 기준으로 작성해야 한다. 즉,site_user와 같은 테이블명 대신SiteUser처럼 엔티티명을 사용해야 하고 조인문에서 보듯이q.author_id=u1.id와 같은 컬럼명 대신q.author=u1처럼 엔티티의 속성명을 사용해야 한다.
@Query에 파라미터로 전달할 kw 문자열은 메서드의 매개변수에@Param("kw")처럼 @Param 애너테이션을 사용해야 한다. 검색어를 의미하는 kw 문자열은 @Query 안에서:kw로 참조된다.
작성한 findAllByKeyword 메서드를 사용하기 위해 QuestionService를 다음과 같이 수정하자.
(... 생략 ...)
public class QuestionService {
(... 생략 ...)
public Page<Question> getList(int page, String kw) {
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return this.questionRepository.findAllByKeyword(kw, pageable);
}
(... 생략 ...)
}