[v2] Spring 게시판 조금 알고 따라하기 - 7

예외처리핸들러 설정과 User 서비스 리팩토링
HootJem's avatar
Aug 23, 2024
[v2] Spring 게시판 조금 알고 따라하기 - 7

1. HTTP 응답 코드

throw 로 던진 예외를 처리하기 전 HTTP 응답코드를 간단히 알아보겠습니다.
💡
HTTP 응답코드는 주로 다음과 같은 의미를 갖고 있습니다.
  • 1XX (임시 응답): 요청을 처리하고 있음을 나타내는 임시 응답입니다.
  • 2XX (성공): 클라이언트의 요청이 성공적으로 수신되어 처리되었음을 나타냅니다.
  • 3XX (리다이렉션): 클라이언트가 요청한 리소스가 다른 위치로 이동되었음을 나타냅니다.
  • 4XX (클라이언트 오류): 클라이언트의 잘못된 요청으로 인해 발생하는 오류입니다.
  • 5XX (서버 오류): 서버가 요청을 처리하는 동안 오류가 발생했음을 나타냅니다. 이 오류는 반드시 로그로 기록되어야 합니다.
 
따라서 error 패키지의 ex 내부에 Exception400~404 , 500 에러 처리 클래스를 정의했습니다.
notion image
notion image
public class Exception400 extends RuntimeException { public Exception400(String message) { super(message); } }
해당 Exception400 클래스는 RuntimeException 을 상속 받아 커스텀 하고 있습니다.
RuntimeException 을 상속 받는 이유는 애플리케이션 실행중 발생하는 오류를 다룰 것이기 때문입니다.
 
에러메세지는 다음과 같이 JavaScript 로 처리합니다.
입력받은 에러 메세지를 출력하고 이전 페이지로 이동하게 됩니다.
public static String back(String msg) { String errMsg = """ <script> alert('$msg'); history.back(); </script> """.replace("$msg", msg); return errMsg; }
 
GlobalExceptionHandler 원리
notion image
notion image
실행중 RuntimeException 이 발생하면 여기로 오게됩니다.
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public String ex(Exception e) { String errMsg = """ <script> alert('$msg'); history.back(); </script> """.replace("$msg", e.getMessage()); return errMsg; } }
ex 의 매개변수 Exception ethrow new RuntimeException("게시글 id를 찾을 수 없습니다");
를 갖고있다.
이를 실행하면
notion image
팝업이 뜨고 확인을 누르면 이전 페이지로 돌아가게 된다.
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public String ex(Exception e) { return Script.back(e.getMessage()); } }
@RestControllerAdvice 어노테이션을 사용하여 모든 RuntimeException 을 처리합니다.
 
예외 처리는 여기까지 하고 User 서비스 리팩토링을 해 보겠습니다.

2. User 서비스 리팩토링

기존 회원가입 기능에 아이디 중복 검사를 추가합니다.
@RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public void 회원가입(UserRequest.JoinDTO joinDTO) { User oldUser = userRepository.findByUsername(joinDTO.getUsername()); if (oldUser != null) { throw new Exception400("이미 존재하는 유저네임입니다."); } userRepository.save(joinDTO.toEntity()); } }
입력한 아이디가 이미 존재하는지 확인합니다. 중복인 경우 findByUsername 의 값이 리턴되겠죠? (아니면 null 이 오도록 작성되어있습니다)
if 조건문을 설정하여 이전 에 작성한 Exception400 로 예외를 던집니다.
 

2.1 레포지토리 로직

@RequiredArgsConstructor @Repository public class UserRepository { private final EntityManager em; public User findByUsername(String username) { Query query = em.createQuery("select u from User u where u.username = :username", User.class); query.setParameter("username", username); User user = (User) query.getSingleResult(); return user; }
기존의 코드입니다. db 에 있는 이름을 체크할 때는 상관없지만 우리는 db 에 없는 값을 조회하더라도 처리해야 합니다. 이대로 실행시 아래와 같이 NoResultException 이 나오게 되는데 회원 가입때 마다 발생하면 곤란하겠죠..
 
