TIL/[TIL]

[TIL]Servlet 기초부터 요청/응답, 파라미터 처리, JSON API까지

namerong 2026. 5. 20. 16:39

1. 학습 주제

단순히 “서블릿 문법”만 본 게 아니라,
브라우저에서 요청을 보내면 서버 안에서 어떤 객체들이 어떻게 움직이는지 전체 흐름을 이해하는 과정이 핵심이었다.

특히 이번 범위는 평소보다 추상적으로 느껴질 수 있다.
왜냐하면 자바 콘솔 프로그램처럼 main()에서 바로 실행되는 게 아니라,
브라우저, 톰캣, 서블릿, 요청 객체, 응답 객체가 함께 움직이는 구조를 머릿속에 그려야 하기 때문이다.

이번 학습 흐름은 크게 다음과 같았다.

  • 서블릿이 무엇인지 이해하기
  • 톰캣이 서블릿을 어떻게 생성하고 관리하는지 보기
  • 브라우저의 GET/POST 요청이 어떻게 doGet(), doPost()로 연결되는지 이해하기
  • 요청 파라미터와 헤더를 읽는 법 익히기
  • 응답으로 HTML을 직접 만들어 보내는 방식 이해하기
  • JSON 요청을 자바 객체로 바꾸고, 다시 JSON 응답으로 보내는 흐름 익히기

2. Servlet이란 무엇인가

서블릿은 HTTP 요청을 처리할 수 있는 자바 클래스이다.

쉽게 말하면:

  • 브라우저가 서버에 요청을 보냈을 때
  • 그 요청을 자바 코드로 받아서
  • 필요한 처리 후
  • 응답을 돌려주는 서버 측 자바 프로그램

이다.

즉 서블릿은 “웹 요청을 처리하는 자바 진입점”이라고 보면 된다.

예제의 HelloServlet이 가장 기본적인 형태였다.

@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet

여기서 중요한 점:

  • HttpServlet을 상속받아 만든다
  • @WebServlet으로 URL 주소를 연결한다
  • 브라우저가 /hello-servlet로 요청하면 이 서블릿이 실행된다

3. 브라우저 요청이 서블릿까지 가는 흐름

이 부분이 가장 중요하다.

브라우저에서 주소를 입력하거나 버튼을 누르면 바로 서블릿이 실행되는 게 아니라,
중간에 톰캣(Tomcat)이라는 웹 컨테이너가 끼어 있다.

흐름을 순서대로 보면:

  1. 사용자가 브라우저에서 URL 요청
  2. 톰캣이 요청을 받음
  3. URL에 매핑된 서블릿을 찾음
  4. 필요한 경우 서블릿 객체를 생성
  5. 요청 정보를 HttpServletRequest 객체에 담음
  6. 응답용 HttpServletResponse 객체를 준비
  7. 서블릿의 service() 또는 doGet(), doPost() 호출
  8. 서블릿이 응답 데이터를 작성
  9. 톰캣이 그 응답을 브라우저에 전달
  10. 브라우저가 화면에 보여줌

즉 서블릿은 혼자 동작하는 게 아니라,
톰캣이 “요청을 받아 객체를 만들고, 적절한 메서드를 호출해주는 구조” 안에서 동작한다.


4. HttpServlet과 서블릿 상속 구조

예제 주석에도 나온 것처럼 서블릿은 이런 상속 구조 안에 있다.

  • Servlet interface
  • GenericServlet
  • HttpServlet
  • 사용자 정의 Servlet

즉 우리가 직접 만드는 서블릿은 보통 HttpServlet을 상속받는다.

이유는 HttpServlet이 이미 다음 같은 웹 처리 기능을 갖고 있기 때문이다.

  • GET 요청 처리 구조
  • POST 요청 처리 구조
  • request/response 객체 처리 기반

그래서 우리는 doGet(), doPost() 같은 메서드만 재정의해서 사용하면 된다.


5. Servlet Life Cycle

chap01_servlet-lifecycle는 서블릿이 한 번 요청받고 끝나는 게 아니라,
톰캣이 객체를 생성해서 재사용한다는 점을 보여주는 파트였다.

5.1 생성자

public LifeCycleTestServlet() {
    System.out.println("constructor 생성자!");
}
  • 서블릿 객체가 처음 만들어질 때 호출된다

5.2 init()

