Controller에서 발생하는 코드의 중복
질문 삭제 기능을 아래와 같이 웹 어플리케이션과 모바일 모두에 대응하기 위해 2개의 Controller를 구현할 때 생기는 중복을 어떻게 제거할까?.. 고민해 볼 만한 문제이다.
우선, 코드 구현을 위해 아래 요구사항을 보자.
- 댓글이 없는 경우 질문 삭제가 가능하다.
- 질문자와 답변자가 모두 같은 경우에만 질문 삭제가 가능하다.
- 질문자와 답변자가 다른 답변이 하나라도 있으면 질문을 삭제할 수 없다.
위 요구사항을 만족하는 컨트롤러를 구현한 코드를 살펴보자.
모바일을 지원하는 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)만 다르고, 구현 로직과 모델 데이터는 같다. 거의 모든 코드가 중복이다.
어떻게 제거할 수 있을까?
두 가지 방법으로 제거할 수 있다.
- 두 클래스에 대한 부모 클래스를 추가해 중복 로직을 부모 클래스로 이동한 후 상속을 통해 중복을 제거한다.
- 컨트롤러가 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 구조로 웹 애플리케이션을 개발하는 것이 일반적이다. 이 같은 구조의 아키텍처를 계층형 아키텍처라고 부른다.
'Java' 카테고리의 다른 글
[Java] @Controller 애노테이션 설정 클래스 스캔하는 ControllerScanner 클래스 뜯어보기 (0) | 2023.04.04 |
---|---|
[Java] Class 클래스 / 리플렉션 / 동적로딩 (0) | 2023.04.04 |
[Servlet-jsp] DAO(Data Access Object)에서 데이터베이스 접근 로직을 구현할 때 사용하는 JdbcTemplate를 싱글톤 패턴으로 구현하기 (0) | 2023.03.31 |
[Servlet-jsp] 스택과 힙 메모리 / 멀티쓰레드 상황에서 문제가 발생할 가능성이 있는 Controller의 코드 (0) | 2023.03.31 |
[Servlet-jsp] 첫 화면에 접근했을 때 사용자 요청부터 응답까지의 흐름 (0) | 2023.03.31 |
댓글