Skip to content

Commit

Permalink
Merge pull request #761 from woowacourse-teams/521/feat-be-error-code
Browse files Browse the repository at this point in the history
에러 코드 구현 및 적용
  • Loading branch information
zeus6768 authored Oct 11, 2024
2 parents dd9b670 + 776fac1 commit c5cf32f
Show file tree
Hide file tree
Showing 35 changed files with 176 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public interface SpringDocAuthController {
})
@ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/login", errorCases = {
@ErrorCase(description = "아이디 불일치", exampleMessage = "존재하지 않는 아이디 moly 입니다."),
@ErrorCase(description = "비밀번호 불일치", exampleMessage = "로그인에 실패하였습니다. 아이디 또는 비밀번호를 확인해주세요."),
@ErrorCase(description = "비밀번호 불일치", exampleMessage = "로그인에 실패하였습니다. 비밀번호를 확인해주세요."),
})
ResponseEntity<LoginResponse> login(LoginRequest request, HttpServletResponse response);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;

@Component
public class SHA2PasswordEncryptor implements PasswordEncryptor {
Expand All @@ -17,7 +17,7 @@ public SHA2PasswordEncryptor() {
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new CodeZapException(HttpStatus.INTERNAL_SERVER_ERROR, "암호화 알고리즘이 잘못 명시되었습니다.");
throw new CodeZapException(ErrorCode.INTERNAL_SERVER_ERROR, "암호화 알고리즘이 잘못 명시되었습니다.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;

@Component
public class CookieCredentialManager implements CredentialManager {
Expand All @@ -29,7 +29,7 @@ public String getCredential(final HttpServletRequest httpServletRequest) {

private void checkCookieExist(final Cookie[] cookies) {
if (cookies == null) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요.");
throw new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요.");
}
}

Expand All @@ -47,7 +47,7 @@ private Cookie extractTokenCookie(final Cookie[] cookies) {
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(CREDENTIAL_COOKIE_NAME))
.findFirst()
.orElseThrow(() -> new CodeZapException(HttpStatus.UNAUTHORIZED,
.orElseThrow(() -> new CodeZapException(ErrorCode.UNAUTHORIZED_USER,
"인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import java.nio.charset.StandardCharsets;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import codezap.auth.provider.CredentialProvider;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import codezap.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -33,7 +33,7 @@ public Member extractMember(String credential) {

private void checkMatchPassword(Member member, String password) {
if (!member.matchPassword(password)) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 일치하지 않습니다.");
throw new CodeZapException(ErrorCode.UNAUTHORIZED_PASSWORD, "비밀번호가 일치하지 않습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.springframework.http.HttpStatus;

import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

Expand All @@ -26,13 +25,13 @@ private static String decodeBase64(String base64Credentials) {
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
return new String(credDecoded, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "잘못된 Base64 인코딩입니다.");
throw new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "잘못된 Base64 인코딩입니다.");
}
}

private static void validateBasicAuth(String[] values) {
if (values.length != BASIC_AUTH_LENGTH || values[0].isEmpty() || values[1].isEmpty()) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "인증 정보가 올바르지 않습니다. 다시 로그인 해주세요.");
throw new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증 정보가 올바르지 않습니다. 다시 로그인 해주세요.");
}
}
}
4 changes: 2 additions & 2 deletions backend/src/main/java/codezap/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package codezap.auth.service;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import codezap.auth.dto.LoginAndCredentialDto;
Expand All @@ -9,6 +8,7 @@
import codezap.auth.encryption.PasswordEncryptor;
import codezap.auth.provider.CredentialProvider;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import codezap.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -37,7 +37,7 @@ private void validateCorrectPassword(Member member, String password) {
String salt = member.getSalt();
String encryptedPassword = passwordEncryptor.encrypt(password, salt);
if (!member.matchPassword(encryptedPassword)) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "로그인에 실패하였습니다. 아이디 또는 비밀번호를 확인해주세요.");
throw new CodeZapException(ErrorCode.UNAUTHORIZED_PASSWORD, "로그인에 실패하였습니다. 비밀번호를 확인해주세요.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public interface SpringDocCategoryController {
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "모든 필드 중 null인 값이 있는 경우", exampleMessage = "카테고리 이름이 null 입니다."),
@ErrorCase(description = "카테고리 이름이 15자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 15자까지 입력 가능합니다."),
})
@ApiErrorResponse(status = HttpStatus.CONFLICT, instance = "/categories", errorCases = {
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다."),
})
ResponseEntity<CreateCategoryResponse> createCategory(
Expand All @@ -49,7 +51,11 @@ ResponseEntity<CreateCategoryResponse> createCategory(
@ApiResponse(responseCode = "200", description = "카테고리 수정 성공")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories/1", errorCases = {
@ErrorCase(description = "카테고리 이름이 15자를 초과한 경우", exampleMessage = "카테고리 이름은 최대 15자까지 입력 가능합니다."),
})
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/categories/1", errorCases = {
@ErrorCase(description = "해당하는 id 값인 카테고리가 없는 경우", exampleMessage = "식별자 1에 해당하는 카테고리가 존재하지 않습니다."),
})
@ApiErrorResponse(status = HttpStatus.CONFLICT, instance = "/categories", errorCases = {
@ErrorCase(description = "동일한 이름의 카테고리가 존재하는 경우", exampleMessage = "이름이 Spring 인 카테고리가 이미 존재합니다."),
})
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/categories/1", errorCases = {
Expand Down
5 changes: 2 additions & 3 deletions backend/src/main/java/codezap/category/domain/Category.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import org.springframework.http.HttpStatus;

import codezap.global.auditing.BaseTimeEntity;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -67,7 +66,7 @@ public void updateName(String name) {

public void validateAuthorization(Member member) {
if (!getMember().equals(member)) {
throw new CodeZapException(HttpStatus.UNAUTHORIZED, "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.");
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;

import codezap.category.domain.Category;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;

@SuppressWarnings("unused")
public interface CategoryJpaRepository extends CategoryRepository, JpaRepository<Category, Long> {

default Category fetchById(Long id) {
return findById(id).orElseThrow(
() -> new CodeZapException(HttpStatus.NOT_FOUND, "식별자 " + id + "에 해당하는 카테고리가 존재하지 않습니다."));
() -> new CodeZapException(ErrorCode.RESOURCE_NOT_FOUND, "식별자 " + id + "에 해당하는 카테고리가 존재하지 않습니다."));
}

List<Category> findAllByMemberIdOrderById(Long memberId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package codezap.category.service;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -11,6 +10,7 @@
import codezap.category.dto.response.FindAllCategoriesResponse;
import codezap.category.repository.CategoryRepository;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import codezap.template.repository.TemplateRepository;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -52,7 +52,7 @@ public void update(Member member, Long id, UpdateCategoryRequest updateCategoryR

private void validateDuplicatedCategory(String categoryName, Member member) {
if (categoryRepository.existsByNameAndMember(categoryName, member)) {
throw new CodeZapException(HttpStatus.CONFLICT, "이름이 " + categoryName + "인 카테고리가 이미 존재합니다.");
throw new CodeZapException(ErrorCode.DUPLICATE_CATEGORY, "이름이 " + categoryName + "인 카테고리가 이미 존재합니다.");
}
}

Expand All @@ -62,10 +62,10 @@ public void deleteById(Member member, Long id) {
category.validateAuthorization(member);

if (templateRepository.existsByCategoryId(id)) {
throw new CodeZapException(HttpStatus.BAD_REQUEST, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다.");
throw new CodeZapException(ErrorCode.CATEGORY_HAS_TEMPLATES, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다.");
}
if (category.isDefault()) {
throw new CodeZapException(HttpStatus.BAD_REQUEST, "기본 카테고리는 삭제할 수 없습니다.");
throw new CodeZapException(ErrorCode.DEFAULT_CATEGORY, "기본 카테고리는 삭제할 수 없습니다.");
}
categoryRepository.deleteById(id);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package codezap.global.exception;

import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;

import lombok.Getter;

@Getter
public class CodeZapException extends RuntimeException {

private final HttpStatusCode httpStatusCode;
private final ErrorCode errorCode;

public CodeZapException(HttpStatusCode httpStatusCode, String message) {
public CodeZapException(ErrorCode errorCode, String message) {
super(message);
this.httpStatusCode = httpStatusCode;
this.errorCode = errorCode;
}

public ProblemDetail toProblemDetail() {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
errorCode.getHttpStatus(),
getMessage());
return GlobalExceptionHandler.setProperties(problemDetail, errorCode.getCode());
}
}
31 changes: 31 additions & 0 deletions backend/src/main/java/codezap/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package codezap.global.exception;

import org.springframework.http.HttpStatus;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ErrorCode {

SPRING_GLOBAL_EXCEPTION(1000, HttpStatus.BAD_REQUEST),

INVALID_REQUEST(1101, HttpStatus.BAD_REQUEST),
CATEGORY_HAS_TEMPLATES(1102, HttpStatus.BAD_REQUEST),
DEFAULT_CATEGORY(1103, HttpStatus.BAD_REQUEST),

RESOURCE_NOT_FOUND(1201, HttpStatus.NOT_FOUND),
DUPLICATE_ID(1202, HttpStatus.CONFLICT),
DUPLICATE_CATEGORY(1203, HttpStatus.CONFLICT),

UNAUTHORIZED_USER(1301, HttpStatus.UNAUTHORIZED),
UNAUTHORIZED_ID(1302, HttpStatus.UNAUTHORIZED),
UNAUTHORIZED_PASSWORD(1303, HttpStatus.UNAUTHORIZED),
FORBIDDEN_ACCESS(1304, HttpStatus.FORBIDDEN),

INTERNAL_SERVER_ERROR(2000, HttpStatus.INTERNAL_SERVER_ERROR);

private final int code;
private final HttpStatus httpStatus;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package codezap.global.exception;

import java.time.LocalDateTime;
import java.util.List;

import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -21,40 +22,59 @@
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

private static final String PROPERTY_ERROR_CODE = "errorCode";
private static final String PROPERTY_TIMESTAMP = "timestamp";

@ExceptionHandler
public ResponseEntity<ProblemDetail> handleCodeZapException(CodeZapException codeZapException) {
log.info("[CodeZapException] {}가 발생했습니다.", codeZapException.getClass().getName(), codeZapException);
return ResponseEntity.status(codeZapException.getHttpStatusCode())
.body(ProblemDetail.forStatusAndDetail(
codeZapException.getHttpStatusCode(),
codeZapException.getMessage())
);

return ResponseEntity.status(codeZapException.getErrorCode().getHttpStatus())
.body(codeZapException.toProblemDetail());
}

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException exception, HttpHeaders headers, HttpStatusCode status, WebRequest request
) {
log.info("[MethodArgumentNotValidException] {}가 발생했습니다. \n", exception.getClass().getName(), exception);

BindingResult bindingResult = exception.getBindingResult();
List<String> errorMessages = bindingResult.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
CodeZapException codeZapException =
new CodeZapException(ErrorCode.INVALID_REQUEST, String.join("\n", errorMessages));

log.info("[MethodArgumentNotValidException] {}가 발생했습니다. \n", exception.getClass().getName(), exception);
return ResponseEntity.badRequest()
.body(ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
String.join("\n", errorMessages))
);
return ResponseEntity.status(codeZapException.getErrorCode().getHttpStatus())
.body(codeZapException.toProblemDetail());
}

@ExceptionHandler
public ResponseEntity<ProblemDetail> handleException(Exception exception) {
log.error("[Exception] 예상치 못한 오류 {} 가 발생했습니다.", exception.getClass().getName(), exception);
CodeZapException codeZapException =
new CodeZapException(ErrorCode.INTERNAL_SERVER_ERROR, "서버에서 예상치 못한 오류가 발생하였습니다.");
return ResponseEntity.internalServerError()
.body(ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"서버에서 예상치 못한 오류가 발생하였습니다.")
);
.body(codeZapException.toProblemDetail());
}

@Override
protected ResponseEntity<Object> createResponseEntity(
@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request
) {
ProblemDetail problemDetail = ProblemDetail.forStatus(statusCode);
if (body instanceof Exception) {
problemDetail.setDetail(((Exception) body).getMessage());
}
return ResponseEntity.status(statusCode)
.body(setProperties(problemDetail, ErrorCode.SPRING_GLOBAL_EXCEPTION.getCode()));
}

public static ProblemDetail setProperties(ProblemDetail problemDetail, int code) {
problemDetail.setProperty(PROPERTY_ERROR_CODE, code);
problemDetail.setProperty(PROPERTY_TIMESTAMP, LocalDateTime.now());

return problemDetail;
}
}
Loading

0 comments on commit c5cf32f

Please sign in to comment.