@Override
public void init(ServletConfig config) throws ServletException {
    System.out.println("init(): " + initCount++);
}
  • 서블릿이 생성된 뒤 최초 한 번 초기화 작업을 수행할 때 호출된다
  • DB 연결 준비, 설정 읽기 같은 초기화 작업에 활용할 수 있다

5.3 service()

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
  • 실제 요청이 들어올 때마다 호출된다
  • 요청 방식에 따라 내부적으로 doGet(), doPost() 등을 연결해준다

5.4 destroy()

@Override
public void destroy() {
    System.out.println("destroy(): " + destroyCount++);
}
  • 톰캣이 종료되거나 서블릿이 제거될 때 호출된다
  • 자원 정리용으로 사용된다

6. 서블릿이 싱글톤처럼 관리된다는 의미

예제 설명에서 매우 중요한 포인트가 있었다.

톰캣은 URL별 서블릿 객체를 하나 만들어 두고 재사용한다.

즉 요청이 올 때마다 new Servlet()을 계속 하는 방식이 아니다.

이 말은 곧:

  • 여러 요청이 같은 서블릿 객체를 공유할 수 있다
  • 인스턴스 변수 사용에 조심해야 한다
  • 요청마다 달라지는 값은 지역변수로 처리하는 습관이 중요하다

는 뜻이다.

이 부분이 어려운 이유는
콘솔 프로그램은 한 번 실행되고 끝나지만,
서블릿은 “서버 위에서 살아 있으면서 여러 요청을 계속 처리”하기 때문이다.


7. GET 요청과 POST 요청

chap02_request_and_response에서 GET과 POST 흐름을 분리해서 보여줬다.

GET

<a href="/request-service">GET 방식으로 서블릿 호출</a>
  • 주소창 요청
  • 링크 클릭
  • 주로 조회 목적

POST

<form action="/request-service" method="POST">
    <button type="submit">POST 방식으로 서블릿 호출</button>
</form>
  • 폼 전송
  • 주로 데이터 등록/전송 목적

예제의 ServiceMethodTestServlet은 요청 메서드를 직접 확인해서 분기했다.

String httpMethod = req.getMethod();

그리고:

if("GET".equals(httpMethod)){
    doGet(req, resp);
} else if("POST".equals(httpMethod)){
    doPost(req, resp);
}

즉 브라우저 요청 방식에 따라 서버에서 실행되는 메서드가 달라진다.


8. service(), doGet(), doPost() 관계

이 부분도 많이 헷갈릴 수 있다.

서블릿 요청이 들어오면 가장 먼저 큰 흐름을 받는 건 service()이다.
그리고 그 안에서 요청 메서드에 따라 doGet(), doPost() 같은 메서드가 호출된다.

즉 개념적으로는 이렇게 이해하면 된다.

  • service() : “이 요청이 GET이냐 POST냐 판단하는 접수창구”
  • doGet() : GET 처리 담당
  • doPost() : POST 처리 담당

실무에서는 보통 service()를 직접 오버라이드하기보다
doGet(), doPost()만 구현하는 경우가 더 많다.


9. HttpServletRequest: 요청을 읽는 객체

브라우저가 보내는 데이터는 HttpServletRequest 안에 담겨 들어온다.

예제에서 파라미터를 읽는 방식은 다음과 같았다.

String name = req.getParameter("name");

중요한 점:

  • getParameter()는 항상 문자열을 반환한다
  • 숫자처럼 보여도 일단 String으로 받고 필요하면 변환해야 한다

체크박스처럼 값이 여러 개 올 수 있으면 배열로 받는다.

String[] values = req.getParameterValues("value");

즉 request 객체는 “브라우저가 보낸 데이터 상자”라고 이해하면 좋다.


10. 요청 헤더 읽기

요청 데이터는 파라미터만 있는 게 아니다.
브라우저나 클라이언트 환경 정보도 헤더로 함께 온다.

예제:

System.out.println("User-Agent 헤더 정보: " + req.getHeader("User-Agent"));

여기서 User-Agent는 어떤 브라우저에서 요청했는지 알려주는 정보이다.

즉 request 객체는:

  • 파라미터
  • 헤더
  • 요청 방식
  • 요청 URL
  • 바디 데이터

같은 정보를 담고 있는 요청 정보 묶음이다.


11. HttpServletResponse: 응답을 만드는 객체

요청을 읽었다면, 이제 서버는 응답을 만들어서 브라우저에 보내야 한다.
그 역할을 하는 객체가 HttpServletResponse이다.

