본문 바로가기
Java

[Servlet-jsp] 서비스(Service) 클래스 추가를 통한 컨트롤러 중복 제거

by bkuk 2023. 4. 1.

Controller에서 발생하는 코드의 중복

질문 삭제 기능을 아래와 같이 웹 어플리케이션과 모바일 모두에 대응하기 위해 2개의 Controller를 구현할 때 생기는 중복을 어떻게 제거할까?.. 고민해 볼 만한 문제이다.

우선, 코드 구현을 위해 아래 요구사항을 보자.

  1. 댓글이 없는 경우 질문 삭제가 가능하다.
  2. 문자와 답변자가 모두 같은 경우에만 질문 삭제가 가능하다.
  3. 질문자와 답변자가 다른 답변이 하나라도 있으면 질문을 삭제할 수 없다.

 

위 요구사항을 만족하는 컨트롤러를 구현한 코드를 살펴보자.

모바일을 지원하는 DeleteQuestion컨트롤러이다. JsonView를 반환한다.

public class ApIDeleteQuestionController extends AbstractController {
	private QuestionDao questionDao = QuestionDao.getInstance();
	private AnswerDao answerDao = AnswerDao.getInstance();
	@Override
	public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if( !UserSessionUtils.isLogined(request.getSession()) ) {
			return jsonView()
					.addObject("result", Result.fail("login is required"));
		}
		
		Long questionId = Long.parseLong( request.getParameter("questionId") );
		Question question = questionDao.findById(questionId);
		if( question == null ) {
			return jsonView()
					.addObject("result", Result.fail("존재하지 않는 질문입니다."));
		}
		
		User user = UserSessionUtils.getUserFromSession(request.getSession());
		if( !question.isSameUser(user) ) {
			return jsonView()
					.addObject("result", Result.fail("다른 사용자가 쓴 글입니다."));
		}
		List<Answer> answers = answerDao.findAllByQuestionId(question.getQuestionId());
		
		if( answers == null ) {
			questionDao.delete(questionId);
			answerDao.deleteAll(questionId);
			return jsonView()
					.addObject("result", Result.ok());
		}
		
		boolean canDelete = true;
		String originalWriter = question.getWriter();
		for( Answer answer : answers ) {
			if( !originalWriter.equals(answer.getWriter())) {
				canDelete = false;
				break;
			}
		}
		
		if( canDelete ) {
			questionDao.delete(questionId);
			answerDao.deleteAll(questionId);
			return jsonView()
					.addObject("result", Result.ok());
		}
		return jsonView()
				.addObject("result", Result.fail("다른 사용자가 추가한 댓글이 있습니다."));
	}
}

 

웹을 지원하는 DeleteQuestion컨트롤러이다. JspView를 반환한다.

public class DeleteQuestionController extends AbstractController {
	private QuestionDao questionDao = QuestionDao.getInstance();
	private AnswerDao answerDao = AnswerDao.getInstance();
	@Override
	public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if( !UserSessionUtils.isLogined(request.getSession()) ) {
			return jspView("redirect:/");
		}
		
		Long questionId = Long.parseLong( request.getParameter("questionId") );
		Question question = questionDao.findById(questionId);
		if( question == null ) {
			throw new IllegalAccessException("존재하지 않는 글입니다.");
		}
		
		User user = UserSessionUtils.getUserFromSession(request.getSession());
		if( !question.isSameUser(user) ) {
			return jspView("/qna/show.jsp")
					.addObject("question", question)
					.addObject("answers", answerDao.findAllByQuestionId(questionId))
					.addObject("errorMessage", "다른 사용자가 작성한 글입니다.");
		}
		List<Answer> answers = answerDao.findAllByQuestionId(question.getQuestionId());
		
		if( answers == null ) {
			questionDao.delete(questionId);
			answerDao.deleteAll(questionId);
			return jspView("redirect:/");
		}
		
		boolean canDelete = true;
		String originalWriter = question.getWriter();
		for( Answer answer : answers ) {
			if( !originalWriter.equals(answer.getWriter())) {
				canDelete = false;
				break;
			}
		}
		
		if( canDelete ) {
			questionDao.delete(questionId);
			answerDao.deleteAll(questionId);
			return jspView("redirect:/");
		}
		
		return jspView("/qna/show.jsp")
				.addObject("question", question)
				.addObject("answers", answerDao.findAllByQuestionId(questionId))
				.addObject("errorMessage", "다른 사용자가 작성한 댓글이 있어 삭제할 수 없습니다.");
	}
}

 

위 코드를 보면 응답할 뷰(JspView, JsonView)만 다르고, 구현 로직과 모델 데이터는 같다. 거의 모든 코드가 중복이다.

 

어떻게 제거할 수 있을까?

두 가지 방법으로 제거할 수 있다.

  1. 두 클래스에 대한 부모 클래스를 추가해 중복 로직을 부모 클래스로 이동한 후 상속을 통해 중복을 제거한다.
  2. 컨트롤러가 DAO와 의존관계를 통해 데이터베이스 접근 로직을 제거했듯이, 로운 클래스를 추가해 로직 처리를 위임함으로써 중복을 제거

