설모의 기록

[우아한테크캠프] 8일차 본문

일상/우아한테크캠프

[우아한테크캠프] 8일차

HA_Kwon 2018. 7. 12. 01:10

Spring @Entity 간의 관계 + 프론트엔드 코드 구성



오늘은 어제 구현했던 게시판을 좀 더 리팩토링하는 시간을 가졌습니다. 친구들과 프로젝트를 하다보면 데이터베이스 테이블끼리 서로 Join 해야 하는 것에 항상 고민을 많이 했었는데요. ORM 표준인 JPA 를 사용하다보니 이런 고민을 많이 안할 수 있다는 것에 놀라웠습니다. 이번 글에서는 캠프 동기와 함께 예외 처리에 대해 고민한 부분과 ManyToOne 관계인 테이블 처리에 대해 말씀드리겠습니다. 그리고 서버에게 비동기적으로 요청하거나 서버가 보내는 데이터를 받아 비동기적으로 처리하는 클라이언트 코드에 대해 알아보겠습니다.




예외처리

우아한테크캠프에 참여하지 못했다면 깨닫지 못했을 예외처리에 대해 말씀드리겠습니다. 이전에는 예외처리를 try-catch 문으로 해결 하고는 했는데요. 그러다보면 try-catch 문 마저 중복이 되며, 코드가 상당히 더러워짐을 느끼실 겁니다. '원래 이런거겠지~' 하고 대수롭지 않게 넘겼던 부분이었는데, 오늘 동기와 함께 코드를 구현하며 예외처리조차 재사용 가능한 코드로 구현할 수 있다는 것을 알았습니다.

저희팀은 해당하는 에러에 대한 클래스를 만들고, @ControllerAdvice 클래스를 만들어 예외처리를 관리하도록 했습니다.


public class UnAuthorizedUserException extends RuntimeException {

}
public class UnDeletableQuestionException extends RuntimeException {

}
public class UnidentifiedUserException extends RuntimeException {

}

먼저 필요한 예외 클래스를 생성합니다. 저희 팀은 권한이 없는 유저에 대한 예외, 지울 수 없는 질문에 대한 예외, 식별되지 않은 (로그인되지 않은) 유저에 대한 예외로 총 3개의 클래스를 생성했습니다. 세 개의 클래스 모두 RuntimeException 클래스를 상속받습니다. 


@ControllerAdvice
public class ExceptionHandlerUtils {
@ExceptionHandler(UnidentifiedUserException.class)
public String unidentifiedUserExceptionHandler() {
return "redirect:/users/login";
}

@ExceptionHandler(UnAuthorizedUserException.class)
public String unAuthorizedUserExceptionHandler() {
return "/error/error";
}

@ExceptionHandler(UnDeletableQuestionException.class)
public String unDeletableQuestionExceptionHandler() {
return "/error/error";
}
}

위의 ExceptionHandlerUtils 는 예외 처리를 관리해주는 클래스로 @ControllerAdvice 클래스입니다. @ControllerAdvice 는 @Controller 내의 중복코드를 제거해주는 클래스로서 예외처리와 같이 중복되는 코드를 모아주는 역할을 합니다.

저희는 총 3가지의 @ExceptionHandler 메소드를 구현했습니다. 3개의 메소드 모두 단순히 html 경로를 알려주거나, Get 요청을 권하는 응답을 보내는 메소드입니다. 이렇게 Controller 에서 발생할 수 있는 예외에 대한 처리를 모아둡니다.

따라서 Controller 내에서 UnidentifiedUserException, UnAuthorizedUserException, UnDeletableQuestionException 예외가 발생하면 이 ExceptionHandlerUtils 클래스의 각 메소드가 실행되게 됩니다.


@DeleteMapping("/{id}")
public String delete(@PathVariable Long id, HttpSession session) {
checkLoginAndSameUser(id, session);
Question question = getQuestionByIndex(id);
if(!question.isDeletable()) {
throw new UnDeletableQuestionException();
}

question.delete();
questionRepository.save(question);
return "redirect:/";
}

위의 코드는QuestionController 클래스의 하나의 메소드입니다. 중간에 if 문을 보시면 질문을 삭제할 수 없는 경우에 UnDeletableQuestionException 이 발생하게 됩니다. 그러면 ExceptionHandlerUtils 의 unDeletableQuestionExceptionHandler 메소드가 호출되어 /error/error.html 을 반환하게 됩니다.

