TIL/[TIL]

[TIL]Servlet JSON API에서 JDBC 연동까지: 프론트엔드, 서블릿, 서비스, DAO, DB 흐름 이해하기

namerong 2026. 5. 21. 15:50

1. 학습 주제

기존의 “서블릿이 JSON을 주고받는 예제”에서 한 단계 더 나아가서
프론트엔드(Next.js) - Servlet API - Service - DAO - JDBC - MySQL 까지를 하나의 흐름으로 연결해 보는 것이었다.

즉 이번 학습은 단순히 서블릿 문법을 더 배우는 것이 아니라,
실제로 웹 애플리케이션이 어떤 계층으로 나뉘고 어떤 순서로 데이터가 흐르는지를 이해하는 쪽에 더 가까웠다.

학습한 흐름은 대략 다음과 같다.

  • 프론트엔드가 fetch()로 API 요청을 보냄
  • 서블릿이 HTTP 요청을 받음
  • JSON body를 DTO로 변환함
  • Service가 비즈니스 흐름을 담당함
  • DAO가 DB에 접근함
  • JDBC로 MySQL에 SQL을 실행함
  • DB 결과를 DTO로 다시 만들고
  • 서블릿이 JSON 응답으로 돌려줌
  • 프론트엔드가 그 JSON을 받아 화면에 다시 반영함

이번 범위가 어렵게 느껴지는 가장 큰 이유는,
코드가 “한 파일에서 끝나는” 구조가 아니라 여러 계층으로 나뉘기 때문이다.
그래서 각 파일을 따로 외우기보다 요청이 어디서 시작해서 어디로 흘러가는지를 먼저 이해하는 게 중요했다.


2. 이번 프로젝트를 한 문장으로 설명하면

이번 프로젝트는:

메모 목록을 조회하고 메모를 등록할 수 있는 작은 웹 애플리케이션을,
Servlet + JDBC + MySQL + Next.js 구조로 구현한 것

이라고 볼 수 있다.

즉 이전에는 메모를 서버 메모리의 static List에 저장했다면,
이번에는 그 메모를 실제 데이터베이스 테이블 tbl_memo에 저장하도록 바뀌었다.

이 변화가 중요한 이유는:

  • 서버가 꺼져도 데이터가 남고
  • 여러 요청이 와도 같은 DB를 바라보며
  • 실제 서비스 구조에 더 가까워졌기 때문이다

3. 전체 구조부터 보기

이번 프로젝트를 계층으로 나누면 이렇게 볼 수 있다.

프론트엔드

  • frontend/src/app/page.js
  • frontend/src/api/memoApi.js
  • frontend/src/store/memoStore.js

역할:

  • 사용자 화면 보여주기
  • 버튼 클릭, 입력 처리
  • 서버에 API 요청 보내기
  • 받은 응답을 화면에 반영하기

서블릿 API 계층

  • MemoApiServlet.java

역할:

  • 브라우저의 HTTP 요청 받기
  • JSON 요청 읽기
  • 응답 형식과 상태 코드 결정하기

서비스 계층

  • MemoService.java

역할:

  • 요청 처리 흐름 조정
  • DAO 호출
  • Connection 생성/반납 흐름 연결

DAO 계층

  • MemoDAO.java

역할:

  • SQL 작성 및 실행
  • DB 결과를 DTO로 변환
  • 실제 DB 접근 담당

공통 JDBC 유틸

  • JDBCTemplate.java
  • db.properties

역할:

  • DB 연결 생성
  • 자원 닫기
  • 설정 파일 읽기

DB

  • memo.sql

역할:

  • 실제 테이블 구조와 초기 데이터 준비

이 구조를 이해하면, 나중에 Spring을 배울 때도 훨씬 덜 낯설다.
왜냐하면 Spring도 결국 비슷하게 Controller - Service - Repository(DAO) - DB 흐름을 가지기 때문이다.


4. 데이터베이스 준비

먼저 memo.sql 파일에서 메모 테이블을 만들었다.

