spring:
security:
oauth2:
client:
registration: # Client 설정
keycloak:
authorizationGrantType: authorization_code # OAuth 2.0 권한 부여 타입
clientId: oauth2-client-app # 서비스 공급자에 등록된 클라이언트 아이디
clientName: oauth2-client-app # 클라이언트 이름
clientSecret: ******************************** # 서비스 공급자에 등록된 클라이언트 비밀번호
redirectUrl: http://localhost:8080/login/oauth2/code/keycloak # 인가서버에서 권한 코드 부여 후 클라이언트로 리다이렉트 하는 위치
clientAuthenticationMethod: client_secret_post # 클라이언트 자격증명 전송방식(basic, post, none)
scope: openid, email, profile
provider: # 공급자 설정
keycloak:
authorizationUri: http://localhost:8081/realms/oauth2/protocol/openid-connect/auth # Authorization code 부여 엔드 포인트
issuerUri: http://localhost:8081/realms/oauth2 # 서비스 공급자 위치 (issuer)
jwkSetUri: http://localhost:8081/realms/oauth2/protocol/openid-connect/certs # JwkSetUri 엔드 포인트 (JWT 를 암호화하기 위한 검증)
tokenUri: http://localhost:8081/realms/oauth2/protocol/openid-connect/token # AccessToken 등 토큰 요청할때 엔드 포인트
userInfoUri: http://localhost:8081/realms/oauth2/protocol/openid-connect/userinfo # 사용자 정보 가져오기
userNameAttribute: preferred_username # 사용자명을 추출하는 클레임명
이제 이 application.yml 에 정의한 속성들을 다음 OAuth2ClientProperties 객체로 매핑시켜야 한다.
@ConfigurationProperties(
prefix = "spring.security.oauth2.client"
)
public class OAuth2ClientProperties implements InitializingBean {
private final Map<String, Provider> provider = new HashMap();
private final Map<String, Registration> registration = new HashMap();
...
public static class Registration {
private String provider;
private String clientId;
private String clientSecret;
private String clientAuthenticationMethod;
private String authorizationGrantType;
private String redirectUri;
private Set<String> scope;
private String clientName;
...
(getters and setters)
}
public static class Provider {
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String userInfoAuthenticationMethod;
private String userNameAttribute;
private String jwkSetUri;
private String issuerUri;
...
(getters and setters)
}
}
Open Authorization의 약자로, 웹 애플리케이션이나 모바일 앱 등에서 인증(Authentication)과 인가(Authorization)를 처리하기 위한 표준 프로토콜이다. 표준이기 때문에, 사용자가 직접 자신의 자격 증명을 애플리케이션에 직접 제공하지 않고도, 신뢰할 수 있는 제 3자 서비스(Goolge, Kakao 등)을 통해 자신의 자원을 안전하게 공유할 수 있도록 설계되었다. 뒤에 붙은 2.0 은 버전을 의미한다.
OAuth 2.0 의 특징
주로 사용자의 권한을 관리한다는 것에 목적이 있다. 사용자가 누구인지 인증하는 것에도 쓰이지만, 결국 주된 목적은 권한 관리에 있다.
사용자의 민감한 정보(비밀번호)를 노출하지 않고도, 제 3자가 사용자의 자원(이메일, 캘린더 등)에 접근할 수 있도록 설계할 수 있다.
Token 기반으로 서버 간의 인증을 처리한다. 이때 AccessToken 과 RefreshToken 의 개념이 들어간다.
서명을 사용했던 1.0 버전대 대신 2.0 버전에서는 Bearer Token 기반 인증을 사용한다.
권한을 부여하는 여러 Grant Type 이 있다. (Authorization Code, Client Credentials 등).
OAuth 2.0 Roles (역할) 에 대하여.
요소
역할
예시
Client
리소스 요청자 (애플리케이션)
쇼핑몰 앱, 모바일 앱
User
일반(실제) 사용자
Google 계정을 가진 사용자
Resource Owner
리소스 소유자
Gmail 사용자
Resource Server
보호된 리소스를 호스팅하는 서버
Google Drive API
Authorization Server
사용자 인증 및 Access Token 발급 서버
Google OAuth 서버, Keycloak
Client 를 Resource Server 에 등록하는 방법
필요한 요소들
Client ID : 애플리케이션 식별자
Client Secret : 애플리케이션에 따른 비밀번호
Authorized Redirect URIs : Authorized Code 를 전달해줄 url
중요한 것은, Client ID 와 Redirect URI 를 Resource Server 에 전달해주면, Client Secret 을 발급해준다는 것이다.
Resource Owner 에게 개인정보를 수집해도 되는지 물어봐야 한다.
위 이미지는, Resource Owner 인 나에게 Google 에게 나의 이름, 이메일 주소 등을 공유해도 되는지 물어보는 창이다. 이 창이 뜨고, 계속(Allow) 버튼을 누르는 과정에서 어떤 일이 일어나는 걸까?
< 이 예시의 역할정리 먼저 >
Resource Owner : 사용자
Client : Leetcode.com
Resource Server : Google
Authorization Server : Google
1. 일단 Client 와 Resource Server 는 (Client 를 이미 Resource Server 에 등록했으므로) 등록한 Client ID 와 Password, Redirect URL 를 알고있다.
2. 이 시점에서 Resource Owner(사용자)가 `구글로 로그인하기` 버튼을 클릭하면 다음과 같은 정보가 담긴 url 이 Resource Server 로 날아간다.
url 구조
GET/ (Resource Server의 지정된 주소) ?client_id=(Client_id) &scope=B,C &redirect_uri=(Redirect URL)
이 때 Scope 란, 모든 자원에 대해서 인가를 허용할 수 없으니, 그 범위를 지정하는 것이다.
여기서는 이름, 이메일일것이다.
3. 이제 Resource Owner(사용자)가 로그인을 하고, Allow 버튼을 클릭했다. 그렇게되면 Resource Server 는 특정 UserA 가 scope B 와 C 에 대해서 허용했다. 라는 내용을 알게된다. 즉, 내(사용자)가 Google 에게 이렇게 말한것이다. "leetcode(Client) 에게 너가 이미 알고있는 내 이름과 이메일 등을 공유하도록 허용할게."
4. 그러면 이제 그 증거로 Resource Server 인 Google 인 Resource Owner 인 나(사용자)에게 Authorization code 가 담긴 url 를 전송한다. Redirect Url 에 담아서!
HttpSecurity 객체는 Spring Security에서 인증(Authentication)과 인가(Authorization)를 설정하기 위한 핵심 클래스이기 때문에 무려 꽉 찬 별을 3개나!! 붙였다. 이 객체는 보안 설정의 중심 역할을 하며, 보안 필터 체인(Security Filter Chain)을 구성한다.
다음 문장은 일단 외워놓고 시작하자.
Spring Security 는 필터들을 여러 개 붙여놓는 행위, 즉 "필터 체이닝" 을 통해서 여러 인증 및 인가 로직들을 구현한다. 이때 필터들을 덕지덕지 붙여놓고 build 하는 객체가 HttpSecurity 이다.
Spring Security 는 HttpSecurity 객체로부터 시작된다.
Spring Security의 모든 설정은 이 객체를 통해 정의된다.
HTTP 요청을 어떻게 인증하고 인가할지, 어떤 보안 기능을 활성화하거나 비활성화할지 결정한다.
HttpSecurity는 설정을 도와주는 DSL (Domain-Specific Language) 역할을 하며, 가독성이 좋고 직관적으로 보안 규칙을 작성할 수 있게 한다.
필터들을 다 붙일 때, Builder 패턴처럼 속성을 메소드 형식으로 붙이는 것처럼 붙인다.
결론적으로 SecurityFilterChain 객체를 반환하도록 한다. 여러 필터들이 붙어서 커스텀된 체인 객체이다. (필터 무리(단체)이다.)
1. Spring Security 를 설정하는 방법.
Spring Boot 프로젝트를 실행하면 가장 먼저 포함하는 어노테이션이 있다.
바로 이것이다.
@SpringBootApplicaion
package promo.back.fromat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication
public class FromatApplication {
public static void main(String[] args) {
SpringApplication.run(FromatApplication.class, args);
}
}
@SpringBootApplicaion 어노테이션 내부로 들어가보면, 다음 어노테이션이 나온다.
@EnableAutoConfiguration
이 어노테이션은 Spring Boot 가 Spring Framework 와 다른 점을 보여주기도 한다. 이 어노테이션은 Spring Boot의 핵심 기능이라고 볼 수 있는 간결한 자동 설정을 활성화한다. 이 어노테이션은 Spring Boot 가 실행될 때 어플리케이션의 클래스 경로를 스캔하여, 필요한 Bean과 설정을 자동으로 등록하도록 만든다. Spring Boot 는 다양한 Starter 라이브러리와 연동하여 특정 라이브러리를 사용하기 위한 기본 걸정을 자동으로 구성한다. 실제로 Gradle 에서 라이브러리를 가져올 때, 왠만한 라이브러리의 명은 spring-starter 가 붙는 것을 많이 보았을 것이다.
여기서 중요한 점은, 만약 Spring-Security 에 관련된 라이브러리를 가져온다면, 자동 설정하는 라이브러리에서 Spring Security 와 관련된 라이브러리도 함께 가져온다는 것이다.
커스텀 Security 설정 파일이 존재하지 않는다면 현재 기본 시큐리티 설정 파일을 사용한다는 뜻이다.
현재 defaultSecurityFilterChain 메소드를 살펴보면, 모든 설정이 Defaults 로 설정되어 있는 것을 확인할 수 있다.
자, 우리는 이런 기본 설정 파일을 사용하고 싶은 것이 아니다.
직접 Security Config 클래스를 생성해보자.
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
/*
HttpSecurity 를 DI 주입받아서, 여러 기본 Filter 들을 가져와야 한다.
* 인증 API : formLogin(), logout(), csrf(), httpBasic(), SessionManagement(), RememberMe(), ExceptionHandling(), addFilter()
* 인가 API : authorizeHttpRequests(auth -> auth.requestMatchers(/admin).hasRole(USER).permitAll().authenticated().fullyAuthentication().access(hasRole(USER)).denyAll())
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// 보통 그냥 Default 로 사용한다.
.httpBasic(httpSecurityHttpBasicConfigurer ->
httpSecurityHttpBasicConfigurer
/*
인증되지 않은 사용자가 보호된 리소스에 접근했을 때 호출되는 Spring Security 의 컴포넌트.
HTTP Basic 인증에서는 클라이언트에게 401 Unauthorized 응답과 WWW-Authenticate 헤더를 반환한다.
현재 구상한 메소드는 기본 예외처리와 에러응답 그대로 작성해보았다.
*/
.authenticationEntryPoint(((request, response, authException) -> {
response.setHeader("WWW-Authenticate", "Basic realm=security");
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
}))
);
return http.build();
}
}
@EnableWebSecurity
이 태그를 붙인 @Configuration 클래스 파일은, Spring Security 에서 Custom-Security 설정 파일로 인식하고, 기본 Security 설정 파일을 가져다가 사용하지 않는다.
자 지금까지는 프롤로그였다.
가장 중요한 것은, 사용자 정의 SecurityConfig 클래스에서 지정해야하는, securityFilterChain 메소드이다.
실은 그냥 ControllerAdvice도 있는데, 앞에 Rest가 붙게되면, RESTful API, 즉 json 통신을 위한 ControllerAdvice 이다. 마치 @Controller 와 @RestController 의 차이와 같다고 보면 된다.
이 어노테이션을 달아주면 Spring MVC 컨테이너가 사용자 정의 예외 처리 로직을 모아둔 클래스구나 - 하고 인식하여, 전역적으로 예외들을 낚아채서 처리해준다.
☆ @ExceptionHandler(특정 Exception.class)
이 어노테이션을 달아주면 Spring MVC 컨테이너가 연결한 특정 Exception.class 에 해당하는 에러가 터질때마다 해당 메소드로 연결해준다. 만약 이 어노테이션(@ExceptionHandler)을 단 메서드가 특정 컨트롤러 내부에 있다면, 그 컨트롤러 내부에서 터지는 에러들만 낚아채겠지만, 만약 @RestControllerAdvice 어노테이션이 달려있는 클래스 내부에 이 어노테이션(@ExceptionHandler)을 단 메소드를 배치해놓는다면, 전역적으로 해당 에러가 터졌을 때, 어느 컨트롤러에서 터지든 모든 해당 에러를 낚아채서 처리하고 사용자가 정의한 방식대로 반환한다.
☆ ResponseEntity
Spring Framework 에서 제공하는 클래스 중 하나이다. HTTP 응답의 전체를 제어할 수 있는 클래스이다. RESTful API 개발에서 클라이언트에게 상태 코드, 헤더, 바디를 포함한 완전한 HTTP 응답을 반환하는 데 사용된다.
2. ResponseStatusExceptionResolver
예외 클래스에 선언된 @ResponseStatus 어노테이션을 처리하여 HTTP 상태 코드를 반환한다.
하지만 헤더나 바디 없이 그저 상태 코드만을 반환하기도 하고, Restful API 에서 자주 사용하는 방식인 RestControllerAdvice 에서 잘 안쓰이기 때문에, 세밀한 제어가 안되어 잘 안쓰인다.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class PageNotFoundException extends RuntimeException {
public PageNotFoundException(String message) {
super(message);
}
}
@Controller
public class HtmlController {
@GetMapping("/page")
public String getPage() {
throw new PageNotFoundException("Page not found");
}
}
☆ @ResponseStatus
이 어노테이션은 단순한 상태 로직만 반환하는데다가, @RestControllerAdvice + ResponseEntity 조합에 밀려 잘 사용되지 않는 이유로 RESTful API 서비스에서는 잘 사용되지는 않는다.
그러나 전통적인 Java 웹 애플리케이션(JSP, Thymeleaf 등) 에서는 HTML 뷰 페이지를 반환하는 것이 주된 목표가 되기 때문에, 예외 발생 시 상태 코드와 함께 특정 에러 페이지를 반환하는 방식이 일방적이다. 따라서 간단한 예외 처리나 상태 코드 반환이 필요할 때 예외 클래스에 @ResponseStatus를 붙여 처리한다.
3. DefaultHandlerExceptionResolver
Spring MVC가 정의한 표준 예외이다. 자주 발생하는 기본 예외에 대한 처리를 제공한다.
발생한 예외를 Spring 의 표준 예외 목록과 비교하여 해당 예외를 처리한다.
HTTP 상태 코드와 기본 에러 페이지를 반환한다.
보통 별도의 커스텀 처리가 필요하지 않은 표준 예외들에 해당한다.
예를 들어,
HttpRequestMethodNotSupportedException → 405 Method Not Allowed.
HttpMediaTypeNotSupportedException → 415 Unsupported Media Type.
HttpMessageNotReadableException → 400 Bad Request.
기타 Spring 내부 표준 예외들.
실제 해당 구현체를 들어가보면 구현이 되어 있다.
더 자세한 코드는 직접 스프링 라이브러리를 통해서 들어가보면 된다.
기본 구현체 들의 호출 순서.
Spring MVC는 기본적으로 우선순위에 따라 HandlerExceptionResolver를 호출한다.
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
HandlerExceptionResolverComposite가 이들 구현체를 관리하며, 첫 번째로 예외를 처리할 수 있는 Resolver가 동작하면 이후 Resolver는 호출되지 않는다.
스프링에는 검증에 관련한 표준이 존재한다. JSR 380 은 Java Bean Validation 2.0의 표준사양이다. 이름하여 Java Specification Request 의 약자로, Java 기술 표준에 대한 제안서를 의미한다. 뒤에 붙은 380은 버전의 고유 식별 번호이다. Java 객체의 속성 값이 특정 조건을 충족하는지 검증하는 표준을 제공하며, 객체의 속성 값을 검증하기 위한 어노테이션 기반의 유효성 검증 메커니즘을 제공한다.
Spring은 이 표준을 기반으로 Hibernate Validator와 같은 구현체를 활용하여 유효성 검증을 지원한다. 이때, Hibernate Validator는 JSR 380 (Bean Validation 2.0) 표준의 `참조 구현체(reference implementation)` 로, Java 객체의 유효성 검증을 지원하는 오픈 소스 라이브러리이다. 즉, 쉽게 말해서, 검증을 쉽게 도와주도록 Spring 이 미리 짜둔 구현체를 의미한다. 어느정도 구현해놓은 기본적인 검증 로직을 제공하기 위해서는 그 코드(구현체)를 어딘가에는 라이브러리의 형태로 저장해놓고 편리하게 불러와서 사용할 수 있어야 할 것이다. 그 구현체가 바로 Hibernate Validator이다.
커스텀 Validator를 만드는 방법.
요약(미리보기) : 사용자 정의 어노테이션과ConstraintValidator인터페이스를 조합하여 원하는 검증 로직을 작성하면 된다.
애노테이션에서는 속성이 추상 메서드 형태로 정의되기 때문에, 메소드처럼 속성의 마지막에 () 을 붙인다. 애노테이션의 속성은 정적 데이터를 나타내지만, 이 데이터를 메서드 호출처럼 동적으로 접근할 수 있도록 설계한 것이다. 이렇게 설계한 이유는 애노테이션의 값도 타입 안정성과 일관성을 유지하기 위함이다.
또한 기본값을 제공하여, 값이 생략되더라도 안정적으로 사용할 수 있도록 하며, 속성을 메서드로 정의했기 때문에 런타임 시점에 리플렉션을 이용하여 값을 쉽게 조회할 수도 있다.
이 인터페이스는, JSR 380 표준에 정의된 인터페이스로, 특정 검증 어노테이션과 해당 검증 로직을 연결하기 위한 기반 인터페이스이다.
★ Hibernate Validation 구현체에 대하여.
위 코드에서 작성한 ValidEnumValidator 는 커스텀 Validation 구현체이지만, 스프링에는 다양한 기본 HibernateValidation 구현체가 존재한다. 흔히 검증 로직으로 사용하는 @NotNull, @Size, @Email 등이 모두 위의 방식으로 구현되어 있다. 나는 그 방식을 그대로 채택하여 커스텀 검증 구현체를 만들었을 뿐이다.
3. 이제 이렇게 구현한 커스텀 어노테이션을 적용해보자.
package promo.back.fromat.common.status;
public enum Gender {
MALE,
FEMALE
;
}
public String someServiceLogic() {
if (someCondition) {
throw new NullPointerException("예외 발생");
}
return "success";
}
그냥 서비스단에서 냅다 throw 를 던져버린다.
그러나 이런 방식으로 처리했을 때, 다음과 같은 특징이 있다.
예외 처리를 서비스 로직, 컨트롤러 등에서 처리해야 한다. (try-catch)
예외 처리 응답이 일관성이 지켜지지 않을 수 있다. 각 메서드마다 독립적으로 작성하기 때문이다.
각 메서드에 지저분(?)하게 중복된 try-catch 로직이 포함될 가능성이 있다.
유지보수성 측면에서도 좋지 않다. 예외 로직 변경시, 모든 메서드를 수정하게 되기 때문이다.
공통되거나 혹은 일관적으로 규칙이 있는 예외 처리 로직에 대해서 관리하기 불편하다.
Spring MVC 는 예외 처리 매커니즘으로 HandlerExceptionResolver를 만들었다.
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
// 특정 예외에 대한 처리
if (ex instanceof NullPointerException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("{\"error\":\"Null Pointer Exception 발생\"}");
} else if (ex instanceof IllegalArgumentException) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("{\"error\":\"Invalid argument\"}");
} else {
// 기본 처리
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write("{\"error\":\"Unexpected error occurred\"}");
}
} catch (IOException e) {
e.printStackTrace();
}
// 뷰 반환하지 않음 (REST API라면 null)
return new ModelAndView();
}
}
이런식으로 HandlerExceptionResolver 를 상속받은 다음에, 해당 커스텀 Resolver 클래스에,
원하는 Exception 종류를 if문을 통해서 판별한 다음에, response 로 보낼 처리 로직을 작성해주면 되는 것이다.
이렇게하면 내가 서비스 로직에서 throw를 통해 던진 예외를 resolver 가 낚아채서, 예외를 처리해준다.
그렇다. 나는 throw로 예외만 던지면 되는것이다! !
하지만 Resolver 클래스를 작성만 해준다고, 스프링이 해당 Resolver를 인식할 수 있는 것은 아니다.
다음과 같이 Config 에 등록해주어야 한다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new CustomExceptionResolver());
}
}
여러 Resolver 들을 등록할 수 있는 List<HandlerExceptionResolver> resolvers 라는 리스트에, 추가하고 싶은 resolver 들을 add 메소드를 통해서 추가하면, 리졸버가 등록된다.
이제 나는 다음과 같이 서비스 로직에서 NullPointerException만 날리면 Resolver 가 낚아채서 예외 처리 로직을 수행해준다!!
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
public String executeLogic(boolean throwError) {
if (throwError) {
throw new NullPointerException("Test NullPointerException");
}
return "Success";
}
}
Spring MVC에서 예외 처리를 커스터마이징해서,
발생한 예외를 Spring MVC의 예외 처리 흐름에서 중앙 집중적으로 처리하려면,
1. HandlerExceptionResolver를 구현하여 resolveException 메서드에 예외 처리 로직을 작성
종업원(API 처리 도구)이 없다면 고객(사용자)과 주방(서버) 사이의 소통이 원활하지 않을 것이다. 그도 그럴것이, 특정 음식점(웹 서비스)에 처음 들어온 고객(사용자)은 그 음식점(웹 서비스)의 메뉴(서비스 요청 규격)에 대해 잘 모를 수 밖에 없다. 메뉴판(API 규격)이나 안내해주는 종업원(API 처리 도구)이 없다면, 고객은 자신이 원하는 음식을 어떻게 알고 주문할 것인가.
API와 서블릿의 역할
이처럼 API는 사용자(고객)의 요청과 서버(주방)의 응답 사이를 연결해주고, 각 요청과 응답을 효율적으로 처리하는 중요한 역할을 맡고 있다.
즉, `음식점 API = 메뉴판`, `음식점의 주문을 처리해주는 도구 = 종업원` 인 셈이다.
이 논리를 고~ 대로 서블릿에 갖다 넣으면 된다.
서블릿 API란?
서블릿 API는 서블릿을 통해 요청과 응답을 처리하는 표준 규격이다.
서블릿 API는 다음과 같은 중요한 역할을 한다.
클라이언트(사용자)의 HTTP 요청을 받아들여 처리.
요청 데이터를 분석하고 필요한 작업을 실행.
서버에서 처리된 결과(응답)를 사용자에게 반환.
서블릿 API는 이를 효율적으로 처리하기 위한 표준 인터페이스와 규칙을 제공한다. 서블릿 컨테이너(Tomcat, Jetty 등)는 이 규격을 따라 동작하며, 개발자는 서블릿 API를 기반으로 서블릿을 작성해 클라이언트와 서버 간의 소통을 원활히 할 수 있다.
이때 스프링은 내장 톰캣에 서블릿 API가 있다! !
스프링 부트는 서블릿 API를 내장 톰캣을 통해 포함하고 있다. 이를 통해 개발자는 서블릿 컨테이너를 따로 설정하거나 설치하지 않고도 HTTP 요청과 응답을 효율적으로 처리할 수 있다. 특히 스프링 MVC의 핵심인 `DispatcherServlet`은 서블릿 API의 `HttpServlet`을 확장하여 만들어졌으며, 요청-응답 흐름의 중심 역할을 담당한다.
스프링이 서블릿 API를 사용하는 핵심 요소
DispatcherServlet: 스프링의 핵심 서블릿
DispatcherServlet은 스프링 MVC에서 요청과 응답을 처리하는 프론트 컨트롤러 역할을 한다.
서블릿 API의 HttpServlet 클래스를 확장하여 만들어졌으며, 모든 HTTP 요청을 받아 처리한다.
동작 과정:
클라이언트의 요청이 들어오면 서블릿 컨테이너가 DispatcherServlet에 요청을 전달한다.
DTO = Data Transfer Object = 데이터를 전송하고 변환하는데 초점맞춘 객체
그렇다.
비즈니스 로직과 데이터 표현을 분리하여 " 관심사를 분리 " 하는 것이다.
Entity는 데이터베이스와 직접적으로 매핑되어 비즈니스 로직을 처리하는 역할을 담당하며, 데이터베이스의 구조와 밀접하게 연결된다. 반면, DTO(Data Transfer Object)는 클라이언트와의 데이터 교환을 위한 객체로, 주로 요청과 응답에 필요한 데이터만을 담아 효율적인 데이터 전송에 초점을 둔다. 이렇게 구분하면 Entity는 비즈니스 로직에 집중할 수 있고, DTO는 필요한 데이터만 선택적으로 제공하여 데이터 전송을 최적화할 수 있다.
이렇게하면 각 객체가 자신의 책임에만 집중할 수 있어 유지보수성이 향상되고, 코드의 구조가 명확해진다.
그러니까 유효성 검사라는 것은 결국 "사용자"가 어떤 값을 입력할지 모르기 때문에 진행하는 것이다. 그와 반면에 엔티티의 경우는 이미 DB로 확정되어 만들어진 것이기 때문에 형식이 안맞을 일이 거의 없다.
DTO에서 유효성 검사를 하는 핵심 이유는 사용자가 입력할 데이터의 형식이나 내용이 예상과 다를 수 있기 때문이다. 클라이언트에서 서버로 전달되는 데이터는 신뢰할 수 없기 때문에, 서버는 안전하고 일관된 데이터만 비즈니스 로직으로 전달되도록 해야 한다. DTO는 이러한 데이터를 검증하고 필터링하는 역할을 담당한다. 이것이 DTO의 존재이유다.
반면에 Entity는 이미 데이터베이스와 구조가 확정된 상태로, 엔티티에 저장된 데이터는 기본적으로 데이터베이스 스키마와 일치해야 한다. 즉, 데이터베이스에 저장되는 데이터는 스키마에 의해 형식이 이미 보장되므로, 데이터의 형식이 맞지 않거나 유효하지 않을 가능성이 매우 낮다. 따라서 Entity는 데이터 형식 검증보다는 비즈니스 로직에만 집중할 수 있다.
결론.
DTO에서 유효성 검사를 수행하는 이유는 클라이언트로부터 받은 데이터가 형식이나 값의 조건을 만족하는지 검증하여, 서버로 유효한 데이터만 전달하기 위해서이다.
Entity는 데이터베이스와 매핑된 구조로 데이터 형식이 미리 결정되어 있으며, 데이터의 일관성을 보장하는 역할을 하기 때문에 추가적인 형식 검증이 필요하지 않다.