ResponseEntity vs Exception 예외처리

ssongyi·2025년 4월 2일
1

Java/Spring TIL

목록 보기
5/11

다음은 예외처리의 흐름을 도식화한 것이다.

이 부분에서 예외가 발생하지 않을시 '정상 응답 반환' 하는 부분에서 이해가 가지 않는 부분이 있었다.

ResponseEntity<> 는 예외처리로도 활용하지 않나?

ResponseEntity<> 를 쓰는 이유는 HTTP 상태 코드와 응답 바디를 함께 제어할 수 있기 때문이다.
따라서 예외 상황에서도 JSON 응답 구조를 직접 구성하고, 원하는 상태 코드를 함께 내려보낼 수 있다.

예외 처리를 ResponseEntity 로 처리했던 것은,
해당 예외를 직접 return 하는 것이 아니라 예외를 throw 하는 개념이다.

@GetMapping("/{id}")
public ResponseEntity<MemberResponseDto> getMember(@PathVariable Long id) {
    Member member = memberService.findByIdOrElseThrow(id); // 여기서 예외 날 수 있음
    return ResponseEntity.ok(new MemberResponseDto(member));
}

이렇게 쓰면, 정상일 땐 200 OKResponseEntity 가 동작하고,
예외가 발생하면 @ExceptionHandlerSpring 기본 에러 처리로 빠진다.

ResponseEntity.notFound() 같은 메서드가 Exception 과 어떤 차이가 있을까?

ResponseEntity.method()

  • 예외가 아니라 정상적인 흐름에서 응답을 명시적으로 정의하는 것
  • 클라이언트의 요청이 올바른 상황에서, 비즈니스 로직 상 실패라고 판단될 때 사용
  • 예외를 던지는 것이 아니라, 상태 코드(ex. 404) 를 직접 반환
@GetMapping("/{id}")
public ResponseEntity<MemberResponseDto> getMember(@PathVariable Long id) {
    Optional<Member> member = memberService.findById(id);
    if (member.isEmpty()) {
        return ResponseEntity.notFound().build();  // 비즈니스 로직 내에서 실패 응답을 직접 반환
    }
    return ResponseEntity.ok(new MemberResponseDto(member.get()));
}

ResponseStatusException + @ExceptionHandler(예외처리)

  • 예외를 던지는 방식
  • 예외가 발생하면, 이를 처리하는 @ExceptionHandler 에서 HTTP 상태 코드와 응답을 정의하고 클라이언트에 보냄
  • 이때는 예외 상황이므로, 요청 자체가 잘못된 것이 아니거나 비즈니스 로직이 실패했다고 판단되면 예외를 던져 @ExceptionHandler 로 처리
public Member findByIdOrElseThrow(Long id) {
    return memberRepository.findById(id)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "없는 유저입니다"));
}

@GetMapping("/{id}")
public ResponseEntity<MemberResponseDto> getMember(@PathVariable Long id) {
    Member member = memberService.findByIdOrElseThrow(id);  // 예외가 발생할 경우 @ExceptionHandler에서 처리됨
    return ResponseEntity.ok(new MemberResponseDto(member));
}

정리

  1. 비즈니스 실패나 예측 가능한 오류는 ResponseEntity로 처리

    • 비밀번호 틀림, 중복 이메일, 잘못된 요청 값 등
    • 이런 경우에는 예외를 던지지 않고 ResponseEntity.status().body()로 응답
  2. 예기치 못한 오류는 ResponseStatusException이나 @ExceptionHandler를 사용

    • 서버에서 발생한 예외적인 상황일 때 사용 (DB 오류, 외부 API 오류 등)
    • 예외가 발생하면 @ExceptionHandler가 예외를 잡아서 응답 처리

정상 흐름인데 실패 응답을 리턴하는 이유가 뭘까?

“정상 흐름인데 실패 응답을 리턴한다”는 개념은 비즈니스적으로는 실패지만, 애플리케이션 입장에선 예외가 아닐 때 사용된다.

  • 예외(Exception) = 시스템 레벨에서 "예상하지 못한 오류"
    - 예: DB 연결 실패, NullPointerException, 존재하지 않는 리소스 접근 등
  • 실패 응답(ResponseEntity 400/401/403/422 등) = 비즈니스 로직상 실패지만, 예상 가능한 정상 흐름
    - 예: 비밀번호 틀림, 중복된 이메일, 권한 없음 등

예시 1) 로그인 실패

