서블릿이 뭘까?
우선, 정적 웹사이트와 동적 웹사이트의 차이를 보자.
정적 웹사이트 : http://www.chrissawyergames.com/
(이상한 사이트는 아니고, 롤러코스터 타이쿤을 만든 개발자의 홈페이지이다.)
동적 웹사이트 : https://www.naver.com/
우리가 아는 네이버이다.
쉽게 말하면, 동적 웹사이트는 웹에서의 내 행동에 따라(로그인, 메일, 등등) 다른 정보를 렌더링하고 보여주는 요청에 따라 응답이 일정하지 않은 웹사이트이다.
그에 반해, 정적 웹사이트는 정해져있는 방식대로만 보여주고, 이미 정해져 있는 UI를 보여준다.
그런데 이 말을 갑자기 왜하냐고?
그럼 요청에 따라 다른 방식으로 작동되는
동적인 웹사이트를 만들기 위해서는,
서버와의 상호작용이 필요하다.
예를 들어,
등등의 상호작용이 있다.
그러면, 해당 내용을 누가 해주냐?
바로 그게 서블릿이다.
한마디로 서블릿을 정의하자면,
동적 웹 어플리케이션을 사용하기 위해, 각각의 요청마다 다르게 구체화된 인터페이스 라고 할 수 있다.
Servlet에 대해 더 알고싶다면? (저도 여기 보고 공부했습니다!)
https://www.youtube.com/watch?v=2pBsXI01J6M
그러면, 잘 생각해보자.
DB와 같은 외부 요청을 제외하고 http 요청만 생각하더라도,
쉽게만 생각해도 웹 어플리케이션이 동작하려면
와 같은 어마무시하게 많은 일들을 해야 한다.
아래의 사진에서, 초록색 부분을 빼고 나머지 부분을 해야한다는 소리이다.
즉, 소스 코드 안에서의 비즈니스 로직 말고도 웹과 연결되어있고 동적이기에 신경써야 할 일이 너무 많다.
하지만 서블릿이 해당 신경써야 할 일들을 수행하여
신경 끄게 만들어주고,
개발자가 오직 소스코드 내에서 비즈니스 로직을 잘 만들어낼 수 있도록 도와준다.
웹 어플리케이션에 Http요청이 들어오면,
수많은 서블릿들 중 Http 요청을 처리하는 서블릿인
HttpServlet이
위 사진의 초록색 박스,
즉 비즈니스 로직 수행을 위해
개발자 대신에 Http 요청을 잘 파싱하여
HttpServletRequest, HttpServletResponse객체로 바꿔준다.
해당 Request, Response객체를 사용하는 법을 보자.
그렇다면,
어. 그래 서블릿이 많은 일을 해주니까, 우리가 할 일이 거의 없는건 알겠어.
근데 어쩌라고? 라는 생각이 든다면 정상이다.
여기서,
우리가 할 일이 거의 없긴 해도 있긴 있다.
해당 할 일을이 비즈니스 로직 수행이고(혹은 DB, Security등 요청과 응답에서 값을 꺼내 사용할때),
HttpServletRequest, HttpServletResponse
클래스를를 통해
우리가 해내는 것이다.
서블릿에게 들어온 요청과
서블릿이 하는 응답을 가지고,
우리가 해야할 일을 한다.
물론 이렇게 말하면 잘 이해가지 않는다.
코드로 예시를 들겠다.
//localhost:8080/hello
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("헬로서블릿");
String username = req.getParameter("username");
System.out.println("username = " + username);
response.setHeader("hello", username);
}
}
다음과 같이 /hello
경로로 들어오는 요청을 HelloServlet
서블릿이 받는다.
그 이후 service
매서드가 실행된다.
해당 어플리케이션을 실행시키고
http://localhost:8080/hello?username=Lee
와 같은 요청을 보낸다면,
request
요청에는 username이라는 정보가 담기기에,
서블릿이 해당 내용Lee
를 파싱하고 객체username
로 저장시켜놓아서
System.out.println("username = " + username);
로직을 실행할 수 있고,
response.setHeader("hello", username);
로직을 통해 응답을 얻을 수 있다.
결국
아래와 같은 로그를 확인할 수 있다.
(Hello: Lee 부분이 내가 추가해준 .setHeader() )
물론 이러한 로직은 그냥 봐도 파라미터를 얻어서 값을 얻는것,
응답 헤더에 String을 넣는것 이므로
어렵지 않다.
그런데, 위 내용은 단순 사용 예시이다.
그러면,
보통 공부를 위한 예시코드 말고 어떻게 쓰인다는걸까?
설명을 위해 본인 소스코드 예시를 적겠다.
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws ServletException, IOException {
Member member = (Member) authResult.getPrincipal();
String accessToken = delegateAccessToken(member);
String refreshToken = delegateRefreshToken(member);
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("memberId", String.valueOf(member.getMemberId()));
response.setHeader("role", String.valueOf(member.getRoles()));
response.setHeader("Refresh", refreshToken);
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
코드는 Spring Security의 JWT와 관련해서 인증을 성공시켰을때 작동하는 로직이다.
해당 매서드에서 보시다시피 인증 성공 후 JWT에 대한 정보들을
response.setHeader()
를 통해 Client에게 넘기고 있다.
다음 예시이다.
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String authorization = request.getHeader("Authorization");
return authorization == null || !authorization.startsWith("Bearer");
}
요청을 받아서, 해당 요청의
Authorization
파라미터 값이 없거나
지정해준 값으로 시작하지 않는다면
검증할 필요도 없이 예외를 터뜨리기 위한 boolean 로직이다.
이런식으로 서블릿들의 요청과 응답을 간단하게 사용 할 수 있도록 만드는 클래스가
HttpServletRequest, HttpServletResponse
클래스들이고,
프로젝트, 실무에서 쓰이기 때문에 잘 알아두어야 한다.
추가적 기능들은 아래와 같이 구현했고, 그에 따른 로그 기록도 첨부하겠다.
@WebServlet(name = "requestHeaderServlet", urlPatterns = "/request-header")
public class RequestHeaderServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
printStartLine(request);
printHeaders(request);
printHeaderUtils(request);
printEtc(request);
response.getWriter().write("ok");
}
//private
//start line 정보
private void printStartLine(HttpServletRequest request) {
System.out.println("--- REQUEST-LINE - start ---");
System.out.println("request.getMethod() = " + request.getMethod()); //GET
System.out.println("request.getProtocol() = " + request.getProtocol()); //HTTP/1.1
System.out.println("request.getScheme() = " + request.getScheme()); //http
System.out.println("request.getRequestURL() = " + request.getRequestURL()); // http://localhost:8080/request-header
System.out.println("request.getRequestURI() = " + request.getRequestURI()); // /request-header
System.out.println("request.getQueryString() = " + request.getQueryString());
System.out.println("request.isSecure() = " + request.isSecure()); //https 사용 유무
System.out.println("--- REQUEST-LINE - end ---");
System.out.println();
}
//Header 모든 정보
private void printHeaders(HttpServletRequest request) {
System.out.println("--- Headers - start ---");
request.getHeaderNames().asIterator()
.forEachRemaining(headerName -> System.out.println(headerName + ": " + request.getHeader(headerName)));
System.out.println("--- Headers - end ---");
System.out.println();
}
//Header 편리한 조회
private void printHeaderUtils(HttpServletRequest request) {
System.out.println("--- Header 편의 조회 start ---");
System.out.println("[Host 편의 조회]");
System.out.println("request.getServerName() = " + request.getServerName()); //Host 헤더
System.out.println("request.getServerPort() = " + request.getServerPort()); //Host 헤더
System.out.println();
System.out.println("[Accept-Language 편의 조회]");
request.getLocales().asIterator()
.forEachRemaining(locale -> System.out.println("locale = " +
locale));
System.out.println("request.getLocale() = " + request.getLocale());
System.out.println();
System.out.println("[cookie 편의 조회]");
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
System.out.println(cookie.getName() + ": " + cookie.getValue());
}
}
System.out.println();
System.out.println("[Content 편의 조회]");
System.out.println("request.getContentType() = " + request.getContentType());
System.out.println("request.getContentLength() = " + request.getContentLength());
System.out.println("request.getCharacterEncoding() = " + request.getCharacterEncoding());
System.out.println("--- Header 편의 조회 end ---");
System.out.println();
}
//기타 정보
private void printEtc(HttpServletRequest request) {
System.out.println("--- 기타 조회 start ---");
System.out.println("[Remote 정보]");
System.out.println("request.getRemoteHost() = " + request.getRemoteHost()); //
System.out.println("request.getRemoteAddr() = " + request.getRemoteAddr()); //
System.out.println("request.getRemotePort() = " + request.getRemotePort()); //
System.out.println();
System.out.println("[Local 정보]");
System.out.println("request.getLocalName() = " + request.getLocalName()); //
System.out.println("request.getLocalAddr() = " + request.getLocalAddr()); //
System.out.println("request.getLocalPort() = " + request.getLocalPort()); //
System.out.println("--- 기타 조회 end ---");
System.out.println();
}
}
--- REQUEST-LINE - start ---
request.getMethod() = GET
request.getProtocol() = HTTP/1.1
request.getScheme() = http
request.getRequestURL() = http://localhost:8080/request-header
request.getRequestURI() = /request-header
request.getQueryString() = null
request.isSecure() = false
--- REQUEST-LINE - end ---
--- Headers - start ---
host: localhost:8080
connection: keep-alive
cache-control: max-age=0
sec-ch-ua: "Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site: none
sec-fetch-mode: navigate
sec-fetch-user: ?1
sec-fetch-dest: document
accept-encoding: gzip, deflate, br
accept-language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
cookie: grafana_session_expiry=1690466197; grafana_session=ab34edd624c79eaff04d75a33111000b
--- Headers - end ---
--- Header 편의 조회 start ---
[Host 편의 조회]
request.getServerName() = localhost
request.getServerPort() = 8080
[Accept-Language 편의 조회]
locale = ko_KR
locale = ko
locale = en_US
locale = en
request.getLocale() = ko_KR
[cookie 편의 조회]
grafana_session_expiry: 1690466197
grafana_session: ab34edd624c79eaff04d75a33111000b
[Content 편의 조회]
request.getContentType() = null
request.getContentLength() = -1
request.getCharacterEncoding() = UTF-8
--- Header 편의 조회 end ---
--- 기타 조회 start ---
[Remote 정보]
request.getRemoteHost() = 0:0:0:0:0:0:0:1
request.getRemoteAddr() = 0:0:0:0:0:0:0:1
request.getRemotePort() = 49835
[Local 정보]
request.getLocalName() = 0:0:0:0:0:0:0:1
request.getLocalAddr() = 0:0:0:0:0:0:0:1
request.getLocalPort() = 8080
--- 기타 조회 end ---