CREATE TABLE tbl_memo (
    memo_id INT AUTO_INCREMENT PRIMARY KEY,
    content VARCHAR(255) NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

이 테이블 구조를 보면:

  • memo_id : 메모 번호, 자동 증가, 기본 키
  • content : 메모 내용
  • created_at : 메모 생성 시간, 자동 현재 시간 입력

즉 메모를 등록하면:

  • 번호는 DB가 자동 생성하고
  • 시간도 DB가 자동 저장한다

이 부분은 실무에서도 자주 쓰는 방식이다.
애플리케이션이 직접 모든 값을 만들기보다,
DB가 잘하는 일은 DB에 맡기는 구조다.


5. DB 연결 설정

DB 연결 정보는 db.properties에 분리되어 있다.

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/memo_db
user=ohgiraffers
password=ohgiraffers

이렇게 분리하는 이유는:

  • 코드에 접속 정보를 하드코딩하지 않기 위해
  • 나중에 DB 주소나 계정이 바뀌어도 설정 파일만 바꾸면 되게 하려고
  • 유지보수성을 높이기 위해

실무에서는 이런 방식이 거의 기본이다.


6. JDBCTemplate의 역할

JDBCTemplate.java는 JDBC에서 반복되는 공통 작업을 모아둔 클래스다.

주요 역할은 두 가지다.

6.1 Connection 만들기

public static Connection getConnection()

여기서 하는 일:

  1. db.properties 읽기
  2. 드라이버 클래스 로딩
  3. DriverManager.getConnection()으로 연결 생성

6.2 자원 닫기

close(Connection con)
close(Statement stmt)
close(ResultSet rset)

JDBC는 사용한 자원을 반드시 닫아야 한다.
그래서 닫는 로직을 공통 메서드로 분리해둔 것이다.

즉 JDBCTemplate는
“DB 연결 만들기와 자원 반납을 도와주는 공통 도구”라고 보면 된다.


7. MemoDTO는 무엇인가

MemoDTO.java는 메모 한 건의 데이터를 담는 객체다.

private int id;
private String content;
private String createdAt;

역할:

  • DB에서 읽어온 한 행을 자바 객체로 담기
  • JSON 요청을 자바 객체로 받기
  • 응답용 데이터를 자바 객체로 정리하기

즉 MemoDTO는 메모 데이터의 이동용 상자다.

이런 DTO가 필요한 이유는:

  • 데이터를 한 덩어리로 다루기 쉽고
  • 계층 간 전달이 편하고
  • JSON <-> Java 객체 변환에도 잘 맞기 때문이다

8. MemoDAO의 역할

MemoDAO는 실제 DB 접근을 담당한다.

즉:

  • SQL을 작성하고
  • PreparedStatement를 만들고
  • ResultSet을 읽고
  • DTO를 만들어 반환한다

이번 예제에서 핵심 메서드는 두 개였다.

8.1 전체 메모 조회

public List<MemoDTO> selectAllMemos(Connection con)

SQL:

SELECT memo_id, content, created_at
FROM tbl_memo
ORDER BY memo_id DESC

흐름:

  1. PreparedStatement 생성
  2. 쿼리 실행
  3. ResultSet을 한 줄씩 읽음
  4. 각 줄을 MemoDTO로 변환
  5. List<MemoDTO>에 담아 반환

즉 DB의 여러 행을 자바의 리스트로 바꾸는 역할이다.

8.2 메모 등록

public MemoDTO insertMemo(Connection con, String content)

SQL:

INSERT INTO tbl_memo (content) VALUES (?)

여기서 중요한 포인트는 RETURN_GENERATED_KEYS이다.

pstmt = con.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);

이 옵션을 주면 INSERT 후 자동 생성된 memo_id를 다시 읽을 수 있다.

흐름:

  1. 메모 내용 INSERT
  2. 생성된 PK 읽기
  3. 그 PK로 다시 SELECT
  4. 방금 저장된 메모 전체를 DTO로 반환

즉 “저장만 끝내는 것”이 아니라,
저장된 결과를 다시 조회해서 완성된 메모 객체로 돌려주는 구조다.

이 방식이 좋은 이유는:

  • DB가 자동 생성한 ID를 정확히 받을 수 있고
  • created_at 같은 DB 자동값도 함께 포함해서 응답할 수 있기 때문이다

9. created_at을 문자열로 바꾸는 이유

DAO 안에는 formatTimestamp()도 있었다.

private String formatTimestamp(Timestamp timestamp)

그리고 여기서:

timestamp.toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

형태로 바꾸고 있다.

왜 이렇게 하냐면:

  • DB에서는 Timestamp
  • 프론트엔드에서는 보통 보기 쉬운 문자열

이 더 편하기 때문이다.

즉 DB 타입을 그대로 넘기기보다,
프론트엔드가 쓰기 좋은 형태로 한번 가공해서 주는 작업이라고 보면 된다.


10. MemoService의 역할

MemoService는 아주 짧지만 계층 구조에서 중요한 의미를 가진다.

public List<MemoDTO> findAllMemos()
public MemoDTO registMemo(String content)

Service는 지금 단계에서는 DAO 호출만 하는 것처럼 보일 수 있다.
그래서 “이게 왜 필요하지?” 싶을 수 있다.

하지만 역할을 잘 보면:

  • Connection을 열고
  • DAO를 호출하고
  • finally에서 Connection을 닫는다

즉 Service는:

  • 요청 처리 흐름 조정
  • 여러 DAO 호출 조합
  • 트랜잭션 처리 위치
  • 비즈니스 로직 위치

