1. HTTP 응답 코드
throw 로 던진 예외를 처리하기 전 HTTP 응답코드를 간단히 알아보겠습니다.HTTP 응답코드는 주로 다음과 같은 의미를 갖고 있습니다.
- 1XX (임시 응답): 요청을 처리하고 있음을 나타내는 임시 응답입니다.
- 2XX (성공): 클라이언트의 요청이 성공적으로 수신되어 처리되었음을 나타냅니다.
- 3XX (리다이렉션): 클라이언트가 요청한 리소스가 다른 위치로 이동되었음을 나타냅니다.
- 4XX (클라이언트 오류): 클라이언트의 잘못된 요청으로 인해 발생하는 오류입니다.
- 5XX (서버 오류): 서버가 요청을 처리하는 동안 오류가 발생했음을 나타냅니다. 이 오류는 반드시 로그로 기록되어야 합니다.
따라서 error 패키지의 ex 내부에 Exception400~404 , 500 에러 처리 클래스를 정의했습니다.
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 원리


실행중 
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 e 는 
throw new RuntimeException("게시글 id를 찾을 수 없습니다");를 갖고있다.
이를 실행하면 

팝업이 뜨고 확인을 누르면 이전 페이지로 돌아가게 된다.
@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);
        
    }
따라서 아래와 같이 수정합니다.
- 레포지토리 수정
    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

