1~7은 축이 없고 난잡하다. 지금 글들은 전부 "이 클래스가 뭐냐"는 사전(dictionary) 방식이다. 이걸 "요청 하나가 태어나서 죽을 때까지" 라는 서사로 바꾸면 흐름이 생긴다. 축을 정하면 새 글을 쓸 때마다 "이건 요청 생애의 몇 번째 구간이지?"로 자리를 찾을 수 있다.
구간을 이렇게 끊는 걸 추천한다.
브라우저 폼 → HTTP 메시지 → 톰캣 → 필터 → 서블릿
→ Service(트랜잭션 경계) → DAO → JDBCTemplate → DB
→ 결과 반환 → forward/redirect → JSP 렌더링 → 응답
그리고 모든 글에 공통 마무리 섹션을 하나 고정으로 넣자. "Spring에서는 이걸 누가 대신 해줬는가". 이미 Spring Boot를 아는 상태라는 게 이 시리즈의 최대 무기인데, 지금은 그게 글마다 산발적으로만 나온다. 고정 섹션으로 만들면 시리즈 전체가 "Spring 역공학 노트"라는 정체성을 갖게 된다.
여기가 약한데 실제로 시리즈에 거의 없다. 이 구간의 핵심 질문은 하나다. "내가 JSP에 쓴 <input name="userId">가 어떻게 req.getParameter("userId")가 되는가."
1-1. form 태그 하나가 만드는 HTTP 메시지
개발자도구 Network 탭에서 회원가입 요청을 캡처해서 Raw로 알아보자 . Content-Type: application/x-www-form-urlencoded, body가 userId=abc&userPw=1234인 걸 눈으로 보여주는 게 이 글의 전부다. method="get"으로 바꾸면 같은 데이터가 쿼리스트링으로 이동하는 것까지 비교.
1-2. 톰캣은 그 문자열을 어떻게 Map으로 만드는가
getParameter가 왜 항상 String인지, getParameterValues는 언제 쓰는지, 체크박스 미선택 시 왜 null이 오는지. 여기서 Integer.parseInt(req.getParameter("memoNo"))가 왜 위험한지 자연스럽게 연결된다. 지금 DetailServlet이 딱 그 코드다.
1-3. 한글은 어디서 깨지는가
현재 코드에 req.setCharacterEncoding("UTF-8")이 한 군데도 없다. GET은 톰캣 URIEncoding, POST는 body 인코딩으로 처리 주체가 다르다는 걸 짚고, 이걸 매 서블릿마다 쓰지 않으려면 어디에 둬야 하는지로 Filter 도입 글을 자연스럽게 잇자. Spring의 CharacterEncodingFilter가 정확히 이 물건이라는 걸 마지막에 붙이면 깔끔하다.
1-4. form 대신 fetch로 보내면 무엇이 달라지는가
메모 작성을 AJAX로 바꿔보는 실습편. Content-Type: application/json이 되는 순간 getParameter()가 null을 반환하는 이유(톰캣은 urlencoded body만 파싱한다)가 이 글의 핵심이다. req.getReader()로 직접 읽고 Gson으로 파싱하는 걸 손으로 해보면, Spring의 @RequestBody가 뭘 대신해줬는지 체감된다. 이 글이 시리즈에서 제일 값어치 있을 가능성이 높다.
1-5. forward와 redirect, 그리고 사라진 환영 메시지
현재 코드에 실제 버그가 있어서 글감으로 딱 좋다. SignUpServlet은 가입 성공 시 session.setAttribute("signUpMessage", ...) 후 /signin으로 리다이렉트하는데, SignInServlet의 doGet은 첫 줄에서 session.invalidate()를 호출한다. 환영 메시지는 화면에 뜰 수가 없다. 이걸 소재로 PRG 패턴과 "1회성 메시지는 어디에 담아야 하는가"(Spring의 RedirectAttributes.addFlashAttribute)를 쓰면 된다.
덤으로, 지금 유효성 검사 실패 메시지들(PWEqualsError, spaceError, REGError)이 전부 session에 들어가는데 아무도 지우지 않는다. forward라서 request로 충분한 데이터가 session에 쌓여서 다음 요청에도 살아있다. 이미 쓴 "스코프는 수명인가?" 글의 후속편으로 붙이기 좋다.
2-1. 서블릿은 몇 개 만들어지는가
싱글톤이고 요청마다 스레드가 다르다는 것. 그래서 인스턴스 필드를 두면 안 된다는 것. 현재 코드는 필드가 없어서 안전한데, JDBCTemplate에 static Connection conn이 있다면 그건 안전하지 않다. 본인 코드 확인해보고 있으면 이게 3단계의 킬러 소재가 된다.
2-2. new MemoServiceImpl()을 요청마다 하고 있다
CreateServlet, SignInServlet, SignUpServlet, DetailServlet 전부 매 요청마다 new 한다. 이걸 static final 하나로 바꿔보고, 그러면 왜 Service가 상태를 가지면 안 되는지, 그게 Spring의 싱글톤 빈과 DI로 어떻게 이어지는지. 시리즈 후반 Spring 연결의 포석이다.
개념이 약한 부분인데, 사실 이 구간이 JSP/Servlet 프로젝트에서 배울 게 가장 많은 곳이다. 순서는 이렇다.
3-1. Connection 한 개의 생애
DriverManager.getConnection()이 실제로 무슨 일을 하는지(TCP 연결, 인증, 세션 생성). 그리고 이게 비싸다는 걸 숫자로 보여주자. 반복문으로 100번 열고 닫는 데 걸리는 시간을 측정해서 찍으면 다음 글의 동기가 만들어진다.
3-2. JDBCTemplate은 무엇을 감췄나
본인 프로젝트의 JDBCTemplate 코드를 한 줄씩 뜯어라. getConnection()이 어디서 driver/url/user/password를 읽는지(properties든 xml이든), close() 오버로딩이 왜 Connection/Statement/ResultSet 별로 나뉘어 있는지, 왜 setAutoCommit(false)를 하는지도 다루자.
3-3. 트랜잭션 경계는 왜 Service에 있는가
DAO가 아니라 Service가 commit/rollback을 호출하는 이유. "메모 등록 + 첨부파일 등록"처럼 두 DAO 호출이 한 단위여야 하는 상황을 예시로 만들면 설명이 쉽다. 그리고 이게 @Transactional 한 줄로 압축된 거라는 마무리.
3-4. 예외가 터지면 Connection은 어떻게 되는가
현재 서블릿들이 전부 catch (Exception e) { e.printStackTrace(); }로 끝난다. Service에서 예외가 나면 rollback은 되는지, close는 되는지, 그리고 사용자에게는 200 OK와 빈 화면이 가는 문제. finally 블록과 try-with-resources 비교까지. 실무에서 제일 자주 지적받는 패턴이라 값어치 있다.
3-5. 커넥션 풀로 갈아끼우기 (실습편)
JDBCTemplate.getConnection() 내부만 DBCP DataSource로 교체해보는 것. 나머지 코드는 한 줄도 안 바뀐다는 게 포인트다. 3-1에서 측정한 시간과 비교해서 그래프 하나 넣으면 글이 산다. 여기서 close()가 이제 "진짜 닫기"가 아니라 "풀에 반납"으로 의미가 바뀐다는 것도 짚자.
3-6. ResultSet을 DTO로 옮기는 코드는 왜 항상 똑같은가
DAO에 반복되는 while(rs.next()) 매핑 코드를 보고, 이걸 자동화하면 뭐가 되는지. 이미 쓴 #{} vs ${} 글과 "DAO와 sql.xml" 글이 여기서 합류한다. MyBatis가 왜 필요했는지가 이 지점에서 완성된다.
4-1. JSP는 결국 서블릿이다
톰캣 work 디렉토리에서 signUp_jsp.java를 직접 열어서 보여주자. <% %>가 그대로 _jspService() 안에 들어가 있고 HTML은 out.write()가 된 걸 눈으로 보면 EL/JSTL을 왜 쓰는지가 설명 없이 이해된다.
4-2. 메모 내용에 <script>를 넣어보기
현재 detail.jsp가 content를 EL로 그냥 출력하고 있다면 XSS가 열려 있다. <c:out>과 fn:escapeXml()의 차이, 그리고 Spring/Thymeleaf가 왜 기본 이스케이프인지.
지금 코드에 소재가 이미 다 있다.
5-1. 남의 메모를 볼 수 있다
DetailServlet은 로그인 여부만 확인하고 service.memoDetail(memoNo)를 호출한다. memoNo가 내 것인지 검사하지 않는다. URL의 memoNo만 바꾸면 남의 메모가 열린다. IDOR이라는 이름이 붙은 취약점이고, 수정 방향은 memoDetail(memoNo, userNo)로 SQL WHERE에 소유자를 넣는 것. 이건 단독 글로 충분하다.
5-2. 비밀번호가 평문으로 저장돼 있다
memoLogin(userId, userPw)가 비밀번호로 WHERE를 걸고, memoCheck()가 자바에서 다시 비교하는 구조라 DB에 평문이 있다는 뜻이다. 해시가 왜 필요한지, 왜 SHA-256이 아니라 BCrypt인지(salt와 의도적 느림), 그리고 해시를 도입하면 로그인 쿼리 구조 자체가 바뀐다는 것(id로 조회 → 자바에서 검증). 지금 코드의 memoCheck()가 그 형태의 전조라는 게 재밌는 포인트다.
5-3. 중복 아이디 검사와 동시성
userSelect()로 확인하고 signUp()으로 삽입하는 사이에 다른 요청이 끼어들면? 애플리케이션 검증은 UX용이고 최종 방어선은 DB UNIQUE 제약이라는 결론. 짧고 강한 글이 나온다.
1-4(fetch로 보내면 getParameter()가 null이 되는 이유) 를 먼저 다룬다. 약한 파트인 프론트→백엔드 구간의 정중앙이고, "폼 전송과 AJAX는 뭐가 다른가"는 검색량도 많은데 제대로 설명한 글이 드물다. 그리고 이 글을 쓰고 나면 나머지 1단계 글들의 자리가 저절로 정해진다.
정리하면 1단계(5편) → 3단계(6편) → 2단계(2편) → 4단계(2편) → 5단계(3편) 순서를 권한다. 2단계를 뒤로 미룬 건, 앞뒤 구간을 알고 나서 봐야 "그래서 컨트롤러는 뭘 하면 안 되는가"가 설득력이 생기기 때문이다.