๐Ÿ’ Spring/๊ฐœ๋… ๋ฐ ์ดํ•ด

Spring ์กด์žฌํ•˜์ง€ ์•Š๋Š” API ์‘๋‹ต ์ปค์Šคํ„ฐ๋งˆ์ด์ง• (feat. ๋‚ด๋ถ€ ๋™์ž‘๋„ ์‚ดํŽด๋ณด๊ธฐ)

iseunghan 2023. 9. 21. 22:44
๋ฐ˜์‘ํ˜•

Intro

๊ธฐ๋ณธ์ ์œผ๋กœ Spring MVC๋Š” ์กด์žฌํ•˜์ง€ ์•Š๋Š” API ์š”์ฒญ ์‹œ ์‘๋‹ต์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

{
	"timestamp":"2023-09-21T11:30:05.517+00:00",
	"status":404,
	"error":"Not Found",
	"path":"/api/404"
}

์œ„ ์‘๋‹ต์€ Spring MVC๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ๋Š” ์˜ค๋ฅ˜ ์‘๋‹ต ์ž…๋‹ˆ๋‹ค. ๋ณดํ†ต ์‹ค๋ฌด์—์„œ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ๋Š” ์‘๋‹ต๊ฐ’์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋ณด๋‹จ ๊ณตํ†ต ์‘๋‹ต ํฌ๋งท์„ ๋งž์ถฐ์„œ ์‚ฌ์šฉํ•  ๊ฒƒ ์ž…๋‹ˆ๋‹ค. ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด Not Found์— ๋Œ€ํ•œ ์‘๋‹ต์„ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ๋Š”์ง€์™€ ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๊ณ  ์žˆ๋Š”์ง€๋„ ํ•จ๊ป˜ ์—๋Ÿฌ ๋กœ๊ทธ์™€ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์—๋Ÿฌ ์‘๋‹ต์ด ์ƒ์„ฑ๋˜๊ธฐ๊นŒ์ง€์˜ ์—ฌ์ •

๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ์Šคํ”„๋งMVC์—์„œ๋Š” ์—๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์„๊นŒ์š”?

๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด์„œ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class APINotFoundExceptionTest {
    @LocalServerPort private int port;

    @Test
    void ์—†๋Š”_API๋ผ๋ฉด_404์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() throws Exception {
        // given
        RestTemplate restTemplate = new RestTemplate();

        // when & then
        assertThatThrownBy(() -> restTemplate.getForObject("<http://localhost>:" + port + "/api/404", String.class));
    }
}

์ž์„ธํ•œ ๋กœ๊ทธ๋ฅผ ์‚ดํŽด๋ณด๊ธฐ ์œ„ํ•ด application.yaml์— web ๊ด€๋ จ ๋กœ๊ทธ ๋ ˆ๋ฒจ์„ DEBUG๋กœ ์„ค์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

logging:
  level:
    web: debug

์‹คํ–‰์„ ํ•ด์ฃผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋กœ๊ทธ๊ฐ€ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

