<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Exception Handling on Chanyeol Dev</title>
    <link>https://chanyeols.com/tags/exception-handling/</link>
    <description>Recent content in Exception Handling on Chanyeol Dev</description>
    <generator>Hugo</generator>
    <language>ko-kr</language>
    <lastBuildDate>Tue, 17 Mar 2026 21:00:00 +0900</lastBuildDate>
    <atom:link href="https://chanyeols.com/tags/exception-handling/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Spring Boot 예외 처리 통일하기 — @ControllerAdvice와 @ExceptionHandler</title>
      <link>https://chanyeols.com/posts/spring-boot-exception-handling/</link>
      <pubDate>Tue, 17 Mar 2026 21:00:00 +0900</pubDate>
      <guid>https://chanyeols.com/posts/spring-boot-exception-handling/</guid>
      <description>&lt;h2 id=&#34;1-서론-왜-예외-처리를-통일해야-하는가&#34;&gt;1. 서론: 왜 예외 처리를 통일해야 하는가?&lt;/h2&gt;
&lt;p&gt;API를 개발하다 보면 다양한 예외 상황이 발생합니다. 데이터가 없는 경우, 입력값이 잘못된 경우, 서버 내부 로직 오류 등이 대표적입니다. 이때 각 컨트롤러에서 &lt;code&gt;try-catch&lt;/code&gt;로 예외를 개별 처리하면 다음과 같은 문제가 발생합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;코드 중복&lt;/strong&gt;: 비슷한 예외 처리 로직이 여러 컨트롤러에 반복됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응답 일관성 부족&lt;/strong&gt;: 어떤 API는 JSON으로 에러를 주는데, 어떤 API는 HTML 에러 페이지를 주는 등 응답 형식이 제각각이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;비즈니스 로직 집중도 저하&lt;/strong&gt;: 예외 처리 코드가 섞여 있어 핵심 로직을 파악하기 힘듭니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Spring은 이를 우아하게 해결할 수 있도록 &lt;strong&gt;@ControllerAdvice&lt;/strong&gt;와 &lt;strong&gt;@ExceptionHandler&lt;/strong&gt;를 제공합니다.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="1-서론-왜-예외-처리를-통일해야-하는가">1. 서론: 왜 예외 처리를 통일해야 하는가?</h2>
<p>API를 개발하다 보면 다양한 예외 상황이 발생합니다. 데이터가 없는 경우, 입력값이 잘못된 경우, 서버 내부 로직 오류 등이 대표적입니다. 이때 각 컨트롤러에서 <code>try-catch</code>로 예외를 개별 처리하면 다음과 같은 문제가 발생합니다.</p>
<ol>
<li><strong>코드 중복</strong>: 비슷한 예외 처리 로직이 여러 컨트롤러에 반복됩니다.</li>
<li><strong>응답 일관성 부족</strong>: 어떤 API는 JSON으로 에러를 주는데, 어떤 API는 HTML 에러 페이지를 주는 등 응답 형식이 제각각이 됩니다.</li>
<li><strong>비즈니스 로직 집중도 저하</strong>: 예외 처리 코드가 섞여 있어 핵심 로직을 파악하기 힘듭니다.</li>
</ol>
<p>Spring은 이를 우아하게 해결할 수 있도록 <strong>@ControllerAdvice</strong>와 <strong>@ExceptionHandler</strong>를 제공합니다.</p>
<h2 id="2-핵심-어노테이션-이해하기">2. 핵심 어노테이션 이해하기</h2>
<h3 id="21-exceptionhandler">2.1. @ExceptionHandler</h3>
<p>특정 컨트롤러 내부에서 발생하는 특정 예외를 잡아 처리하는 메서드를 정의합니다. 해당 컨트롤러 안에서만 유효하다는 특징이 있습니다.</p>
<h3 id="22-restcontrolleradvice-controlleradvice">2.2. @RestControllerAdvice (@ControllerAdvice)</h3>
<p>여러 컨트롤러에서 발생하는 예외를 한곳에서 전역적으로 처리할 수 있게 해주는 &ldquo;공통 관심사&rdquo; 클래스입니다. <code>@RestControllerAdvice</code>는 <code>@ResponseBody</code>가 포함되어 있어 JSON 형태로 에러를 응답하기에 적합합니다.</p>
<h2 id="3-실무적인-공통-예외-처리-구현">3. 실무적인 공통 예외 처리 구현</h2>
<h3 id="31-에러-응답-규격-정의-errorresponse">3.1. 에러 응답 규격 정의 (ErrorResponse)</h3>
<p>모든 에러 응답은 동일한 구조를 가져야 클라이언트(프론트엔드)에서 처리하기 쉽습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@Getter</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@Builder</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">public</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">class</span> <span style="color:#f0883e;font-weight:bold">ErrorResponse</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>LocalDateTime<span style="color:#6e7681"> </span>timestamp<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>LocalDateTime.now();<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">int</span><span style="color:#6e7681"> </span>status;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>error;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>code;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>message;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><h3 id="32-전역-예외-처리기-구현-globalexceptionhandler">3.2. 전역 예외 처리기 구현 (GlobalExceptionHandler)</h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@RestControllerAdvice</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@Slf4j</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">public</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">class</span> <span style="color:#f0883e;font-weight:bold">GlobalExceptionHandler</span><span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#8b949e;font-style:italic">// 모든 비즈니스 예외 처리</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#d2a8ff;font-weight:bold">@ExceptionHandler</span>(BusinessException.class)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">protected</span><span style="color:#6e7681"> </span>ResponseEntity<span style="color:#ff7b72;font-weight:bold">&lt;</span>ErrorResponse<span style="color:#ff7b72;font-weight:bold">&gt;</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">handleBusinessException</span>(BusinessException<span style="color:#6e7681"> </span>e)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>log.error(<span style="color:#a5d6ff">&#34;handleBusinessException&#34;</span>,<span style="color:#6e7681"> </span>e);<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>ErrorCode<span style="color:#6e7681"> </span>errorCode<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>e.getErrorCode();<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>ErrorResponse<span style="color:#6e7681"> </span>response<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>ErrorResponse.builder()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.status(errorCode.getStatus())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.error(errorCode.name())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.code(errorCode.getCode())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.message(e.getMessage())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.build();<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">new</span><span style="color:#6e7681"> </span>ResponseEntity<span style="color:#ff7b72;font-weight:bold">&lt;&gt;</span>(response,<span style="color:#6e7681"> </span>HttpStatus.valueOf(errorCode.getStatus()));<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#8b949e;font-style:italic">// 그 외 예상치 못한 모든 예외 처리</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#d2a8ff;font-weight:bold">@ExceptionHandler</span>(Exception.class)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">protected</span><span style="color:#6e7681"> </span>ResponseEntity<span style="color:#ff7b72;font-weight:bold">&lt;</span>ErrorResponse<span style="color:#ff7b72;font-weight:bold">&gt;</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">handleException</span>(Exception<span style="color:#6e7681"> </span>e)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>log.error(<span style="color:#a5d6ff">&#34;handleException&#34;</span>,<span style="color:#6e7681"> </span>e);<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>ErrorResponse<span style="color:#6e7681"> </span>response<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>ErrorResponse.builder()<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.status(HttpStatus.INTERNAL_SERVER_ERROR.value())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.error(<span style="color:#a5d6ff">&#34;INTERNAL_SERVER_ERROR&#34;</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.code(<span style="color:#a5d6ff">&#34;COMMON-001&#34;</span>)<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.message(e.getMessage())<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">                </span>.build();<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">return</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">new</span><span style="color:#6e7681"> </span>ResponseEntity<span style="color:#ff7b72;font-weight:bold">&lt;&gt;</span>(response,<span style="color:#6e7681"> </span>HttpStatus.INTERNAL_SERVER_ERROR);<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><h2 id="4-사용자-정의-예외custom-exception-활용">4. 사용자 정의 예외(Custom Exception) 활용</h2>
<p>단순히 <code>RuntimeException</code>을 던지기보다, 도메인의 의미를 담은 사용자 정의 예외를 만드는 것이 좋습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@Getter</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">public</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">class</span> <span style="color:#f0883e;font-weight:bold">BusinessException</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">extends</span><span style="color:#6e7681"> </span>RuntimeException<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>ErrorCode<span style="color:#6e7681"> </span>errorCode;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">public</span><span style="color:#6e7681"> </span><span style="color:#d2a8ff;font-weight:bold">BusinessException</span>(String<span style="color:#6e7681"> </span>message,<span style="color:#6e7681"> </span>ErrorCode<span style="color:#6e7681"> </span>errorCode)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">super</span>(message);<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">this</span>.errorCode<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>errorCode;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 예외 상황에 따른 코드 정의</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#d2a8ff;font-weight:bold">@Getter</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">public</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">enum</span><span style="color:#6e7681"> </span>ErrorCode<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>USER_NOT_FOUND(404,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;U001&#34;</span>,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;사용자를 찾을 수 없습니다.&#34;</span>),<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>INVALID_INPUT_VALUE(400,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;C001&#34;</span>,<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;잘못된 입력값입니다.&#34;</span>);<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">int</span><span style="color:#6e7681"> </span>status;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>code;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#ff7b72">private</span><span style="color:#6e7681"> </span><span style="color:#ff7b72">final</span><span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>message;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>ErrorCode(<span style="color:#ff7b72">int</span><span style="color:#6e7681"> </span>status,<span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>code,<span style="color:#6e7681"> </span>String<span style="color:#6e7681"> </span>message)<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">this</span>.status<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>status;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">this</span>.code<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>code;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#ff7b72">this</span>.message<span style="color:#6e7681"> </span><span style="color:#ff7b72;font-weight:bold">=</span><span style="color:#6e7681"> </span>message;<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>}<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#6e7681">
</span></span></span></code></pre></div><h2 id="5-결론-일관된-에러-응답의-가치">5. 결론: 일관된 에러 응답의 가치</h2>
<p>공통 예외 처리를 구축하면 개발자는 비즈니스 로직에서 <code>throw new UserNotFoundException()</code>처럼 예외를 던지기만 하면 됩니다. 나머지는 <code>@RestControllerAdvice</code>가 알아서 처리하여 일관된 JSON 응답을 클라이언트에 전달합니다.</p>
<p>이러한 구조는 코드의 가독성을 높일 뿐만 아니라, 프론트엔드와의 협업 효율성을 극대화하는 핵심적인 설계 패턴입니다. 지금 바로 프로젝트의 예외 처리 로직을 리팩토링해 보세요!</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