가 되는 계층이다.

지금은 단순하지만, 나중에 기능이 커지면 Service 계층의 의미가 훨씬 커진다.


11. MemoApiServlet의 역할

이 클래스는 브라우저와 직접 만나는 “입구”이다.

@WebServlet("/api/memos")
public class MemoApiServlet extends HttpServlet

즉 브라우저가 /api/memos로 요청하면 이 서블릿이 실행된다.

11.1 GET 요청 처리

protected void doGet(...)

흐름:

  1. 응답 타입을 application/json으로 설정
  2. memoService.findAllMemos() 호출
  3. 목록을 JSON으로 응답
List<MemoDTO> memos = memoService.findAllMemos();
mapper.writeValue(resp.getWriter(), memos);

즉 GET은 “메모 목록 조회 API” 역할이다.

11.2 POST 요청 처리

protected void doPost(...)

흐름:

  1. JSON body를 MemoDTO로 읽음
  2. 내용이 비었는지 검사
  3. 서비스에 저장 요청
  4. 저장된 메모를 JSON으로 응답
  5. 상태코드를 201 Created로 설정
MemoDTO requestMemo = mapper.readValue(req.getReader(), MemoDTO.class);

이 부분은 브라우저가 보낸 JSON 문자열을 자바 객체로 바꾸는 과정이다.

즉 서블릿은:

  • 요청을 받고
  • 입력 검증하고
  • Service 호출하고
  • 응답을 만들어 반환하는 입구

역할을 한다.


12. ErrorResponse가 필요한 이유

에러 응답도 객체로 만들었다.

new ErrorResponse("content is required")

이걸 JSON으로 응답하면:

{ "message": "content is required" }

처럼 전달된다.

이게 중요한 이유는,
에러도 그냥 문자열 한 줄로 보내기보다
일정한 JSON 구조로 보내야 프론트엔드가 다루기 쉽기 때문이다.

즉 성공 응답뿐 아니라 실패 응답도 “규칙 있는 형식”으로 보내는 습관이 중요하다.


13. 프론트엔드에서 API를 호출하는 흐름

이제 브라우저 쪽을 보면 memoApi.js가 API 호출 전담 역할을 한다.

목록 조회

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

메모 등록

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

즉 프론트엔드는:

  • 메모 목록이 필요하면 GET 요청
  • 메모 등록이 필요하면 POST 요청
  • 응답은 JSON으로 받음

이 흐름으로 서버와 통신한다.


14. Zustand store가 하는 일

memoStore.js는 상태 관리 역할을 한다.

상태:

  • memos
  • loading
  • error

액션:

  • fetchMemos
  • addMemo

즉 역할은 다음과 같다.

fetchMemos

  • 로딩 시작
  • 서버에서 메모 목록 가져옴
  • 성공하면 상태에 저장
  • 실패하면 에러 상태 저장

addMemo

  • 서버에 새 메모 등록 요청
  • 성공하면 기존 배열 뒤에 새 메모 추가
  • 실패하면 에러 상태 저장

이렇게 하면 React 컴포넌트는 API 호출 상세 구현보다
“화면 표시”에 더 집중할 수 있다.


15. page.js가 하는 일

page.js는 실제 화면 컴포넌트이다.

여기서 중요한 흐름은 세 가지다.

15.1 첫 렌더링 시 목록 조회

useEffect(() => {
    fetchMemos();
}, [fetchMemos]);

즉 페이지가 열리면 자동으로 메모 목록을 서버에서 받아온다.

15.2 입력값 관리

const [content, setContent] = useState('');

사용자가 입력한 메모 내용을 관리한다.

15.3 등록 버튼 처리

const handleSubmit = async (e) => {
    e.preventDefault();
    if(!content.trim()) return;
    await addMemo(content);
    setContent('');
}

즉 사용자가 등록 버튼을 누르면:

  1. 빈 값 검사
  2. 서버에 메모 등록 요청
  3. 성공하면 입력창 비우기
  4. 상태가 갱신되면서 화면도 다시 반영

이 된다.


16. 화면에서 실제로 어떤 순서로 일이 일어나는가

이 부분을 한 번 전체로 그려보는 게 중요하다.

메모 목록 조회

  1. 페이지가 열림
  2. useEffect() 실행
  3. fetchMemos() 호출
  4. memoApi.getMemos() 실행
  5. 브라우저가 GET /api/memos 요청 보냄
  6. MemoApiServlet.doGet() 실행
  7. MemoService.findAllMemos() 호출
  8. MemoDAO.selectAllMemos() 실행
  9. JDBC로 DB 조회
  10. DB 결과를 List<MemoDTO>로 변환
  11. 서블릿이 JSON 응답
  12. 프론트엔드가 JSON을 상태에 저장
  13. 화면에 목록 렌더링