2023-09-21T21:09:26.457+09:00 DEBUG 56326 [1] [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/api/404", parameters={}
2023-09-21T21:09:26.460+09:00 DEBUG 56326 [2] [o-auto-1-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
2023-09-21T21:09:26.463+09:00 DEBUG 56326 [3] [o-auto-1-exec-1] o.s.w.s.r.ResourceHttpRequestHandler     : Resource not found
2023-09-21T21:09:26.463+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 404 NOT_FOUND
2023-09-21T21:09:26.466+09:00 DEBUG 56326 [4] [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2023-09-21T21:09:26.467+09:00 DEBUG 56326 [5] [o-auto-1-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-09-21T21:09:26.477+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [text/plain, application/json, application/*+json, */*] and supported [application/json, application/*+json]
2023-09-21T21:09:26.478+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Thu Sep 21 21:09:26 KST 2023, status=404, error=Not Found, path=/api/404}]
2023-09-21T21:09:26.493+09:00 DEBUG 56326 --- [    Test worker] o.s.web.client.RestTemplate              : Response 404 NOT_FOUND
2023-09-21T21:09:26.493+09:00 DEBUG 56326 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 404
  • [1]: RestTemplate์œผ๋กœ ๋ณด๋ƒˆ๋˜ ์š”์ฒญ์„ DispatcherServlet์ด ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค
  • [2]: ์šฐ๋ฆฌ์˜ ์š”์ฒญ์ด ResourceHttpRequestHandler์—๊ฒŒ ๋งคํ•‘๋˜์—ˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค? (*๋’ค์—์„œ ์ž์„ธํ•˜๊ฒŒ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค)
  • [3]: Resource๋ฅผ ๋ชป์ฐพ์•˜๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.
  • [4]: ๊ทธ๋Ÿฌ๊ณ ๋Š” ์šฐ๋ฆฌ์˜ ์š”์ฒญ์„ /error๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค
  • [5]: ์ตœ์ข…์ ์œผ๋กœ BasicErrorController๊ฐ€ ์šฐ๋ฆฌ์—๊ฒŒ ์‘๋‹ต์„ ๋‚ด๋ ค์ฃผ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ž ์ด์ œ ๋กœ๊ทธ ๋ถ„์„์ด ๋๋‚ฌ์œผ๋‹ˆ ํ•œ๋ฒˆ ์ฝ”๋“œ๋กœ ์‚ดํŽด๋ณผ๊นŒ์š”?

ResourceHttpRequestHandler

public class ResourceHttpRequestHandler extends WebContentGenerator
		implements HttpRequestHandler, EmbeddedValueResolverAware, InitializingBean, CorsConfigurationSource {
	...

	@Override
	public void handleRequest(HttpServletRequest request, HttpServletResponse response)	throws ServletException, IOException {
		// For very general mappings (e.g. "/") we need to check 404 first
		Resource resource = getResource(request);
		if (resource == null) {
			logger.debug("Resource not found"); // **[1]
			response.sendError(HttpServletResponse.SC_NOT_FOUND); // **[2]
			return;
	 }
	...
}
  • [1]: ๋กœ๊ทธ์— ์ฐํ˜”๋˜ “Resource not found”๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค!
  • [2]: ์šฐ๋ฆฌ์—๊ฒŒ 404 ์‘๋‹ต์„ ์—ฌ๊ธฐ์„œ ๋ณด๋‚ด๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ์—๋Ÿฌ๋Š” BasicErrorController์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

๊ทผ๋ฐ ๋ญ”๊ฐ€ ์ด์ƒํ•ฉ๋‹ˆ๋‹ค.. ResourceHttpRequestHandler๋Š” ์ •์  ๋ฆฌ์†Œ์Šค๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ์ธ๋ฐ, ์–˜๋Š” ์šฐ๋ฆฌ์˜ ์š”์ฒญ์„ ์ •์  ๋ฆฌ์†Œ์Šค ์š”์ฒญ์œผ๋กœ ๋ณด๊ณ  ์ •์  ํŒŒ์ผ์„ ์ฐพ์œผ๋ ค๊ณ  ์‹œ๋„ํ•˜๋Š” ๊ฒƒ์ด์ฃ . ์™ค๊นŒ์š”?

SimpleUrlHandlerMapping

2023-09-21T21:09:26.460+09:00 DEBUG 56326 [2] [o-auto-1-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]

[2] ๋กœ๊ทธ์—์„œ SimpleUrlHandlerMapping์ด ResourceHttpRequestHandler๋ฅผ ๋งคํ•‘ํ–ˆ๋‹ค๊ณ  ๋กœ๊ทธ๋ฅผ ์ฐ์—ˆ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์š”์ฒญํ•œ ์ € /api/404๋ฅผ ์ฒ˜๋ฆฌํ•  ํ•ธ๋“ค๋Ÿฌ๋Š” ์—†์„ํ…๋ฐ Handler๊ฐ€ ์žˆ๋‹ค๋Š”๊ฒŒ ๋ญ”๊ฐ€ ์ด์ƒํ•ฉ๋‹ˆ๋‹ค.

DispatcherServlet์„ ์‚ดํŽด๋ด…์‹œ๋‹ค

๊ธฐ๋ณธ์ ์ธ Handler๋ฅผ ๊ฐ€์ง€๊ณ  ๋ฐ˜๋ณต๋ฌธ์„ ๋Œ๋ ค์„œ ์šฐ๋ฆฌ์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  SimpleUrlHandlerMapping ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ฐพ์•„๋ƒ…๋‹ˆ๋‹ค. ์ฆ‰, ์šฐ๋ฆฌ์˜ ์š”์ฒญ์„ ์ •์  ๋ฆฌ์†Œ์Šค ์š”์ฒญ์œผ๋กœ ๊ฐ„์ฃผํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์Šคํ”„๋ง ๊ณต์‹๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

By default, Spring MVC will send a 404 Not Found error response if a handler is not found for a request. To have a NoHandlerFoundException thrown instead, set configprop:spring.mvc.throw-exception-if-no-handler-found to true. Note that, by default, the serving of static content is mapped to /** and will, therefore, provide a handler for all requests. For a NoHandlerFoundException to be thrown, you must also set spring.mvc.static-path-pattern to a more specific value such as /resources/** or set spring.web.resources.add-mappings to false to disable serving of static content entirely.

๊ธฐ๋ณธ์ ์œผ๋กœ Spring MVC๋Š” ์š”์ฒญ์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ 404 Not Found ์˜ค๋ฅ˜ ์‘๋‹ต์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค. ๋Œ€์‹  NoHandlerFoundException์ด ๋ฐœ์ƒํ•˜๋„๋ก ํ•˜๋ ค๋ฉด configprop:spring.mvc.throw-exception-if-no-handler-found๋ฅผ true๋กœ ์„ค์ •ํ•˜์„ธ์š”. ๊ธฐ๋ณธ์ ์œผ๋กœ ์ •์  ์ฝ˜ํ…์ธ  ์ œ๊ณต์€ /**์— ๋งคํ•‘๋˜๋ฏ€๋กœ ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. NoHandlerFoundException์ด ๋ฐœ์ƒํ•˜๋ ค๋ฉด spring.mvc.static-path-pattern์„ /resources/**์™€ ๊ฐ™์€ ๋ณด๋‹ค ๊ตฌ์ฒด์ ์ธ ๊ฐ’์œผ๋กœ ์„ค์ •ํ•˜๊ฑฐ๋‚˜ spring.web.resources.add-mappings๋ฅผ false๋กœ ์„ค์ •ํ•˜์—ฌ ์ •์  ์ฝ˜ํ…์ธ ์˜ ์„œ๋น™์„ ์™„์ „ํžˆ ๋น„ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

spring.web.resources.add-mappings: false ๋กœ ์„ค์ •ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค!

NoHandlerFoundException ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ

2023-09-21T22:01:09.091+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : GET "/api/404", parameters={}
2023-09-21T22:01:09.094+09:00  WARN 57850 [1] [o-auto-1-exec-1] o.s.web.servlet.PageNotFound             : No mapping for GET /api/404
2023-09-21T22:01:09.095+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed 404 NOT_FOUND
2023-09-21T22:01:09.098+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2023-09-21T22:01:09.100+09:00 DEBUG 57850 --- [o-auto-1-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-09-21T22:01:09.167+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [text/plain, application/json, application/*+json, */*] and supported [application/json, application/*+json]
2023-09-21T22:01:09.169+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Thu Sep 21 22:01:09 KST 2023, status=404, error=Not Found, path=/api/404}]
2023-09-21T22:01:09.197+09:00 DEBUG 57850 --- [    Test worker] o.s.web.client.RestTemplate              : Response 404 NOT_FOUND
2023-09-21T22:01:09.198+09:00 DEBUG 57850 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 404
  • [1]: ์ด์ œ ๊ทธ ์–ด๋– ํ•œ HandlerMapping์ด ํ• ๋‹น๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

DispatcherServlet์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

public class DispatcherServlet {
	...
	protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (pageNotFoundLogger.isWarnEnabled()) {
			pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
		}
		if (this.throwExceptionIfNoHandlerFound) { // **
			throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
					new ServletServerHttpRequest(request).getHeaders());
		}
		else {
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
		}
	}
	...
}
  • ํ˜„์žฌ throwExceptionIfNoHandlerFound๊ฐ’์ด ์ฐธ์ด๋ฉด NoHandlerFoundException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’์€ false๋กœ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ NoHandlerFoundException๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋„๋ก ํ•ด์„œ ์šฐ๋ฆฌ๊ฐ€ ExceptionHandler๋ฅผ ํ†ตํ•ด ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฉด ๋์ž…๋‹ˆ๋‹ค! ๊ณต์‹๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด [spring.mvc.throw-exception-if-no-handler-found](<https://docs.spring.io/spring-boot/docs/3.1.4/reference/htmlsingle/#application-properties.web.spring.mvc.throw-exception-if-no-handler-found>) ๋ผ๋Š” ์˜ต์…˜์„ true๋กœ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค! ์ด๋ ‡๊ฒŒ ์„ค์ • 1์ค„๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค๋‹ˆ ๋„ˆ๋ฌด ํŽธ๋ฆฌํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค..!

๋‹ค์‹œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ด๋ณด๋ฉด?

2023-09-21T22:11:07.767+09:00  WARN 58155 --- [o-auto-1-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.servlet.NoHandlerFoundException: No endpoint GET /api/404.]

DefaultHandlerExceptionResolver์—์„œ NoHandlerFoundException์„ ์ฒ˜๋ฆฌํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

์ปค์Šคํ…€ ExceptionHandler ์ƒ์„ฑ

์กด์žฌํ•˜์ง€ ์•Š๋Š” API๋กœ ์š”์ฒญ์ด ์™”์„ ๊ฒฝ์šฐ, NoHandlerFoundException์ด ๋ฐœ์ƒํ•˜๊ฒŒ๋” ์„ค์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์˜ˆ์™ธ๋ฅผ Advice์—์„œ ์žก์•„์„œ ์šฐ๋ฆฌ๊ฐ€ ์ปค์Šคํ…€ํ•˜๊ฒŒ ์‘๋‹ต์„ ๋‚ด๋ ค์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ง์ด์ฃ .

@RestControllerAdvice
public class GlobalControllerAdvice {
    @ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<Map<String, String>> noHandlerFoundHandle(NoHandlerFoundException e) {
        return ResponseEntity.status(NOT_FOUND)
                .body(Map.of(
                        "statusCode", "404",
                        "message", "์กด์žฌํ•˜์ง€ ์•Š๋Š” API ์ž…๋‹ˆ๋‹ค."
                ));
    }
}

Outro

์กด์žฌํ•˜์ง€ ์•Š๋Š” API ์š”์ฒญ์— ๋Œ€ํ•ด์„œ ์ถ”์ ์„ ํ•˜๋Š”๋ฐ ๊ฝค ์• ๋ฅผ ๋งŽ์ด ๋จน์—ˆ์Šต๋‹ˆ๋‹ค. ์™œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” URL์„ ๊ฐ€์ง€๊ณ  Static Resource๋ฅผ ์ฐพ๋Š”์ง€ ์Šคํ”„๋ง์ด ๊ธฐ๋ณธ์ ์œผ๋กœ SimpleUrlHandlerMapping์„ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•˜๋Š”์ง€, ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์ด๋ฒˆ ๊ธฐํšŒ์— ์ž์„ธํ•˜๊ฒŒ ์•Œ๊ฒŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์•ž์œผ๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์ฐจ๊ทผ์ฐจ๊ทผ ํ•œ๋‹จ๊ณ„์‹ ์ถ”์ ํ•ด๋‚˜๊ฐ€๋ฉฐ ๋‚ด๋ถ€๋™์ž‘์ด ์–ด๋–ป๊ฒŒ ์ด๋ค„์ง€๋Š”์ง€, ๋˜ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋„์›€์ด ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

REFERENCES

๋ฐ˜์‘ํ˜•