Skip to content

Commit 0b30264

Browse files
authored
Merge pull request #29 from Hoyoung027/Hoyoung027
[22기_변호영] 동시성 & 결제 연동 미션 제출합니다.
2 parents 5eacb1e + 82aa23a commit 0b30264

File tree

100 files changed

+2445
-711
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+2445
-711
lines changed

README.md

Lines changed: 466 additions & 1 deletion
Large diffs are not rendered by default.

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ dependencies {
4141
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
4242
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
4343
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
44+
45+
// redisson
46+
implementation("org.redisson:redisson-spring-boot-starter:3.52.0")
47+
implementation(platform("io.netty:netty-bom:4.1.127.Final"))
48+
implementation("io.netty:netty-resolver-dns")
4449
}
4550

4651
tasks.named('test') {

src/main/java/com/ceos22/cgv/codes/ErrorCode.java

Lines changed: 0 additions & 55 deletions
This file was deleted.

src/main/java/com/ceos22/cgv/codes/SuccessCode.java

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.ceos22.cgv.common.codes;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum ErrorCode {
7+
8+
// 잘못된 서버 요청
9+
BAD_REQUEST_ERROR(400, "Bad Request Exception"),
10+
11+
// @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
12+
NOT_VALID_ERROR(400, "handle Validation Exception"),
13+
14+
// @RequestBody 데이터 미 존재
15+
REQUEST_BODY_MISSING_ERROR(400,"Required request body is missing"),
16+
17+
// Request Header 가 누락된 경우
18+
MISSING_REQUEST_HEADER_ERROR(400, "Missing Request Header Exception"),
19+
20+
// 중복 닉네임 존재
21+
DUPLICATE_NICKNAME_ERROR(400, "Nickname already exist"),
22+
23+
// 유효하지 않은 타입
24+
INVALID_TYPE_VALUE(400, " Invalid Type Value"),
25+
26+
// Request Parameter 로 데이터가 전달되지 않을 경우
27+
MISSING_REQUEST_PARAMETER_ERROR(400, "Missing Servlet RequestParameter Exception"),
28+
29+
// Request Parameter가 Valid 하지 않은 경우
30+
INVALID_PARAMETER_ERROR(400, "Invalid RequestParameter Exception"),
31+
32+
// 인증 실패
33+
UNAUTHORIZED_ERROR(401, "Unauthorized Exception"),
34+
35+
// 권한이 없음
36+
FORBIDDEN_ERROR(403, "Forbidden Exception"),
37+
38+
// handler 존재 하지 않음
39+
NOT_FOUND_ERROR(404, "Not Found Exception"),
40+
41+
// 잘못된 경로로의 요청
42+
NO_RESOURCE_FOUND_ERROR(404, "No Resource Found Exception"),
43+
44+
// 지원하지 않는 HTTP Method
45+
METHOD_NOT_ALLOWED_ERROR(405, "Method Not Allowed"),
46+
47+
// 기존 데이터와 충돌
48+
CONFLICT_ERROR(409, "Conflict Exception"),
49+
50+
// 서버가 처리 할 방법을 모르는 경우 발생
51+
INTERNAL_SERVER_ERROR(500, "Internal Server Error Exception"),
52+
53+
;
54+
55+
private final int statusCode;
56+
57+
private final String message;
58+
59+
ErrorCode(final int statusCode, final String message) {
60+
this.statusCode = statusCode;
61+
this.message = message;
62+
}
63+
64+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.ceos22.cgv.common.codes;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum SuccessCode {
7+
8+
// 조회 성공
9+
GET_SUCCESS(200, "GET_SUCCESS"),
10+
11+
// 조회 성공
12+
LOGIN_SUCCESS(200, "LOGIN_SUCCESS"),
13+
14+
// 삭제 성공
15+
DELETE_SUCCESS(200, "DELETE_SUCCESS"),
16+
17+
// 삽입 성공
18+
INSERT_SUCCESS(201, "INSERT_SUCCESS"),
19+
20+
// 수정 성공
21+
UPDATE_SUCCESS(204, "UPDATE_SUCCESS"),
22+
23+
;
24+
25+
private final int statusCode;
26+
27+
private final String message;
28+
29+
SuccessCode(final int statusCode, final String message) {
30+
this.statusCode = statusCode;
31+
this.message = message;
32+
}
33+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.ceos22.cgv.common.config.exception;
2+
3+
import com.ceos22.cgv.common.codes.ErrorCode;
4+
import com.ceos22.cgv.common.response.ErrorResponse;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.http.converter.HttpMessageNotReadableException;
9+
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.security.core.AuthenticationException;
12+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
13+
import org.springframework.security.core.context.SecurityContextHolder;
14+
import org.springframework.web.HttpRequestMethodNotSupportedException;
15+
import org.springframework.web.bind.MethodArgumentNotValidException;
16+
import org.springframework.web.bind.MissingRequestHeaderException;
17+
import org.springframework.web.bind.MissingServletRequestParameterException;
18+
import org.springframework.web.bind.annotation.ControllerAdvice;
19+
import org.springframework.web.bind.annotation.ExceptionHandler;
20+
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
21+
import org.springframework.web.servlet.resource.NoResourceFoundException;
22+
import org.springframework.validation.BindException;
23+
24+
import jakarta.validation.ConstraintViolationException;
25+
import org.springframework.security.access.AccessDeniedException;
26+
import org.springframework.web.server.ResponseStatusException;
27+
28+
@Slf4j
29+
@ControllerAdvice
30+
public class GlobalExceptionHandler {
31+
32+
/**
33+
* @RequestBody 검증 실패 등 Parameter 값이 유효하지 않은 경우
34+
*/
35+
@ExceptionHandler(MethodArgumentNotValidException.class)
36+
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception){
37+
log.error("handleMethodArgumentNotValidException", exception);
38+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR);
39+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
40+
}
41+
42+
/**
43+
* 이미 존재하는 nickname으로 회원가입을 시도하는 경우 (예시)
44+
*/
45+
@ExceptionHandler(IllegalArgumentException.class)
46+
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException exception) {
47+
log.error("handleIllegalArgumentException", exception);
48+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.DUPLICATE_NICKNAME_ERROR);
49+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
50+
}
51+
52+
/**
53+
* parameter 바인딩 과정의 오류 (ex. @ModelAttribute 바인딩 실패 등)
54+
*/
55+
@ExceptionHandler(BindException.class)
56+
public ResponseEntity<ErrorResponse> handleBindException(BindException exception) {
57+
log.error("handleBindException", exception);
58+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR);
59+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
60+
}
61+
62+
/**
63+
* Body로 데이터가 넘어오지 않았을 경우
64+
*/
65+
@ExceptionHandler(HttpMessageNotReadableException.class)
66+
protected ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) {
67+
log.error("HttpMessageNotReadableException", exception);
68+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.REQUEST_BODY_MISSING_ERROR);
69+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
70+
}
71+
72+
/**
73+
* 필수 Request Parameter가 누락된 경우
74+
*/
75+
@ExceptionHandler(MissingServletRequestParameterException.class)
76+
protected ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) {
77+
log.error("handleMissingServletRequestParameterException", exception);
78+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.MISSING_REQUEST_PARAMETER_ERROR);
79+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
80+
}
81+
82+
/**
83+
* 필수 Request Header가 누락된 경우
84+
*/
85+
@ExceptionHandler(MissingRequestHeaderException.class)
86+
protected ResponseEntity<ErrorResponse> handleMissingRequestHeaderException(MissingRequestHeaderException exception) {
87+
log.error("handleMissingRequestHeaderException", exception);
88+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.MISSING_REQUEST_HEADER_ERROR);
89+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
90+
}
91+
92+
/**
93+
* 타입 변환 실패 (예: /api/{id} 에 문자열 전달)
94+
*/
95+
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
96+
protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) {
97+
log.error("handleMethodArgumentTypeMismatchException", exception);
98+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.INVALID_TYPE_VALUE);
99+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
100+
}
101+
102+
/**
103+
* @RequestParam, @PathVariable 등에서의 제약(@NotNull 등) 위반
104+
*/
105+
@ExceptionHandler(ConstraintViolationException.class)
106+
protected ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException exception) {
107+
log.error("handleConstraintViolationException", exception);
108+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR);
109+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
110+
}
111+
112+
/**
113+
* 지원하지 않는 HTTP Method 호출
114+
*/
115+
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
116+
protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) {
117+
log.error("handleHttpRequestMethodNotSupportedException", exception);
118+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.METHOD_NOT_ALLOWED_ERROR);
119+
return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED);
120+
}
121+
122+
/**
123+
* 잘못된 주소로 요청 한 경우
124+
*/
125+
@ExceptionHandler(NoResourceFoundException.class)
126+
protected ResponseEntity<ErrorResponse> handleNoResourceFoundException(NoResourceFoundException exception) {
127+
log.error("handleNoResourceFoundException", exception);
128+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NO_RESOURCE_FOUND_ERROR);
129+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
130+
}
131+
132+
133+
/**
134+
* @PreAuthorize 등 메서드 보안에서 인증이 없거나(anonymous) 권한이 부족한 경우 처리
135+
* anonymous 이면 401, 그 외 권한 부족은 403
136+
*/
137+
@ExceptionHandler(AccessDeniedException.class)
138+
protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException exception) {
139+
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
140+
boolean isAnonymous = (auth == null) || (auth instanceof AnonymousAuthenticationToken);
141+
142+
if (isAnonymous) {
143+
log.warn("Unauthorized (Anonymous) Access Exception: {}", exception.getMessage());
144+
return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.UNAUTHORIZED_ERROR), HttpStatus.UNAUTHORIZED);
145+
}
146+
147+
log.warn("Forbidden Access Exception: {}", exception.getMessage());
148+
return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.FORBIDDEN_ERROR), HttpStatus.FORBIDDEN);
149+
}
150+
151+
/**
152+
* 인증 과정에서의 일반 AuthenticationException 은 401로 반환
153+
*/
154+
@ExceptionHandler({AuthenticationException.class, AuthenticationCredentialsNotFoundException.class})
155+
protected ResponseEntity<ErrorResponse> handleAuthenticationException(RuntimeException exception) {
156+
log.warn("Authentication Exception: ", exception);
157+
return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.UNAUTHORIZED_ERROR), HttpStatus.UNAUTHORIZED);
158+
}
159+
160+
/**
161+
* 서비스에서 던진 ResponseStatusException을 표준 ErrorResponse로 변환
162+
*/
163+
@ExceptionHandler(ResponseStatusException.class)
164+
protected ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException exception) {
165+
int status = exception.getStatusCode().value();
166+
ErrorCode code;
167+
switch (status) {
168+
case 400 -> code = ErrorCode.BAD_REQUEST_ERROR;
169+
case 401 -> code = ErrorCode.UNAUTHORIZED_ERROR;
170+
case 403 -> code = ErrorCode.FORBIDDEN_ERROR;
171+
case 404 -> code = ErrorCode.NOT_FOUND_ERROR;
172+
case 405 -> code = ErrorCode.METHOD_NOT_ALLOWED_ERROR;
173+
case 409 -> code = ErrorCode.CONFLICT_ERROR;
174+
default -> code = ErrorCode.INTERNAL_SERVER_ERROR;
175+
}
176+
// 정확한 Error 사유는 로그로 기록
177+
log.warn("ResponseStatusException: status={}, reason={}", status, exception.getReason());
178+
179+
// 클라이언트에는 표준화된 메시지 전달 (정확한 에러 발생 원인에 대한 보안 유지)
180+
return new ResponseEntity<>(ErrorResponse.fromErrorCode(code), HttpStatus.valueOf(status));
181+
}
182+
183+
/**
184+
* 그 외 모든 Exception 경우
185+
*/
186+
@ExceptionHandler(Exception.class)
187+
protected final ResponseEntity<ErrorResponse> handleAllExceptions(Exception exception) {
188+
log.error("Exception", exception);
189+
final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.INTERNAL_SERVER_ERROR);
190+
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
191+
}
192+
193+
}

0 commit comments

Comments
 (0)