자바를 사용하여 웹 페이지를 동적으로 생성하는 서버측 프로그램 혹은 그 사양을 말하며, 흔히 서블릿이라 불린다.
서블릿은자바 코드안에 HTML을 포함하고 있다.
서블릿은 웹에 다양한 기능이 요구되면서 정적인 자료 뿐 아니라 동적인 페이지들을 만들 필요가 생겼기 때문에 만들어졌다. 서블릿은 클라이언트의 요청에 맞춰 동적인 결과를 만들어 주는 자바 웹 프로그래밍 기술이며, 서블릿은WAS의 서블릿 컨테이너 안에서 동작하게 된다.
특징
1. 클라이언트의 요청에 동적으로 응답하는 웹 어플리케이션 요소(Component)
2. HTML을 사용하여 응답
3. JAVA의 Thread 이용
4. MVC 패턴의 Controller 역할을 함
5. HTTP 프로토콜 서비스를 지원하는 javax.servlet.http.HttpServlet클래스를 상속받는다
6. UDP보다 처리 속도가 느림
7. HTML변경 시 서블릿을 재컴파일해야 한다.
서블릿(웹) 컨테이너
웹 컨테이너(서블릿 컨테이너)와 동작과정
서블릿 컨테이너는 서블릿을 담고 관리해주는 컨테이너다. 서블릿 클래스의 규칙에 맞게 서블릿을 관리하며, 클라이언트의 요청을 받으면 HttpServletRequest와 HttpServletResponse 객체를 생성하여 post, get에 따라 동적인 페이지를 생성하여 응답한다.
1. 서블릿 생명주기 관리
서블릿의 생명주기를 관리한다. 서블릿 클래스를 로딩하여 인스턴스화하고 초기화 메서드를 호출하고, 요청이 들어오면 적절한 서블릿 메서드를 찾아서 동작한다. 또한 서블릿의 생명이 다하면 가비지 컬렉션을 통해 메모리에서 제거한다.
2. 통신 지원
웹 서버와 소켓을 만들어서 클라이언트의 요청을 받고 응답할 수 있는 통신을 지원해 준다. 통신을 하기 위한 listen, accept등의 과정을 API로 제공하여 복잡한 과정을 생략해주기 때문에 개발자가 비즈니스 로직 개발에 집중할 수 있게 도와준다.
3. 멀티쓰레드 지원 및 관리
클라이언트의 요청을 받을 때마다 새로운 자바 스레드를 생성한다. 그래서 동시에 여러 요청이 들어와도 멀티쓰레딩 환경에서 동시다발적 작업을 관리할 수 있다.
4. 보안 관리
보안 관련 기능을 제공하기 때문에 개발자는 서블릿에 보안 관련 메서드를 구현하지 않아도 된다.
서블릿의 동작 과정
1. 클라이언트의 요청 2. HttpServletRequest, HttpServletResponse 객체 생성 3. Web.xml이 어느 서블릿에 대해 요청한 것인지 탐색 4. 해당하는 서블릿에서 service()메서드 호출 5. doGet()또는 doPost()호출 6. 동적 페이지 생성 후 ServletResponse 객체에 응답 전송 7. HttpServletRequest, HttpServletResponse 소멸
서블릿의 생명 주기
서블릿 생명주기 메서드
init() / 초기화 서블릿 요청 시 맨 처음 한번만 호출됨
doGet(), doPost() / 작업 메서드 서블릿 요청 시 매번 호출되며, 클라이언트가 요청한 것에 맞춰 실행됨
destroy() / 종료 메모리에서 소멸될 때 호출된다.
1. 서블릿 객체 생성
최초 호출 시 Tomcat(WAS)에서 서블릿 파일을 불러와 컴파일한 후 메모리에 로드한다.
Tomcat은 서블릿 파일이 변경되는게 아니라면 서버 종료 전까지 해당 객체를 계속 재활용한다.
JSP파일이나 서블릿파일이 수정된다면 서블릿 자원을 해제시킨 뒤 새로운 객체를 생성해서 메모리에 로드하는 형식이다.
2. 선처리 작업 수행(객체 생성 시 최초 1회 수행)
서블릿에서 메서드를 임의로 작성하고 @PostConstruct 어노테이션을 부여하여 가장 먼저 해당 메서드를 실행한다.
서블릿 객체가 생성될 때 먼저 처리하고 싶은 로직이 있다면 만들어준다.
init()메서드로 수행해도 큰 문제는 없으나, 성격이 다른 로직이라면 별도로 분리해서 작성 가능하다.
@PostConstruct
의존성 주입이 이루어진 후 초기화를 수행하며, service 로직을 수행하기 전에 발생한다.
3. init() 실행
GenericServlet 클래스에서 정의하는 메서드
서블릿 객체 생성 시 초기화 작업을 수행하는 역할
Tomcat 서버 환경설정 파일인 web.xml의 내용을 불러와 ServletConfig 객체로 생성해준다.
오버로딩 구조로 되어있고 Tomcat에서 ServletConfig객체를 파라미터로 넣어 실행하며, init()메서드를 오버라이딩하지 않으면 아무것도 실행시키지 않는다.
4. Service() : 자원 해제 전까지 요청이 올때마다 반복 실행
클라이언트에서 요청이 오면, 요청에 맞춰 GET, POST등이 실행된다.
5. destroy() : 소멸
컨테이너가 서블릿에 종료 요청 시 실행되며, 종료시에 처리해야할 작업들은 destroy() 메서드를 오버라이딩하여 구현한다.
서블릿 작성 예시
위의 그림처럼, 프로젝트 선택 후 New - Servlet을 선택하여 생성하고, 패키지명, 클래스명과 URL 매핑, 메서드를 선택하여 만들면 되는데, 자세한 사항은 해당 블로그를 참조하자. (링크)
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);
}
(... 생략 ...)
}