[자바 웹 프로그래밍 Next-Step] 5주차 CH08: Ajax를 활용해 새로고침 없이 데이터 갱신하기

이호석·2023년 3월 10일
0
post-thumbnail

자바 웹 프로그래밍 Next-Step - 박재성 저자 책으로 스터디를 하며 진행했던 내용들을 기록하고 있습니다.

5주차에 진행했던 Chapter 08의 목표는 다음과 같습니다.

  • Chapter 08: Ajax를 활용한 새로고침 없는 데이터 갱신 및 MVC 컨트롤러 리팩토링(View, Model, ModelAndView)

모든 코드들은 다음 저장소에서 확인할 수 있습니다.

📌 5주차 8장

✅ 8.1 질문/답변 게시판 구현

기능 목록 정의하기

우테코에서 배웠던 것처럼 하나의 기능을 구현하기 위해 필요한 기능 목록들을 정리해 기능 구현을 시작함

장점
한 번에 하나의 기능을 구현하는것이 집중 할 수있어 확실히 효율적이라 느낌

→ 현재 프로젝트는 단순히 자바만 사용하는것이 아닌 서블릿/JSP를 활용한 프로젝트이다.
→ 따라서 조금더 체계적인 기능 목록정의가 더 빠른 개발 속도를 만들어 낼 수 있다.


고민했던 부분: RollBack 기능의 필요성(AJAX를 사용하기 전 )

지금까지 구현했던 로직들은 단순 CRUD이므로 크게 비즈니스 로직이 존재하진 않았다.
현재 아주 작은 비즈니스 로직인 답변 추가 기능을 구현할때 2개의 테이블을 업데이트 해야 하는 상황이 존재한다.

  • 테이블 변경
    1. ANSWERS: 질문에 대한 답변이 달리면, 해당 질문에 대한 questionId를 가진 질문을 생성 (INSERT)
    2. QUESTIONS: 추가된 답변으로 인해 기존 질문의 답변 수(countOfAnswer)의 업데이트(UPDATE)

위의 로직을 다음과 같이 구현했다.

코드는 일단 한가지 취약점이 존재하는데

answerDao.insert(…)의 로직이 정상 실행되고 두번째 로직인 questionDao.updateCountOfAnswerById() 로직이 정상적으로 update 되지 않았을 경우
insert한 Answer 객체에 대한 롤백이 없다는 것이다.

처음엔 try-catch를 이용해 롤백을 구현해보려 했으나 과연 JDBC에 롤백 기능이 없을까? 해서 찾아보았다.

JDBC는 autoCommit이라는 기능을 가지고 있는데 JDBC를 구현한 h2 DB의 Session 구현체를 보면
h2 DB는 다음과 같이 default autoCommit 값이 true임을 알 수 있다.

따라서 트랜잭션을 직접 구현하려면 해당 autoCommit을 false로 변경하고 update 혹은 insert가 정상 실행되면 수동 commit을
그렇지 않다면 수동 rollback을 해주면 구현할 수 있다.

하지만,

우리가 필요한 로직은 A라는 dao가 정상적으로 update 혹은 insert 쿼리가 실행됐지만, B라는 dao에서 오류가 발생했을때 A dao에서 실행했던 쿼리를 롤백해야 한다.
JDBC가 제공하는 롤백기능은 현재 실행중인 쿼리에 해당되므로 어려워 보인다. 어떻게 해야할까???????

❗️참고
데이터베이스 프로그래밍 기초(6) : JDBC에서 트랜잭션 처리



✅ 8.2 삭제하기(AJAX..)

Ajax부분은 책에서 크게 언급하고 있지 않아 간단하게 넘김

// 삭제시 AJAX 동작
$(".link-delete-article").click(deleteAnswer);

function deleteAnswer(e) {
    e.preventDefault();

    var deleteBtn = $(this);
    var queryString = deleteBtn.closest('form').serialize();

    $.ajax({
        type: 'post',
        url: '/api/qna/deleteAnswer',
        data: queryString,
        dataType: 'json',
        error: onError,
        success: function (json, status) {	// 성공하게 되면 해당 근처의 article 태그를 삭제한다.
            if (json.result.status) {
                deleteBtn.closest('article').remove();
            }
        }
    });
}

// 서버 응답 실패시
function onError(xhr, status) {
    alert("error");
}



✅ 8.3 MVC 프레임워크 요구사항 2단계

찾을 수 있는 문제점

  1. JSON 응답시 JSP 페이지가 없다보니 불필요한 null을 반환
  2. api 컨트롤러에서 자바 객체에 대한 JSON 변환 및 응답 부분에서 중복이 발생한다.

8.3.1 View 인터페이스 추가

현재 DispatcherServlet에서 절반 이상이 view를 렌더링하는데 사용되고 있다.

