π API μμ² νλ¦ λ° μλ¬ μ²λ¦¬ κ°μ΄λ
μ΄ λ¬Έμλ νλ‘μ νΈμ λͺ¨λ API μμ²μ΄ μ΄λ»κ² μ²λ¦¬λλμ§, μ±κ³΅/μ€ν¨ μ μ΄λ€ μ½λκ° μ€νλμ΄ μ΄λ€ μλ΅μ΄ μμ±λλμ§λ₯Ό μ 리ν©λλ€.
π λͺ©μ°¨
- νν° μ²΄μΈ κ΅¬μ‘°
- λ‘κ·ΈμΈ κ΄λ ¨ νλ¦
- μΈμ¦μ΄ νμν API νλ¦
- λΉμ¦λμ€ λ‘μ§ μλ¬ νλ¦
- μ ν¨μ± κ²μ¦ μλ¬ νλ¦
- μλ΅ νμ μ 리
1. νν° μ²΄μΈ κ΅¬μ‘°
1.1 μμ² μ²λ¦¬ μμ
[ν΄λΌμ΄μΈνΈ μμ²]
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 1. JwtAccessTokenCheckAndSaveUserInfoFilter β
β - ν ν° μΆμΆ λ° κ²μ¦ β
β - μ ν¨νλ©΄ SecurityContextμ μΈμ¦ μ 보 μ μ₯ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 2. JwtLoginFilter ("/api/login" URLλ§ λμ) β
β - λ‘κ·ΈμΈ μμ² μ²λ¦¬ β
β - JWT ν ν° λ°κΈ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 3. Spring Security Authorization β
β - URLλ³ μΈμ¦/μΈκ° κ²μ¬ β
β - .authenticated() μ€μ λ URLμ μΈμ¦ μμΌλ©΄ μ°¨λ¨ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 4. Controller β Service β Repository β
β - λΉμ¦λμ€ λ‘μ§ μ²λ¦¬ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 5. GlobalExceptionHandler (μμΈ λ°μ μ) β
β - λͺ¨λ μμΈλ₯Ό ν΅ν© μ²λ¦¬ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
[μλ΅ λ°ν]
1.2 νν° λ±λ‘ μμΉ
π νμΌ: SecurityConfig.java (Line 167-174)
http
.addFilterAt(
new JwtLoginFilter(..., "/api/login"),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(
new JwtAccessTokenCheckAndSaveUserInfoFilter(jwtUtil, customUserDetailsService),
UsernamePasswordAuthenticationFilter.class);
2. λ‘κ·ΈμΈ κ΄λ ¨ νλ¦
2.1 β λ‘κ·ΈμΈ μ±κ³΅ (ID/PW μΌμΉ)
μμ²:
POST /api/login
Content-Type: application/json
{
"username": "testuser",
"password": "correct_password"
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ π JwtAccessTokenCheckAndSaveUserInfoFilter.java (Line 35-36)
β String token = getTokenFromRequest(request); // ν ν° μμ β null
β if (token == null) { chain.doFilter(); return; } // κ·Έλ₯ ν΅κ³Ό
β
β
[2] JwtLoginFilter.attemptAuthentication()
β
ββ π JwtLoginFilter.java (Line 40-56)
β // /api/login URLμ΄λ―λ‘ μ΄ νν° λμ
β Map<String, String> credentials = new ObjectMapper()
β .readValue(request.getInputStream(), ...);
β String username = credentials.get("username");
β String password = credentials.get("password");
β return authenticationManager.authenticate(authRequest);
β
β
[3] CustomUserDetailsService.loadUserByUsername()
β
ββ π CustomUserDetailsService.java (Line 19-25)
β UserEntity userEntity = userRepository.findByUsername(username)
β .orElseThrow(() -> new UsernameNotFoundException(...));
β return new CustomUserAccount(UserDTO.from(userEntity));
β
β
[4] Spring Securityκ° λΉλ°λ²νΈ κ²μ¦ (BCryptPasswordEncoder)
β
β
[5] JwtLoginFilter.successfulAuthentication()
β
ββ π JwtLoginFilter.java (Line 63-93)
β String accessToken = jwtUtil.createAccessToken(username);
β String refreshToken = jwtUtil.createRefreshToken(username);
β refreshService.saveRefresh(refreshToken);
β
β // λΈλΌμ°μ : μΏ ν€ μ€μ
β addCookie(response, "access_token", accessToken, -1);
β addCookie(response, "refresh_token", refreshToken, -1);
β response.setStatus(HttpServletResponse.SC_OK);
β
β
[μλ΅] HTTP 200 OK + Set-Cookie ν€λ
μλ΅:
HTTP/1.1 200 OK
Set-Cookie: access_token=eyJhbGc...; HttpOnly; Path=/
Set-Cookie: refresh_token=eyJhbGc...; HttpOnly; Path=/
2.2 β λ‘κ·ΈμΈ μ€ν¨ (ID/PW λΆμΌμΉ)
μμ²:
POST /api/login
Content-Type: application/json
{
"username": "testuser",
"password": "wrong_password"
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ ν ν° μμ β κ·Έλ₯ ν΅κ³Ό
β
β
[2] JwtLoginFilter.attemptAuthentication()
β
ββ π JwtLoginFilter.java (Line 53)
β return authenticationManager.authenticate(authRequest);
β // λΉλ°λ²νΈ λΆμΌμΉ β AuthenticationException λ°μ!
β
β
[3] JwtLoginFilter.unsuccessfulAuthentication()
β
ββ π JwtLoginFilter.java (Line 107-117)
β response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
β response.setContentType("application/json;charset=UTF-8");
β
β ErrorResponse errorResponse = ErrorResponse.of(
β "μμ΄λ λλ λΉλ°λ²νΈκ° μΌμΉνμ§ μμ΅λλ€.",
β "AUTHENTICATION_FAILED"
β );
β response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
β
β
[μλ΅] HTTP 401 Unauthorized
μλ΅:
{
"success": false,
"message": "μμ΄λ λλ λΉλ°λ²νΈκ° μΌμΉνμ§ μμ΅λλ€.",
"errorCode": "AUTHENTICATION_FAILED",
"timestamp": "2026-01-16T14:30:00"
}
2.3 β λ‘κ·ΈμΈ μ€ν¨ (μ‘΄μ¬νμ§ μλ μ¬μ©μ)
μμ²:
POST /api/login
Content-Type: application/json
{
"username": "nonexistent",
"password": "any_password"
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter β ν΅κ³Ό
β
β
[2] JwtLoginFilter.attemptAuthentication()
β
β
[3] CustomUserDetailsService.loadUserByUsername()
β
ββ π CustomUserDetailsService.java (Line 20-22)
β UserEntity userEntity = userRepository.findByUsername(username)
β .orElseThrow(() -> new UsernameNotFoundException(
β "μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€: " + username));
β // UsernameNotFoundException λ°μ!
β
β
[4] Spring Securityκ° AuthenticationExceptionμΌλ‘ λ³ν
β
β
[5] JwtLoginFilter.unsuccessfulAuthentication() μ€ν
β
β
[μλ΅] HTTP 401 + ErrorResponse
3. μΈμ¦μ΄ νμν API νλ¦
3.1 β λ‘κ·ΈμΈν μ¬μ©μκ° κ²μκΈ μμ±
μμ²:
POST /api/communities
Cookie: access_token=eyJhbGc...
Content-Type: application/json
{
"title": "ν
μ€νΈ μ λͺ©",
"content": "ν
μ€νΈ λ΄μ©μ
λλ€."
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ π JwtAccessTokenCheckAndSaveUserInfoFilter.java (Line 35-67)
β String token = getTokenFromRequest(request); // μΏ ν€μμ ν ν° μΆμΆ
β
β String tokenType = jwtUtil.getTokenType(token); // "access"
β
β if (!jwtUtil.validateToken(token)) { ... } // μ ν¨ν¨ β ν΅κ³Ό
β
β // β
SecurityContextμ μΈμ¦ μ 보 μ μ₯
β String username = jwtUtil.extractUsername(token);
β UserDetails userDetails = userDetailsService.loadUserByUsername(username);
β SecurityContextHolder.getContext().setAuthentication(authenticationToken);
β
β chain.doFilter(request, response); // μΈμ¦λ μνλ‘ ν΅κ³Ό!
β
β
[2] JwtLoginFilter
β
ββ /api/loginμ΄ μλλ―λ‘ λμ μ ν¨ β ν΅κ³Ό
β
β
[3] Spring Security Authorization
β
ββ π SecurityConfig.java (Line 107-109)
β .requestMatchers(HttpMethod.POST, "/api/communities", "/api/communities/**")
β .authenticated()
β // SecurityContextμ μΈμ¦ μ 보 μμ β ν΅κ³Ό!
β
β
[4] CommunityController.createCommunity()
β
ββ π CommunityController.java (Line 51-57)
β @PostMapping
β public ResponseEntity<ApiResponse<Long>> createCommunity(
β @Valid @RequestBody CommunityCreateDTO createDTO,
β @AuthenticationPrincipal CustomUserAccount userAccount) {
β
β Long communityId = communityService.createCommunity(
β createDTO, userAccount.getUsername());
β return ResponseEntity.status(HttpStatus.CREATED)
β .body(ApiResponse.success("κ²μκΈμ΄ μμ±λμμ΅λλ€", communityId));
β }
β
β
[5] CommunityService.createCommunity()
β
ββ π CommunityService.java (Line 29-35)
β UserEntity user = userRepository.findByUsername(username)
β .orElseThrow(() -> EntityNotFoundException.of("μ¬μ©μ", username));
β CommunityEntity community = createDTO.toEntity(user);
β CommunityEntity savedCommunity = communityRepository.save(community);
β return savedCommunity.getId();
β
β
[μλ΅] HTTP 201 Created
μλ΅:
{
"success": true,
"message": "κ²μκΈμ΄ μμ±λμμ΅λλ€",
"data": 123
}
3.2 β λ‘κ·ΈμΈ μ ν μ¬μ©μκ° κ²μκΈ μμ± μλ
μμ²:
POST /api/communities
Content-Type: application/json
(ν ν° μμ)
{
"title": "ν
μ€νΈ μ λͺ©",
"content": "ν
μ€νΈ λ΄μ©μ
λλ€."
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ π JwtAccessTokenCheckAndSaveUserInfoFilter.java (Line 35-39)
β String token = getTokenFromRequest(request); // null
β if (token == null) {
β chain.doFilter(request, response); // μΈμ¦ μμ΄ ν΅κ³Ό
β return;
β }
β
β
[2] JwtLoginFilter β /api/login μλλ―λ‘ ν΅κ³Ό
β
β
[3] Spring Security Authorization
β
ββ π SecurityConfig.java (Line 107-109)
β .requestMatchers(HttpMethod.POST, "/api/communities", "/api/communities/**")
β .authenticated()
β // SecurityContextμ μΈμ¦ μ 보 μμ β μ°¨λ¨!
β
β
[4] AuthenticationEntryPoint μ€ν
β
ββ π SecurityConfig.java (Line 178-205)
β http.exceptionHandling(ex -> ex
β .authenticationEntryPoint((request, response, authException) -> {
β response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
β response.setContentType("application/json;charset=UTF-8");
β
β String errorCode = "NOT_AUTHENTICATED";
β String errorMessage = "μΈμ¦μ΄ νμν©λλ€.";
β
β // ErrorResponse νμμΌλ‘ μλ΅
β String jsonResponse = String.format(
β "{\"success\":false,\"message\":\"%s\",\"errorCode\":\"%s\",...}",
β errorMessage, errorCode);
β response.getWriter().write(jsonResponse);
β })
β );
β
β
[μλ΅] HTTP 401 Unauthorized
μλ΅:
{
"success": false,
"message": "μΈμ¦μ΄ νμν©λλ€.",
"errorCode": "NOT_AUTHENTICATED",
"timestamp": "2026-01-16T14:30:00"
}
3.3 β Access Token λ§λ£ μνλ‘ μμ²
μμ²:
POST /api/communities
Cookie: access_token=eyJhbGc...(λ§λ£λ ν ν°)
Content-Type: application/json
{
"title": "ν
μ€νΈ μ λͺ©",
"content": "ν
μ€νΈ λ΄μ©μ
λλ€."
}
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ π JwtAccessTokenCheckAndSaveUserInfoFilter.java (Line 50-54)
β String token = getTokenFromRequest(request); // ν ν° μμ
β
β if (!jwtUtil.validateToken(token)) { // β λ§λ£λ¨!
β request.setAttribute("ERROR_CAUSE", "ν ν°λ§λ£");
β chain.doFilter(request, response); // μΈμ¦ μμ΄ ν΅κ³Ό
β return;
β }
β
β
[2] Spring Security Authorization β μΈμ¦ μμ β μ°¨λ¨!
β
β
[3] AuthenticationEntryPoint μ€ν
β
ββ π SecurityConfig.java (Line 178-205)
β String errorCause = request.getAttribute("ERROR_CAUSE"); // "ν ν°λ§λ£"
β
β if ("ν ν°λ§λ£".equals(errorCause)) {
β errorMessage = "Access Tokenμ΄ λ§λ£λμμ΅λλ€. ν ν°μ μ¬λ°κΈν΄μ£ΌμΈμ.";
β errorCode = "TOKEN_EXPIRED";
β }
β
β
[μλ΅] HTTP 401 Unauthorized
μλ΅:
{
"success": false,
"message": "Access Tokenμ΄ λ§λ£λμμ΅λλ€. ν ν°μ μ¬λ°κΈν΄μ£ΌμΈμ.",
"errorCode": "TOKEN_EXPIRED",
"timestamp": "2026-01-16T14:30:00"
}
4. λΉμ¦λμ€ λ‘μ§ μλ¬ νλ¦
4.1 β λ‘κ·ΈμΈν μ¬μ©μκ° λ€λ₯Έ μ¬λ κΈ μμ μλ
μμ²:
DELETE /api/communities/123
Cookie: access_token=eyJhbGc...(userBμ ν ν°)
κ²μκΈ 123μ μμ±μλ userA
νλ¦:
[1] JwtAccessTokenCheckAndSaveUserInfoFilter.doFilterInternal()
β
ββ ν ν° μ ν¨ β SecurityContextμ userB μΈμ¦ μ 보 μ μ₯
β
β
[2] Spring Security Authorization
β
ββ π SecurityConfig.java (Line 113-115)
β .requestMatchers(HttpMethod.DELETE, "/api/communities/**")
β .authenticated()
β // userB μΈμ¦λ¨ β ν΅κ³Ό!
β
β
[3] CommunityController.deleteCommunity()
β
ββ π CommunityController.java (Line 75-81)
β @DeleteMapping("/{communityId}")
β public ResponseEntity<ApiResponse<Void>> deleteCommunity(
β @PathVariable Long communityId,
β @AuthenticationPrincipal CustomUserAccount userAccount) {
β
β communityService.deleteCommunity(communityId, userAccount.getUsername());
β // userAccount.getUsername() = "userB"
β }
β
β
[4] CommunityService.deleteCommunity()
β
ββ π CommunityService.java (Line 77-87)
β CommunityEntity community = communityRepository.findByIdAndIsDeletedFalse(communityId)
β .orElseThrow(() -> EntityNotFoundException.of("κ²μκΈ", communityId));
β
β // community.getUser().getUsername() = "userA"
β // username = "userB"
β if (!community.isWrittenBy(username)) { // userA != userB
β throw AccessDeniedException.forDelete("κ²μκΈ"); // β μμΈ λ°μ!
β }
β
β
[5] GlobalExceptionHandler.handleBusinessException()
β
ββ π GlobalExceptionHandler.java (Line 31-37)
β @ExceptionHandler(BusinessException.class)
β public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
β log.warn("Business Exception: {} - {}", e.getErrorCode(), e.getMessage());
β
β ErrorResponse response = ErrorResponse.of(e.getMessage(), e.getErrorCode());
β return ResponseEntity.status(e.getStatus()).body(response);
β // e.getStatus() = HttpStatus.FORBIDDEN (403)
β }
β
β
[μλ΅] HTTP 403 Forbidden
μλ΅:
{
"success": false,
"message": "λ³ΈμΈμ κ²μκΈλ§ μμ ν μ μμ΅λλ€.",
"errorCode": "ACCESS_DENIED",
"timestamp": "2026-01-16T14:30:00"
}
4.2 β μ‘΄μ¬νμ§ μλ κ²μκΈ μ‘°ν
μμ²:
GET /api/communities/999999
νλ¦:
[1] νν° ν΅κ³Ό (κ³΅κ° API)
β
β
[2] CommunityController.getCommunityDetail()
β
ββ π CommunityController.java (Line 40-44)
β @GetMapping("/{communityId}")
β public ResponseEntity<ApiResponse<CommunityDTO>> getCommunityDetail(
β @PathVariable Long communityId) {
β CommunityDTO community = communityService.getCommunityDetail(communityId);
β }
β
β
[3] CommunityService.getCommunityDetail()
β
ββ π CommunityService.java (Line 50-58)
β CommunityEntity community = communityRepository
β .findByIdAndIsDeletedFalse(communityId) // 999999 μμ!
β .orElseThrow(() -> EntityNotFoundException.of("κ²μκΈ", communityId));
β // EntityNotFoundException λ°μ!
β
β
[4] GlobalExceptionHandler.handleBusinessException()
β
ββ π GlobalExceptionHandler.java (Line 31-37)
β // EntityNotFoundException extends BusinessException
β // e.getStatus() = HttpStatus.NOT_FOUND (404)
β // e.getMessage() = "κ²μκΈμ(λ₯Ό) μ°Ύμ μ μμ΅λλ€: 999999"
β // e.getErrorCode() = "NOT_FOUND"
β
β
[μλ΅] HTTP 404 Not Found
μλ΅:
{
"success": false,
"message": "κ²μκΈμ(λ₯Ό) μ°Ύμ μ μμ΅λλ€: 999999",
"errorCode": "NOT_FOUND",
"timestamp": "2026-01-16T14:30:00"
}
4.3 β λΉμ¦λμ€ κ·μΉ μλ° μμ
π‘ μ΄ νλ‘μ νΈμμλ 리뷰 κΈ°λ₯μ΄ μμ§λ§,
BusinessRuleExceptionμ λμ μ리λ₯Ό μ€λͺ νκΈ° μν μμμ λλ€. μ€μ λ‘ λΉμ¦λμ€ κ·μΉ μλ°μ΄ νμν κ²½μ° μλ ν¨ν΄μΌλ‘ ꡬννλ©΄ λ©λλ€.
Serviceμμ λΉμ¦λμ€ κ·μΉ κ²μ¦ μ:
// μ: κ²μκΈ μμ μ μ΄λ―Έ μμ λ κΈμΈ κ²½μ°
if (entity.getIsDeleted()) {
throw DuplicateResourceException.alreadyDeleted("κ²μκΈ");
}
// μ: νΉμ 쑰건μ λ°λ₯Έ λΉμ¦λμ€ κ·μΉ μλ°
throw new BusinessRuleException("ν΄λΉ μμ
μ νμ©λμ§ μμ΅λλ€.");
νλ¦:
[1] νν° ν΅κ³Ό (μΈμ¦λ¨)
β
β
[2] Serviceμμ λΉμ¦λμ€ κ·μΉ κ²μ¦
β
ββ BusinessRuleException λλ DuplicateResourceException λ°μ!
β
β
[3] GlobalExceptionHandler.handleBusinessException()
β
ββ // BusinessRuleException extends BusinessException
β // e.getStatus() = HttpStatus.BAD_REQUEST (400)
β // e.getErrorCode() = "BUSINESS_RULE_VIOLATION"
β
β
[μλ΅] HTTP 400 Bad Request
μλ΅:
{
"success": false,
"message": "ν΄λΉ μμ
μ νμ©λμ§ μμ΅λλ€.",
"errorCode": "BUSINESS_RULE_VIOLATION",
"timestamp": "2026-01-16T14:30:00"
}
4.4 β μ΄λ―Έ μμ λ κ²μκΈ λ€μ μμ μλ
μμ²:
DELETE /api/communities/123
Cookie: access_token=eyJhbGc...(μμ±μ λ³ΈμΈ)
κ²μκΈ 123μ μ΄λ―Έ μμ λ μν (isDeleted = true)
νλ¦:
[1] νν° ν΅κ³Ό β 컨νΈλ‘€λ¬ λλ¬
β
β
[2] CommunityService.deleteCommunity()
β
ββ π CommunityService.java (Line 77-79)
β CommunityEntity community = communityRepository
β .findByIdAndIsDeletedFalse(communityId) // isDeleted=trueλΌμ μμ!
β .orElseThrow(() -> EntityNotFoundException.of("κ²μκΈ", communityId));
β
β
[3] GlobalExceptionHandler β HTTP 404
λλ (isDeleted μ²΄ν¬ μ νλ μΏΌλ¦¬λ‘ μ‘°νν κ²½μ°):
[2] CommunityEntity.softDelete()
β
ββ π CommunityEntity.java (Line 76-80)
β public void softDelete() {
β if (this.isDeleted) {
β throw DuplicateResourceException.alreadyDeleted("κ²μκΈ"); // β
β }
β this.isDeleted = true;
β }
β
β
[3] GlobalExceptionHandler.handleBusinessException()
β
ββ // DuplicateResourceException extends BusinessException
β // e.getStatus() = HttpStatus.CONFLICT (409)
β // e.getErrorCode() = "DUPLICATE_RESOURCE"
β
β
[μλ΅] HTTP 409 Conflict
μλ΅:
{
"success": false,
"message": "μ΄λ―Έ μμ λ κ²μκΈμ
λλ€.",
"errorCode": "DUPLICATE_RESOURCE",
"timestamp": "2026-01-16T14:30:00"
}
5. μ ν¨μ± κ²μ¦ μλ¬ νλ¦
5.1 β κ²μκΈ μμ± μ μ λͺ© λλ½
μμ²:
POST /api/communities
Cookie: access_token=eyJhbGc...
Content-Type: application/json
{
"title": "",
"content": "λ΄μ©λ§ μμ"
}
νλ¦:
[1] νν° ν΅κ³Ό β 컨νΈλ‘€λ¬ λλ¬
β
β
[2] CommunityController.createCommunity()
β
ββ π CommunityController.java (Line 51-53)
β @PostMapping
β public ResponseEntity<ApiResponse<Long>> createCommunity(
β @Valid @RequestBody CommunityCreateDTO createDTO, // β @Valid κ²μ¦!
β ...
β
ββ π CommunityCreateDTO.java (Line 16-17)
β @NotBlank(message = "μ λͺ©μ νμμ
λλ€")
β @Size(max = 200, message = "μ λͺ©μ 200μ μ΄νμ¬μΌ ν©λλ€")
β private String title;
β
β // titleμ΄ λΉ λ¬Έμμ΄ β @NotBlank μλ°!
β // MethodArgumentNotValidException λ°μ!
β
β
[3] GlobalExceptionHandler.handleValidationException()
β
ββ π GlobalExceptionHandler.java (Line 44-67)
β @ExceptionHandler(MethodArgumentNotValidException.class)
β public ResponseEntity<ErrorResponse> handleValidationException(
β MethodArgumentNotValidException e) {
β
β List<ErrorResponse.FieldError> fieldErrors = e.getBindingResult()
β .getFieldErrors()
β .stream()
β .map(error -> ErrorResponse.FieldError.builder()
β .field(error.getField()) // "title"
β .message(error.getDefaultMessage()) // "μ λͺ©μ νμμ
λλ€"
β .rejectedValue(error.getRejectedValue()) // ""
β .build())
β .collect(Collectors.toList());
β
β ErrorResponse response = ErrorResponse.of(
β "μ
λ ₯κ°μ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€.",
β "VALIDATION_ERROR",
β fieldErrors
β );
β return ResponseEntity.badRequest().body(response);
β }
β
β
[μλ΅] HTTP 400 Bad Request
μλ΅:
{
"success": false,
"message": "μ
λ ₯κ°μ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€.",
"errorCode": "VALIDATION_ERROR",
"timestamp": "2026-01-16T14:30:00",
"errors": [
{
"field": "title",
"message": "μ λͺ©μ νμμ
λλ€",
"rejectedValue": ""
}
]
}
5.2 β νμκ°μ μ μ¬λ¬ νλ κ²μ¦ μ€ν¨
μμ²:
POST /api/users
Content-Type: application/json
{
"username": "ab",
"password": "12",
"email": "invalid-email",
"nickname": ""
}
νλ¦:
[1] JoinController.join()
β
ββ π JoinController.java (Line 23)
β @PostMapping
β public ResponseEntity<ApiResponse<Void>> join(
β @Valid @RequestBody JoinDTO joinDTO) // β @Valid κ²μ¦!
β
ββ π JoinDTO.java
β @Size(min = 4, ...) username = "ab" β μλ°! (4μ λ―Έλ§)
β @Size(min = 4, ...) password = "12" β μλ°! (4μ λ―Έλ§)
β @Email email = "invalid-email" β μλ°! (μ΄λ©μΌ νμ X)
β @NotBlank nickname = "" β μλ°! (λΉ λ¬Έμμ΄)
β
β
[2] GlobalExceptionHandler.handleValidationException()
β
β
[μλ΅] HTTP 400 Bad Request
μλ΅:
{
"success": false,
"message": "μ
λ ₯κ°μ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€.",
"errorCode": "VALIDATION_ERROR",
"timestamp": "2026-01-16T14:30:00",
"errors": [
{
"field": "username",
"message": "μμ΄λλ 4~20μμ¬μΌ ν©λλ€",
"rejectedValue": "ab"
},
{
"field": "password",
"message": "λΉλ°λ²νΈλ 4μ μ΄μμ΄μ΄μΌ ν©λλ€",
"rejectedValue": "12"
},
{
"field": "email",
"message": "μ΄λ©μΌ νμμ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€",
"rejectedValue": "invalid-email"
},
{
"field": "nickname",
"message": "λλ€μμ νμμ
λλ€",
"rejectedValue": ""
}
]
}
6. μλ΅ νμ μ 리
6.1 μ±κ³΅ μλ΅ (ApiResponse)
{
"success": true,
"message": "μμ
μ΄ μλ£λμμ΅λλ€.",
"data": { ... } // μ€μ λ°μ΄ν° (μμ μλ μμ)
}
6.2 μλ¬ μλ΅ (ErrorResponse)
{
"success": false,
"message": "μλ¬ λ©μμ§",
"errorCode": "ERROR_CODE",
"timestamp": "2026-01-16T14:30:00",
"errors": [...] // μ ν¨μ± κ²μ¦ μλ¬ μμλ§ μ‘΄μ¬
}
6.3 μλ¬ μ½λ μ 리
| errorCode | HTTP μν | λ°μ μμΉ | μ€λͺ |
|---|---|---|---|
AUTHENTICATION_FAILED |
401 | JwtLoginFilter | λ‘κ·ΈμΈ μ€ν¨ (ID/PW λΆμΌμΉ) |
NOT_AUTHENTICATED |
401 | SecurityConfig | μΈμ¦ νμ (ν ν° μμ) |
TOKEN_EXPIRED |
401 | SecurityConfig / RefreshController | Access/Refresh Token λ§λ£ |
TOKEN_REQUIRED |
401 | RefreshController | Refresh Token ν€λ μμ |
TOKEN_DISCARDED |
401 | RefreshController | νκΈ°λ ν ν° (λ‘κ·Έμμλ¨) |
NOT_FOUND |
404 | GlobalExceptionHandler | 리μμ€ μμ |
ACCESS_DENIED |
403 | GlobalExceptionHandler | κΆν μμ (λ³ΈμΈ μλ) |
BUSINESS_RULE_VIOLATION |
400 | GlobalExceptionHandler | λΉμ¦λμ€ κ·μΉ μλ° |
DUPLICATE_RESOURCE |
409 | GlobalExceptionHandler | μ€λ³΅/μ΄λ―Έ μ²λ¦¬λ¨ |
VALIDATION_ERROR |
400 | GlobalExceptionHandler | μ ν¨μ± κ²μ¦ μ€ν¨ (@Valid) |
INVALID_JSON |
400 | GlobalExceptionHandler | JSON νμ± μ€ν¨ |
MISSING_PARAMETER |
400 | GlobalExceptionHandler | νμ νλΌλ―Έν° λλ½ |
TYPE_MISMATCH |
400 | GlobalExceptionHandler | νλΌλ―Έν° νμ λΆμΌμΉ |
INTERNAL_SERVER_ERROR |
500 | GlobalExceptionHandler | μμμΉ λͺ»ν μλ² μ€λ₯ |
π κ΄λ ¨ νμΌ μμΉ μμ½
| νμΌ | μν |
|---|---|
jwt/config/SecurityConfig.java |
νν° λ±λ‘, URL κΆν μ€μ , μΈμ¦ μ€ν¨ νΈλ€λ¬ |
jwt/filter/JwtAccessTokenCheckAndSaveUserInfoFilter.java |
ν ν° κ²μ¦ λ° μΈμ¦ μ 보 μ μ₯ |
jwt/filter/JwtLoginFilter.java |
λ‘κ·ΈμΈ μ²λ¦¬, ν ν° λ°κΈ |
jwt/service/CustomUserDetailsService.java |
μ¬μ©μ μ‘°ν (λ‘κ·ΈμΈ μ) |
common/exception/GlobalExceptionHandler.java |
λͺ¨λ μμΈ ν΅ν© μ²λ¦¬ |
common/exception/EntityNotFoundException.java |
404 μλ¬ |
common/exception/AccessDeniedException.java |
403 μλ¬ |
common/exception/BusinessRuleException.java |
400 μλ¬ (λΉμ¦λμ€ κ·μΉ) |
common/exception/DuplicateResourceException.java |
409 μλ¬ |
common/dto/ApiResponse.java |
μ±κ³΅ μλ΅ DTO |
common/dto/ErrorResponse.java |
μλ¬ μλ΅ DTO |