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()
여기서 하는 일:
- db.properties 읽기
- 드라이버 클래스 로딩
- 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
흐름:
- PreparedStatement 생성
- 쿼리 실행
- ResultSet을 한 줄씩 읽음
- 각 줄을 MemoDTO로 변환
- 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를 다시 읽을 수 있다.
흐름:
- 메모 내용 INSERT
- 생성된 PK 읽기
- 그 PK로 다시 SELECT
- 방금 저장된 메모 전체를 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(...)
흐름:
- 응답 타입을 application/json으로 설정
- memoService.findAllMemos() 호출
- 목록을 JSON으로 응답
List<MemoDTO> memos = memoService.findAllMemos();
mapper.writeValue(resp.getWriter(), memos);
즉 GET은 “메모 목록 조회 API” 역할이다.
11.2 POST 요청 처리
protected void doPost(...)
흐름:
- JSON body를 MemoDTO로 읽음
- 내용이 비었는지 검사
- 서비스에 저장 요청
- 저장된 메모를 JSON으로 응답
- 상태코드를 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('');
}
즉 사용자가 등록 버튼을 누르면:
- 빈 값 검사
- 서버에 메모 등록 요청
- 성공하면 입력창 비우기
- 상태가 갱신되면서 화면도 다시 반영
이 된다.
16. 화면에서 실제로 어떤 순서로 일이 일어나는가
이 부분을 한 번 전체로 그려보는 게 중요하다.
메모 목록 조회
- 페이지가 열림
- useEffect() 실행
- fetchMemos() 호출
- memoApi.getMemos() 실행
- 브라우저가 GET /api/memos 요청 보냄
- MemoApiServlet.doGet() 실행
- MemoService.findAllMemos() 호출
- MemoDAO.selectAllMemos() 실행
- JDBC로 DB 조회
- DB 결과를 List<MemoDTO>로 변환
- 서블릿이 JSON 응답
- 프론트엔드가 JSON을 상태에 저장
- 화면에 목록 렌더링
메모 등록
- 사용자가 입력 후 등록 버튼 클릭
- handleSubmit() 실행
- addMemo(content) 호출
- memoApi.createMemo(content) 실행
- 브라우저가 POST /api/memos 요청 보냄
- 요청 body에 JSON 포함
- MemoApiServlet.doPost() 실행
- JSON을 MemoDTO로 역직렬화
- 빈 값 검증
- MemoService.registMemo(content) 호출
- MemoDAO.insertMemo() 실행
- DB에 INSERT
- 생성된 PK 읽기
- 방금 저장된 행 다시 조회
- 저장된 메모를 JSON으로 응답
- 프론트엔드가 응답을 상태에 추가
- 화면에 새 메모 반영
이 전체 흐름을 머릿속에 그릴 수 있으면 이번 범위 이해가 훨씬 쉬워진다.
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. 핵심 정리
- 이번 프로젝트는 Servlet API에 JDBC를 연결해 실제 DB 저장형 메모 API를 만든 구조이다.
- 프론트엔드는 fetch()로 /api/memos에 GET, POST 요청을 보낸다.
- 서블릿은 브라우저의 요청을 받아 JSON을 DTO로 변환하고 응답을 다시 JSON으로 만든다.
- MemoService는 요청 처리 흐름을 조정하고 Connection을 관리하는 역할을 한다.
- MemoDAO는 실제 SQL 실행과 DB 접근을 담당한다.
- MemoDTO는 요청 데이터, 응답 데이터, DB 조회 결과를 담는 객체이다.
- 메모 등록 시 RETURN_GENERATED_KEYS를 사용하면 DB가 생성한 기본 키를 읽을 수 있다.
- 저장 후 다시 조회해서 응답하면 자동 생성된 id, createdAt까지 포함한 완성된 데이터를 돌려줄 수 있다.
- 프론트엔드는 Zustand로 메모 목록, 로딩 상태, 에러 상태를 관리한다.
- 이번 범위의 핵심은 개별 기술보다 “브라우저 요청이 DB 저장까지 어떻게 흐르는지”를 이해하는 것이다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Spring Core: DI, Spring Container, Bean, 계층 구조, @Autowired와 컴포넌트 스캔 (0) | 2026.05.26 |
|---|---|
| [TIL]쿠키, 세션, 인증/인가, 그리고 Front Controller 흐름 (0) | 2026.05.22 |
| [TIL]Servlet 기초부터 요청/응답, 파라미터 처리, JSON API까지 (0) | 2026.05.20 |
| [TIL]JDBC 연결, Statement/PreparedStatement, SQL Injection, CRUD와 DAO 구조 (0) | 2026.05.19 |
| [TIL]JDBC 기초: 연결, Statement, PreparedStatement, SQL Injection, CRUD 흐름 (0) | 2026.05.18 |