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를 사용하여 처리합니다.
예외 처리 흐름:
- MethodArgumentNotValidException 처리:
- 이 예외는 유효성 검사 실패로 발생하며, 예를 들어 @Valid를 사용한 필드가 잘못된 경우입니다.
- exception.getBindingResult().getAllErrors()를 사용하여 각 필드의 오류 메시지를 추출하여 Map<String, String>으로 저장합니다.
- 이를 바탕으로 API 응답을 생성하고, BAD_REQUEST (400) 상태 코드와 함께 반환합니다.
- GeneralException 처리:
- GeneralException은 커스텀 예외로, 코드에서 정의된 errorCode, errorReason을 응답으로 반환합니다.
- 예외 처리 시, HttpStatus를 GeneralException에서 제공된 상태 코드에 따라 설정합니다.
- 기타 예외 처리:
- 모든 다른 예외는 **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 애플리케이션에서 발생할 수 있는 예외를 전역적으로 처리하는 방식을 제공합니다.
- ResourceErrorController는 잘못된 엔드포인트로의 요청을 처리하며, GeneralException을 던집니다.
- ExceptionAdvice는 예외를 처리하는 글로벌 핸들러로, MethodArgumentNotValidException (유효성 검사 실패), GeneralException (커스텀 예외), 기타 모든 예외를 적절히 처리하고, 클라이언트에게 적절한 오류 응답을 반환합니다.
- 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);
}
'대외활동 > DRACONIST-백엔드' 카테고리의 다른 글
<DRACONIST(코딩용사들)>를 소개합니다 (1) | 2025.03.16 |
---|---|
null처리 오류-회원정보 이미지 수정 (0) | 2025.03.16 |
05.이미지 버킷 생성부터 이미지 저장하기까지 DRACONIST (0) | 2025.03.04 |
01. jwt 토큰 구현하기 DRACONIST (1) | 2025.03.04 |
07. ec2 인스턴스만들기 부터 도커. DRACONIST (0) | 2025.03.04 |