메모 등록

  1. 사용자가 입력 후 등록 버튼 클릭
  2. handleSubmit() 실행
  3. addMemo(content) 호출
  4. memoApi.createMemo(content) 실행
  5. 브라우저가 POST /api/memos 요청 보냄
  6. 요청 body에 JSON 포함
  7. MemoApiServlet.doPost() 실행
  8. JSON을 MemoDTO로 역직렬화
  9. 빈 값 검증
  10. MemoService.registMemo(content) 호출
  11. MemoDAO.insertMemo() 실행
  12. DB에 INSERT
  13. 생성된 PK 읽기
  14. 방금 저장된 행 다시 조회
  15. 저장된 메모를 JSON으로 응답
  16. 프론트엔드가 응답을 상태에 추가
  17. 화면에 새 메모 반영

이 전체 흐름을 머릿속에 그릴 수 있으면 이번 범위 이해가 훨씬 쉬워진다.


17. 왜 Servlet + JDBC + Next.js를 같이 붙여보는가

이번 학습이 중요한 이유는,
기술을 따로따로 배우는 게 아니라 연결해서 보기 시작했기 때문이다.

Servlet만 보면

  • 요청 받고 응답하는 자바 서버 코드

JDBC만 보면

  • DB에 접속해서 SQL 실행하는 자바 코드

Next.js만 보면

  • 화면 그리고 fetch로 API 부르는 프론트엔드

그런데 실제 웹 서비스는 이 셋이 따로 존재하지 않는다.
항상 연결되어 같이 움직인다.

즉 이번 학습은:

  • 프론트엔드가 API를 호출하고
  • 백엔드가 JSON을 읽고
  • DB에 저장한 뒤
  • 다시 JSON으로 돌려주는

실제 웹 개발의 최소 단위를 직접 연결해 본 것에 의미가 있다.


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

18.1 Servlet은 API 서버처럼도 사용할 수 있다

예전처럼 HTML만 직접 만들어 보내는 게 아니라,
JSON을 주고받는 API 역할도 충분히 할 수 있다.

18.2 JSON <-> DTO 변환이 핵심이다

브라우저는 JSON을 보내고 받고,
자바는 DTO 객체로 처리한다.

18.3 Service와 DAO는 역할이 다르다

  • Service: 흐름 조정
  • DAO: DB 접근 전담

18.4 JDBC는 DAO 안에 숨기는 게 좋다

서블릿이 바로 JDBC 코드를 다루면 코드가 너무 지저분해진다.
그래서 DAO 계층으로 분리하는 구조가 중요하다.

18.5 프론트엔드는 상태를 관리해야 한다

요청만 보내는 게 아니라, 결과를 메모 배열 상태로 저장하고 화면에 반영해야 한다.


19. 학습 흐름 정리

백엔드

  • db.properties로 DB 설정 분리
  • JDBCTemplate로 연결/닫기 공통 처리
  • MemoDAO에서 SQL 실행
  • MemoService에서 흐름 조정
  • MemoApiServlet에서 HTTP 요청/응답 처리

프론트엔드

  • memoApi.js에서 fetch 요청 분리
  • memoStore.js에서 상태 관리
  • page.js에서 사용자 입력과 화면 렌더링 처리

데이터 흐름

  • 브라우저 → JSON 요청 → Servlet
  • Servlet → Service → DAO → JDBC → DB
  • DB 결과 → DAO → Service → Servlet → JSON 응답 → 브라우저

20. 핵심 정리

  1. 이번 프로젝트는 Servlet API에 JDBC를 연결해 실제 DB 저장형 메모 API를 만든 구조이다.
  2. 프론트엔드는 fetch()로 /api/memos에 GET, POST 요청을 보낸다.
  3. 서블릿은 브라우저의 요청을 받아 JSON을 DTO로 변환하고 응답을 다시 JSON으로 만든다.
  4. MemoService는 요청 처리 흐름을 조정하고 Connection을 관리하는 역할을 한다.
  5. MemoDAO는 실제 SQL 실행과 DB 접근을 담당한다.
  6. MemoDTO는 요청 데이터, 응답 데이터, DB 조회 결과를 담는 객체이다.
  7. 메모 등록 시 RETURN_GENERATED_KEYS를 사용하면 DB가 생성한 기본 키를 읽을 수 있다.
  8. 저장 후 다시 조회해서 응답하면 자동 생성된 id, createdAt까지 포함한 완성된 데이터를 돌려줄 수 있다.
  9. 프론트엔드는 Zustand로 메모 목록, 로딩 상태, 에러 상태를 관리한다.
  10. 이번 범위의 핵심은 개별 기술보다 “브라우저 요청이 DB 저장까지 어떻게 흐르는지”를 이해하는 것이다.