개주 훈련일지/🏋️ 전집중 호흡 훈련

게시글 댓글 정렬(최신순/오래된순) 비동기 서블릿 구현

lshfood2 2025. 12. 19. 23:13

[ 구현 목표 ]

지난 포스팅에서는 게시글 상세보기 페이지 이동을 위한

쿠키 검사, 조회수 업데이트 등의 로직을 구현했다.

 

이번엔 비동기 서블릿을 통해 게시글 상세 페이지에서

댓글을 다시 로드하지 않고(새로고침/페이지 이동 없이)

정렬 기준만 바꿔 댓글 목록을 재출력하는

비동기 기능을 구현해보려고 한다.

 

▼ 전체 흐름 요약

클라이언트는 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);
	}
}