본문 바로가기
대외활동/DRACONIST-백엔드

06. errorstatus 처리 DRACONIST

by 피스타0204 2025. 3. 4.

 

1. 에러 핸들 컨트롤러 

우리는 response에에러를 반환할 것입니다. 이것을 간단하고 쉽게 하기 위해 에러 핸들러를 먼저 작성하겠습니다.

1. ResourceErrorController

package com.draconist.goodluckynews.global.exception.controller;


import com.draconist.goodluckynews.global.enums.statuscode.ErrorStatus;
import com.draconist.goodluckynews.global.exception.GeneralException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// 잘못된 엔드포인트로 api가 요청된 경우
@RestController
public class ResourceErrorController {
    @RequestMapping("/**")
    public void handleNotFound() {
        throw new GeneralException(ErrorStatus.RESOURCE_NOT_FOUND);
    }
}
  • 이 클래스는 잘못된 엔드포인트로 API가 요청된 경우를 처리합니다. @RestController 애노테이션을 사용하여 RESTful 웹 서비스에서 이 컨트롤러를 사용하도록 지정합니다.
  • @RequestMapping("/**")는 모든 URL 요청에 대해 처리하도록 설정합니다. /**는 모든 경로를 의미하는 와일드카드입니다.
  • 잘못된 엔드포인트가 요청되면 handleNotFound() 메서드가 호출되어 GeneralException을 던집니다.
  • GeneralException은 커스텀 예외로, ErrorStatus.RESOURCE_NOT_FOUND라는 오류 상태를 사용하여 예외를 발생시킵니다.
  • 결과적으로 잘못된 경로로 요청된 API는 GeneralException을 발생시키고, 그에 따른 예외 처리 로직이 실행됩니다.

2. ExceptionAdvice

package com.draconist.goodluckynews.global.exception;

import com.draconist.goodluckynews.global.enums.statuscode.BaseCode;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

// 커스텀  예외 처리 일반화
@RequiredArgsConstructor
public class GeneralException extends RuntimeException{
    private final BaseCode errorStatus;

    public String getErrorCode() {
        return errorStatus.getCode();
    }

    public String getErrorReason() {
        return errorStatus.getMessage();
    }

    public HttpStatus getHttpStatus() {
        return errorStatus.getHttpStatus();
    }

    @Override
    public String getMessage() {
        return errorStatus.getMessage();
    }
}
  • @RestControllerAdvice: Spring의 전역 예외 처리 어드바이저로, 모든 컨트롤러에서 발생하는 예외를 처리합니다.
  • 예외마다 @ExceptionHandler를 사용하여 처리합니다.

예외 처리 흐름:

  1. MethodArgumentNotValidException 처리:
    • 이 예외는 유효성 검사 실패로 발생하며, 예를 들어 @Valid를 사용한 필드가 잘못된 경우입니다.
    • exception.getBindingResult().getAllErrors()를 사용하여 각 필드의 오류 메시지를 추출하여 Map<String, String>으로 저장합니다.
    • 이를 바탕으로 API 응답을 생성하고, BAD_REQUEST (400) 상태 코드와 함께 반환합니다.
  2. GeneralException 처리:
    • GeneralException은 커스텀 예외로, 코드에서 정의된 errorCode, errorReason을 응답으로 반환합니다.
    • 예외 처리 시, HttpStatus를 GeneralException에서 제공된 상태 코드에 따라 설정합니다.
  3. 기타 예외 처리:
    • 모든 다른 예외는 **Exception.class**를 통해 처리됩니다.
    • 기본적인 서버 오류를 의미하는 INTERNAL_SERVER_ERROR (500) 상태 코드를 사용하며, 문제 해결을 위한 안내 메시지도 포함됩니다.

3. GeneralException

package com.draconist.goodluckynews.global.exception;

import com.draconist.goodluckynews.global.enums.statuscode.ErrorStatus;
import com.draconist.goodluckynews.global.response.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {

    // 요청 파라미터 유효성 검사 실패 시 호출
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<?>> handleValidationExceptions(MethodArgumentNotValidException exception) {
        Map<String, String> errors = new HashMap<>();
        exception.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        ApiResponse<Object> response = ApiResponse
                .onFailure(ErrorStatus._BAD_REQUEST.getCode(), "유효성 검사 실패", errors);
        log.error("Validation error: {}", errors);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

    // Custom Error Handler
    // GeneralException 및 하위 클래스에 대한 예외 처리
    @ExceptionHandler(GeneralException.class)
    public ResponseEntity<ApiResponse<?>> handleGeneralException(GeneralException exception) {
        ApiResponse<Object> response = ApiResponse
                .onFailure(exception.getErrorCode(), exception.getErrorReason(), null);
        log.error("General exception: {}", exception.getErrorReason());
        return new ResponseEntity<>(response, exception.getHttpStatus());
    }

    // 그 외의 모든 예외의 경우
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<?>> handleException(Exception exception) {
        ApiResponse<Object> response = ApiResponse.onFailure(
                ErrorStatus._INTERNAL_SERVER_ERROR.getCode(),
                "오류가 발생하였습니다. " +
                        "1. 토큰을 삽입했는지 확인 해주세요. " +
                        "2. 토큰의 유효기간을 확인 해주세요.(새로 발급하여 시도해보세요.)" +
                        "문제가 해결되지 않는다면, 관리자에게 문의 해주세요.",
                exception.getMessage());
        log.error("Unhandled exception: {}", exception.getMessage(), exception);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
}
  • **GeneralException**은 커스텀 예외 클래스로, BaseCode라는 열거형(enum)을 사용하여 오류 코드, 메시지, HTTP 상태 코드를 정의합니다.
  • BaseCode는 오류 코드와 메시지를 정의하는 enum으로 보이며, GeneralException은 이를 통해 발생한 오류에 대한 상세 정보를 제공합니다.
  • GeneralException은 RuntimeException을 상속하고 있기 때문에, 예외 발생 시 자동으로 던져지고, 전역 예외 처리에서 이를 잡아 처리합니다.
  • getErrorCode(), getErrorReason(), getHttpStatus() 메서드를 통해 오류 코드와 상태를 가져올 수 있습니다.

요약

이 코드는 Spring Boot 애플리케이션에서 발생할 수 있는 예외를 전역적으로 처리하는 방식을 제공합니다.

  1. ResourceErrorController는 잘못된 엔드포인트로의 요청을 처리하며, GeneralException을 던집니다.
  2. ExceptionAdvice는 예외를 처리하는 글로벌 핸들러로, MethodArgumentNotValidException (유효성 검사 실패), GeneralException (커스텀 예외), 기타 모든 예외를 적절히 처리하고, 클라이언트에게 적절한 오류 응답을 반환합니다.
  3. GeneralException은 커스텀 예외 클래스로, 오류 코드와 메시지, HTTP 상태 코드를 정의합니다. 이 예외가 발생하면 ExceptionAdvice에서 처리되어 클라이언트에게 응답됩니다.

이 코드의 주요 목적은 애플리케이션 전반에서 발생할 수 있는 예외를 중앙집중식으로 처리하여 클라이언트에게 일관된 형식의 오류 메시지를 반환하는 것입니다.

 

2. Errorstatus

HttpStatus는 Spring Boot에서 제공하는 HTTP 응답 상태 코드(Enum) 입니다.
Spring의 org.springframework.http.HttpStatus Enum 클래스에서 가져온 것입니다.
각 상태 코드는 클라이언트가 요청한 작업의 성공 여부나 실패 원인을 나타냅니다.

이것으로 큼직큼직한 상태코드(200, 400 등등..) 을 묶어서 표시할 수 있습니다.

 

HTTP 상태 코드설명

200 OK 요청이 성공적으로 처리됨
201 CREATED 새로운 리소스가 생성됨
400 BAD_REQUEST 잘못된 요청 (클라이언트 측 오류)
401 UNAUTHORIZED 인증되지 않은 사용자 (로그인 필요)
403 FORBIDDEN 권한이 없음 (로그인해도 접근 불가)
404 NOT_FOUND 요청한 리소스를 찾을 수 없음
500 INTERNAL_SERVER_ERROR 서버 내부 오류

 

 

Enum 상수는 객체이기 때문에 객체의 필드 값을 설정하기 위해서는 Enum상수 정의 후 final필드(상속되지 않는 필드)를 선언하는 것이 자연스럽습니다.

 

이제 status를 작성해보겠습니다.

BaseCode.js

package com.draconist.goodluckynews.global.enums.statuscode;

import org.springframework.http.HttpStatus;

// Status Base Code
public interface BaseCode {
    String getCode();
    String getMessage();
    HttpStatus getHttpStatus();
    Integer getStatusValue();
}

Errorstatus.java

package com.draconist.goodluckynews.global.enums.statuscode;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum ErrorStatus implements BaseCode {

	// 공통 오류
	_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
	_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
	_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
	_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

	// Member Error
	MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "해당하는 사용자를 찾을 수 없습니다."),
	PASSWORD_NOT_CORRECT(HttpStatus.FORBIDDEN, "MEMBER4002", "비밀번호가 일치하지 않습니다."),
	_MEMBER_IS_EXISTS(HttpStatus.FORBIDDEN, "MEMBER4003", "해당하는 사용자가 이미 존재합니다."),

	// Resource Error
	RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "RESOURCE4001", "잘못된 API 요청입니다. 요청 형식을 다시 확인해주세요." +
			"반복적인 오류 발생 시 관리자에게 문의해주세요."),

	// 로그인 실패 사유
	INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH4001", "아이디 또는 비밀번호가 잘못되었습니다."),

	// S3 에러
	_S3_REMOVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S35004", "S3 파일 삭제 중 오류가 발생하였습니다."),

	// JWT Error
	TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "TOKEN4001", "토큰이 없거나 만료 되었습니다."),
	TOKEN_NO_AUTHORIZATION(HttpStatus.UNAUTHORIZED, "TOKEN4002", "토큰에 권한이 없습니다."),

	// **✅ 추가된 Article 관련 오류**
	_CRAWLFAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ARTICLE4001", "기사 크롤링 중 오류가 발생했습니다."),
	_ARTICLE_TITLE_MISSING(HttpStatus.BAD_REQUEST, "ARTICLE4002", "기사 제목이 누락되었습니다."),
	_ARTICLE_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "ARTICLE4003", "기사 본문이 누락되었습니다."),
	_ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4004", "해당하는 기사를 찾을 수 없습니다."),

	// **✅ 추가된 Heart(좋아요) 관련 오류**
	_ALREADY_HEARTED(HttpStatus.BAD_REQUEST, "HEART4001", "이미 좋아요를 누른 상태입니다."),
	_HEART_NOT_FOUND(HttpStatus.NOT_FOUND, "HEART4002", "좋아요를 찾을 수 없습니다."),

	// **✅ 추가된 CompletedTime(기사 완료) 관련 오류**
	_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "ARTICLE4005", "이미 완료된 기사입니다."),

	// **✅ 추가된 페이지네이션 관련 오류**
	_PAGE_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "PAGE4001", "잘못된 페이지네이션 요청입니다. 페이지 번호는 0 이상이어야 합니다."),
	_PAGE_EMPTY_RESULT(HttpStatus.NOT_FOUND, "PAGE4002", "조회된 페이지가 없습니다. 다시 확인해주세요."),

	// Place (커뮤니티) 에러
	PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE4001", "삭제되었거나 존재하지 않는 커뮤니티입니다."),
	_DUPLICATE_PLACE_NAME(HttpStatus.FORBIDDEN, "PLACE4002", "해당하는 이름의 플레이스가 이미 존재합니다. 다른 이름으로 작성해주세요."),
	_PLACE_UPDATE_FAILED(HttpStatus.BAD_REQUEST, "PLACE4003", "플레이스 정보를 수정하는 중 오류가 발생하였습니다."),
	_PLACE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "PLACE4004", "플레이스 삭제 중 오류가 발생하였습니다."),
	UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "PLACE4005", "해당 플레이스를 수정 또는 삭제할 권한이 없습니다.");

	private final HttpStatus httpStatus;
	private final String code;
	private final String message;

	// implement of BaseCode
	@Override
	public String getCode() {
		return code;
	}

	@Override
	public String getMessage() {
		return message;
	}

	@Override
	public HttpStatus getHttpStatus() {
		return httpStatus;
	}

	@Override
	public Integer getStatusValue() {
		return httpStatus.value();
	}
}

 

SuccessStatus.java

package com.draconist.goodluckynews.global.enums.statuscode;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@RequiredArgsConstructor
public enum ErrorStatus implements BaseCode {

	// 공통 오류
	_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
	_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
	_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
	_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

	// Member Error
	MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "해당하는 사용자를 찾을 수 없습니다."),
	PASSWORD_NOT_CORRECT(HttpStatus.FORBIDDEN, "MEMBER4002", "비밀번호가 일치하지 않습니다."),
	_MEMBER_IS_EXISTS(HttpStatus.FORBIDDEN, "MEMBER4003", "해당하는 사용자가 이미 존재합니다."),

	// Resource Error
	RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "RESOURCE4001", "잘못된 API 요청입니다. 요청 형식을 다시 확인해주세요." +
			"반복적인 오류 발생 시 관리자에게 문의해주세요."),

	// 로그인 실패 사유
	INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "AUTH4001", "아이디 또는 비밀번호가 잘못되었습니다."),

	// S3 에러
	_S3_REMOVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S35004", "S3 파일 삭제 중 오류가 발생하였습니다."),

	// JWT Error
	TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "TOKEN4001", "토큰이 없거나 만료 되었습니다."),
	TOKEN_NO_AUTHORIZATION(HttpStatus.UNAUTHORIZED, "TOKEN4002", "토큰에 권한이 없습니다."),

	// **✅ 추가된 Article 관련 오류**
	_CRAWLFAILED(HttpStatus.INTERNAL_SERVER_ERROR, "ARTICLE4001", "기사 크롤링 중 오류가 발생했습니다."),
	_ARTICLE_TITLE_MISSING(HttpStatus.BAD_REQUEST, "ARTICLE4002", "기사 제목이 누락되었습니다."),
	_ARTICLE_CONTENT_MISSING(HttpStatus.BAD_REQUEST, "ARTICLE4003", "기사 본문이 누락되었습니다."),
	_ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4004", "해당하는 기사를 찾을 수 없습니다."),

	// **✅ 추가된 Heart(좋아요) 관련 오류**
	_ALREADY_HEARTED(HttpStatus.BAD_REQUEST, "HEART4001", "이미 좋아요를 누른 상태입니다."),
	_HEART_NOT_FOUND(HttpStatus.NOT_FOUND, "HEART4002", "좋아요를 찾을 수 없습니다."),

	// **✅ 추가된 CompletedTime(기사 완료) 관련 오류**
	_ALREADY_COMPLETED(HttpStatus.BAD_REQUEST, "ARTICLE4005", "이미 완료된 기사입니다."),

	// **✅ 추가된 페이지네이션 관련 오류**
	_PAGE_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "PAGE4001", "잘못된 페이지네이션 요청입니다. 페이지 번호는 0 이상이어야 합니다."),
	_PAGE_EMPTY_RESULT(HttpStatus.NOT_FOUND, "PAGE4002", "조회된 페이지가 없습니다. 다시 확인해주세요."),

	// Place (커뮤니티) 에러
	PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE4001", "삭제되었거나 존재하지 않는 커뮤니티입니다."),
	_DUPLICATE_PLACE_NAME(HttpStatus.FORBIDDEN, "PLACE4002", "해당하는 이름의 플레이스가 이미 존재합니다. 다른 이름으로 작성해주세요."),
	_PLACE_UPDATE_FAILED(HttpStatus.BAD_REQUEST, "PLACE4003", "플레이스 정보를 수정하는 중 오류가 발생하였습니다."),
	_PLACE_DELETE_FAILED(HttpStatus.BAD_REQUEST, "PLACE4004", "플레이스 삭제 중 오류가 발생하였습니다."),
	UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "PLACE4005", "해당 플레이스를 수정 또는 삭제할 권한이 없습니다.");

	private final HttpStatus httpStatus;
	private final String code;
	private final String message;

	// implement of BaseCode
	@Override
	public String getCode() {
		return code;
	}

	@Override
	public String getMessage() {
		return message;
	}

	@Override
	public HttpStatus getHttpStatus() {
		return httpStatus;
	}

	@Override
	public Integer getStatusValue() {
		return httpStatus.value();
	}
}

 

 

사용)

import com.draconist.goodluckynews.domain.member.entity.Member;
import com.draconist.goodluckynews.domain.member.repository.MemberRepository;
import com.draconist.goodluckynews.global.awss3.service.AwsS3Service;
import com.draconist.goodluckynews.global.enums.statuscode.ErrorStatus;
import com.draconist.goodluckynews.global.exception.GeneralException;
import com.draconist.goodluckynews.global.jwt.util.JwtUtil;
import com.draconist.goodluckynews.global.response.ApiResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final BCryptPasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final AwsS3Service awsS3Service;
    private final MemberConverter memberConverter;

    // 로그인
    @Transactional(readOnly = true)
    public ResponseEntity<?> login(LoginRequestDTO dto) {
        String email = dto.getEmail();
        String password = dto.getPassword();
        Member member = memberRepository.findMemberByEmail(email)
                .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND));

        // 비밀번호 검증
        if(!passwordEncoder.matches(password, member.getPassword())) {
            throw new GeneralException(ErrorStatus.PASSWORD_NOT_CORRECT); //여기보세요
        }

        return getJwtResponseEntity(member);
    }