์์๋ณด๊ธฐ ์ข์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ํด Spring Boot์์ Custom Exception์ ์ค์ ํด๋ณด์!!
ErrorCode enum ์์ฑ ๋ฐ ์ค์
springboot ์์ ์๋ฌ๋ฅผ ๋ฐ์์์ผฐ์ ๋, ์ ๋ฌํ ์๋ฌ ์ฝ๋๋ฅผ ๊ด๋ฆฌํ๋ enum์ ์์ฑํด์ค๋ค!
enum(์ด๊ฑฐํ)์ ์๋ก ์ฐ๊ด๋ ์์๋ค์ ์งํฉ์ ์ ์ํ ๋ ์ฌ์ฉํ๋ ํน๋ณํ ์๋ฃํ์!
=> ์ฃผ๋ก ๋ช ๊ฐ์ง ๊ฐ ์ค ํ๋๋ง ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ์ ์ฌ์ฉ๋จ(๊ณ์ , ์์ผ, ์ํ ๋ฑ)
package berich.backend.exception;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ErrorCode {
DUPLICATED_MEMBER(HttpStatus.BAD_REQUEST, "001_DUPLICATED_EMAIL", "์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ์
๋๋ค."),
INVALID_ARGUMENT(HttpStatus.BAD_REQUEST, "002_INVALID_ARGUMENT", "๋๋ฝ๋ ์ ๋ณด๊ฐ ์๋์ง ํ์ธํด์ฃผ์ธ์."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "003_USER_NOT_FOUND", "์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "004_INVALID_PASSWORD", "๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค.");
private final HttpStatus status;
private final String code;
private final String msg;
}
status: HTTP ์ํ ์ฝ๋
code: ํ๋ก ํธ๋ก ์ ๋ฌ๋๋ ๋ฐ์ดํฐ๋ก, ์ ๋ฌ๋ฐ์ ์ฝ๋๋ฅผ ํตํด ํด๋น ์๋ฌ๋ฅผ ๋ฐ์์ ๋, ์ด๋ ํ ์กฐ์น๋ฅผ ์ทจํด์ผํ ์ง ๋ช ์ํ ์ ์์
msg: ์ ๋ฌ ๋ฐ์ ์๋ฌ ์ฝ๋์ ๋ํ ์ค๋ช ์ผ๋ก, ํด๋น ์๋ฌ๊ฐ ์ด๋ ํ ์ํฉ์ ๋ฐ์ํ ์๋ฌ์ธ์ง ๋ช ์
Custom Exception ์์ฑ ๋ฐ ์ค์
์์์ ๋ง๋ ErrorCode๋ ์ด๊ฑฐํ(enum)์ผ๋ก ์ ์๋๋๋ฐ, ์ด ์์ฒด๋ ์์ธ ํด๋์ค๊ฐ ์๋!
(Spring์ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ ๋ฉ์ปค๋์ฆ์ ErrorCode์ ๊ฐ์ ์ปค์คํ ์ ๋ณด๋ฅผ ์ธ์ํ๊ฑฐ๋ ์ฒ๋ฆฌํ ์ ์๋๋ก ์ค๊ณ๋์ด ์์ง ์์)
=> CustomException์ ํตํด ErrorCode์ ํจ๊ป ์์ธ๋ฅผ ๋์ง๋ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด์ผ, ํ๋ก์ ํธ ์ ๋ฐ์์ ํต์ผ๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํจ
package berich.backend.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{
ErrorCode errorCode;
}
RuntimeException์ ์์๋ฐ๋ ์ด์ ?
๊ฒฐ๋ก ์ CustomException์ ๋น๊ฒ์ฌ ์์ธ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด์์ด๋ค!
์๋ฐ์ ์์ธ๋ ํฌ๊ฒ 2๊ฐ์ง๋ก ๋๋๋๋ฐ ๋น๊ฒ์ฌ ์์ธ(Unchecked Exception)์ ๊ฒ์ฌ ์์ธ(Checked Exception)์ด๋ค
RuntimeException์ ์์๋ฐ์ ์์ธ๋ค์ ๋น๊ฒ์ฌ ์์ธ๋ก ๊ฐ์ฃผ๋๋๋ฐ ์ด๋ ์ปดํ์ผ๋ฌ๊ฐ ์์ธ ์ฒ๋ฆฌ ์ฌ๋ถ๋ฅผ ๊ฐ์ ํ์ง ์์ผ๋ฉฐ, ๊ฐ๋ฐ์๊ฐ ์ ํ์ ์ผ๋ก try-catch๋ก ์ฒ๋ฆฌํ ์ ์๋ค!
Exception์ ์์๋ฐ์ ์์ธ๋ค์ ๊ฒ์ฌ ์์ธ๋ก ๊ฐ์ฃผ๋๋๋ฐ ์ด๋ ๋ฐ๋์ ์์ธ ์ฒ๋ฆฌ๊ฐ ํ์ํ๋ฉฐ, ํธ์ถํ๋ ์ชฝ์์ try-catch๋ก ์ฒ๋ฆฌํ๊ฑฐ๋ throws ํค์๋๋ก ๋ช ์ํด์ผ ํ๋ค!
์ผ๋ฐ์ ์ธ ๋ก์ง ํ๋ฆ์์ ์์ธ๋ฅผ ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ๊ณ ์์ธ ์ฒ๋ฆฌ์ ๋ํ ๊ฐ์ ์ฑ์ ์ค์ด๊ธฐ ์ํด RuntimeException์ ์์๋ฐ์ CustomException์ ๋น๊ฒ์ฌ ์์ธ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด๋ค!
Custom handler ์์ฑ ๋ฐ ์ค์
์ฐ์ ์์ธ ์ฒ๋ฆฌ๋ฅผํด์ CustomException ๋ฐ์ ์, ํ๋ก ํธ์๊ฒ ๋ฐํํ ์๋ฌ ์๋ต์ ์ ์ํ๋ ErrorDTO๋ฅผ ๋ง๋ ๋ค
package berich.backend.exception;
import lombok.Builder;
import lombok.Data;
import org.springframework.http.ResponseEntity;
@Builder
@Data
public class ErrorDTO {
private int status;
private String code;
private String msg;
public static ResponseEntity<ErrorDTO> toResponseEntity(ErrorCode e) {
return ResponseEntity.status(e.getStatus().value())
.body(ErrorDTO.builder()
.status(e.getStatus().value())
.code(e.getCode())
.msg(e.getMsg())
.build());
}
}
=> ์์ฑํ๋ ErrorCode์ ๋ํด Builder ํจํด์ ์ด์ฉ
์ด์ Custom Handler๋ฅผ ๋ง๋ ๋ค
Custom Handler๋ฅผ ํตํด CustomException ์์ฑ ์, ErrorDTO์ ๊ตฌํ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ๋ง์ถฐ ํ๋ก ํธ๋ก ๋ฐ์ดํฐ๊ฐ ๋ณด๋ด์ง๊ฒ ๋จ!
package berich.backend.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice // ์ ์ญ ์์ธ ์ฒ๋ฆฌ ํด๋์ค์์ ๋ช
์
public class CustomExceptionHandler {
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorDTO> handleCustomException(CustomException e) {
return ErrorDTO.toResponseEntity(e.getErrorCode());
}
}
์ด ์ฝ๋๋ ErrorDTO ํด๋์ค์ toResponseEntity๋ผ๋ ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ, ์์ธ์ ์๋ฌ ์ฝ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ResponseEntity ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ๋ฐํํ๋ ์ญํ ์ ํจ
@ExceptionHandler๋ Spring์์ ํน์ ์์ธ๊ฐ ๋ฐ์ํ์ ๋ ๊ทธ ์์ธ๋ฅผ ์ฒ๋ฆฌํ ๋ฉ์๋๋ฅผ ์ง์ ํ๊ธฐ ์ํด ์ฌ์ฉํจ
(@ExceptionHandler์ ๋งค๊ฐ๋ณ์๋ "์ด๋ค ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ง"๋ฅผ ์ง์ ํ๊ธฐ ์ํ ํด๋์ค ํ์ ์ ์ ๋ฌ๋ฐ๊ธฐ ๋๋ฌธ์ .class ๋ฅผ ๋ถ์ด์ง ์์ผ๋ฉด ๊ทธ๋ฅ ํ์ ์ผ๋ก๋ง ์ธ์ํ์ฌ ์ปดํ์ผ ์๋ฌ ๋ฐ์)
handleCustomException์ ๊ธฐ๋ณธ์ ์ผ๋ก Spring์ ์ํด ์๋์ผ๋ก ํธ์ถ๋๋ ๋ฉ์๋์ด๊ธฐ ๋๋ฌธ์, ์ธ๋ถ ํด๋์ค๋ ํด๋ผ์ด์ธํธ ์ฝ๋์์ ์ง์ ํธ์ถ๋ ํ์๊ฐ ์์
(public์ผ๋ก ์ ์ธํ ํ์๊ฐ X)
=> ์์์ ๊ณ ๋ คํ ํ์ฅ ๊ฐ๋ฅ์ฑ, ๊ฐ์ ํจํค์ง ๋ด์์์ ์ ๊ทผ ํ์ฉ, ๊ทธ๋ฆฌ๊ณ ์ธ๋ถ์์์ ์ง์ ํธ์ถ์ ๋ฐฉ์งํ๊ธฐ ์ํด protected๋ก ์ ์ธ
ControllerAdvice vs RestControllerAdvice
Spring์ ์ ์ญ์ ์ผ๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ ์ ์ด๋ ธํ ์ด์ ์ธ ControllerAdvice ์ RestControllerAdvice๋ฅผ ์ ๊ณตํ๋ค!
๊ทธ๋ผ ์ด ๋์ ์ฐจ์ด๋ ๋ญ๊น?
@RestControllerAdvice๋ @ControllerAdvice์ @ResponseBody์ ๊ฒฐํฉ์ด๋ค
@RestControllerAdvice๋ JSON ๋๋ XML๋ก ์๋ต์ ๋ฐํํจ
=> RESTful API์์ ์ฃผ๋ก ์ฌ์ฉ
@ControllerAdvice๋ ๋ทฐ ํ
ํ๋ฆฟ์ ๋ ๋๋งํ๋ ์ผ๋ฐ์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฌ์ฉํจ
=> ํ์ํ ๋ @ResponseBody๋ฅผ ํตํด JSON์ ๋ฐํํ ์ ์์
Spring์์์ ์์ธ์ฒ๋ฆฌ ํ๋ฆ์ ๋ค์ ์๊ฐํด๋ณด๋ฉด
1. ์์ธ๊ฐ ๋ฐ์ํ๋ฉด Spring์ ๋จผ์ ์ ์ญ์ ์ผ๋ก ์ค์ ๋ @ControllerAdvice ๋๋ @RestControllerAdvice ํด๋์ค์์ ํด๋น ์์ธ๋ฅผ ์ฒ๋ฆฌํ @ExceptionHandler๋ฅผ ์ฐพ์
2. ์ผ์นํ๋ ํธ๋ค๋ฌ๊ฐ ์๋ค๋ฉด, ๊ฐ๋ณ ์ปจํธ๋กค๋ฌ์์ @ExceptionHandler๋ฅผ ์ฐพ๊ฒ ๋จ
3. ๊ทธ๋๋ ์ผ์นํ๋ ํธ๋ค๋ฌ๊ฐ ์๋ค๋ฉด, Spring์ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ ๋ก์ง์ด ์ ์ฉ๋์ด ๊ธฐ๋ณธ ์ค๋ฅ ํ์ด์ง๋ JSON ์๋ต์ด ๋ฐํ๋จ
springConfig ์ค์
package berich.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
// Spring Security ์น ๋ณด์ ๊ธฐ๋ฅ ํ์ฑํ => FilterChain ์ ๊ณต
@EnableWebSecurity
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return webSecurity -> webSecurity.ignoring()
.requestMatchers("/error");
}
...
}
WebSecurityCustomizer๋ ๋ณด์ ํํฐ ์ฒด์ธ ์ค์ ์ ์ปค์คํฐ๋ง์ด์ฆํ๋ ๋ฐ ์ฌ์ฉ๋๋ ์ธํฐํ์ด์ค์
=> .ignoring์ ํตํด /error ์ฒ๋ผ ํน์ ๊ฒฝ๋ก๋ฅผ ๋ณด์ ํํฐ ์ฒด์ธ์์ ์ ์ธํจ
WebSecurity๋ HttpSecurity์ ์์์ ์์!
=> WebSecurity์ ignoring์ endpoint๋ฅผ ๋ง๋ค๋ฉด, Security Filter Chain์ด ์ ์ฉ๋์ง X
HttpSecurity vs WebSecurity
Security Config๋ฅผ ์์ฑํ๋ค ๊ถ๊ธ์ฆ์ด ๋ค์๋ค
์ด์ฉํผ ํน์ url ๊ฒฝ๋ก๋ฅผ ํ์ฉํด์ฃผ๋ ์์ ์ด๋ผ๋ฉด SecurityFilterChain์์ HttpSecurit๋ก permitAll์ ํ๋ฉด ๋๋๊ฑฐ ์๋๊ฐ?
์ฐ์ permitAll์ ๊ฒฝ์ฐ Spring Security์ ํํฐ ์ฒด์ธ์ ๊ทธ๋๋ก ์๋ํ๋, ํน์ ๊ฒฝ๋ก์ ๋ํด ๋ชจ๋ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ์ ์๋๋ก ํ์ฉํ๋ ๊ฒ์ด๋ค!
=> ์ด ๋ฐฉ์์ ์ฌ์ ํ Spring Security์ ๋ค๋ฅธ ๋ณด์ ๊ธฐ๋ฅ๋ค(CORS ์ค์ , CSRF ๋ณดํธ ๋ฑ)์ด ์ ์ฉ๋จ!
์์์ ๋ดค๋ ignoring์ ๊ฒฝ์ฐ ์ง์ ํ ๊ฒฝ๋ก๋ ์์ Spring Security์ ๊ด๋ฆฌ ๋์์์ ๋ฒ์ด๋๊ธฐ ๋๋ฌธ์ ๋ณด์ ํํฐ ์ฒด์ธ์ ๊ฑฐ์น์ง X
=> CORS, CSRF ๊ฐ์ ๋ชจ๋ ๋ณด์ ๊ฒ์ฌ๊ฐ ์ ์ฉ๋์ง ์์ ๋ฟ๋๋ฌ ์ธ์ฆ, ๊ถํ ๊ฒ์ฌ๋ ๋น์ฐํ ๋ชปํจ
๊ทธ๋์ ๊ฒฐ๋ก ์ WebSecurity(ignoring์ ์ฐ๋ ๊ฒฝ์ฐ)๋ ๋ณด์๊ณผ ์ ํ ์๊ด์๋ API์ ์ฌ์ฉํ๊ณ , ๊ทธ ์ด์ธ์๋ HttpSecurity๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
ํ์๊ฐ์ ์์ธ ์ฒ๋ฆฌ ํ ์คํธ
์ ์๋ํ๋ ๊ฒ์ ํ์ธํ ์ ์์!
'๐ซ Backend > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring Boot์์ Supabase ์ฌ์ฉํ๊ธฐ (Kotlin) (0) | 2025.02.25 |
---|---|
Spring boot ์์ H2 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉํ๊ธฐ (Kotlin) (0) | 2025.02.20 |
JPA ํบ์๋ณด๊ธฐ (0) | 2025.02.16 |
[Spring Boot] Docker๋ฅผ ์ด์ฉํด EC2์ ๋ฐฐํฌํด๋ณด๊ธฐ (4) | 2024.08.27 |
[Spring Boot] MySQL JPA ์ฐ๋ (3) | 2024.07.22 |