내일배움캠프/TIL

[Spring_4기 본캠프] Spring 숙련 - 2주차 - 예외처리 | Day 43

austindynasty 2024. 12. 15. 20:22

◆ 예외처리에 대한 고찰

  강의를 들으면서 프로그래밍 실습을 하다가 '예외처리는 어느 계층에서 하는 것이 맞을까?' 라는 궁금증이 생겼다.

특정 id로 회원을 조회하는 기능을 구현한다고 했을 때, 사용자가 존재하지 않는 id를 입력한다면 이에 대한 예외처리는 어느 계층에서 처리하는 것이 맞는걸까 하는 것이었다.

이 의문증에 답을 하기 위해선 각 계층의 역할에 대한 완벽한 개념 분리가 필요해보였다.

 

▶ 서비스 계층에서의 예외 처리

1) 서비스 계층의 역할

- 비즈니스 로직을 담당한다.

- 데이터 검증, 처리, 계산 등을 수행한다.

 

2) 예외 처리의 목적

- 비즈니스 로직과 관련된 예외를 처리한다.

- 데이터 처리 중 문제가 생겼을 때 예외를 발생시켜 컨트롤러에 알린다.

 

3) 어떤 예외를 처리해야 하나

- 비즈니스 규칙 위반 ex. 사용자가 요청한 데이터가 존재하지 않을 때, 중복 데이터 삽입 시

- 데이터 처리 중 오류 ex. 데이터베이스와 관련된 오류가 발생했을 때 

 

4) 서비스 계층 예외 처리 코드 예제

@Service
public class TodoServiceImpl implements TodoService {

    private final TodoRepository todoRepository;

    public TodoServiceImpl(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @Override
    public TodoResponseDto getTodoById(Long id) {
        // 비즈니스 규칙: ID에 해당하는 Todo가 없으면 예외 발생
        Todo todo = todoRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Todo not found with ID: " + id));

        return new TodoResponseDto(todo);
    }
}

 

▶ 컨트롤러 계층에서의 예외 처리

1) 컨트롤러 계층의 역할

- 클라이언트 요청을 받고, 응답을 처리한다.

- 서비스계층과 클라이언트 사이를 연결하는 역할을 한다.

 

2) 예외 처리의 목적

- 클라이언트에게 적절한 HTTP 상태 코드와 메시지를 전달한다.

- 서비스 계층에서 발생한 예외를 잡아 사용자 친화적인 응답으로 변환한다.

 

3) 어떤 예외를 처리해야 하나

- 서비스 계층에서 발생한 예외를 잡아 사용자에게 전달한다.

- HTTP 상태 코드와 메시지를 설정한다. ex. 데이터가 없을 경우 ( 404 Not Found ), 잘못된 요청일 경우 ( 400 Bad Request )

 

4) 컨트롤러 계층 예외 처리 코드 예제

@RestController
@RequestMapping("/api/todos")
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<TodoResponseDto> getTodoById(@PathVariable Long id) {
        try {
            // 서비스 계층에서 처리한 결과를 반환
            TodoResponseDto responseDto = todoService.getTodoById(id);
            return ResponseEntity.ok(responseDto);
        } catch (IllegalArgumentException e) {
            // 예외를 잡아서 클라이언트에 적절한 응답 전달
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(null); // 실제 서비스에서는 에러 메시지를 포함한 응답을 반환하는 것이 일반적
        }
    }
}

 

▶ GlobalExceptionHandler 사용하기

- 예외 처리 로직을 컨트롤러마다 반복하지 않으려면 @ControllerAdvice를 사용해 전역적으로 처리할 수도 있다.

- Exception 만을 처리하는 클래스를 따로 만들어 관리해주면 된다.

 

- GlobalExceptionHandler 예외 처리 코드 예제

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGenericException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("An unexpected error occurred: " + e.getMessage());
    }
}

 

 

◇ 정리

1) 서비스 계층에서의 예외 처리 

- 비즈니스 로직에서 오류를 감지하고 예외를 던져야 할 때 사용한다.

- 서비스 계층은 예외를 잡아 처리하기보단 필요한 정보를 예외를 통해 상위 계층에 전달하는 역할을 한다.

 

2) 컨트롤러 계층에서의 예외 처리

- 클라이언트에게 예외에 대한 적절한 응답을 전달해야 할 때 사용한다.

- 예외를 HTTP 상태 코드와 메시지로 변환하는 역할을 한다.

 

→ 상황에 따라 예외 처리 위치는 다를 수 있지만 서비스는 문제를 감지하고 예외를 던지며, 컨트롤러는 클라이언트에게 전달할 응답을 결정하는 것이 일반적이다! 하지만 반복되는 코드 작성을 편하게 처리하려면 GlobalExceptionHandler를 사용해보자. 정말 편하다! 

 

  그렇다면, 서비스 계층에서 데이터베이스와 관련된 예외를 처리하는 이유는 뭘까? 

왜냐하면 레포지토리 계층은 데이터 접근만 담당해야 하기 때문이다. 레포지토리 계층은 단순히 데이터베이스 작업을 수행하는 도구로서 동작해야 하기 때문에 데이터베이스에서 발생한 예외를 직접 처리하게 되면 책임이 분산되고 역할이 비대해진다. 

 따라서 데이터베이스와 관련된 예외 역시 서비스 계층에서 담당하는 것이다. 

  데이터베이스에서 발생한 오류가 단순한 접근 오류인지, 비즈니스 로직에서 중요한 의미를 가지는 오류인지는 서비스 계층에서 판단한다.

가령, 사용자가 특정 ID로 회원을 조회하려고 할 때 데이터베이스에서 "해당 ID 없음" 이라는 오류를 서비스 계층으로 넘기면 서비스 계층에서는 "사용자가 존재하지 않음" 이라는 비즈니스적인 메시지로 변환할 수 있다. 

 따라서 서비스 계층은 데이터베이스 오류를 받아 적절하게 해석하고 처리해야 한다.