2번째 방법을 조합이라고 한다. 상속의 경우 장점도 많지만 부모 클래스에 변경이 발생하면 자식 클래스에 영향을 미칠 가능성이 높기 때문에 상속보다는 조합을 통해 중복을 제거할 것을 추천한다.

자바 진영은 이와 같이 컨트롤러에서 발생하는 중복을 제거하고, 컨트롤러의 역할 분리 등을 목적으로 서비스( 또는 Manager)라는 클래스를 추가해 담당하도록 구현하는 것으로 발전해 왔다.

따라서, 서비스 클래스를 추가해 중복되는 로직 처리를 위임해보자

 

서비스 클래스 추가

QnaService 또한 상태 값을 가지지 않기 때문에, 싱글톤 패턴을 적용해 구현이 가능하다.

public class QnaService {
	private static final Logger logger = LoggerFactory.getLogger(QnaService.class);
	private static QnaService qnaService = new QnaService();
	private QuestionDao questionDao = QuestionDao.getInstance();
	private AnswerDao answerDao = AnswerDao.getInstance();
	
	private QnaService() {}
	public static QnaService getInstance() {
		return qnaService;
	}

	public void deleteQuestion(HttpServletRequest request) throws CannotDeleteException {
		
		Long questionId = Long.parseLong( request.getParameter("questionId") );
		Question question = questionDao.findById(questionId);
		if( question == null ) {
			logger.debug("situation : {}", "존재하지 않는 글, 삭제 X");
			throw new CannotDeleteException("존재하지 않는 글입니다.");
		}
		
		User user = UserSessionUtils.getUserFromSession(request.getSession());
		if( !question.isSameUser(user) ) {
			logger.debug("situation : {}", "다른 사용자가 작성한 글, 삭제 X");
			throw new CannotDeleteException("다른 사용자가 작성한 글입니다.");
		}
		
		List<Answer> answers = answerDao.findAllByQuestionId(question.getQuestionId());
		if( answers.isEmpty() ) {
			logger.debug("situation : {}", "답변이 없는 글, 삭제 O");
			questionDao.delete(questionId);
			return;
		}
		
		boolean canDelete = true;
		String originalWriter = question.getWriter();
		for( Answer answer : answers ) {
			if( !originalWriter.equals(answer.getWriter())) {
				canDelete = false;
				break;
			}
		}
		
		if( !canDelete ) {
			logger.debug("situation : {}", "다른 사용자가 작성한 댓글이 있는 글, 삭제 X");
			throw new CannotDeleteException("다른 사용자가 작성한 댓글이 있습니다.");
		}
		
		questionDao.delete(questionId);
	}
}

 

 

중복이 제거된 Controller 클래스

기존 Controller는 아래와 같이 리팩토링할 수 있다.

public class ApiDeleteQuestionController extends AbstractController {
	private QnaService qnaService = QnaService.getInstance();
	
	@Override
	public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		Long questionId = Long.parseLong( request.getParameter("questionId") );
		if( !UserSessionUtils.isLogined(request.getSession()) ) {
			return jsonView().addObject("result", Result.fail("login is required"));
		}
		try {
			qnaService.deleteQuestion(request);
			return jsonView().addObject("result", Result.ok());
		} catch (CannotDeleteException e) {
			return jsonView().addObject("result", Result.fail(e.getMessage()));
		}
	}
}
public class DeleteQuestionController extends AbstractController {
	private QnaService qnaService = QnaService.getInstance();
	private QuestionDao questionDao = QuestionDao.getInstance();
	private AnswerDao answerDao = AnswerDao.getInstance();
	@Override
	public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception {
		Long questionId = Long.parseLong( request.getParameter("questionId") );
		if( !UserSessionUtils.isLogined(request.getSession()) ) {
			return jspView("redirect:/users/login");
		}
		try {
			qnaService.deleteQuestion(request);
			return jspView("redirect:/");
		} catch (CannotDeleteException e) {
			return jspView("redirect:/qna/show.jsp")
					.addObject("question", questionDao.findById(questionId))
					.addObject("answers", answerDao.findAllByQuestionId(questionId))
					.addObject("errorMessage", e.getMessage());
		}
	}
}

 

컨트롤러에서 구현하던 복잡한 로직을 모두 QnaService 클래스의 deleteQuestion() 메서드로 위임했기 때문에 두 컨트롤러는 정상적으로 삭제되는 경우와 에러가 발생하는 경우에 따른 처리만 구현하면 된다. 이와 같이 중복을 제거함으로써 컨트롤러는 컨트롤러의 역할에 집중할 수 있게 된다.

 

계층형 아키텍쳐

자바 진영은 컨트롤러, 서비스, DAO 구조로 웹 애플리케이션을 개발하는 것이 일반적이다. 이 같은 구조의 아키텍처를 계층형 아키텍처라고 부른다.

댓글