예제에서는 먼저 응답 타입을 설정했다.

resp.setContentType("text/html; charset=utf-8");

이건 브라우저에게:

  • 이 응답은 HTML 문서이고
  • UTF-8 인코딩을 사용한다

고 알려주는 것이다.

그 다음 출력 스트림을 얻는다.

PrintWriter out = resp.getWriter();

그리고 HTML 문자열을 직접 써서 응답한다.

out.print("<h1>안녕 서블릿! 반가워~</h1>");

즉 response 객체는 “브라우저로 돌려보낼 문서를 만드는 도구”이다.


12. HTML을 직접 응답으로 보내는 방식

초기 서블릿에서는 HTML까지 자바 코드로 직접 작성하는 경우가 많았다.

out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");

이 방식은 원리는 이해하기 좋지만,
화면이 복잡해질수록 자바 코드 안에 HTML 문자열이 너무 많아져서 관리가 어렵다.

그래서 나중에는:

  • JSP
  • 템플릿 엔진
  • 프론트엔드 프레임워크

등으로 역할을 나누게 된다.

즉 이번 예제는 “응답이 어떻게 만들어지는지 원리”를 이해하는 데 초점이 있다.


13. JSON 요청 처리

chap03_json_handling과 chap03-1_json-test에서는
브라우저가 JSON을 보내고, 서블릿이 JSON으로 응답하는 흐름을 다뤘다.

이 부분은 현대 웹 개발에서 매우 중요하다.
요즘은 서버가 HTML 전체를 직접 만들어서 보내기보다,
JSON 데이터를 보내고 프론트엔드가 화면을 그리는 방식이 훨씬 많기 때문이다.


14. JSON 요청이 들어오는 흐름

예제에서는 브라우저가 fetch()로 JSON을 보낸다.

fetch('/api/memos', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ content })
})

이때 중요한 점은:

  • Content-Type: application/json 헤더를 붙인다
  • body에 JSON 문자열을 담아 보낸다

이 요청은 form 방식과 다르게
getParameter()로 바로 읽는 구조가 아니다.

JSON body는 직접 읽어야 한다.


15. request body 읽기

JsonTestServlet에서는 요청 body를 직접 읽었다.

BufferedReader reader = req.getReader();
StringBuilder sb = new StringBuilder();

String line;
while((line = reader.readLine()) != null) {
    sb.append(line);
}

즉 request body 안에 들어있는 JSON 문자열을 먼저 꺼내야 한다.

예를 들어 브라우저가 보낸 값이:

{
  "id": "user01",
  "nickname": "홍길동",
  "age": 20
}

이라면, 서버는 먼저 이 JSON 문자열을 읽고 난 뒤 자바 객체로 바꿔야 한다.


16. JSON 문자열을 자바 객체로 바꾸기

여기서 Jackson의 ObjectMapper를 사용했다.

ObjectMapper mapper = new ObjectMapper();
UserDTO user = mapper.readValue(sb.toString(), UserDTO.class);

이걸 역직렬화라고 한다.

즉:

  • JSON 문자열
  • -> 자바 객체(UserDTO)

로 바꾸는 작업이다.

이때 UserDTO에는 기본 생성자와 getter/setter가 있어야 한다.

public UserDTO() {
}

이유는 Jackson이 객체를 만들고 필드 값을 넣기 위해서다.


17. 자바 객체를 다시 JSON으로 응답하기

반대로 서버가 자바 객체를 다시 JSON 문자열로 바꿔서 응답할 수도 있다.

resp.setContentType("application/json; charset=UTF-8");
String jsonResponse = mapper.writeValueAsString(user);
out.print(jsonResponse);

이건 직렬화이다.

즉:

  • 자바 객체
  • -> JSON 문자열

로 바꾸는 작업이다.

요청과 응답을 정리하면:

  • 요청: JSON -> 자바 객체
  • 응답: 자바 객체 -> JSON

18. Memo API 흐름

MemoApiServlet은 훨씬 실제 API에 가까운 예제였다.

GET /api/memos

mapper.writeValue(resp.getWriter(), memos);
  • 현재 메모 목록 전체를 JSON 배열로 응답

POST /api/memos

MemoDTO requestMemo = mapper.readValue(req.getReader(), MemoDTO.class);
  • 브라우저가 보낸 JSON을 MemoDTO로 읽음
  • 검증 후 저장
  • 저장된 메모를 다시 JSON으로 반환