이런식으로 예외처리를 모아 하나의 클래스로 빼둔다면 코드의 재사용성과 유지보수에 드는 시간과 비용을 줄일 수 있습니다.




Cascade

데이터베이스 테이블을 설계해보신 분이라면 cascade 키워드를 들어보신 적이 있으실 겁니다. A 테이블과 B 테이블이 1 : n 관계이고 cascade 설정을 하셨다면, A 테이블의 하나의 데이터가 지워지면 그 데이터와 관련된 B 테이블의 데이터 또한 지워집니다. 

저희가 구현한 예제로는 질문과 그 질문에 대한 답변입니다. 질문이 지워지면 그에 대한 답변 또한 지워져야하기 때문에 cascade 를 설정해야 합니다.


@OneToMany(mappedBy = "question")
@Cascade(value = org.hibernate.annotations.CascadeType.REMOVE)
private List<Answer> answers = new ArrayList<>();

위의 코드는 Question 의 컬럼으로 List<Answer> 타입의 인스턴스 변수를 생성한 모습입니다. 관계는 OneToMany이며, 본인의 데이터 구조에 따라 CascadeType을 정하시면 됩니다. 


@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "fk_answer_parent_question_id"))
private Question question;

Answer 또한 Question 을 멤버변수로 가지고 있습니다. 관계는 ManyToOne 이며 컬럼을 조인해 외래키를 설정하는 것입니다. 이렇게 하시면 질문이 삭제될 때 그에 해당하는 답변 또한 삭제됩니다. 

이 때, 주의하셔야 할 점이 있는데요. Question 을 지울 때 Answer 가 지워지지 않아 헤매시는 분이 있을 수도 있습니다. 그 이유는 다음과 같습니다.

질문을 지우면 그에 해당하는 Answer 들을 지워야 합니다. 하지만 Answer 또한 Question 을 가지고 있기 때문에 Question 도 지워야 하는데요. Answer 클래스에 있는 Question 은 cascade 설정이 되어있지 않기 때문에 함께 삭제되지 않습니다. 따라서 Answer 가 삭제되지 않는 경우가 발생할 수 있습니다. 이 때는 answer.setQuestion(null) 과 같이 Answer 가 참조하고 있는 Question 을 null 로 만들어주시면 됩니다.




Ajax

오랜만에 클라이언트 이야기를 듣게 되었습니다. AjaxAsynchronous JAvascript Xml 로 비동기적으로 서버에게 요청을 보내기 위해 사용되는 기술입니다. 

요즘의 웹 페이지를 보면 서버에게 요청을 보내기 위해 페이지를 리로드하거나 리다이렉트하는 경우는 거의 없습니다. 필요한 데이터만 비동기적으로 서버에게 요청해 데이터를 받고, 필요한 UI 만 업데이트하는 구조이기 때문에 사용자 경험 측면에서도 우수한데요, 그 기반에는 Ajax 가 내포되어 있습니다.

제가 사용할 때만 해도 정석인 코드로 이용했었는데, 요즘은 ES6 의 Promise 패턴이 적용된 세련된 코드가 있었습니다. Promise 에 대한 내용은 이전에 썼던 글을 참고해주시기 바랍니다.


fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

위의 코드는 MDN 사이트에 기본적으로 구현되어 있는 코드입니다. 위의 코드가 바로 Promise 를 적용한 코드인데요. 먼저 fetch 함수를 실행한 후 차례대로 앞의 메소드 수행이 끝나면 바로 가까이에 있는 then() 에 있는 콜백함수를 실행하게 됩니다. 그리고 위의 코드에는 나와있지 않지만 .then() 뿐만 아니라 .catch() 메소드도 있습니다. 코드를 비동기적으로 수행하다면 예외 처리나 에러와 같은 것들이 발생할 수 있습니다. 그 때는 그 다음의 어떠한 then() 도 수행하지 않고 catch() 의 콜백함수가 실행됩니다. 따라서 에러 핸들링이 용이한 Promise 패턴을 적용한 Ajax 구문을 사용하시는 것이 좋을 것 같습니다.

'일상 > 우아한테크캠프' 카테고리의 다른 글

[우아한테크캠프] 10일차  (0) 2018.07.14
[우아한테크캠프] 9일차  (0) 2018.07.12
[우아한테크캠프] 7일차  (0) 2018.07.11
[우아한테크캠프] 6일차  (0) 2018.07.09
[우아한테크캠프] 5일차  (0) 2018.07.07
Comments