[ 구현 목표 ]
지난 포스팅에서는 게시글 상세보기 페이지 이동을 위한
쿠키 검사, 조회수 업데이트 등의 로직을 구현했다.
이번엔 비동기 서블릿을 통해 게시글 상세 페이지에서
댓글을 다시 로드하지 않고(새로고침/페이지 이동 없이)
정렬 기준만 바꿔 댓글 목록을 재출력하는
비동기 기능을 구현해보려고 한다.
▼ 전체 흐름 요약
클라이언트는 boardId와 condition(정렬조건)을 전달한다.
↓
서버(서블릿)는 파라미터를 검증한다.
↓
DAO로 댓글 목록을 조회한다.
↓
결과를 JSON으로 응답한다.
↓
프론트는 JSON을 받아 댓글 영역만 다시 렌더링한다.
[ 요청/응답 설계 ]
페이지에서 넘어오는 요청 URL은 아래와 같다.
GET /ReplyListOrder?boardId=15&condition=REPLY_LIST_RECENT
GET /ReplyListOrder?boardId=15&condition=REPLY_LIST_OLDEST
게시글 상세보기 페이지에서 댓글 정렬 기준에
DAO 메서드 구분을 위한 컨디션 값을 담아서 넘겨주기 때문에
위와 같은 URL이 넘어오게 된다. (=RECENT / OLDEST)
| 파라미터 | 타입 | 필수 | 설명 |
| boardId | int | O | 게시글 PK |
| condition | String | X | 정렬 기준 |
응답은 JSON으로 진행된다.
- 성공: ReplyDTO 리스트(JSON 배열)
- 실패: { success:false, message:"..." } 형태로 반환 + HTTP 400
▼ 기능 수행을 위한 로직 순서
[상세 페이지]
└─ (정렬 버튼 클릭)
└─ fetch("/ReplyListOrder?boardId=...&condition=...")
└─ ReplyListOrder 서블릿
├─ boardId 검증
├─ condition 화이트리스트 적용(Recent/Oldest 외 default)
├─ ReplyDAO.selectAll(...)
└─ JSON(list) 응답
└─ (프론트) JSON 받아 댓글 DOM 재생성
[ 핵심 설계 포인트 ]
설계하면서 반드시 체크해야하는
중요한 설계 포인트이다.
1) JSON 응답 세팅
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=UTF-8");
브라우저가 JSON을 UTF-8로 해석하도록 명확히 지정하는 부분이다.
2) boardId 검증 3단계(널/빈값 → 숫자 파싱 → 범위)
이 부분이 이 서블릿의 퀄리티를 올려주는 핵심이다.
- 1단계: null/빈 문자열 차단
- 2단계: 숫자 파싱 실패 차단
- 3단계: 0 이하 차단(실제 PK로 쓰기 어려움)
// 검증 1단계 > null/빈값 확인
if (boardIdCheck == null || boardIdCheck.isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "boardId가 비었습니다.");
response.getWriter().print(gson.toJson(error));
return;
}
// 검증 2단계 > 숫자 변환
int boardId;
try {
// 웹 파라미터는 String으로 넘어오므로 int로 변환(파싱)
boardId = Integer.parseInt(boardIdCheck);
} catch (NumberFormatException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
// 검증 3단계 > 숫자 범위 검증(0 이하 확인)
if (boardId <= 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
검증에서 실패하면 즉시 400 Bad Request + JSON 에러를 반환한다.
“이상한 입력은 DAO까지 내려보내지 않는다”는 원칙을 지키는 구조이다.
3) condition은 화이트리스트로 제한
if (oldest.equals(condition)) condition = oldest;
else if (recent.equals(condition)) condition = recent;
else condition = recent; // default
클라이언트가 condition=DROP_TABLE 같은 값을
보내도 서버가 허용한 값만 사용한다.
DAO에서 올바르지 않은 컨디션값으로 실행되는
SQL로직을 막기 위한 안전장치가 된다.
default를 recent로 두면, 예상치 못한 값이 와도
기능이 깨지지 않는다(확장성 + 안정성).
4) 댓글 조회 후 JSON 반환
List<ReplyDTO> replyList = replyDAO.selectAll(replyDTO);
String json = gson.toJson(replyList);
response.getWriter().print(json);
여기까지 오면 “정렬 기준에 맞춘 댓글 목록”을
프론트로 전달하는 역할을 완료한 것이다.
[ 최종 구현 코드 ]
@WebServlet("/ReplyListOrder")
public class ReplyListOrder extends HttpServlet {
private static final long serialVersionUID = 1L;
public ReplyListOrder() {
super();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("[댓글 정렬 서블릿 로그] GET 요청");
//JSON 응답 세팅
//request.setCharacterEncoding("UTF-8"); 이것도 있어야하는지 체크
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=UTF-8");
Gson gson = new Gson();
//컨디션 비교용 변수 세팅
String recent = "REPLY_LIST_RECENT";
String oldest = "REPLY_LIST_OLDEST";
// 1) boardId 검증
String boardIdCheck = request.getParameter("boardId");
System.out.println("[댓글 정렬 서블릿 로그] boardId검증 : ["+boardIdCheck+"]");
// 검증 1단계 > null/빈값 확인
if (boardIdCheck == null || boardIdCheck.isEmpty()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "boardId가 비었습니다.");
response.getWriter().print(gson.toJson(error));
return;
}
// 검증 2단계 > 숫자 변환
int boardId;
try {
// 웹 파라미터는 String으로 넘어오므로 int로 변환(파싱)
boardId = Integer.parseInt(boardIdCheck);
} catch (NumberFormatException e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "boardId가 숫자가 아닙니다.");
response.getWriter().print(gson.toJson(error));
return;
}
// 검증 3단계 > 숫자 범위 검증(0 이하 확인)
if (boardId <= 0) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "boardId는 1 이상이어야 합니다.");
response.getWriter().print(gson.toJson(error));
return;
}
// 2) condition 대입
// 비교 결과에 따른 컨디션값 대입
String condition = request.getParameter("condition");
System.out.println("[댓글 정렬 서블릿 로그] condition검증 : ["+condition+"]");
if (oldest.equals(condition)) {
condition = oldest;
}
else if (recent.equals(condition)) {
condition = recent;
}
else {
//어떤 경우도 아닌 오류상황에선 일단 기본값 정렬인 recent로 실행
//추후 확장성을 고려한 안전장치
condition = recent;
}
// 3) 댓글 조회
ReplyDAO replyDAO = new ReplyDAO();
ReplyDTO replyDTO = new ReplyDTO();
replyDTO.setBoardId(boardId);
replyDTO.setCondition(condition);
List<ReplyDTO> replyList = replyDAO.selectAll(replyDTO);
System.out.println("[댓글 정렬 서블릿 로그] 댓글 조회 완료 : count=["+replyList.size()+"]");
// 4) JSON 반환
String json = gson.toJson(replyList);
response.getWriter().print(json);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("[댓글 정렬 서블릿 로그] POST 요청 : doGet 실행");
doGet(request, response);
}
}'개주 훈련일지 > 🏋️ 전집중 호흡 훈련' 카테고리의 다른 글
| n8n으로 뉴스 요약 자동화 기능 만들기 (0) | 2025.12.20 |
|---|---|
| Generative AI 시대에서 Agentic Workflow 시대로 (0) | 2025.12.20 |
| 게시글 상세보기 구현 설계(조회수 중복 방지, 쿠키 생성, URL 조작 방어) (0) | 2025.12.18 |
| 로그인 로직 설계(관리자 권한 분기와 탈퇴 계정 차단) (0) | 2025.12.17 |
| Hexagonal Architecture 리뷰를 보고 느낀 점 (1) | 2025.12.16 |