또한 View응답이 JSP 뿐만 아니라 JSON도 추가되면서 View에 대한 처리를 각 컨트롤러에게 강요하고 있고,
JSON 응답에 대한 중복된 코드가 맘에들지 않는다!


  • 반복되는 코드의 발생은 리팩토링의 신호: View 인터페이스 추가

    View를 구현하여 JSP와 JSON의 처리를 각각 알맞은 View가 처리하도록 분리한다. Controller는 String이 아니라 View 타입을 반환하며
    반환 이후 뷰 렌더링에 대한 처리는 View를 구현한 객체에게 위임한다.

    • JspView

      DispatcherServlet에서 처리해주었던 JSP 렌더링, redirect 명령을 해당 JspView에 둔다.

    • JsonView

      JsonView에서도 Json 응답에 대한 렌더링 처리를 맡고 있다.

    • View 분리에 대한 효과

      • View 인터페이스를 구현한 View 구현체들은 사용자들이 원하는 View 반환을 선택할 수 있게 할 수 있다.

      • 다른 렌더링 방식을 원한다면 View 인터페이스를 구현하게되면 얼마든지 새로운 View를 추가할 수 있다.

      • DispatcherServlet에서 View 렌더링에 대한 책임을 각 View 구현체에게 위임하여 책임을 분리한다.

        • : 간결해진 DispatcherServlet의 코드
      • 각 View 구현체에게 렌더링에 대한 책임을 위임하여 Controller 코드가 간결해진다.

        JsonView 도입 전

        JsonView 도입 후


8.3.1 ModelAndView 추가

JsonView에서 createModel을 할 때 request(HttpServletRequest)에 담긴 모든 attribute를 Model에 담아 클라이언트에 응답한다.
request는 서블릿 필터부터 시작해 여러 단계를 거치게 되므로 개발자가 원치 않는 값이 담아져 있을 수 있기에 Model을 따로 관리하는 편이 적절하다.


  • ModelAndView

    ModelAndView는 View와 Model을 관리하는 역할을 한다. model 저장소를 따로 두어 의미없는 값이 model에 포함되지 않도록 한다.

  • Controller 인터페이스

    각 컨트롤러는 생성한 ModelAndView를 반환한다.

  • View, JspView, JsonView

    View에 Map 타입의 model 인자가 추가된다. JsonView에서는 model인자를 직접 json 변환하므로 의미없는 값이 View로 전달되는것을 방지한다.

    각 Controller에서도 Model에 값을 담으므로 JspView를 렌더링 하기전에 HttpServletRequest객체에 model값을 담아준다.


  • View의 편리한 사용을 위한 AbstractController

    Controller 인터페이스를 각 컨트롤러가 직접 구현해도 되지만, 조금 더 View를 편리하게 사용하기 위해 추상 클래스를 만들어보자

    현재 View의 구현체인 JspView와 JsonView를 편리하게 사용할 수 있다.


  • 컨트롤러에서 AbstractController 구현 및 ModelAndView 반환 적용

    • 적용 전

      public class DeleteAnswerController implements Controller {
          private final AnswerService answerService = new AnswerService();
      
          @Override
          public String execute(final HttpServletRequest request, final HttpServletResponse response) {
              long answerId = Long.parseLong(request.getParameter("answerId"));
              int deleteResult = answerService.deleteAnswer(answerId);
              ObjectMapper objectMapper = new ObjectMapper();
              response.setContentType("application/json;charset=UTF-8");
              try {
                  PrintWriter out = response.getWriter();
                  if (deleteResult <= 0) {
                      out.print(objectMapper.writeValueAsString(Result.fail("삭제 실패")));
                      return null;
                  }
                  out.print(objectMapper.writeValueAsString(Result.ok()));
                  return null;
              } catch (IOException e) {
                  throw new RuntimeException(e);
              }
          }
      }
    • 적용 후

      public class DeleteAnswerController extends AbstractController {
          private final AnswerService answerService = new AnswerService();
      
          @Override
          public ModelAndView execute(final HttpServletRequest request, final HttpServletResponse response) {
              long answerId = Long.parseLong(request.getParameter("answerId"));
              int deleteResult = answerService.deleteAnswer(answerId);
              if (deleteResult <= 0) {
                  return jsonView().addObject("result", Result.fail("삭제 실패"));
              }
              return jsonView().addObject("result", Result.ok());
          }
      }

    View 관련 로직들을 View 구현체에 모아두고, Model 마저도 ModelAndView로 관리하여 반환하므로
    컨트롤러에는 컨트롤러라는 이름에 걸맞은 제어하는 역할만 수행하는 로직만 남아있다.


  • DispatcherServlet

    디스패처 서블릿 또한 핸들러의 실행을 통해 받아온 ModelAndView 객체를 이용해 view를 렌더링하지만,
    단지 흐름의 제어만 해줄뿐 실질적인 동작은 해당 역할의 객체에게 위임한다.

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글