์๋ธ๋ฆฟ ํํฐ๋ HTTP ์์ฒญ ๋๋ ์๋ต์ DispatcherServlet
์ด ๋ฐ๊ธฐ ์ ์ ๊ฐ๋ก์ฑ์ ์ฒ๋ฆฌํ ์ ์๋ ์ปดํฌ๋ํธ์ด๋ค.
์ฆ, ํํฐ๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์์์ ์์ฒญ & ์๋ต์ ์ฌ์ & ์ฌํ ์ฒ๋ฆฌํ ์ ์๋๋ก ๋์์ฃผ๋ ์๋ธ๋ฆฟ ์ปดํฌ๋ํธ
์ด๋ค.
์๋ธ๋ฆฟ ์ปดํฌ๋ํธ๋ ํฌ๊ฒ 3๊ฐ์ง ์ข ๋ฅ๊ฐ ์กด์ฌํ๋ค.
HttpServlet
์ ์์ ๋ฐ์ ๊ตฌํํ๋ค.Spring ์์ ์ฝ๋
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.getWriter().write("Hello, Servlet!");
}
}
Spring Boot์์ ์ฝ๋
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.getWriter().write("Hello, Spring Boot!");
}
}
@SpringBootApplication
@ServletComponentScan
public class MyApplication { ... }
@RestController
, @RequestMapping
์ ์ฌ์ฉํ๋ค. ์ฐธ๊ณ ๊ธSpring ์์ ์ฝ๋
@WebFilter("/*")
public class LogFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
System.out.println("Request Start");
chain.doFilter(req, res); // ๋ค์ ํํฐ ๋๋ ์๋ธ๋ฆฟ ํธ์ถ
System.out.println("Response End");
}
}
Spring Boot ์์ ์ฝ๋
@Component
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("๐ ํํฐ ๋์");
chain.doFilter(request, response);
}
}
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new LogFilter());
registrationBean.addUrlPatterns("/*"); // ์ ์ฒด ์์ฒญ
registrationBean.setOrder(1); // ํํฐ ์์
return registrationBean;
}
}
Spring ์์ ์ฝ๋
@WebListener
public class AppListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
System.out.println("์ ํ๋ฆฌ์ผ์ด์
์์๋จ");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("์ ํ๋ฆฌ์ผ์ด์
์ข
๋ฃ๋จ");
}
}
Spring Boot ์์ ์ฝ๋
@Component
public class AppListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("๐ ์ ํ๋ฆฌ์ผ์ด์
์์");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("๐ ์ ํ๋ฆฌ์ผ์ด์
์ข
๋ฃ");
}
}
@WebListener
๋์ ์ @Component
๋ก ์๋ ๋ฑ๋กํ๋ค.@ServletComponentScan
๋ํ ํ์ํ์ง ์๋ค.Spring Boot์๋ ์ด๋ฏธ @RestController, Interceptor, AOP
๋ฑ ๋ก์ง์ ๋ถ๋ฆฌํ ์ ์๋ ๋ค์ํ ๋ฐฉ์๋ค์ด ์กด์ฌํ๋ค.
๊ทธ๋ผ์๋ Filter๋ฅผ ์ฌ์ฉํ๋ ์ด์ ๋ ๋ฌด์์ผ๊น?
์๋ Spring ์ ํ๋ฆฌ์ผ์ด์ ๋์ ๊ณผ์ ์ ๊ฐ๋ตํ ์ดํด๋ณด์.
[ํด๋ผ์ด์ธํธ ์์ฒญ]
โ
[์๋ธ๋ฆฟ ์ปจํ
์ด๋ (Tomcat ๋ฑ)]
โ
[Filter] โ ์ฌ๊ธฐ๊น์ง๋ ์๋ธ๋ฆฟ API ์์ญ
โ
[DispatcherServlet] โ ์ฌ๊ธฐ์๋ถํฐ Spring MVC
โ
[HandlerMapping โ Controller โ View ์ฒ๋ฆฌ]
ํํฐ๋ Spring MVC๋ณด๋ค ๋จผ์ ์คํ๋๋ค.
๋ฐ๋ฉด, Interceptor, ControllerAdvice, @Controller
๋ฑ์ ๋ชจ๋ DispatcherServlet
์ดํ์ ์๋ํ๋ค.
๋๋ฌธ์ ํํฐ๋ Spring ์ง์ ์ , ๊ณตํต ์ฒ๋ฆฌ ๋ก์ง์ ์คํํ ์ ์๋ ์ ์ผํ ์๋จ์ด๊ธฐ์ ์ฌ์ฉํ๋ค.
์ค์ ์ฝ๋์ ์ฌ์ฉ ์ฌ๋ก๋ฅผ ๋ณด๋ฉด์ ํํฐ์ ๋ํด ๋์ฑ ๊น์ด ์ดํดํด๋ณด์.
์์ ์ฝ๋์ด๊ธฐ ๋๋ฌธ์ ๊ทธ๋๋ก ํ๋ก์ ํธ์ ์ ์ฉํ๊ธฐ์๋ ๋ฌด๋ฆฌ๊ฐ ์์ ์ ์๋ค!
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
HttpServletRequest req = (HttpServletRequest) request;
log.info("๐ฅ ์์ฒญ URI: {}", req.getRequestURI());
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
log.info("๐ค ์ฒ๋ฆฌ ์๋ฃ ({}ms)", duration);
}
}
๊ฒฐ๊ณผ ์์
INFO [LoggingFilter] ๐ฅ ์์ฒญ URI: /api/v1/users INFO [LoggingFilter] ๐ค ์ฒ๋ฆฌ ์๋ฃ (87ms)
์์ฒ๋ผ ๊ตฌ์ฑํ์ฌ ๋ฑ๋กํด ๋๋ค๋ฉด ๋ค์ด์ค๋ ๋ชจ๋ ์์ฒญ์ ๋ํด์ ์ฒ๋ฆฌ ์๋๋ฅผ ํ์ ํ ์ ์๊ณ , ์ด๋ค API์์ ๋ณ๋ชฉ์ด ์๊ธฐ๋ ์ง ํ์ ์ด ์ฉ์ดํ๋ค.
import jakarta.servlet.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class EncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
log.debug("๐ UTF-8 ์ธ์ฝ๋ฉ ์ค์ ์๋ฃ");
chain.doFilter(request, response);
}
}
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<EncodingFilter> encodingFilter() {
FilterRegistrationBean<EncodingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new EncodingFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(1); // ๊ฐ์ฅ ๋จผ์ ์คํ๋๋๋ก ์ค์
return registrationBean;
}
}
์์ ๊ฐ์ด ํํฐ๋ฅผ ๋ง๋ค์ด ์ค ํ์, ๋ฑ๋กํด ์ฃผ๋ฉด ์ ์ฉ๋๋ค.
๐จโ๐ป ํ์ง๋ง Spring Boot 1.2.0 ๋ฒ์ ์ดํ๋ก๋
CharacterEncodingAutoConfiguration
์ด ๋ฑ์ฅํ๋ฉฐ,CharacterEncodingFilter
๋ฅผ ์๋์ผ๋ก ๋ฑ๋กํด์ฃผ๋ ๊ตฌ์กฐ๊ฐ ๊ฐ์ถฐ์ก๋ค.
๋๋ฌธ์ ์ธ์ฝ๋ฉ ํํฐ๋ฅผ ๋ง๋ค์ด์ ๋ฑ๋กํ ํ์์ฑ์ด ์ฌ๋ผ์ก๋ค.
org.springframework.boot.autoconfigure.web.servlet.CharacterEncodingAutoConfiguration
ํด๋์ค๋,
spring.http.encoding.enabled = true
์ธ ๊ฒฝ์ฐ์ ์ธ์ฝ๋ฉ ํํฐ๋ฅผ ์๋์ผ๋ก ๋ฑ๋กํด ์ค๋ค.
๋ํดํธ ๊ฐ์ด UTF-8 ์ด๊ธฐ์ ๋ฐ๋ก ๋ณ๊ฒฝํ์ง ์๋๋ค๋ฉด ์ธ์ฝ๋ฉ ํํฐ์ ๋ฑ๋ก ์์ด๋ ์ถฉ๋ถํ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค.
๋ง์ฝ ์ธ์ฝ๋ฉ ๋ฐฉ์์ ๋ช
์์ ์ผ๋ก ์ง์ ํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด .yaml
ํ์ผ์ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
spring:
http:
encoding:
charset: MS949
force: true
CORS์ ๋ํ ์ค๋ช ์ ๋์ด๊ฐ๊ณ , CORS์ OPTIONS์ ๊ด๊ณ์ฑ์ ๋ํด์ ์ ์ด๋ณด๋ ค๊ณ ํ๋ค.
๋ธ๋ผ์ฐ์ ๋ ์์ฒญ์ ๋ณด๋ผ ๋ ์๋์ ๊ฐ์ ๊ฒฝ์ฐ์์ ๋จผ์ OPTIONS
์์ฒญ(Preflight)์ ๋ณด๋ด๊ฒ ๋๋ค.
1) ๋ค๋ฅธ Origin์ผ๋ก ์์ฒญ์ ๋ณด๋ด๋ ๊ฒฝ์ฐ
2) ๋์ผ Origin์ด์ง๋ง, ๋ณต์กํ ์์ฒญ์ธ ๊ฒฝ์ฐ
- ์ปค์คํ ํค๋๊ฐ ์กด์ฌ (Authorization ๋ฑ..)
- PATCH, PUT ๋ฑ ๋ฉ์๋
- ๋ฐํ ํ์ ์ด urlencoded๊ฐ ์๋ (application/json ๋ฑ..)
๋์ผ Origin์ด๋ผ๋ฉด CORS๋ฅผ ๊ณ ๋ คํ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์, 1๋ฒ ๋ค๋ฅธ Origin์ผ๋ก ์์ฒญ์ ๋ณด๋ด๋ ๊ฒฝ์ฐ์ ์ง์คํ๋ฉด ๋๋ค.
OPTIONS
์์ฒญ์ ๋์คํจ์น ์๋ธ๋ ์์ ์ฒ๋ฆฌํ๋ฉฐ, ์์ฒญ ๋ธ๋ผ์ฐ์ ์์ 200ok
๋ฅผ ๋ฐ๋๋ค๋ฉด ์ค์ ๋ฉ์๋ (GET, POST..) ์์ฒญ์ ๋ณด๋ธ๋ค.
ํ์ง๋ง ๋ฐ์ ์๋ต์ ํค๋์ CORS ํ์ฉ ์ค์ ์ธ Access-Control-Allow-*
๊ฐ์ด ์๋ค๋ฉด, ๋ธ๋ผ์ฐ์ ๋ ์ค์ ์์ฒญ์ ์ฐจ๋จํ์ฌ CORS ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์ฆ, ์์ฒญ ์์ฒด๋ ๋ฌธ์ ์์ด ์ฒ๋ฆฌ๊ฐ ๋๋๋ผ๋ CORS ํ์ฉ ํค๋๊ฐ ์์ด์ผ ํ๋ ๊ฒ์ด๋ค.
๊ทธ๋ ๊ธฐ์ CORS ํ์ฉ ํค๋๋ฅผ ๋์คํจ์น ์๋ธ๋ ๋์ ์ ์ ์ถ๊ฐํด์ฃผ์ด์ผ ํ๋ ๊ฒ์ด๊ณ , ์ฌ๊ธฐ์ ํํฐ๊ฐ ์ฐ์ด๊ฒ ๋๋ค.
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.setHeader("Access-Control-Allow-Origin", "*"); // ๋ชจ๋ origin ํ์ฉ
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
chain.doFilter(request, response);
}
}
Spring Security๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด, ๋์ผํ๊ฒ ํํฐ๋ฅผ ๋ง๋ค์ด์ค ํ ์ค์ ์์ ๋ฑ๋กํด ์ฃผ๋ฉด ๋๋ค.
ํ์ง๋ง ์ต๊ทผ์๋ Spring Security๋ฅผ ๋ง์ด๋ค ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ํด๋น ์์๋ ๋ณด๋๋ก ํ๊ฒ ๋ค.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(withDefaults()) // CORS ์ค์ ํ์ฑํ
.csrf().disable()
.authorizeHttpRequests()
.anyRequest().permitAll();
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
์ฐ์ corsConfigurationSource()
๋ฉ์๋๋ฅผ ๋ง๋ค์ด ์ค์ ๊ฐ๋ค์ ์ง์ ํ๋ค.
์ดํ ์ํ๋ฆฌํฐ ํํฐ ์ฒด์ธ์ .cors(withDefaults())
๋ก ๋ฑ๋กํด ์ฃผ๋ฉด ๋์ด๋ค.
ํด๋น ์ฝ๋๋ "CORS ์ค์ ์ ์ฌ์ฉํ๊ฒ ๋ค"
๋ผ๋ ๋ป์ด๋ฉฐ, ๋์ ๊ณผ์ ์ ์ ๋ฆฌํ์๋ฉด ์๋์ ๊ฐ๋ค.
@Bean CorsConfigurationSource
๋ฅผ ๋ฑ๋กํด ๋.cors(withDefaults())
๊ฐ ์ด๋ฅผ ๊ฐ์ง- Spring Security ๋ด๋ถ์์
CorsFilter
๋ฅผ ์์ฑSecurity Filter Chain
๋ด์CorsFilter
๊ฐ ์๋์ผ๋ก ์ถ๊ฐ
ํํฐ๊ฐ ํฐ์ผ์ด ๊ด๋ฆฌํ๋ ๊ฐ์ฒด์ธ ๊ฒ ๊ฐ์๋ฐ ์คํ๋ง์์ ํํฐ๊ฐ ์ด๋ป๊ฒ ๋น์ผ๋ก ๋ฑ๋ก๋๋์ง๋ ์ฐพ์๋ณด๋ฉด ์ข์๋ฏ?