  • 검증
    • 중복 검사를 위해 name 으로 조회하는 것이기 때문에
      noresult Exception 이 나오는게 정상입니다.
      @Test public void findByUsername() { String username = "haha"; User user = userRepository.findByUsername(username); }
notion image
 
따라서 아래와 같이 수정합니다.
  • 레포지토리 수정
public User findByUsername(String username) { Query query = em.createQuery("select u from User u where u.username = :username", User.class); query.setParameter("username", username); try { User user = (User) query.getSingleResult(); return user; } catch (Exception e) { return null; } }
수정한 뒤 실행하면 존재하는 값을 주거나, null 을 리턴하게 됩니다. 중복 체크이기 때문에 null 이 정상상태, 있는것은 중복인 상황이죠.
 
 

3. 로그인 기능 리팩토링

3.1 서비스 로직

public void 로그인(UserRequest.LoginDTO loginDTO) { User user = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword()); }
 

3.2 레포지토리 수정

public User findByUsernameAndPassword(String username, String password) { Query query = em.createQuery("select u from User u where u.username = :username and u.password = :password", User.class); query.setParameter("username", username); query.setParameter("password", password); try { User user = (User) query.getSingleResult(); return user; } catch (Exception e) { throw new Exception401("인증되지 않았습니다."); } }
이렇게 적절하게 예외를 처리할 수 있도록 합니다.
저희는 개발하며 e.getMessage 를 통해 정확한 에러 메세지를 확인할 수 있지만 사용자에게는 위와 같은 메세지를 띄워주는 편이 좋겠죠
 

3.3 컨트롤러 수정

@PostMapping("/login") public String login(UserRequest.LoginDTO loginDTO) { User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); return "redirect:/board"; } @PostMapping("/join") public String join(UserRequest.JoinDTO joinDTO) { userService.회원가입(joinDTO); return "user/join-form"; }
 

이렇게 컨트롤러 - 서비스 - 레포지토리 로 요청을 하게될 때
조건문이 중첩되는 경우가 있습니다.
if(조건==조건1){ if(조건 == 조건2) { 실행해줘(); } }else{ }
 
가독성을 위해 조건이 맞지 않는 경우 초기에 예외를 던져 흐름을 제어하는 방법을 사용하여 코드를 작성했습니다.
if (조건 != 조건1) { throw new Exception404("조건1이 맞지 않습니다."); } if (조건 != 조건2) { throw new Exception404("조건2가 맞지 않습니다."); } 실행해줘();
이렇게 작성하면 if-else 구조가 없어 코드가 깔끔하게 보이고 , 예외 상황을 보다 확실히 구분할 수 있어 예외 관리에 유용할 듯 싶습니다. 😁
 
스프링부트 게시판 시리즈 v2 -1. https://inblog.ai/hj/27190 (User 테이블 생성 및 쿼리 수정) -2. https://inblog.ai/hj/27193 (User, Board 테이블 조인 과 JPQL) -3. https://inblog.ai/hj/27224 (회원 가입) -4. https://inblog.ai/hj/27225 DTO 를 통한 리팩토링 -5. https://inblog.ai/hj/27310 로그인, 로그아웃 -6. https://inblog.ai/hj/27316 서비스 레이어 추가 및 DTO 활용 -7. https://inblog.ai/hj/27430 예외처리 핸들러 설정과 User 서비스 리팩토링 -8. https://inblog.ai/hj/27431 Board 기능 리팩토링 -9. https://inblog.ai/hj/27560 게시글 수정, 더티체킹(flush) -10. https://inblog.ai/hj/27561 인터셉터, AOP 사용 / 마무리
 
Share article

[HootJem] 개발 기록 블로그