<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>TaeheeLog</title>
    <link>https://tate91.tistory.com/</link>
    <description>https://github.com/TaeheeLim</description>
    <language>ko</language>
    <pubDate>Fri, 14 Aug 2026 00:38:48 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>taeveloper</managingEditor>
    <image>
      <title>TaeheeLog</title>
      <url>https://tistory1.daumcdn.net/tistory/5212913/attach/ea56fb4da4104ff18e83b4ada95f04f6</url>
      <link>https://tate91.tistory.com</link>
    </image>
    <item>
      <title>Spring 대용량 파일 업로드</title>
      <link>https://tate91.tistory.com/78</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1GB 이상의 파일 업로드&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과 문제사항 1&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OOM: Java Heap Space&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpiTA/dJMcai9rnsC/rUI0dzsug8xw9jYvEH9Z6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpiTA/dJMcai9rnsC/rUI0dzsug8xw9jYvEH9Z6k/img.png&quot; data-alt=&quot;JVM heap 최대 사이즈를 1GB로 설정하고 1.8 GB 파일을 업로드 한 경우 OOM 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpiTA/dJMcai9rnsC/rUI0dzsug8xw9jYvEH9Z6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwpiTA%2FdJMcai9rnsC%2FrUI0dzsug8xw9jYvEH9Z6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;135&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JVM heap 최대 사이즈를 1GB로 설정하고 1.8 GB 파일을 업로드 한 경우 OOM 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상세설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request 데이터를 재사용 가능하게 하기위해 HttpServletRequest 를 HttpServletRequestWrapper 로 변환 하는 filter 가 있는데, 해당 필터에서 Request Data 를 InputStream 으로 읽어 들여 ByteArrayOutputStream 으로 캐싱 하는 과정에서 OOM ( OutOfMemory ) 발생.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Request Message 내용의 본문을 Inputstream 을 사용해서 한번이라도 읽어 들인다면 본문의 내용은 비워진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 소스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761788954079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ReadableServletRequestWrapper extends HttpServletRequestWrapper {
	private ByteArrayOutputStream cachedBytes;
	
	private void cacheInputStream() throws IOException {
		cachedBytes = new ByteArrayOutputStream();
		IOUtils.copy(super.getInputStream(), cachedBytes); //OOM 발생
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ByteArrayOutputStream 를 new 연산자를 사용해 heap 에 생성하고 데이터를 캐싱하는 과정에서 OOM이 발생한다. Content-Type이 application/json, application/x-www-form-urlencoded 요청과 같은 경우는 데이터의 크기가 제한 적이지만 multipart/form-data 는 HTTP Message 본문에 파일 binary 데이터가 포함 되어있기 때문에 크기가 굉장히 커져 버릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조치 방안 1&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP Request Message 를 래핑 하는 기능으로 인해서 데이터가 위/변조 되었는지 확인하는 HMAC 필터에서 요청 데이터를 검증하고, 특정 API 들이 호출 되기 전에 스프링 AOP 를 활용 해서 요청, 응답 데이터를 데이터 베이스에 저장하기 때문에 필터 제거는 불가능 하다.&lt;/li&gt;
&lt;li&gt;채택한 방법은 래핑 하는 필터에서 Content-Type이 multipart/form-data 이거나 Content-length 가 300MB 를 초과 한다면 해당 필터를 실행하지 않고 다음 필터를 실행 하는 것이다. 300 MB라는 값은 DB에 저장해서 관리 하도록 했다. 단 여전히 300MB 파일을 수십 개 동시 등록 한다면 같은 오류가 발생 할 수 있기 때문에 multipart/form-data일 경우 해당 필터를 skip 하도록 구현 해도 될 듯 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제소스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1761788982746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ReadableServletRequestFilter implements Filter {

	private static final long MAX_BYTE = 300L * 1024L * 1024L; // 300MB
	
	private long wrapThresholdBytes = MAX_BYTE;
	
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest request  = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 멀티파트는 래핑하지 않음 (대용량 업로드 시 메모리 캐싱 위험)
        final String ct = request.getContentType();
        final boolean isMultipart = ct != null &amp;amp;&amp;amp; ct.toLowerCase(Locale.ROOT).startsWith(&quot;multipart/&quot;);

        long contentLength = getContentLengthLongCompat(request);

        if (isMultipart) {
            chain.doFilter(request, response);
            return;
        }

        // 2) Content-Length 가 알려져 있고, 300MB 초과면 래핑 skip
        if (contentLength &amp;gt;= 0 &amp;amp;&amp;amp; contentLength &amp;gt; wrapThresholdBytes) {
            chain.doFilter(request, response);
            return;
        }

        // 3) Content-Length 를 모르는 경우(= -1, chunked 등) 안전상 래핑 skip
        if (contentLength &amp;lt; 0) {
            chain.doFilter(request, response);
            return;
        }
        
		ReadableServletRequestWrapper multiReadRequest = new ReadableServletRequestWrapper((HttpServletRequest) req);
		chain.doFilter(multiReadRequest, res);
	}
	
	private long getContentLengthLongCompat(HttpServletRequest req) {
    long len = req.getContentLengthLong();
    return len &amp;gt;= 0 ? len : -1; // 혹시 모를 -1 처리
}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 결과 문제 사항 2&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조치 방안 1 로 인해서 대용량 파일 업로드 시 OOM 오류는 더 이상 발생 하지 않으나 로그 이력 저장 기능 에서 오류가 발생 했는데 예제 소스코드는 다음과 같다.&lt;/li&gt;
&lt;li&gt;MultipartResolver는 CommonsMultipartResolver 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761788999493&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CommonsMultipartResolver resolver = (CommonsMultipartResolver) MyApplicationContext.getCtx().getBean(&quot;multipartResolver&quot;);

if(resolver.isMultipart(req)){

	MultipartHttpServletRequest multipartRequest = multipartResolver.resolveMultipart(request);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multipart/form-data 메시지의 part 들의 key : value 데이터를 추출 하기 위해 HttpServletRequest 를 MultipartHttpServletRequest 로 변환 하는 부분이 있는데, 이 필터 / AOP 를 거치고 나니 실제 목적지 API 에서는 form-data 가 전부 비워져 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브 기능 (로그 이력 저장) 은 정상적으로 동작 했지만 본래의 기능 (파일 업로드)이 동작하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조치 방안 2&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;form-data가 비워져 있다는 것은 컨트롤러가 호출 되기 전 필터 나 컨트롤러 프록시 에서 이미 InputStream 을 소비 했을 가능성이 높다고 생각 했다.&lt;/li&gt;
&lt;li&gt;조사를 해보니 CommonsMultipartReolsver 는 resolveMultipart() 메서드 호출 시 Apache Common Fileupload 라이브러리를 사용 해서 원본 HttpServletRequest 의 InputStream을 소비 하고 다시 주입 하지 않는다. 그래서 HMAC 필터 이후 다음 필터로 request를 넘겨 줄 때 원본 데이터가 사라진 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Apache Common Fileupload 1.5 미만 버전은 Dos 공격에 취약 하기 때문에 1.5 이상 버전 사용 할 것을 권장&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CommonsMultipartReolsver는 더 이상 권장 되지 않는 멀티파트 리졸버 이며, Apache Common Fileupload, Apache Common IO 라이브러리와도 의존성이 있다. 현재 ITSM 시스템은 전자정부표준프레임워크 기반 이기 때문에 Default 로 사용 하고 있었지만, 지금 스프링에서는StandardServletMultipartResolver 를 기본으로 사용 하고 있고 StandardServletMultipartResolver 의 resolveMultipart() 소스는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1761789044544&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
		return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
	}
	
	public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
		super(request);
		if (!lazyParsing) {
			parseRequest(request);
		}
	}
	
	private void parseRequest(HttpServletRequest request) {
		try {
			Collection&amp;lt;Part&amp;gt; parts = request.getParts(); //Inputstream 으로 데이터 읽고 다시 원본 request 에 데이터 주입
			this.multipartParameterNames = new LinkedHashSet&amp;lt;String&amp;gt;(parts.size());
			MultiValueMap&amp;lt;String, MultipartFile&amp;gt; files = new LinkedMultiValueMap&amp;lt;String, MultipartFile&amp;gt;(parts.size());
			for (Part part : parts) {
				String filename = extractFilename(part.getHeader(CONTENT_DISPOSITION));
				if (filename != null) {
					files.add(part.getName(), new StandardMultipartFile(part, filename));
				}
				else {
					this.multipartParameterNames.add(part.getName());
				}
			}
			setMultipartFiles(files);
		}
		catch (Exception ex) {
			throw new MultipartException(&quot;Could not parse multipart servlet request&quot;, ex);
		}
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주목할 부분은 parseRequest 의&lt;b&gt; request.getParts()&lt;/b&gt; 이다. getParts() 를 호출 하고 상위 클래스를 따라 가다 보면 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;org.apache.catalina.connector.Request.parseParts&lt;/span&gt;&amp;nbsp; ($TOMCAT_HOME/lib/catalina.jar) 를 호출 하게 되는데 parseParts() 내부에서 원본 request 를 inputstream 으로 읽는 부분, 원본 request에 파라미터 다시 주입 하는 부분, Collection&amp;lt;Part&amp;gt;에 파트를 주입 하는 부분이 있다.&lt;/li&gt;
&lt;li&gt;요약 : &lt;b&gt;CommonsMultipartResolver&lt;/b&gt;를 &lt;b&gt;StandardServletMultipartResolver&lt;/b&gt; 로 변경하고, HttpServletRequest 의 getParts() 를 사용 하기 위해 기존 서블릿 버전 2.5 에서 4.0 으로 변경.&lt;/li&gt;
&lt;li&gt;HttpServletRequest의 getParts() 는 multipart/form-data 요청일 경우 각 part 별로 데이터를 반환 해주는 API 이다. 사용법이 간편해서 소스 코드가 깔끔해서 채택 했다.&lt;/li&gt;
&lt;li&gt;또한 MultipartResolver 의 config 설정은 기존 CommonsMutlipartResolver bean 을 생성 할 때 주입 하는 부분을 제거하고 servlet 설정에서 (web.xml) 다음과 같이 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;⚙️&lt;/span&gt; 설정 변경 (web.xml)&lt;/p&gt;
&lt;pre id=&quot;code_1761789220100&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;multipart-config&amp;gt;
    &amp;lt;max-file-size&amp;gt;2500000000&amp;lt;/max-file-size&amp;gt;        &amp;lt;!-- 개별 파일 2.5GB --&amp;gt;
    &amp;lt;max-request-size&amp;gt;2500000000&amp;lt;/max-request-size&amp;gt;  &amp;lt;!-- 전체 요청 2.5GB --&amp;gt;
    &amp;lt;file-size-threshold&amp;gt;307200&amp;lt;/file-size-threshold&amp;gt;     &amp;lt;!-- 300KB 이상 디스크 저장, 이하는 메모리 사용 --&amp;gt;
&amp;lt;/multipart-config&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  설정 설명 &amp;mdash; file-size-threshold&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;업로드 파일을 &lt;b&gt;메모리에 둘지 / 임시파일로 쓸지&lt;/b&gt; 결정하는 임계값(byte 단위).&lt;/li&gt;
&lt;li&gt;예를 들어 file-size-threshold=300KB 라면:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;300KB까지는 RAM에 저장&lt;/li&gt;
&lt;li&gt;초과분은 /tmp (OS 기본 temp 디렉터리)로 임시 파일 생성&lt;/li&gt;
&lt;li&gt;&amp;lt;location&amp;gt;&amp;lt;/locaiton&amp;gt; 속성으로 임시파일 디렉터리 지정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;요청이 종료되면 Tomcat이 자동으로 임시 파일 삭제.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;⚠️ 주의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값이 너무 크면(예: 1GB) 여러 파일 동시 업로드 시 &lt;b&gt;Heap 메모리 고갈 위험&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;대용량 업로드가 아주 많은 시스템이라면 &lt;b&gt;0 (항상 디스크 사용)&lt;/b&gt; 으로 설정하는 것이 바람직하다.&lt;/li&gt;
&lt;li&gt;단, 0으로 설정하면 mutlipart 의 일반 폼 필드 파트 같은 작은 데이터도 별도의 임시파일에 쓰기 때문에 약간의 값을 설정 하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;반면, 소규모 업로드(수십 KB)만 존재하는 경우에는메모리 저장이 &lt;b&gt;디스크 I/O보다 훨씬 빠르므로 성능상 유리&lt;/b&gt;하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;✅ 최종 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OOM 원인:&lt;/b&gt; Request InputStream을 전부 Heap으로 캐싱하는 필터 구조.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조치:&lt;/b&gt; multipart/form-data 및 대용량 요청은 필터 skip.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;추가 개선:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Commons &amp;rarr; Standard 리졸버 전환&lt;/li&gt;
&lt;li&gt;Servlet 버전 2.5 &amp;rarr; 4.0 업그레이드&lt;/li&gt;
&lt;li&gt;file-size-threshold=0 설정으로 메모리 사용 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &amp;ldquo;작은 파일은 메모리에, 큰 파일은 디스크에&amp;rdquo; &amp;mdash;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;file-size-threshold는 성능과 안정성 사이의 균형을 조절하는 핵심 설정이다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/78</guid>
      <comments>https://tate91.tistory.com/78#entry78comment</comments>
      <pubDate>Thu, 30 Oct 2025 10:58:36 +0900</pubDate>
    </item>
    <item>
      <title>HTML 파일을 이미지 파일로 변환</title>
      <link>https://tate91.tistory.com/77</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 웹에디터에 있는 HTML 을 이미지 파일로 변환 후 타 시스템 API 호출&lt;/p&gt;</description>
      <category>일지</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/77</guid>
      <comments>https://tate91.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 10 Jan 2025 11:43:33 +0900</pubDate>
    </item>
    <item>
      <title>개발자 3년</title>
      <link>https://tate91.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어느덧 솔루션 회사에 취업해서 개발자로 경력을 쌓은지 2년 6개월&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 1년은 고된 출근길 퇴근길이 즐거웠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그 후로는 익숙함에 길들여져서 사회 초년생 때 느꼈던 감사함이 느껴지지 않았다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 이직에 대한 고민이 많다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금보다 더 도전적인 개발을 하고싶다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/76</guid>
      <comments>https://tate91.tistory.com/76#entry76comment</comments>
      <pubDate>Tue, 29 Oct 2024 22:54:24 +0900</pubDate>
    </item>
    <item>
      <title>[Programmers] 서울에서 김서방 찾기</title>
      <link>https://tate91.tistory.com/75</link>
      <description>&lt;ul id=&quot;tab&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서울에서 김서방 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-challengeable-id=&quot;908&quot; data-challengeable-type=&quot;algorithm&quot; data-algorithm-type=&quot;standard&quot; data-language=&quot;java&quot; data-user-id=&quot;402303&quot; data-interface-type=&quot;function&quot; data-challenge-web-evaluation-code=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;tour2&quot;&gt;
&lt;div&gt;문제 설명
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String형 배열 seoul의 element중 &quot;Kim&quot;의 위치 x를 찾아, &quot;김서방은 x에 있다&quot;는 String을 반환하는 함수, solution을 완성하세요. seoul에 &quot;Kim&quot;은 오직 한 번만 나타나며 잘못된 값이 입력되는 경우는 없습니다.&lt;/p&gt;
제한 사항
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;seoul은 길이 1 이상, 1000 이하인 배열입니다.&lt;/li&gt;
&lt;li&gt;seoul의 원소는 길이 1 이상, 20 이하인 문자열입니다.&lt;/li&gt;
&lt;li&gt;&quot;Kim&quot;은 반드시 seoul 안에 포함되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
입출력 예seoulreturn
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;[&quot;Jane&quot;, &quot;Kim&quot;]&lt;/td&gt;
&lt;td&gt;&quot;김서방은 1에 있다&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1651471305162&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;

public class 서울에서김서방찾기 {
    public static void main(String[] args) {
        String[] seoul = {&quot;Jane&quot;, &quot;Kim&quot;};

        System.out.println(solution(seoul));
    }

    public static String solution(String[] seoul){
        int temp = 0;
        for(int i = 0; i &amp;lt; seoul.length; i++){
            if(seoul[i].equals(&quot;Kim&quot;)){
                temp = i;
                break;
            }
        }
        return findLocation(temp);
    }

    public static String findLocation(int index){
        return &quot;김서방은 &quot; + index + &quot;에 있다.&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/75</guid>
      <comments>https://tate91.tistory.com/75#entry75comment</comments>
      <pubDate>Mon, 2 May 2022 15:01:54 +0900</pubDate>
    </item>
    <item>
      <title>[Programmers] 수박수박수박수박수?</title>
      <link>https://tate91.tistory.com/74</link>
      <description>&lt;ul id=&quot;tab&quot; style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수박수박수박수박수박수?&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-challengeable-id=&quot;909&quot; data-challengeable-type=&quot;algorithm&quot; data-algorithm-type=&quot;standard&quot; data-language=&quot;java&quot; data-user-id=&quot;402303&quot; data-interface-type=&quot;function&quot; data-challenge-web-evaluation-code=&quot;&quot;&gt;
&lt;div&gt;
&lt;div id=&quot;tour2&quot;&gt;
&lt;div&gt;문제 설명
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이가 n이고, &quot;수박수박수박수....&quot;와 같은 패턴을 유지하는 문자열을 리턴하는 함수, solution을 완성하세요. 예를들어 n이 4이면 &quot;수박수박&quot;을 리턴하고 3이라면 &quot;수박수&quot;를 리턴하면 됩니다.&lt;/p&gt;
제한 조건
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;n은 길이 10,000이하인 자연수입니다.&lt;/li&gt;
&lt;/ul&gt;
입출력 예nreturn
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&quot;수박수&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&quot;수박수박&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1651470690117&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public String solution(int n) {
        StringBuilder builder = new StringBuilder();

        for (int i = 1; i &amp;lt;= n; i++) {
            if(i % 2 != 0){
                builder.append(&quot;수&quot;);
            } else {
                builder.append(&quot;박&quot;);
            }
        }
        return builder.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/74</guid>
      <comments>https://tate91.tistory.com/74#entry74comment</comments>
      <pubDate>Mon, 2 May 2022 14:51:30 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 어노테이션 @ExceptionHandler</title>
      <link>https://tate91.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이번 포스팅은 ExceptionHandler 어노테이션에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;HTML 화면 오류 vs API 오류&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 HTML 화면을 제공할 때는 오류가 발생하면 BasicErrorController를 사용하는 게 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때는 단순히 5xx, 4xx 관련된 오류 화면을 보여주면 됩니다. BasicErrorController는 이런 메커니즘을 모두 구현해두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 API는 각 시스템 마다 응답의 모양도 다르고, 스펙도 모두 다릅니다. 예외 상황에 단순히 오류 화면을 보여주는 것이 아니라, 예외에 따라서 각각 다른 데이터를 출력해야 할 수도 있습니다. 그리고 같은 예외라고 해도 어떤 컨트롤러에서 발생했는가에 따라서 다른 예외 응답을 내려주어야 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한마디로 매우 세밀한 제어가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 상품 API와 주문 API는 오류가 발생했을 때 응답의 모양이 완전히 다를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 지금 까지 살펴본 BasicErrorController를 사용하거나 HandlerExceptionResolver를 직접 구현하는 방식으로 API 예외를 다루기는 쉽지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API 예외처리의 어려운 점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. HandlerExceptionResolver를 떠올려 보면 ModelAndView를 반환해야 했습니다. 이것은 API 응답에는 필요하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. API 응답을 위해서 HttpServeltResponse에 직접 응답 데이터를 넣어주었습니다. 이것은 매우 불편하고 마치 과거 서블릿을 사용하던 시절로 돌아간 것 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 특정 컨트롤러에서만 발생하는 예외를 별도로 처리하기 어렵습니다. 예를 들어서 회원을 처리하는 컨트롤러에서 발생하는 RuntimeException 예외와 상품을 관리하는 컨트롤러에서 발생하는 동일한 RuntimeException 예외를 서로 다른 방식으로 처리하고 싶다면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@ExceptionHandler&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 API 예외 처리문제를 해결하기 위해 @ExceptionHandler라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 ExceptionHandlerExceptionResolver입니다. 스프링은 ExceptionHandlerExceptionResolver를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver 중에 우선순위도 가장 높습니다. 실무에서는 API 예외 처리는 대부분 이 기능을 사양한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제로 알아 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ErrorResult&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649745277536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.AllArgsCon
structor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ErrorResult {
     private String code;
     private String message;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생했을 때 API 응답으로 사용하는 객체를 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionV2 Controller&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649745354372&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class ApiExceptionV2Controller {

     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(IllegalArgumentException.class)
     public ErrorResult illegalExHandle(IllegalArgumentException e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         return new ErrorResult(&quot;BAD&quot;, e.getMessage());
     }
     
     @ExceptionHandler
     public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         ErrorResult errorResult = new ErrorResult(&quot;USER-EX&quot;, e.getMessage());
         
         return new ResponseEntity&amp;lt;&amp;gt;(errorResult, HttpStatus.BAD_REQUEST);
     }
     
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler
     public ErrorResult exHandle(Exception e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         
         return new ErrorResult(&quot;EX&quot;, &quot;내부 오류&quot;);
     }
     
     @GetMapping(&quot;/api2/members/{id}&quot;)
     public MemberDto getMember(@PathVariable(&quot;id&quot;) String id) {
         if (id.equals(&quot;ex&quot;)) {
     	    throw new RuntimeException(&quot;잘못된 사용자&quot;);
         }
         if (id.equals(&quot;bad&quot;)) {
         	throw new IllegalArgumentException(&quot;잘못된 입력 값&quot;);
         }
         if (id.equals(&quot;user-ex&quot;)) {
        	 throw new UserException(&quot;사용자 오류&quot;);
         }
        return new MemberDto(id, &quot;hello &quot; + id);
     }

     @Data
     @AllArgsConstructor
     static class MemberDto {
         private String memberId;
         private String name;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@ExceptionHandler 예외 처리 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 애노테이션을 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 컨트롤러에서 예외가 발생하면 이 메서드가 호출됩니다. 참고로 지정한 예외 또는 그 예외의 자식 클래스는 모두 잡을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 IllegalArgumentException 또는 그 하위 자식 클래스를 모두 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649745448322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
     log.error(&quot;[exceptionHandle] ex&quot;, e);
     return new ErrorResult(&quot;BAD&quot;, e.getMessage());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 우선순위는 항상 자세한 것이 우선권을 가집니다. 예를 들어서 부모, 자식 클래스가 있고 다음과 같이 예외가 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wCgIA/btrzdOqV3Vi/9ewrpPNyvRXHYMrPhvkMg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wCgIA/btrzdOqV3Vi/9ewrpPNyvRXHYMrPhvkMg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wCgIA/btrzdOqV3Vi/9ewrpPNyvRXHYMrPhvkMg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwCgIA%2FbtrzdOqV3Vi%2F9ewrpPNyvRXHYMrPhvkMg0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;358&quot; height=&quot;160&quot; data-origin-width=&quot;358&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler에 지정한 부모 클래스는 자식 클래스까지 처리할 수 있습니다. 따라서 자식 예외가 발생하면 부모 예외처리(), 자식 예외처리() 둘 다 호출 대상이 됩니다. 그런데 둘 중 더 자세한 것이 우선권을 가지므로 자식 예외처리()가 호출됩니다. 물론 부모 예외가 호출되면 부모 예외처리()만 호출 대상이 되므로 부모 예외처리()가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다양한 예외&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 다양한 예외를 한 번에 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649745996090&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
 log.info(&quot;exception e&quot;, e);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외 생략&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler에 예외를 생략할 수 있습니다. 생략하면 메서드 파라미터의 예외가 지정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746036365&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler
public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postman으로 실행하면서 테스트해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api2/members/bad를 호출하게 되면 IllegalArgumentException이 발생하고 예외를 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IllegalArgumentException 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746198373&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
 log.error(&quot;[exceptionHandle] ex&quot;, e);
 return new ErrorResult(&quot;BAD&quot;, e.getMessage());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 흐름&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 예외가 발생했기 때문에 ExceptionResolver가 작동하는데 가장 우선순위가 높은 ExceptionHandlerExceptionResolver가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. ExceptionHandlerExceptionResolver는 해당 컨트롤러에 IllegalArgumentException을 처리할 수 있는 @ExceptionHandler가 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. illegalExHandle()를 실행합니다. @RestController 이므로 illgalExHandle() 에도 @ResponseBody가 적용됩니다. 따라서 HTTP 컨버터가 사용되고, 응답은 다음과 같은 JSON으로 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. @ResponseStatus(HttpStatus.BAD_REQUEST)를 지정했으므로 HTT 상태 코드 400으로 응답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746345980&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
 &quot;code&quot;: &quot;BAD&quot;,
 &quot;message&quot;: &quot;잘못된 입력 값&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 http://localhost:8080/api2/members/user-ex로 요청해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserException 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746379348&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler
public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {
 log.error(&quot;[exceptionHandle] ex&quot;, e);
 ErrorResult errorResult = new ErrorResult(&quot;USER-EX&quot;, e.getMessage());
 return new ResponseEntity&amp;lt;&amp;gt;(errorResult, HttpStatus.BAD_REQUEST);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @ExceptionHandler에 예외를 지정하지 않으면 해당 메서드 파라미터 예외를 사용합니다. 여기서는 UserException을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ResponseEntity를 사용해서 HTTP 메시지 바디에 직접 응답합니다. 물론 HTTP 컨버터가 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseEntity를 사용하면 HTTP 응답 코드를 프로그래밍해서 동적으로 변경할 수 있습니다. 앞서 살펴본 @ResponseStatus는 애노테이션이므로 HTTP 응답 코드를 동적으로 변경할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 http://localhost:8080/api2/members/ex를 호출해서 테스트해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Exception&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746508652&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
 log.error(&quot;[exceptionHandle] ex&quot;, e);
 return new ErrorResult(&quot;EX&quot;, &quot;내부 오류&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. throw new RuntimeException(&quot;잘못된 사용자&quot;) 이 코드가 실행되면서, 컨트롤러 밖으로 RuntimeException이 던져집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. RuntimeException은 Exception의 자식 클래스입니다. 따라서 위의 메서드가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)로 HTTP 상태 코드를 500으로 응답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;@ControllerAdvice&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler를 사용해서 예외를 깔끔하게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여있습니다. @ControllerAdvice 또는 @RestControllerAdvice를 사용하면 둘을 분리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ExControllerAdvice&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649746890608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     @ExceptionHandler(IllegalArgumentException.class)
     public ErrorResult illegalExHandle(IllegalArgumentException e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         
         return new ErrorResult(&quot;BAD&quot;, e.getMessage());
     }
     
     @ExceptionHandler
     public ResponseEntity&amp;lt;ErrorResult&amp;gt; userExHandle(UserException e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         ErrorResult errorResult = new ErrorResult(&quot;USER-EX&quot;, e.getMessage());
         
         return new ResponseEntity&amp;lt;&amp;gt;(errorResult, HttpStatus.BAD_REQUEST);
     }
     
     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
     @ExceptionHandler
     public ErrorResult exHandle(Exception e) {
         log.error(&quot;[exceptionHandle] ex&quot;, e);
         
         return new ErrorResult(&quot;EX&quot;, &quot;내부 오류&quot;);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ApiExceptionV2 Controller 코드에 있는 @ExceptionHandler를 모두 제거합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 포스트맨으로 요청해보면 같은 예외처리 응답을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@ControllerAdvice&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. @ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. @ControllerAdvice에 대상을 지정하지 않으면 모든 컨트롤러에 적용됩니다. (글로벌 적용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. @RestControllerAdvice는 @ControllerAdvice와 같고 @ResponseBody가 추가되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. @Controller, @RestController의 차이와 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대상 컨트롤러 지정 방법&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649747062222&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice(&quot;org.example.controllers&quot;)
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 공식 문서 예제에서 보는 것처럼 특정 어노테이션이 있는 컨트롤러를 지정할 수 있고, 특정 패키지를 직접 지정할 수도 있습니다. 패키지 지정의 경우 해당 패키지와 그 하위에 있는 컨트롤러가 대상이 됩니다. 그리고 특정 클래스를 지정할 수도 있습니다.&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/73</guid>
      <comments>https://tate91.tistory.com/73#entry73comment</comments>
      <pubDate>Tue, 12 Apr 2022 16:06:28 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링이 제공하는 ExceptionResolver</title>
      <link>https://tate91.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이번 포스팅은 스프링이 제공하는 ExceptionResolver에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가 기본으로 제공하는 ExceptionHandler는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerExceptionResolverComposite에 다음 순서로 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ExceptionHandlerExceptionResolver&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @ExceptionHandler를 처리합니다. API 예외 처리는 대부분 이 기능으로 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ResponseStatusExceptionResolver&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- HTTP 상태 코드를 지정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예) @ResponseStatus(value = HttpStatus.NOT_FOUND)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. DefaultHandlerExceptionResolver&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 스프링 내부 기본 예외를 처리합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ResponseStatusExceptionResolver&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;ResponseStatusExceptionResolver는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음 두 가지 경우를 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1. @ResponseStatus가 달려있는 예외&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2. ResponseStatusException 예외&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하나씩 확인해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;예외에 다음과 같이 @ResponseStatus 애노테이션을 적용하면 HTTP 상태 코드를 변경해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649743726883&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;잘못된 요청 오류&quot;)
	public class BadRequestException extends RuntimeException {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BadRequestException 예외가 컨트롤러 밖으로 넘어가면 ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서 오류 코드를 HttpStatus.BAD_REQUEST(400)으로 변경하고, 메시지도 담습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseStatusExceptionResolver 코드를 확인해보면 결국 response.sendError(statusCode, resolvedReason)를 호출하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sendError(400)를 호출했기 때문에 WAS에서 다시 오류 페이지 /error를 내부 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController - 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649743968089&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/response-status-ex1&quot;)
public String responseStatusEx1() {
 throw new BadRequestException();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api/response-status-ex1? message= 를 요청하면 다음과 같은 결과를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744037034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
 &quot;status&quot;: 400,
 &quot;error&quot;: &quot;Bad Request&quot;,
 &quot;exception&quot;: &quot;hello.exception.exception.BadRequestException&quot;,
 &quot;message&quot;: &quot;잘못된 요청 오류&quot;,
 &quot;path&quot;: &quot;/api/response-status-ex1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reason을 MessageSource에서 찾는 기능도 제공합니다. reason = &quot;error.bad&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744089955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

//@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;잘못된 요청 오류&quot;)
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;error.bad&quot;)
public class BadRequestException extends RuntimeException {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;messages.properties&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744105023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;error.bad=잘못된 요청 오류입니다. 메시지 사용&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 결과 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744122879&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
     &quot;status&quot;: 400,
     &quot;error&quot;: &quot;Bad Request&quot;,
     &quot;exception&quot;: &quot;hello.exception.exception.BadRequestException&quot;,
     &quot;message&quot;: &quot;잘못된 요청 오류입니다. 메시지 사용&quot;,
     &quot;path&quot;: &quot;/api/response-status-ex1&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResponseStatusException&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ResponseStatus 는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없습니다. (애노테이션을 직접 넣어야 하는데, 내가 코드를 수정할 수 없는 라이브러리의 예외 코드 같은 곳에는 적용할 수 없습니다.) 추가로 애노테이션을 사용하기 때문에 조건에 따라 동적으로 변경하는 것도 어렵습니다. 이때는 ResponseStatusException 예외를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController - 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744176118&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/response-status-ex2&quot;)
public String responseStatusEx2() {
 throw new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;error.bad&quot;, new IllegalArgumentException());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 : http://localhost:8080/api/response-status-ex2&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre id=&quot;code_1649744197044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
 &quot;status&quot;: 404,
 &quot;error&quot;: &quot;Not Found&quot;,
 &quot;exception&quot;: &quot;org.springframework.web.server.ResponseStatusException&quot;,
 &quot;message&quot;: &quot;잘못된 요청 오류입니다. 메시지 사용&quot;,
 &quot;path&quot;: &quot;/api/response-status-ex2&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DefaultHandlerExceptionResolver&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;DefaultHandlerExceptionResolver는 스프링 내부에서 발생하는 스프링 예외를 해결합니다. 대표적으로 파라미터 바인딩 시점에 타입이 맞지 않으면 내부에서 TypeMismatchException이 발생하는데, 이 경우 예외가 발생했기 때문에 그냥 두면 서블릿 컨테이너까지 오류가 올라가고, 결과적으로 500 오류가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 파라미터 바인딩은 대부분 클라이언트가 HTTP 요청 정보를 잘못 호출해서 발생하는 문제입니다. HTTP에서는 이런 경우 HTTP 상태 코드 400을 사용하도록 되어있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;DefaultHandlerExceptionResolver는 이것을 500오류가 아니라 HTTP 상태 코드 400 오류로 변경합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 확인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DefaultHandlerExpcetionResolver.handleTypeMismatch를 보면 다음과 같은 코드를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response.sendError(HttpServletResponse.SC_BAD_REQUEST)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 response.sendError()를 통해서 문제를 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sendError()를 호출했기 때문에 WAS에서 다시 오류 페이지 /error를 내부 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController - 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649744599841&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/default-handler-ex&quot;)
public String defaultException(@RequestParam Integer data) {
     return &quot;ok&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Integer data에 문자를 입력하면 내부에서 TypeMismatchException이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api/default-handler-ex?data=hello&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre id=&quot;code_1649744632835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
 &quot;status&quot;: 400,
 &quot;error&quot;: &quot;Bad Request&quot;,
 &quot;exception&quot;:
&quot;org.springframework.web.method.annotation.MethodArgumentTypeMismatchException&quot;
,
 &quot;message&quot;: &quot;Failed to convert value of type 'java.lang.String' to required 
type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: 
For input string: \&quot;hello\&quot;&quot;,
 &quot;path&quot;: &quot;/api/default-handler-ex&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 결과를 보면 HTTP 상태 코드가 400인 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태 코드를 변경하는 기능도 알아보았습니다. 그런데 HandlerExceptionResolver를 직접 사용하기는 복잡합니다. API 오류 응답의 경우 response에 직접 데이터를 넣어야 해서 매우 불편하고 번거롭습니다. ModelAndView를 반환해야 하는 것도 API에는 잘 맞지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 이 문제를 해결하기 위해 @ExceptionHandler라는 매우 혁신적인 예외 처리 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 @ExceptionHandler에 대해서 알아보겠습니다&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/72</guid>
      <comments>https://tate91.tistory.com/72#entry72comment</comments>
      <pubDate>Tue, 12 Apr 2022 15:26:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] API 예외 처리</title>
      <link>https://tate91.tistory.com/71</link>
      <description>&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;007&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/007.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/007.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이번 포스팅은 API 예외 처리에 대해서 알아보겠습니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API 예외 처리 - 시작&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 페이지의 경우 지금까지 설명했던 것처럼 4xx, 5xx와 같은 오류 페이지만 있으면 대부분의 문제를 해결할 수 있었습니다. 그런데 API의 경우에는 생각할 내용이 더 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 페이지는 단순히 고객에게 오류 화면을 보여주고 끝이지만, API는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 내려주어야 합니다. 지금부터 API의 경우 어떻게 예외 처리를 하면 좋은지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebServerCustomizer&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649740487847&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class WebServerCustomizer implements
WebServerFactoryCustomizer&amp;lt;ConfigurableWebServerFactory&amp;gt; {
     @Override
     public void customize(ConfigurableWebServerFactory factory) {
         ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, &quot;/error page/404&quot;);
         ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, &quot;/error-page/500&quot;);
         ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, &quot;/error page/500&quot;);
         
         factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Customizer를 스프링 빈으로 등록하고 ErrorPage를 설정해주면 WAS에 예외가 전달되거나, resopnse.sendError()가 호출되면 위에 등록한 예외 페이지 경로가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649740576478&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class ApiExceptionController {

 @GetMapping(&quot;/api/members/{id}&quot;)
 public MemberDto getMember(@PathVariable(&quot;id&quot;) String id) {
     if (id.equals(&quot;ex&quot;)) {
    	 throw new RuntimeException(&quot;잘못된 사용자&quot;);
     }
     return new MemberDto(id, &quot;hello &quot; + id);
 }
 
 @Data
 @AllArgsConstructor
 static class MemberDto {
     private String memberId;
     private String name;
 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 회원을 조회하는 기능을 하나 만들었습니다. 예외 테스트를 위해 URL에 전달된 id의 값이 ex 이면 예외가 발생하도록 코드를 심어두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Postman으로 테스트를 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반드시 HTTP Header에 Accept가 application/json인 것을 꼭 확인해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api/members/spring으로 호출하게 되면 다음과 같은 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;149&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sDll9/btrzcQit4qS/Cg9gTBmj03qWByAkSPGuPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sDll9/btrzcQit4qS/Cg9gTBmj03qWByAkSPGuPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sDll9/btrzcQit4qS/Cg9gTBmj03qWByAkSPGuPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsDll9%2FbtrzcQit4qS%2FCg9gTBmj03qWByAkSPGuPk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;297&quot; height=&quot;149&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;149&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 http://localhost:8080/api/members/ex를 호출해서 예외를 발생시키면 ErrorPage에 등록해 두었던 url로 다시 mapping 요청을 하면서 templates 경로에 있는 html 파일을 반환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 저희가 기대하는 바가 아닙니다. 클라이언트는 정상 요청이든, 오류 요청이든 Accept에 application/json으로 설정했기 때문에 JSON으로 반환되기를 기대합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하려면 오류 페이지 컨트롤러도 JSON 응답을 할 수 있도록 수정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ErrorPageController - API 응답 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649740871809&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(value = &quot;/error-page/500&quot;, produces =
MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; errorPage500Api(HttpServletRequest 
request, HttpServletResponse response) {
     log.info(&quot;API errorPage 500&quot;);
     Map&amp;lt;String, Object&amp;gt; result = new HashMap&amp;lt;&amp;gt;();
     
     Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
     result.put(&quot;status&quot;, request.getAttribute(ERROR_STATUS_CODE));
     result.put(&quot;message&quot;, ex.getMessage());
     
     Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
     
     return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;produces = MediaType.APPLICATION_JSON_VALUE의 뜻은 클라이언트가 요청하는 HTTP Header의 Accept의 값이 application/json 일 때 해당 메서드가 호출된다는 것입니다. 결국 클라이언트가 받고 싶은 미디어 타입이 json이면 이 컨트롤러의 메서드가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 데이터를 위해서 Map을 만들고 status, message 키에 값을 할당했습니다. Jackson 라이브러리는 Map을 JSON 구조로 변환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseEntity를 사용해서 응답하기 때문에 메시지 컨버터가 동작하면서 클라이언트에 JSON이 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 포스트맨을 통해서 다시 테스트해보면 다음과 같은 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsnnWC/btry7tou9zQ/HVHyQU9MSdU2kIZ8Bz14aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsnnWC/btry7tou9zQ/HVHyQU9MSdU2kIZ8Bz14aK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsnnWC/btry7tou9zQ/HVHyQU9MSdU2kIZ8Bz14aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsnnWC%2Fbtry7tou9zQ%2FHVHyQU9MSdU2kIZ8Bz14aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;291&quot; height=&quot;139&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API 예외 처리 - 스프링 부트 기본 오류 처리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BasicErrorController 코드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649741048590&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse 
response) {}

@RequestMapping
public ResponseEntity&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; error(HttpServletRequest request) {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/error라는 동일한 경로를 처리하는 errorHtml(), error() 두 메서드를 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. errorHtml() : produces = MediaType.TEXT_HTML_VALUE : 클라이언트 요청의 Accept 헤더 값이 text/html인 경우에는 errorHtml()을 호출해서 view를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. error() : 그 외 경우에 호출되고 ResponseEntity로 HTTP Body에 JSON 데이터를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 부트의 예외 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 공부했듯이 스프링 부트의 기본 설정은 오류 발생 시 /error를 오류 페이지로 요청합니다. BasicErrorController는 이 경로를 기본으로 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebServerCustomizer의 @Component를 주석 처리하고 다음의 url을 호출해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api/members/ex&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5q9wF/btry7rEcPjH/EkFPWkrsH3VBbPVFzzRyRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5q9wF/btry7rEcPjH/EkFPWkrsH3VBbPVFzzRyRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5q9wF/btry7rEcPjH/EkFPWkrsH3VBbPVFzzRyRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5q9wF%2Fbtry7rEcPjH%2FEkFPWkrsH3VBbPVFzzRyRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;688&quot; height=&quot;328&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 BasicController가 제공하는 기본 정보들을 활용해서 오류 API를 생성해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HTML 페이지 vs API 오류&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController를 확장하면 JSON 메시지도 변경할 수 있습니다. 그런데 API 오류는 조금 뒤에 설명할 @ExceptionHandler가 제공하는 기능을 사용하는 것이 더 나은 방법 이므로 지금은 BasicErrorController를 확장해서 JSON 오류 메시지를 변경할 수 있다 정도로만 이해해두겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가 제공하는 BasicErrorController는 HTML 페이지를 제공하는 경우에는 매우 편리합니다. 4xx, 5xx 등등 모두 잘 처리해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 API 오류 처리는 다른 차원의 이야기입니다. API 마다, 각각의 컨트롤러나 예외마다 서로 다른 응답 결과를 출력해야 할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 회원과 관련된 API에서 예외가 발생할 때 응답과, 상품과 관련된 API에서 발생하는 예외에 따라 그 결과가 달라질 수 있습니다. 결과적으로 매우 세밀하고 복잡하다. 따라서 이 방법은 HTML 화면을 처리할 때 사용하고, API는 오류 처리는 뒤에서 설명할 @ExceptionHandler를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 복잡한 API 오류는 어떻게 처리해야 하는지 지금부터 하나씩 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;API 예외 처리 - HandlerExceptionResolver 시작&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생해서 서블릿을 넘어서 WAS까지 예외가 전달되면 HTTP 상태 코드가 500으로 처리됩니다. 발생하는 예외에 따라서 400, 404 등 다른 상태로 처리하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 오류 메시지, 형식 등을 API마다 다르게 처리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;상태코드 변환&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IllegalArgumentException을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 HTTP 상태 코드를 400으로 처리하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController - 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649741499553&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/api/members/{id}&quot;)
public MemberDto getMember(@PathVariable(&quot;id&quot;) String id) {
     if (id.equals(&quot;ex&quot;)) {
   	  throw new RuntimeException(&quot;잘못된 사용자&quot;);
     }
     if (id.equals(&quot;bad&quot;)) {
  	   throw new IllegalArgumentException(&quot;잘못된 입력 값&quot;);
     }
     return new MemberDto(id, &quot;hello &quot; + id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 bad라는 pathVariable이 오면 IllegalArgumentException이 발생하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postman으로 실행해서 상태 코드르 보면 500인 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eplWd6/btrzbeqEsMZ/bZi25mdQ6a32XwdLiwOoa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eplWd6/btrzbeqEsMZ/bZi25mdQ6a32XwdLiwOoa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eplWd6/btrzbeqEsMZ/bZi25mdQ6a32XwdLiwOoa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeplWd6%2FbtrzbeqEsMZ%2FbZi25mdQ6a32XwdLiwOoa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;494&quot; height=&quot;204&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerExceptionResolver&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공합니다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작 방식을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 됩니다. 줄여서 ExceptionHandler라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ExceptionResolver 적용 전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nmZ0c/btrzdaHO7y4/3eFYpMrMhyR3um2hMVbTzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nmZ0c/btrzdaHO7y4/3eFYpMrMhyR3um2hMVbTzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nmZ0c/btrzdaHO7y4/3eFYpMrMhyR3um2hMVbTzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnmZ0c%2FbtrzdaHO7y4%2F3eFYpMrMhyR3um2hMVbTzk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;716&quot; height=&quot;349&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ExceptionResolver 적용 후&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;338&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMNoTa/btry9pGgmO3/QmEQqSxMgswnSulTMa86G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMNoTa/btry9pGgmO3/QmEQqSxMgswnSulTMa86G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMNoTa/btry9pGgmO3/QmEQqSxMgswnSulTMa86G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMNoTa%2Fbtry9pGgmO3%2FQmEQqSxMgswnSulTMa86G1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;338&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;338&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 : ExceptionResolver로 예외를 해결하여도 postHandle()은 호출되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerExceptionResolver - 인터페이스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649741700984&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerExceptionResolver {
 ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. handler : 핸들러(컨트롤러) 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Exception ex : 핸들러(컨트롤러)에서 발생한 예외&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MyHandlerExceptionResolver&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649741784432&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
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

@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
     @Override
     public ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, Object handler, Exception ex) {
         try {
             if (ex instanceof IllegalArgumentException) {
                 log.info(&quot;IllegalArgumentException resolver to 400&quot;);
                 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                
                 return new ModelAndView();
             }
         } catch (IOException e) {
         	log.error(&quot;resolver ex&quot;, e);
         }
     return null;
 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ExceptionResolver 가 ModelAndView를 반환하는 이유는 마치 try, catch를 하듯이, Exception 을 처리해서 정상 흐름 처럼 변경하는 것이 목적입니다. 이름 그대로 Exception 을 Resolver(해결)하는 것이 목적입니다. 여기서는 IllegalArgumentException 이 발생하면 response.sendError(400) 를 호출해서 HTTP 상태 코드를 400으로 지정하고, 빈 ModelAndView 를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반환 값에 따른 동작 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerExceptionResolver의 반환 값에 따른 DispatcherServlet의 동작 방식은 다음과 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 빈 ModelAndView : new ModelAndView()처럼 빈 객체를 반환하면 뷰를 렌더링 하지 않고 정상 흐름으로 서블릿이 리턴됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ModelAndVeiw 지정 : ModelAndView에 View, Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. null : null을 반환하면 다음 ExceptionResolver를 찾아서 실행합니다. 만약 처리할 수 있는 ExceptionResolver가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ExceptionResolver 활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 예외 상태 코드 변환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예외를 response.sendError() 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를 처리하도록 위임합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이후 WAS는 서블릿 오류 페이지를 찾아서 내보 호출, 예를 들어서 스프링 부트가 기본으로 설정한 /error 가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 뷰 템플릿 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ModelAndView에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링을 해서 고객에게 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. API 응답 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- response.getWriter(). println(&quot;hello&quot;);처럼 HTTP&amp;nbsp; 응답 바디에 직접 데이터를 넣어주는 것이 가능합니다. 여기에 JSON으로 응답하면 API 응답 처리를 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebConfig - 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebMvcConfigurer를 통해 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649742104500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 기본 설정을 유지하면서 추가
 */
@Override
public void extendHandlerExceptionResolvers(List&amp;lt;HandlerExceptionResolver&amp;gt;
resolvers) {
     resolvers.add(new MyHandlerExceptionResolver());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;configureHandlerExceptionResolvers()를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver가 제거되므로 주의해야 합니다. 그래서 반드시 extendHandlerExceptionResolver를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 postman으로 http://localhost:8080/api/members/bad로 요청을 하면 HTTP 상태 코드가 400인 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;API 예외 처리 - HandlerExceptionResolver 활용&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생하면 WAS까지 예외가 던져지고, WAS에서 오류 페이지 정보를 찾아서 다시 /error를 호출하는 과정은 생각해보면 너무 복잡합니다. ExceptionResolver를 활용하면 예외가 발생했을 때 이런 복잡한 과정 없이 여기에서 문제를 깔끔하게 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제로 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserException&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649742471459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class UserException extends RuntimeException {
     public UserException() {
     	super();
     }
     public UserException(String message) {
     	super(message);
     }
     public UserException(String message, Throwable cause) {
     	super(message, cause);
     }
     public UserException(Throwable cause) {
     	super(cause);
     }
     protected UserException(String message, Throwable cause, boolean
    enableSuppression, boolean writableStackTrace) {
     	super(message, cause, enableSuppression, writableStackTrace);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ApiExceptionController - 예외 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649742513240&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.exception.exception.UserException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class ApiExceptionController {
     @GetMapping(&quot;/api/members/{id}&quot;)
     public MemberDto getMember(@PathVariable(&quot;id&quot;) String id) {
         if (id.equals(&quot;ex&quot;)) {
        	 throw new RuntimeException(&quot;잘못된 사용자&quot;);
         }
         if (id.equals(&quot;bad&quot;)) {
        	 throw new IllegalArgumentException(&quot;잘못된 입력 값&quot;);
         }
         if (id.equals(&quot;user-ex&quot;)) {
        	 throw new UserException(&quot;사용자 오류&quot;);
         }

         return new MemberDto(id, &quot;hello &quot; + id);
     }
 
     @Data
     @AllArgsConstructor
     static class MemberDto {
         private String memberId;
         private String name;
 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/api/members/user-ex 호출 시 UserException이 발생하도록 해두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 예외를 처리하는 UserHandlerExceptionResolver르 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserHandlerExceptionResolver&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649742710873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.databind.ObjectMapper;
import hello.exception.exception.UserException;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
     private final ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, Object handler, Exception ex) {
         try {
             if (ex instanceof UserException) {
             	log.info(&quot;UserException resolver to 400&quot;);
             	String acceptHeader = request.getHeader(&quot;accept&quot;);
             	response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                 if (&quot;application/json&quot;.equals(acceptHeader)) {
                    Map&amp;lt;String, Object&amp;gt; errorResult = new HashMap&amp;lt;&amp;gt;();
                    errorResult.put(&quot;ex&quot;, ex.getClass());
                    errorResult.put(&quot;message&quot;, ex.getMessage());

                     String result = objectMapper.writeValueAsString(errorResult);
                     response.setContentType(&quot;application/json&quot;);
                     response.setCharacterEncoding(&quot;utf-8&quot;);
                     response.getWriter().write(result);
                     return new ModelAndView();
                 } else {
                 //TEXT/HTML
                 return new ModelAndView(&quot;error/500&quot;);
             	}
             }
         } catch (IOException e) {
       		log.error(&quot;resolver ex&quot;, e);
         }
         return null;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 요청 헤더의 ACCEPT 값이 application/json 이면 JSON으로 오류를 내려주고, 그 외 경우에는 error/500에 있는 HTML 오류 페이지를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebConfig에 UserHandlerExceptionResolver 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649742786820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void extendHandlerExceptionResolvers(List&amp;lt;HandlerExceptionResolver&amp;gt;
resolvers) {
     resolvers.add(new MyHandlerExceptionResolver());
     resolvers.add(new UserHandlerExceptionResolver());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 postman으로 http://localhost:8080/api/members/user-ex에 ACCEPT를 application/json으로 설정하고 요청하면 다음과 같은 결과를 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXkKLQ/btrzac00tc4/vG5XkbUK9jyQNAd3ftKNKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXkKLQ/btrzac00tc4/vG5XkbUK9jyQNAd3ftKNKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXkKLQ/btrzac00tc4/vG5XkbUK9jyQNAd3ftKNKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXkKLQ%2Fbtrzac00tc4%2FvG5XkbUK9jyQNAd3ftKNKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;139&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExceptionResolver를 사용하면 컨트롤러에서 예외가 발생해도 ExceptionResolver에서 예외를 처리해버립니다. 따라서 예외가 발생해도 서블릿 컨테이너까지 예외가 전달되지 않고, 스프링 MVC에서 예외 처리는 끝이 납니다. 결과적으로 WAS입장에서는 정상 처리가 된 것입니다. 이렇게 예외를 이곳에서 모두 처리할 수 있다는 것이 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너까지 예외가 올라가면 복잡하고 지저분하게 추가 프로세스가 실행됩니다. 반면에 ExceptionResolver를 사용하면 예외처리가 상당히 깔끔해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 직접 ExceptionResolver를 구현하려고 하니 상당히 복잡합니다. 다음 포스팅에서는 스프링이 제공하는 ExcpetionResolver 들을 알아보겠습니다.&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/71</guid>
      <comments>https://tate91.tistory.com/71#entry71comment</comments>
      <pubDate>Tue, 12 Apr 2022 14:59:14 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 예외 처리(Exception)와 오류 페이지</title>
      <link>https://tate91.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이번 포스팅은 예외처리와 오류 페이지에 대해서 알아보겠습니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서블릿 예외 처리 시작&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 스프링이 아닌 순수 서블릿 컨테이너는 예외를 어떻게 처리하는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 다음 2가지 방식으로 예외 처리를 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.Exception(예외)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.response.sendError(HTTP 상태 코드, 오류 메시지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Exception(예외)&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바 직접 실행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 메인 메서드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 도중에 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;웹 애플리케이션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행됩니다. 애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없습니다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달되면 어떻게 동작할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4tz45/btry2TN44TF/cb3f3CVml7kPfFoJa1caY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4tz45/btry2TN44TF/cb3f3CVml7kPfFoJa1caY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4tz45/btry2TN44TF/cb3f3CVml7kPfFoJa1caY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4tz45%2Fbtry2TN44TF%2Fcb3f3CVml7kPfFoJa1caY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;77&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 톰캣 같은 WAS까지 예외가 전달됩니다. 그렇다면 WAS는 예외가 올라오면 어떻게 처리해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 테스트 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 스프링 부트가 제공하는 기본 예외 페이지가 있는데 이건 꺼두겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kBuwD/btry2RQf6G1/nsgYfsXWO8sKcgj2BkZAC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kBuwD/btry2RQf6G1/nsgYfsXWO8sKcgj2BkZAC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kBuwD/btry2RQf6G1/nsgYfsXWO8sKcgj2BkZAC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkBuwD%2Fbtry2RQf6G1%2FnsgYfsXWO8sKcgj2BkZAC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;482&quot; height=&quot;134&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ServletExController - 서블릿 예외 컨트롤러&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649653511430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Slf4j
@Controller
public class ServletExController {
     @GetMapping(&quot;/error-ex&quot;)
     public void errorEx() {
	     throw new RuntimeException(&quot;예외 발생!&quot;);
	 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행해보면 다음처럼 tomcat이 기본으로 제공하는 오류 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xVqCm/btryZapyCLv/XvxXMWpOTWGAIRjELL7Eo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xVqCm/btryZapyCLv/XvxXMWpOTWGAIRjELL7Eo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xVqCm/btryZapyCLv/XvxXMWpOTWGAIRjELL7Eo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxVqCm%2FbtryZapyCLv%2FXvxXMWpOTWGAIRjELL7Eo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;678&quot; height=&quot;98&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 개발자 모드로 확인해보면 HTTP 상태 코드가 500으로 보입니다. Exception의 경우 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해서 HTTP 상태 코드 500을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 아무 사이트나 호출해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:8080/no-page&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vjiy5/btryY95hIV3/XvkkgfhaSrMmocDDMWa2bK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vjiy5/btryY95hIV3/XvkkgfhaSrMmocDDMWa2bK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vjiy5/btryY95hIV3/XvkkgfhaSrMmocDDMWa2bK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvjiy5%2FbtryY95hIV3%2FXvkkgfhaSrMmocDDMWa2bK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;349&quot; height=&quot;54&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣이 기본으로 제공하는 404 오류 화면을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;response.sendError(HTTP 상태 코드, 오류 메시지)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생했을 때 HttpServletResponse가 제공하는 sendError라는 메서드를 사용해도 됩니다. 이것을 호출한다고 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있습니다. 이 메서드를 사용하면 HTTP 상태 코드와 오류 메시지도 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ServletExController - 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649653900924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/error-404&quot;)
public void error404(HttpServletResponse response) throws IOException {
 	response.sendError(404, &quot;404 오류!&quot;);
}

@GetMapping(&quot;/error-500&quot;)
public void error500(HttpServletResponse response) throws IOException {
 	response.sendError(500);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qRWDO/btry5DX4GSM/6zECQvKgTPAkeSwjtETslK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qRWDO/btry5DX4GSM/6zECQvKgTPAkeSwjtETslK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qRWDO/btry5DX4GSM/6zECQvKgTPAkeSwjtETslK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqRWDO%2Fbtry5DX4GSM%2F6zECQvKgTPAkeSwjtETslK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;740&quot; height=&quot;182&quot; data-origin-width=&quot;740&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;response.sendError()를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해둡니다. 그리고 서블릿 컨테이너는 고객에게 응답 전에 response에 sendError()가 호출되었는지 확인합니다 그리고 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 매핑 url로 실행을 시키면 다음과 같은 오류 화면이 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wpjlC/btry4TUppjZ/ekMN4PD45UBRBT1FVdQoAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wpjlC/btry4TUppjZ/ekMN4PD45UBRBT1FVdQoAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wpjlC/btry4TUppjZ/ekMN4PD45UBRBT1FVdQoAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwpjlC%2Fbtry4TUppjZ%2FekMN4PD45UBRBT1FVdQoAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;485&quot; height=&quot;109&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서블릿 예외 처리 - 오류 화면 제공&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 고객 친화적이지 않습니다. 서블릿이 제공하는 오류 화면 기능을 사용해보겠습니다. 서블릿은 Exception (예외)가 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError()가 호출되었을 때 각각의 상황에 맞춘 오류 처리 기능을 제공합니다. 이 기능을 사용하면 친절한 오류 처리 화면을 준비해서 고객에게 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 web.xml이라는 파일에 다음과 같이 오류 화면을 등록했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SHL40/btry5Dw03ym/hhePEyrR0tNN1KDZKxlZ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SHL40/btry5Dw03ym/hhePEyrR0tNN1KDZKxlZ5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SHL40/btry5Dw03ym/hhePEyrR0tNN1KDZKxlZ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSHL40%2Fbtry5Dw03ym%2FhhePEyrR0tNN1KDZKxlZ5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;540&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 스프링 부트를 통해서 서블릿 컨테이너를 실행하기 때문에, 스프링 부트가 제공하는 기능을 사용해서 서블릿 오류 페이지를 등록하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서블릿 오류 페이지 등록&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649654350390&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer&amp;lt;ConfigurableWebServerFactory&amp;gt; {

     @Override
     public void customize(ConfigurableWebServerFactory factory) {
         ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, &quot;/error page/404&quot;);
         ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, &quot;/error-page/500&quot;);
         ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, &quot;/error page/500&quot;);
         
         factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;500 예외가 서버 내부에서 발생한 오류라는 뜻을 포함하고 있기 때문에 여기서는 예외가 발생한 경우도 500 오류 화면으로 처리했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 페이지는 예외를 다룰 때 항상 예외와 그 자식 타입의 오류를 함께 처리합니다. 예를 들어서 위의 경우 RuntimeException은 물론이고 RuntimeException의 자식도 함께 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생했을 때 처리할 수 있는 컨트롤러가 필요합니다. 예를 들어서 RuntimeException 예외가 발생하면 errorPageEx에서 지정한 /error-page/500이 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 오류를 처리할 컨트롤러를 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649654537661&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Controller
public class ErrorPageController {
     @RequestMapping(&quot;/error-page/404&quot;)
     public String errorPage404(HttpServletRequest request, HttpServletResponse 
    response) {
         log.info(&quot;errorPage 404&quot;);
         return &quot;error-page/404&quot;;
     }
     
     @RequestMapping(&quot;/error-page/500&quot;)
     public String errorPage500(HttpServletRequest request, HttpServletResponse 
    response) {
         log.info(&quot;errorPage 500&quot;);
         return &quot;error-page/500&quot;;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 처리 View&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/templates/error-page/404.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649654582169&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;404 오류 화면&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/templates/error-page/500.html&lt;/p&gt;
&lt;pre id=&quot;code_1649654601173&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;500 오류 화면&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트를 해보면 설정한 오류 페이지가 정상 노출되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서블릿 예외 처리 - 오류 페이지 작동 원리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 또는 response.sendError()가 호출 되었을 때 설정한 오류 페이지를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNBXNu/btry6EJhjaQ/VApo9CUi2qaSOhCHCWp9Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNBXNu/btry6EJhjaQ/VApo9CUi2qaSOhCHCWp9Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNBXNu/btry6EJhjaQ/VApo9CUi2qaSOhCHCWp9Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNBXNu%2Fbtry6EJhjaQ%2FVApo9CUi2qaSOhCHCWp9Hk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;711&quot; height=&quot;125&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYhVS/btry68pMl8M/sa8XPQz4qgY6nTkr8hvlAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYhVS/btry68pMl8M/sa8XPQz4qgY6nTkr8hvlAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYhVS/btry68pMl8M/sa8XPQz4qgY6nTkr8hvlAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYhVS%2Fbtry68pMl8M%2Fsa8XPQz4qgY6nTkr8hvlAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;727&quot; height=&quot;146&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;24&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdmF7s/btry33pvzaZ/XbmN3YLfQ9bnxSrqhfs4Ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdmF7s/btry33pvzaZ/XbmN3YLfQ9bnxSrqhfs4Ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdmF7s/btry33pvzaZ/XbmN3YLfQ9bnxSrqhfs4Ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdmF7s%2Fbtry33pvzaZ%2FXbmN3YLfQ9bnxSrqhfs4Ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;24&quot; data-origin-width=&quot;629&quot; data-origin-height=&quot;24&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어서 RuntimeException 예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인합니다. 확인해보니 RuntimeException의 오류 페이지로 /error-page/500이 지정되어 있습니다. WAS는 오류 페이지를 출력하기 위해 /error-page/500을 다시 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caeFW2/btryZHUlkow/tgDKHpwOUGaM1P0kLffqGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caeFW2/btryZHUlkow/tgDKHpwOUGaM1P0kLffqGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caeFW2/btryZHUlkow/tgDKHpwOUGaM1P0kLffqGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaeFW2%2FbtryZHUlkow%2FtgDKHpwOUGaM1P0kLffqGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;891&quot; height=&quot;164&quot; data-origin-width=&quot;891&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRgFBp/btryXa3wEGy/C4hbM3J3Bi9LkHz9Oa9jj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRgFBp/btryXa3wEGy/C4hbM3J3Bi9LkHz9Oa9jj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRgFBp/btryXa3wEGy/C4hbM3J3Bi9LkHz9Oa9jj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRgFBp%2FbtryXa3wEGy%2FC4hbM3J3Bi9LkHz9Oa9jj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;865&quot; height=&quot;218&quot; data-origin-width=&quot;865&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 웹 브라우저는 서버 내부에서 이런 일이 일어나는지 전혀 모른다는 점입니다. 오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 예외가 발생해서 WAS까지 전파된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오류 정보 추가&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request의 attribute에 추가해서 넘겨줍니다. 필요하면 오류 페이지에 이렇게 전달된 오류 정보를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ErrorPageController - 오류 출력&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649654975244&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
@Controller
public class ErrorPageController {
     //RequestDispatcher 상수로 정의되어 있음
     public static final String ERROR_EXCEPTION = &quot;javax.servlet.error.exception&quot;;
     public static final String ERROR_EXCEPTION_TYPE =&quot;javax.servlet.error.exception_type&quot;;
     public static final String ERROR_MESSAGE = &quot;javax.servlet.error.message&quot;;
     public static final String ERROR_REQUEST_URI = &quot;javax.servlet.error.request_uri&quot;;
     public static final String ERROR_SERVLET_NAME = &quot;javax.servlet.error.servlet_name&quot;;
     public static final String ERROR_STATUS_CODE = &quot;javax.servlet.error.status_code&quot;;

     @RequestMapping(&quot;/error-page/404&quot;)
     public String errorPage404(HttpServletRequest request, HttpServletResponse 
    response) {
         log.info(&quot;errorPage 404&quot;);
         printErrorInfo(request);
         
         return &quot;error-page/404&quot;;
     }

     @RequestMapping(&quot;/error-page/500&quot;)
     public String errorPage500(HttpServletRequest request, HttpServletResponse 
    response) {
         log.info(&quot;errorPage 500&quot;);
         printErrorInfo(request);
         
         return &quot;error-page/500&quot;;
     }

     private void printErrorInfo(HttpServletRequest request) {
         log.info(&quot;ERROR_EXCEPTION: ex=&quot;,
        request.getAttribute(ERROR_EXCEPTION));
         log.info(&quot;ERROR_EXCEPTION_TYPE: {}&quot;,
        request.getAttribute(ERROR_EXCEPTION_TYPE));
         log.info(&quot;ERROR_MESSAGE: {}&quot;, request.getAttribute(ERROR_MESSAGE)); //
        ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
         log.info(&quot;ERROR_REQUEST_URI: {}&quot;,
        request.getAttribute(ERROR_REQUEST_URI));
         log.info(&quot;ERROR_SERVLET_NAME: {}&quot;,
        request.getAttribute(ERROR_SERVLET_NAME));
         log.info(&quot;ERROR_STATUS_CODE: {}&quot;,
        request.getAttribute(ERROR_STATUS_CODE));
         log.info(&quot;dispatchType={}&quot;, request.getDispatcherType());
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt8NxX/btry4SBhjJr/kVDTy7BsVDPR5jaaVk2nl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt8NxX/btry4SBhjJr/kVDTy7BsVDPR5jaaVk2nl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt8NxX/btry4SBhjJr/kVDTy7BsVDPR5jaaVk2nl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt8NxX%2Fbtry4SBhjJr%2FkVDTy7BsVDPR5jaaVk2nl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;645&quot; height=&quot;290&quot; data-origin-width=&quot;645&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서블릿 예외 처리 - 필터&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BOkWE/btry7ttHgur/kz8wPP3VnE3TcvkBcXZEK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BOkWE/btry7ttHgur/kz8wPP3VnE3TcvkBcXZEK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BOkWE/btry7ttHgur/kz8wPP3VnE3TcvkBcXZEK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBOkWE%2Fbtry7ttHgur%2Fkz8wPP3VnE3TcvkBcXZEK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;855&quot; height=&quot;214&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한번 호출이 발생합니다. 이때 필터, 서블릿, 인터셉터도 모두 다시 호출됩니다. 그런데 로그인 인증 체크 같은 경우를 생각해보면, 이미 한번 필터나, 인터셉터에서 로그인 체크를 완료했습니다. 따라서 서버 내부에서 오류 페이지를 호출한다고 해서 해당 필터나 인터셉트가 한번 더 호출되는 것은 매우 비효율적입니다. 결국 클라이언트로부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 합니다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType이라는 추가 정보를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherType&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터는 이런 경우를 위해서 dispatcherType라는 옵션을 제공합니다. 필터에 log.info()로 request.getDispatcherType()을 추가하고 출력해보면 오류 페이지에서 dispatcherType은 ERROR라고 출력되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 고객이 처음 요청하면 dispatcherType은 REQUEST입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 서블릿 스펙은 실제 고객이 요청한 것인지, 서버가 내부에서 오류 페이지를 요청하는 것인지 DispatcherType으로 구분할 수 있는 방법을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649655297554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum DispatcherType {
     FORWARD,
     INCLUDE,
     REQUEST,
     ASYNC,
     ERROR
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DispatcherType&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUEST : 클라이언트 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ERROR : 오류 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FORWARD : 서블릿에서 다른 서블릿이나 JSP를 호출할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASYNC : 서블릿 비동기 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;필터와 DispatcherType&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 필터와 DispatcherType이 어떻게 사용되는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LogFilter - DispatcherType 로그 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649655437861&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter implements Filter {
     @Override
     public void init(FilterConfig filterConfig) throws ServletException {
     	log.info(&quot;log filter init&quot;);
     }

     @Override
     public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
         HttpServletRequest httpRequest = (HttpServletRequest) request;
         String requestURI = httpRequest.getRequestURI();
         String uuid = UUID.randomUUID().toString();
         
         try {
             log.info(&quot;REQUEST [{}][{}][{}]&quot;, uuid, request.getDispatcherType(), requestURI);
             chain.doFilter(request, response);
         } catch (Exception e) {
         	throw e;
         } finally {
             log.info(&quot;RESPONSE [{}][{}][{}]&quot;, uuid, request.getDispatcherType(), requestURI);
         }
     }

     @Override
     public void destroy() {
     	log.info(&quot;log filter destroy&quot;);
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그를 출력하는 부분에 request.getDispatcherType()을 추가해두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebConfig&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649655490763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.exception.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;

@Configuration
public class WebConfig implements WebMvcConfigurer {
     @Bean
     public FilterRegistrationBean logFilter() {
         FilterRegistrationBean&amp;lt;Filter&amp;gt; filterRegistrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;();
         
         filterRegistrationBean.setFilter(new LogFilter());
         filterRegistrationBean.setOrder(1);
         filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);
         filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        
         return filterRegistrationBean;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출됩니다. 아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST입니다. 즉 클라이언트 요청이 있는 경우만 필터가 적용됩니다. 특별히 오류 페이지 경로도 필터를 적용할 것이 아니면 기본 값을 그대로 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 오류 페이지 요청 전용 필터를 적용하고 싶으면 DispatcherType.ERROR만 지정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서블릿 예외 처리 - 인터셉터&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터셉터 중복 호출 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LogInterceptor - DispatcherType 로그 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649655877328&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
     public static final String LOG_ID = &quot;logId&quot;;

     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse 
    response, Object handler) throws Exception {
         String requestURI = request.getRequestURI();
         String uuid = UUID.randomUUID().toString();
         
         request.setAttribute(LOG_ID, uuid);
         
         log.info(&quot;REQUEST [{}][{}][{}][{}]&quot;, uuid, request.getDispatcherType(), requestURI, handler);
         return true;
     }
     
     @Override
     public void postHandle(HttpServletRequest request, HttpServletResponse 
    response, Object handler, ModelAndView modelAndView) throws Exception {
         log.info(&quot;postHandle [{}]&quot;, modelAndView);
     }
     
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse 
    response, Object handler, Exception ex) throws Exception {
         String requestURI = request.getRequestURI();
         String logId = (String)request.getAttribute(LOG_ID);
         log.info(&quot;RESPONSE [{}][{}][{}]&quot;, logId, request.getDispatcherType(), requestURI);

         if (ex != null) {
         log.error(&quot;afterCompletion error!!&quot;, ex);
        }
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 필터의 경우에는 필터를 등록할 때 어떤 DispatcherType인 경우에 필터를 적용할지 선택할 수 있었습니다. 그런데 인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능입니다. 따라서 DispatcherType과 무관하게 항상 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신에 인터셉터는 다음과 같이 요청 경로에 따라서 추가하거나 제외하기 쉽게 되어 있기 때문에, 이러한 설정을 사용해서 오류 페이지 경로를 excludePathPatterns를 사용해서 빼주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649656022408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.exception.filter.LogFilter;
import hello.exception.interceptor.LogInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;

@Configuration
public class WebConfig implements WebMvcConfigurer {
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new LogInterceptor())
             .order(1)
             .addPathPatterns(&quot;/**&quot;)
             .excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;, &quot;/error-page/**&quot;);
     }
     
     //@Bean
     public FilterRegistrationBean logFilter() {
         FilterRegistrationBean&amp;lt;Filter&amp;gt; filterRegistrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;();
         filterRegistrationBean.setFilter(new LogFilter());
         filterRegistrationBean.setOrder(1);
         filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);
         filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        
         return filterRegistrationBean;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에서 /error-page/**를 제거하면 error-page/500 같은 내부 호출의 경우에도 인터셉터가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전체 흐름 정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DG6pa/btryXbaniuM/hU8O9bTwkkm65q673Kduu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DG6pa/btryXbaniuM/hU8O9bTwkkm65q673Kduu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DG6pa/btryXbaniuM/hU8O9bTwkkm65q673Kduu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDG6pa%2FbtryXbaniuM%2FhU8O9bTwkkm65q673Kduu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;860&quot; height=&quot;56&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 필터는 DispatcherType으로 중복 호출 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 인터셉터는 경로 정보로 중복 호출 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVKECt/btry2ljd94y/yH1XTGF7XTi8nyVMX22kuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVKECt/btry2ljd94y/yH1XTGF7XTi8nyVMX22kuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVKECt/btry2ljd94y/yH1XTGF7XTi8nyVMX22kuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVKECt%2Fbtry2ljd94y%2FyH1XTGF7XTi8nyVMX22kuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;893&quot; height=&quot;215&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스프링 부트 - 오류 페이지 1&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 예외 처리 페이지를 만들기 위해서 다음과 같은 복잡한 과정을 거쳤습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebServerCustomizer를 만들고, 예외 종류에 따라서 ErrorPage를 추가하고 예외 처리용 컨트롤러 ErrorPageController를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 스프링 부트는 이런 과정을 모두 기본으로 제공합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트는 ErrorPage를 자동으로 등록합니다. 이때 /error라는 경로로 기본 오류 페이지를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- new ErrorPage(&quot;/error&quot;), 상태 코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 서블릿 밖으로 예외가 발생하거나, response.sendError(...)가 호출되면 모든 오류는 /error를 호출하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaisController라는 스프링 컨트롤러를 자도 응로 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 오류가 발생했을 때 오류 페이지로 /error를 기본 요청합니다. 스프링 부트가 자동 등록한 BasicErrorController는 이 경로를 기본으로 받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결국 개발자는 오류 페이지만 등록하면 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController는 기본적인 로직이 모두 개발되어 있습니다. 개발자는 오류 페이지 화면만 BasicErrorController가 제공하는 룰과 우선순위에 따라서 등록하면 됩니다. 정적 HTML이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;뷰 선택 우선순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController의 처리 순서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 뷰 템플릿&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/templates/error/500.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/templates/error/5xx.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 정적 리소스(static, public)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/static/error/400.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/static/error/404.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/static/error/4xx.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 적용 대상이 없을 때 뷰 이름(error)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- resources/templates/error.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 경로 위치에 HTTP 상태 코드 이름의 뷰 파일을 넣어두면 됩니다. 뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 500처럼 구체적인 것이 5xx처럼 덜 구체적인 것보다 우선순위가 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5xx, 4xx라고 하면 500대, 400대 오류를 처리해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 뷰 템플릿 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/templates/error/4xx.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649657087762&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;4xx 오류 화면 스프링 부트 제공&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/templates/error/404.html&lt;/p&gt;
&lt;pre id=&quot;code_1649657106172&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;404 오류 화면 스프링 부트 제공&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resources/templates/error/500.html&lt;/p&gt;
&lt;pre id=&quot;code_1649657122219&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;500 오류 화면 스프링 부트 제공&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dviyyY/btry7MfB1ba/KnzAENxjM7GGnGSEK4GLw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dviyyY/btry7MfB1ba/KnzAENxjM7GGnGSEK4GLw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dviyyY/btry7MfB1ba/KnzAENxjM7GGnGSEK4GLw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdviyyY%2Fbtry7MfB1ba%2FKnzAENxjM7GGnGSEK4GLw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;841&quot; height=&quot;232&quot; data-origin-width=&quot;841&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스프링 부트 - 오류 페이지 2&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController가 제공하는 기본 정보들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BasicErrorController 컨트롤러는 다음 정보를 model에 담아서 뷰에 전달합니다. 뷰 템플릿은 이 값을 활용해서 출력할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2rhXb/btry6ECEQHH/Ugyt9MW3iOyd3LCjtRHEXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2rhXb/btry6ECEQHH/Ugyt9MW3iOyd3LCjtRHEXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2rhXb/btry6ECEQHH/Ugyt9MW3iOyd3LCjtRHEXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2rhXb%2Fbtry6ECEQHH%2FUgyt9MW3iOyd3LCjtRHEXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;725&quot; height=&quot;302&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 정보 추가 - resources/templates/error/500.html&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649657242962&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
 &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&amp;gt;
 &amp;lt;div class=&quot;py-5 text-center&quot;&amp;gt;
 &amp;lt;h2&amp;gt;500 오류 화면 스프링 부트 제공&amp;lt;/h2&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;div&amp;gt;
 &amp;lt;p&amp;gt;오류 화면 입니다.&amp;lt;/p&amp;gt;
 &amp;lt;/div&amp;gt;
 &amp;lt;ul&amp;gt;
 &amp;lt;li&amp;gt;오류 정보&amp;lt;/li&amp;gt;
 &amp;lt;ul&amp;gt;
 &amp;lt;li th:text=&quot;|timestamp: ${timestamp}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|path: ${path}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|status: ${status}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|message: ${message}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|error: ${error}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|exception: ${exception}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|errors: ${errors}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;li th:text=&quot;|trace: ${trace}|&quot;&amp;gt;&amp;lt;/li&amp;gt;
 &amp;lt;/ul&amp;gt;
 &amp;lt;/li&amp;gt;
 &amp;lt;/ul&amp;gt;
 &amp;lt;hr class=&quot;my-4&quot;&amp;gt;
&amp;lt;/div&amp;gt; &amp;lt;!-- /container --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 오류 관련 내부 정보들을 고객에게 노출하는 것은 좋지 않습니다. 고객이 해당 정보를 읽어도 혼란만 더해지고 보안상 문제가 될 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 BasicErrorController 오류 컨트롤러에서 다음 오류 정보를 model 포함할지 여부를 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHME6K/btry325nQIm/eqvHDPHByAkyvwwsmACpl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHME6K/btry325nQIm/eqvHDPHByAkyvwwsmACpl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHME6K/btry325nQIm/eqvHDPHByAkyvwwsmACpl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHME6K%2Fbtry325nQIm%2FeqvHDPHByAkyvwwsmACpl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;826&quot; height=&quot;226&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실무에서는 이것들을 노출하면 안 됩니다! 사용자에게는 이쁜 오류 화면과 고객이 이해할 수 있는 간단한 오류 메시지를 보여주고 오류는 서버에 로그로 남겨서 로그로 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리를 하자면 스프링 부트가 기본으로 제공하는 오류 페이지를 활용하면 오류 페이지와 관련된 대부분의 문제는 손쉽게 해결할 수 있습니다.&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/70</guid>
      <comments>https://tate91.tistory.com/70#entry70comment</comments>
      <pubDate>Mon, 11 Apr 2022 15:10:10 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링 인터셉터(Spring Interceptor)</title>
      <link>https://tate91.tistory.com/69</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요 이번 포스팅은 스프링 인터셉터에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스프링 인터셉터(Spring Interceptor) 소개&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술입니다. 서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술입니다. 둘 다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWc8AU/btry2moehyM/6frzAm4Ge1dRzJxv2WUEyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWc8AU/btry2moehyM/6frzAm4Ge1dRzJxv2WUEyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWc8AU/btry2moehyM/6frzAm4Ge1dRzJxv2WUEyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWc8AU%2Fbtry2moehyM%2F6frzAm4Ge1dRzJxv2WUEyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;93&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스프링 인터셉터는 디스패쳐 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 됩니다 스프링 MVC의 시작점이 디스패처 서블릿이라고 생각해보면 이해가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스프링 인터셉터에도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고 매우 정밀하게 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tEirV/btry2S8V3bv/IrJgU5MQqOIqGkut01YESK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tEirV/btry2S8V3bv/IrJgU5MQqOIqGkut01YESK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tEirV/btry2S8V3bv/IrJgU5MQqOIqGkut01YESK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtEirV%2Fbtry2S8V3bv%2FIrJgU5MQqOIqGkut01YESK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;597&quot; height=&quot;137&quot; data-origin-width=&quot;597&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있습니다. 그래서 로그인 여부를 체크하기에 딱 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;93&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cytbez/btryZao0mS4/femfaBRJIUuNAwjrxXgMw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cytbez/btryZao0mS4/femfaBRJIUuNAwjrxXgMw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cytbez/btryZao0mS4/femfaBRJIUuNAwjrxXgMw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcytbez%2FbtryZao0mS4%2FfemfaBRJIUuNAwjrxXgMw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;503&quot; height=&quot;93&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;93&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 인터셉터는 체인으로 구성되는데 중간에 인터셉터를 자유롭게 추가할 수 있습니다. 예를 들어서 로그를 남기는 인터셉터를 먼저 적용하고 그다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 내용을 보면 서블릿 필터와 호출되는 순서만 다르고 제공하는 기능은 비슷해 보입니다. 앞으로 설명하겠지만 스프링 인터셉터는 서블릿 필터보다 편리하고 더 정교하고 다양한 기능을 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 인터셉터 인터페이스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649644782865&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response,
     Object handler) throws Exception {}
     
    default void postHandle(HttpServletRequest request, HttpServletResponse response,
     Object handler, @Nullable ModelAndView modelAndView)
    throws Exception {}
    
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
     Object handler, @Nullable Exception ex) throws Exception {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 서블릿 필터의 경우 단순하게 doFilter() 하나만 제공됩니다. 인터셉터는 컨트롤러 호출 전 preHandle, 호출 후 postHandle, 요청 완료 이후 afterCompletion와 같이 단계적으로 잘 세분화되어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 서블릿 필터의 경우 단순히 request, response만 제공했지만 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있습니다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uPu6G/btry5CqMgwe/rPNqVfVPm9zprKgSvikFrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uPu6G/btry5CqMgwe/rPNqVfVPm9zprKgSvikFrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uPu6G/btry5CqMgwe/rPNqVfVPm9zprKgSvikFrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuPu6G%2Fbtry5CqMgwe%2FrPNqVfVPm9zprKgSvikFrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;616&quot; height=&quot;344&quot; data-origin-width=&quot;616&quot; data-origin-height=&quot;344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정상 흐름&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.preHandle : 컨트롤러 호출 전에 호출됩니다. 더 정확히는 핸들러 어댑터 호출 전에 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- preHandle의 응답 값이 true이면 다음으로 진행하고, false이면 더는 진행하지 않습니다. false인 경우 나머지 인터셉터는 물론이고 핸들러 어댑터도 호출되지 않습니다. 그림에서 1번에서 끝이 나버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.postHandle : 컨트롤러 호출 후에 호출됩니다. 더 정확히는 핸들러 어댑터 호출 후에 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. afterCompletion : 뷰가 렌더링 된 이후에 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 인터셉터 예외 상황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJfbQO/btryXerSk9u/W2HXO3GUmO89G3zN0zAqp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJfbQO/btryXerSk9u/W2HXO3GUmO89G3zN0zAqp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJfbQO/btryXerSk9u/W2HXO3GUmO89G3zN0zAqp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJfbQO%2FbtryXerSk9u%2FW2HXO3GUmO89G3zN0zAqp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;306&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예외가 발생 시&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. preHandle : 컨트롤러 호출 전에 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. postHandle : 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. afterCompletion : afterCompletion은 항상 호출됩니다. 이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 예외가 발생하면 postHandle()는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion()을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 됩니다. 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LogInterceptor - 요청 로그 인터셉터&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649645228898&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
     public static final String LOG_ID = &quot;logId&quot;;
     
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse 
    response, Object handler) throws Exception {
         String requestURI = request.getRequestURI();
         String uuid = UUID.randomUUID().toString();
         request.setAttribute(LOG_ID, uuid);
         
         //@RequestMapping: HandlerMethod
         //정적 리소스: ResourceHttpRequestHandler
         if (handler instanceof HandlerMethod) {
             HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
         }
         
         log.info(&quot;REQUEST [{}][{}][{}]&quot;, uuid, requestURI, handler);
         
         return true; //false 진행X
     }
     
     @Override
     public void postHandle(HttpServletRequest request, HttpServletResponse 
    response, Object handler, ModelAndView modelAndView) throws Exception {
	     log.info(&quot;postHandle [{}]&quot;, modelAndView);
     }
     
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse 
    response, Object handler, Exception ex) throws Exception {
         String requestURI = request.getRequestURI();
         String logId = (String)request.getAttribute(LOG_ID);
         
         log.info(&quot;RESPONSE [{}][{}]&quot;, logId, requestURI);
         
         if (ex != null) {
         	log.error(&quot;afterCompletion error!!&quot;, ex);
         }
     }
}​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. String uuid = UUID.randomUUD(). toString()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 요청 로그를 구분하기 위한 uui를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. request.setAttribute(LOG_ID, uuid)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있습니다. 따라서 preHandle에서 지정한 값을 postHandle , afterCompletion에서 함께 사용하려면 어딘가에 담아두어야 합니다. LogInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하면 위험합니다. 따라서 request 에 담아두었습니다. 이 값은 afterCompletion 에서 request.getAttribute(LOG_ID)로 찾아서 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. return true&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- true면 정상 호출이고 다음 인터셉터나 컨트롤러가 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649645841011&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (handler instanceof HandlerMethod) {
     HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HandlerMethod&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라집니다. 스프링을 사용하면 일반적으로 @Controller , @RequestMapping을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod 가 넘어옵니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ResourceHttpRequestHandler&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Controller 가 아니라 /resources/static 와 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler 가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;postHandle, afterCompletion&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종료 로그를 postHandle이 아니라 afterCompletion에서 실행한 이유는, 예외가 발생한 경우 postHandle가 호출되지 않기 때문입니다. afterCompletion은 예외가 발생해도 호출되는 것을 보장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebConfig - 인터셉터 등록&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649646048751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

 @Override
 public void addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(new LogInterceptor())
         .order(1)
         .addPathPatterns(&quot;/**&quot;)
         .excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
 }
     //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. order(1) : 인터셉터의 호출 순서를 지정합니다. 낮을수록 먼저 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. addPathPatterns(&quot;/**&quot;) : 인터셉터를 적용할 URL 패턴을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. excludePathPatterns(&quot;/css/**&quot;, &quot;/*. ico&quot;, &quot;/error&quot;) : 인터셉터에서 제외할 패턴을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터와 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 매우 정밀하게 URL 패턴을 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 로그&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649646167113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;REQUEST [6234a913-f24f-461f-a9e1-85f153b3c8b2][/members/add]
[hello.login.web.member.MemberController#addForm(Member)]
postHandle [ModelAndView [view=&quot;members/addMemberForm&quot;; 
model={member=Member(id=null, loginId=null, name=null, password=null), 
org.springframework.validation.BindingResult.member=org.springframework.validat
ion.BeanPropertyBindingResult: 0 errors}]]
RESPONSE [6234a913-f24f-461f-a9e1-85f153b3c8b2][/members/add]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스프링 인터셉터 - 인증 체크&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 필터에서 사용했던 인증 체크 기능을 스프링 인터셉터로 개발해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LoginCheckInterceptor&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1649646496379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import hello.login.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse 
    response, Object handler) throws Exception {
         String requestURI = request.getRequestURI();
         log.info(&quot;인증 체크 인터셉터 실행 {}&quot;, requestURI);
         HttpSession session = request.getSession(false);
         
         if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
             log.info(&quot;미인증 사용자 요청&quot;);
             //로그인으로 redirect
             response.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);
             
             return false;
         }
         return true;
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 필터와 비교해서 코드가 매우 간결합니다. 인증이라는 것은 컨트롤러 호출 전에만 호출되면 됩니다. 따라서 preHandle만 구현하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;순서 주의, 세밀한 설정 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1649646608666&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(new LogInterceptor())
             .order(1)
             .addPathPatterns(&quot;/**&quot;)
             .excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
             
         registry.addInterceptor(new LoginCheckInterceptor())
             .order(2)
             .addPathPatterns(&quot;/**&quot;)
             .excludePathPatterns(
             &quot;/&quot;, &quot;/members/add&quot;, &quot;/login&quot;, &quot;/logout&quot;,
             &quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
     }
     //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터를 적용하거나 하지 않을 부분은 addPathPatterns와 excludePathPatterns에 작성하면 됩니다. 기본적으로 모든 경로에 해당 인터셉터를 적용하되 /**, /, /members/add, /login, /css/**, /error와 같은 부분은 로그인 체크 인터셉터를 적용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술입니다. 서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다는 것을 코드로 이해했을 것입니다. 특별한 문제가 없다면 인터셉터를 사용하는 것이 좋습니다.&lt;/p&gt;</description>
      <category>Back-end/Spring</category>
      <author>taeveloper</author>
      <guid isPermaLink="true">https://tate91.tistory.com/69</guid>
      <comments>https://tate91.tistory.com/69#entry69comment</comments>
      <pubDate>Mon, 11 Apr 2022 12:13:22 +0900</pubDate>
    </item>
  </channel>
</rss>