resp.setStatus(HttpServletResponse.SC_CREATED);
mapper.writeValue(resp.getWriter(), savedMemo);

이 흐름은 실무 API와 매우 비슷하다.

  1. 클라이언트가 요청
  2. 서버가 body 읽기
  3. DTO로 변환
  4. 검증
  5. 처리
  6. 상태 코드와 JSON 응답 반환

19. 응답 상태 코드

MemoApiServlet에서는 상태 코드도 함께 다뤘다.

resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
resp.setStatus(HttpServletResponse.SC_CREATED);

이건 단순히 데이터만 보내는 게 아니라,
요청이 성공했는지 실패했는지 HTTP 규칙에 맞게 알려주는 것이다.

예:

  • 200 OK : 정상
  • 201 Created : 생성 성공
  • 400 Bad Request : 잘못된 요청

즉 웹 개발에서는 응답 본문뿐 아니라 상태 코드도 매우 중요하다.


20. index.html과 fetch 흐름

chap03-1_json-test/index.html은 프론트엔드가 서블릿 API를 호출하는 예제를 담고 있었다.

목록 조회

const response = await fetch('/api/memos');
const memos = await response.json();

메모 등록

await fetch('/api/memos', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ content })
});

이걸 통해 알 수 있는 점은:

  • 서블릿은 브라우저 폼 전송만 받는 기술이 아니다
  • 자바스크립트의 fetch() 요청도 받을 수 있다
  • 즉 REST API처럼 사용할 수도 있다

이 부분이 현대적인 웹 개발 흐름과 가장 가까운 지점이다.


21. 전체 흐름을 한 번에 정리

이번 범위를 가장 쉽게 정리하면 다음과 같다.

1. 서블릿은 무엇인가

HTTP 요청을 처리하는 자바 클래스이다.

2. 누가 서블릿을 실행하는가

톰캣이 URL 매핑을 보고 서블릿을 찾아 실행한다.

3. 요청은 어떻게 들어오는가

브라우저가 GET/POST 요청을 보내면
톰캣이 request/response 객체를 만들어 서블릿에 전달한다.

4. 서블릿은 무엇을 하는가

  • request에서 데이터 읽기
  • 필요한 처리 수행
  • response에 결과 작성

5. 응답은 무엇이 될 수 있는가

  • HTML
  • JSON
  • 상태 코드 포함 응답

즉 서블릿은 “웹 요청을 자바 객체로 받고, 자바 코드로 처리한 뒤, 다시 웹 응답으로 돌려주는 기술”이다.


22. 이번 범위에서 특히 중요한 포인트

22.1 생명주기

서블릿은 요청마다 새로 생성되는 게 아니라
톰캣이 만들어 두고 재사용한다.

22.2 request / response

웹 개발은 결국 요청을 읽고 응답을 만드는 작업이다.

22.3 GET / POST 구분

요청 방식에 따라 실행되는 메서드가 달라진다.

22.4 JSON 처리

현대 웹 개발에서는 서블릿도 JSON API 서버처럼 사용될 수 있다.

22.5 DTO 필요성

JSON이나 요청 데이터를 자바 객체로 받으면 훨씬 다루기 쉬워진다.


23. 핵심 정리

  1. 서블릿은 HTTP 요청을 처리하는 자바 클래스이다.
  2. HttpServlet을 상속받아 doGet(), doPost() 같은 메서드로 요청을 처리한다.
  3. 톰캣이 요청을 받아 URL에 맞는 서블릿을 찾아 실행한다.
  4. 서블릿은 생성자, init(), service(), destroy() 생명주기를 가진다.
  5. 톰캣은 서블릿 객체를 재사용하므로 인스턴스 변수 사용에 주의해야 한다.
  6. HttpServletRequest는 클라이언트가 보낸 데이터와 요청 정보를 담고 있다.
  7. HttpServletResponse는 서버가 브라우저에 돌려줄 응답을 만드는 객체이다.
  8. getParameter()는 요청 파라미터를 문자열로 읽을 때 사용한다.
  9. JSON 요청은 req.getReader()로 body를 읽어야 한다.
  10. Jackson ObjectMapper를 사용하면 JSON과 자바 객체를 서로 변환할 수 있다.
  11. 서블릿은 HTML을 직접 응답할 수도 있고, JSON API처럼 동작할 수도 있다.
  12. fetch()와 서블릿을 연결하면 프론트엔드-백엔드 API 흐름을 실습할 수 있다.