커맨드 패턴과 팩토리 메서드 패턴 적용하기 (JDBC로 게시판 만들기)

junto·2024년 11월 9일
0

design-patterns

목록 보기
2/3
post-thumbnail

커맨드 패턴

커맨드 패턴이란

  • 커맨드 패턴이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이라고 한다. (위키백과)
  • 쉽게 말해, 실행할 작업(명령)을 객체로 만들어 호출자와 수신자의 의존성을 낮추는 것이다.

필요한 이유

  • JPA를 사용하지 않고 서블릿을 이용해 게시판을 구현해보는 예시를 생각해보자. HttpServlet을 상속받는 BoardServlet이 앞단에서 /boards/ 로 시작하는 요청을 처리한다고 할 때, 아래 코드를 쉽게 떠올릴 수 있다.
@WebServlet(urlPatterns = "/boards/*")
public class BoardServlet extends HttpServlet {

  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
  
  if (requestURI.equals("/boards/free")) { // 게시판 목록
    viewListBoard(request, response);
  } else if (requestURI.equals("/boards/free/boardId")) { // 게시글 상세보기
    viewDetailBoard(request, response);
  } else if (requestURI.equals("/board/free/write")) { // 게시글 생성
    createBoard(request, response);
  } else if (requestURI.equals("/board/free/update")) { // 게시글 수정
    updateBoard(request, response);
  } else if ...
{
  • 특정 기능(게시글 삭제)이 추가될 때마다 if 문이 추가되어야 한다. Service() 메서드가 단순히 HTTP 요청을 처리하는 게 아닌 어떤 컨트롤러를 호출해야 하는지 결정하여 단일 책임 원칙에 위배된다.
  • 커맨드 패턴을 적용하면 아래와 같이 요청을 캡슐화하고, 요청을 실행할 객체가 게시판 생성 객체라면 게시판 생성을, 게시판 수정 객체라면 게시판 수정을 처리하게 할 수 있다.
protected void service(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

    String requestURI = request.getRequestURI();
    
    // 컨트롤러를 찾는 책임은 ControllerResolver 에게 위임
    BoardController controller = controllerResolver.resolveController(requestURI);

    if (controller == null) {
      throw new RuntimeException(ErrorMessage.NO_HANDLER_MSG + request.getRequestURI());
    }
    // 단순히 컨트롤러를 실행하는 책임
    controller.execute(request, response);
  }

커맨드 패턴 구조

전체 코드: https://github.com/ji-jjang/ebrainsoft/tree/main/JspBoard

  • 먼저 실행될 명령에 대한 인터페이스를 정의한다. URL에 따라 특정 작업을 하는 컨트롤러 메서드를 만드는 것이므로 아래와 같이 선언한다.
public interface BoardController {

  public void execute(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException;
}
  • 위 인터페이스를 구현하는 게시판 생성 객체는 아래와 같이 선언할 수 있다.
public class BoardCreateController implements BoardController {

  private final CategoryDAO categoryDAO;

  public BoardCreateController(CategoryDAO categoryDAO) {
    this.categoryDAO = categoryDAO;
  }

  @Override
  public void execute(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {

    List<String> categories = categoryDAO.getCategories();

    req.setAttribute(Constants.CATEGORIES, categories);
    req.getRequestDispatcher("/createBoard.jsp").forward(req, res);
  }
}
  • execute 메서드 안에서 게시판 생성 작업을 처리한다. 즉, 특정 기능마다 BoardController 인터페이스를 상속하여 기능을 캡슐화하고, execute() 메서드를 통해 URL에 따라 기능이 호출되게 한다. 호출자 코드는 위에 본 것처럼 서블릿이 담당한다.

팩토리 메서드 패턴이란

  • 객체 생성의 책임을 서브클래스나 별도의 클래스에 위임하여 객체 생성 과정과 사용하는 코드를 분리하는 패턴이다.
public class BoardModifyController implements BoardController {
  private final BoardDAO boardDAO = new BoardImpl();
  private final BoardValidator validator = new BoardValidator();
  • 위는 필드 주입 방법이다. 필드 주입 단점은 테스트 코드를 작성할 때 단위 테스트(모킹)이 어렵다는 점이다.

필요한 이유

private final BoardDAO boardDAO;
  private final BoardValidator validator;

public BoardModifyController(BoardDAO boardDAO, BoardValidator validator) {
  this.boardDAO = new boardDAOImpl();
  this.validator = new Validtor();
}
  • 생성자 방식으로 수정해도 직접 인스턴스화하면 여전히 강한 결합되어 변경에 취약해지고, 모킹이 어렵다. 그럼 외부에서 인스턴스화해서 넣어주면 되지 않을까?
public class BoardControllerFactory {
  private final CategoryDAO categoryDAO;
  private final BoardDAO boardDAO;
  private final BoardValidator validator;

  public BoardControllerFactory(
      CategoryDAO categoryDAO, BoardDAO boardDAO, BoardValidator validator) {
    this.categoryDAO = categoryDAO;
    this.boardDAO = boardDAO;
    this.validator = validator;
  }

  public Map<String, BoardController> createExactMappings() {
    Map<String, BoardController> exactMappings = new HashMap<>();
    exactMappings.put("/boards/free/write", new BoardCreateController(categoryDAO));
    exactMappings.put(
        "/boards/free/list", new BoardListController(boardDAO, categoryDAO, validator));
    exactMappings.put("/boards/free/delete", new BoardDeleteController(validator));

    return exactMappings;
  }

  public Map<Pattern, BoardController> createRegexMappings() {
    Map<Pattern, BoardController> regexMappings = new HashMap<>();
    regexMappings.put(
        Pattern.compile("^/boards/free/view/[0-9]+"),
        new BoardDetailController(boardDAO, validator));
    regexMappings.put(
        Pattern.compile("^/boards/free/modify/[0-9]+"),
        new BoardModifyController(boardDAO, validator));
    regexMappings.put(
        Pattern.compile("^/boards/[0-9]+/comments$"),
        new CommentCreateController(boardDAO, validator));
    regexMappings.put(
        Pattern.compile("^/boards/free/delete/[0-9]+"),
        new BoardDeleteExecutionController(boardDAO, validator));

    return regexMappings;
  }

  public BoardDAO createBoardDAO() {
    return boardDAO;
  }

  public BoardValidator createBoardValidator() {
    return validator;
  }
}

전체 코드: https://github.com/ji-jjang/ebrainsoft/tree/main/JspBoard

  • BoardControllerFactory 는 다양한 BoardController 객체 생성을 책임진다. 필요한 의존 관계를 받아 인스턴스를 생성한 후 요청이 오면 해당 객체를 반환한다. BoardModifyController 에서 BoardDAO와 Validator가 필요한데, 내부적으로 이미 해당 객체를 가지고 있어 컨트롤러를 생성할 때 주입해줄 수 있으며, 별도로 해당 객체를 반환해주는 메서드까지 있어 유연하게 사용할 수 있다.
profile
꾸준하게

0개의 댓글