: 비밀번호가 틀린 것은 서버 잘못 X
: 사용자 입력의 문제니까 예외를 throw 하는 것이 아니라 실패 응답을 줘도 됨

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDto request) {
    Member member = memberRepository.findByEmail(request.getEmail());

    if (member == null || !member.getPassword().equals(request.getPassword())) {
        // ❗ 비즈니스 실패 → 401 반환 (정상 흐름)
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("이메일 또는 비밀번호가 틀렸습니다");
    }

    // 로그인 성공
    return ResponseEntity.ok("로그인 성공");
}

예시 2) 회원가입 - 이메일 중복

: 이미 있는 이메일로 요청한 건 예상 가능한 상황이니까,
: 굳이 예외로 처리할 필요 없이 409 CONFLICT 응답으로 처리 가능

@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody SignupRequestDto request) {
    if (memberRepository.existsByEmail(request.getEmail())) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body("이미 존재하는 이메일입니다");
    }

    memberService.createMember(request);
    return ResponseEntity.status(HttpStatus.CREATED).body("회원가입 성공");
}

왜 예외(Exception) 를 안 쓰고 ResponseEntity 로 직접 처리할까?

1. 예외는 비용이 크다 (성능 이슈)

  • 자바에서 throw new Exception() 을 하면 실제로 stack trace 를 캡처하고 처리하는 데 비용이 큼
  • 잦은 흐름 제어에 예외를 남발하면 성능 저하 + 코드 복잡해짐

2. 비즈니스 실패는 예외가 아니다

이메일 중복 ❌ → 409 CONFLICT 응답으로 충분
비밀번호 틀림 ❌ → 401 UNAUTHORIZED 응답으로 충분
DB 연결 오류 ✅ → 예외
존재하지 않는 ID 조회 ✅ → 보통 예외 던짐 (404 Not Found)

Exception 의 stack trace 란 무엇일까?

"Exception 은 Controller 밖에서 처리되기 때문에 비용이 크다" 라는 말이 있다.
이것은 예외가 발생하면 실행 흐름이 '정상 루트'에서 벗어나서 스프링 내부의 예외 처리 시스템으로 빠진다는 뜻이다.

throw new RuntimeException("에러 발생");

이런 코드가 실행되면, 자바는 아래와 같은 과정을 밟는다.

  1. 현재 실행 중인 메서드를 즉시 중단
  2. 스택을 타고 호출자를 거슬러 올라가면서 예외를 처리할 곳을 찾음
  3. 아무도 try-catch 로 잡지 않으면 --> Spring 이 최상단에서 잡음
  4. 그 후에 에러 응답을 만들어서 클라이언트에게 보냄

Exception 은 왜 비용이 클까?

  1. 스택 추적 비용
  • 자바는 예외가 발생하면 Stack trace 정보를 캡처함
  • 이것은 꽤 무거운 연산이다 (Stack 프레임을 전부 캡처해서 메모리에 보관)
  1. 흐름을 강제로 꺾음
  • 메서드를 계속 타고 내려가던 정상 흐름을 중간에 끊고 바깥으로 튀어나가야 함
  • 결국 스프링 내부의 @ExceptionHandler, ResponseEntityExceptionHandler, DispatcherServlet 까지 거슬러 올라가게 됨
  1. 예외는 느린 연산
  • JIT 컴파일러가 예외 흐름은 최적화 하지 않음
  • 따라서 성능 튜닝시, 예외 남용은 절대 금지!


but, 성능 걱정은 하지 않아도 된다고 한다🌟🌟🌟

Java 에서 예외가 비용이 크다는 말은,
try-catch 를 반복적으로 마이크로성능 튜닝할 때와 같은 경우의 고려사항이라고 한다.
HTTP 요청/응답 단위에서 던지는 예외는 비용 측면에서 무시해도 됨

stack trace 가 생성되더라도 예외 처리의 비용은 크지 않다!
HTTP 단위에서는 신경쓰지 말자!

Java 예외 처리 비용은 왜 논쟁이 될까?

Java 에서 예외는 정상 흐름이 아니고, 내부적으로 다음 작업이 발생함

  1. stack trace 캡처 (비용 발생)
  2. 예외 객체 생성 (힙 메모리 생성)
  3. call stack 따라 올라가며 예외 전달

그래서 tight loop 안에서 try-catch 를 수천, 수만 번 반복하면 --> 성능 저하 있음

하지만! Web API 에서는 전혀 해당사항 없음!🌟🌟🌟

HTTP 요청 1건 처리 단위

  • 컨트롤러 --> 서비스 --> 예외 발생 --> 핸들러 --> 응답 생성
  • 이 전체 흐름에서 예외가 한 번 발생하는 것은 아주 가벼운 일
  • 대부분의 비용은 DB I/O, HTTP 직렬화/역직렬화, 네트워크 전송에 있음

0개의 댓글