<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>joyful coding</title>
        <link>https://velog.io/</link>
        <description>기쁘게 코딩하고 싶은 백엔드 개발자</description>
        <lastBuildDate>Sun, 12 Apr 2026 09:00:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. joyful coding. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yu-jin-song" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring] 실무에서 잘 사용하지 않는 트랜잭션 전파 옵션 파헤치기]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%84%ED%8C%8C-%EC%98%B5%EC%85%98-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%A0%84%ED%8C%8C-%EC%98%B5%EC%85%98-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Sun, 12 Apr 2026 09:00:14 GMT</pubDate>
            <description><![CDATA[<p>스프링 트랜잭션 전파 옵션 중 <code>REQUIRED</code>랑 <code>REQUIRES_NEW</code>까지는 실무에서도 많이 쓰이고, 왜 존재하는지도 납득이 간다. 근데 <code>SUPPORT</code>, <code>NOT_SUPPORTED</code>, <code>MANDATORY</code>, <code>NEVER</code> 같은 옵션들을 마주하면 좀 당황스럽다. &#39;실무에서 쓸 일이 거의 없는데, 굳이 왜 이렇게까지 옵션을 세분화해둔 거지?&#39; 하는 의구심이 들기 때문이다.</p>
<p>결론부터 말하면, 이 옵션들은 단순히 기능을 구현하기 위한 도구라기보다 <strong>&quot;내 로직에 실수가 끼어들지 못하게 만드는 안전장치&quot;</strong>이자 <strong>&quot;이 코드는 이렇게 돌아가야만 해&quot;라고 못 박아두는 선언</strong>에 가깝다. 왜 이런 &#39;까칠한&#39; 옵션들이 만들어졌는지, 실무적인 관점에서 가볍게 정리해 보려고 한다.</p>
<hr>
<h2 id="1-mandatory--never-실수하면-차라리-터뜨려라">1. MANDATORY &amp; NEVER: &quot;실수하면 차라리 터뜨려라&quot;</h2>
<p>이 옵션들은 주로 <strong>데이터의 정합성</strong>이 극도로 중요할 때 사용하는 <strong>안전장치</strong>다.</p>
<ul>
<li><strong>MANDATORY (트랜잭션 필수)</strong><ul>
<li><strong>적용 상황</strong>: 금융 시스템의 <strong>&#39;잔액 차감&#39;</strong> 로직.</li>
<li><strong>예시</strong>: 잔액 차감은 단독으로 발생하면 안 된다. 반드시 &#39;상품 구매&#39;나 &#39;계좌 이체&#39;라는 <strong>상위 트랜잭션 안에서만 수행</strong>되어야 한다. 만약 개발자가 실수로 잔액 차감 로직만 따로 호출하는 API를 만들었다면? MANDATORY는 트랜잭션이 없으면 <strong>예외(IllegalTransactionStateException)</strong>를 던져 실행을 막음으로써, 근거 없이 돈이 빠져나가는 상황을 원천 봉쇄한다.</li>
</ul>
</li>
<li><strong>NEVER (트랜잭션 금지)</strong><ul>
<li><strong>적용 상황</strong>: <strong>대량의 외부 이미지 업로드</strong> 또는 <strong>리소스를 많이 먹는 파일 변환</strong>.</li>
<li><strong>예시</strong>: 만약 실수로 DB 트랜잭션 안에서 100장의 이미지를 S3에 업로드하는 무거운 작업을 호출한다면? 업로드가 끝날 때까지 <strong>DB 커넥션이 불필요하게 오래 점유</strong>되는 최악의 상황이 발생할 수 있다. NEVER를 걸어두면 트랜잭션 안에서 이 작업이 호출되는 순간 바로 예외를 발생시켜, <strong>DB 커넥션이 장시간 묶이는 일을 사전에 방지</strong>한다.</li>
</ul>
</li>
</ul>
<br>

<h2 id="2-not_supported-내-작업-때문에-메인-db를-멈출-순-없지">2. NOT_SUPPORTED: &quot;내 작업 때문에 메인 DB를 멈출 순 없지&quot;</h2>
<p>트랜잭션이 아예 없는 게 아니라, <strong>기존 트랜잭션을 잠시 멈추는(Suspend)</strong> 것이 핵심이다.</p>
<ul>
<li><strong>적용 상황</strong>: 주문 완료 직후 보여주는 <strong>비실시간 통계 조회</strong>.</li>
<li><strong>예시</strong>: 주문은 0.1초 만에 끝나야 하지만, 그 밑에 붙는 &quot;이 상품을 구매한 연령대 통계&quot; 쿼리는 3초가 걸린다. 주문 트랜잭션 안에 이 3초짜리 조회를 묶어두면 그동안 트랜잭션이 불필요하게 길어지고 <strong>DB 커넥션 점유 시간이 증가</strong>할 수 있다. 이때 통계 로직에 NOT_SUPPORTED를 걸면, 메인 트랜잭션은 잠시 숨을 고르고(<strong>커넥션 점유 최소화</strong>), 통계는 <strong>트랜잭션 없이 가볍게 실행</strong>된다.</li>
</ul>
<br>

<h2 id="3-supports-있으면-따라가고-없으면-혼자-가기">3. SUPPORTS: &quot;있으면 따라가고, 없으면 혼자 가기&quot;</h2>
<p>단순히 <code>@Transactional</code>을 생략했을 때와 가장 큰 차이점은 <strong>&#39;트랜잭션 컨텍스트를 공유하느냐&#39;</strong>에 있다.(JPA의 경우 영속성 컨텍스트, MyBatis나 JdbcTemplate의 경우 동일한 DB 커넥션)</p>
<ul>
<li><strong>적용 상황</strong>: 여러 서비스에서 공통으로 쓰는 <strong>&#39;상품 정보 조회&#39;</strong> 모듈.</li>
<li><strong>예시</strong>: 단순 상품 상세 페이지에서는 상품 정보를 트랜잭션 없이 빠르게 읽어야 하지만, &#39;주문 로직&#39; 중간에 상품 정보를 조회할 때는 <strong>방금 전 주문 로직이 수정한 상품 재고 상태를 반영한 동일한 트랜잭션 컨텍스트(JPA의 경우 영속성 컨텍스트)</strong>를 읽어와야 할 때가 있다. SUPPORTS를 쓰면 <strong>상위 트랜잭션</strong>이 있을 땐 그 흐름에 합류해 <strong>동일한 트랜잭션 컨텍스트</strong>를 공유하고, 없을 땐 트랜잭션 없이 가볍게 동작한다.</li>
</ul>
<br>

<h2 id="4-nested-부분-손절은-가능-하지만-운명-공동체">4. NESTED: &quot;부분 손절은 가능, 하지만 운명 공동체&quot;</h2>
<p>REQUIRES_NEW와 비슷해 보이지만, <strong>부모 트랜잭션의 운명에 종속</strong>된다는 점이 결정적이다.</p>
<ul>
<li><strong>적용 상황</strong>: 주문 프로세스 중의 <strong>&#39;이벤트 알림톡 발송&#39;</strong>.</li>
<li><strong>예시</strong>: 주문은 성공해야 하지만, 알림톡 발송 실패 때문에 주문 자체가 취소(Rollback)되는 건 원치 않는다. 그렇다고 알림톡 발송만 성공하고 주문이 실패하는 것도 안 된다. 이 경우, NESTED를 사용하면 예외를 내부에서 적절히 처리한다는 전제 하에, 알림톡 발송이 실패해도 <strong>알림톡 로직만 롤백</strong>하고 주문은 성공시킬 수 있다. 내부적으로 세이브포인트(savepoint)를 찍고 동작하기 때문인데, 덕분에 알림톡 로직이 시작된 지점(savepoint)까지만 되돌릴 수 있다. 하지만 <strong>주문 자체가 실패</strong>하면 (savepoint 위에서 동작하기 때문에) <strong>알림톡 기록도 함께 롤백</strong>되어 데이터의 일관성을 맞춘다.</li>
</ul>
<hr>
<h2 id="💡마치며">💡마치며</h2>
<p>결국 이 전파 옵션들은 <strong>&quot;이 비즈니스 로직을 DB 커넥션과 트랜잭션이라는 울타리 안에 어디까지 가둘 것인가?&quot;</strong>를 고민하는 도구인 것 같다. 사실 실무에서 이 옵션들을 쓸 일은 거의 없지만, 적당히 선을 긋는 감각은 확실히 필요해 보인다.</p>
<p>예를 들어, 데이터의 정합성이 목숨보다 중요한 금융 로직에서는 <strong>MANDATORY</strong>나 <strong>NEVER</strong>로 아예 실수를 못 하게 막아버리는 식이다. 반대로 성능과 효율이 우선인 대규모 조회나 통계 작업에서는 <strong>NOT_SUPPORTED</strong>나 <strong>SUPPORTS</strong>를 써서 DB 커넥션이 불필요하게 묶이지 않게 교통정리를 해줄 수도 있다. 또, 알림 발송처럼 &#39;주문은 성공해도 이건 실패할 수 있는&#39; 상황에선 <strong>NESTED</strong>로 살짝 보험을 들어두는 것도 방법이다.</p>
<p>단순히 기능을 구현하는 것을 넘어, 내가 짠 코드가 DB 자원을 얼마나 점유하고 다른 로직에 어떤 영향을 줄지 한 번쯤 고민해 본 것만으로도 공부한 보람은 있는 것 같다. 당장 실무에서 다 쓰진 않더라도, 나중에 비슷한 고민이 생길 때 &quot;아, 그때 그 옵션!&quot;하고 떠올릴 수만 있어도 충분하지 않을까.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Spring Data JPA 예외 추상화, 왜 어떤 건 되고 어떤 건 안 될까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-Spring-Data-JPA-%EC%98%88%EC%99%B8-%EC%B6%94%EC%83%81%ED%99%94-%EC%99%9C-%EC%96%B4%EB%96%A4-%EA%B1%B4-%EB%90%98%EA%B3%A0-%EC%96%B4%EB%96%A4-%EA%B1%B4-%EC%95%88-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-Spring-Data-JPA-%EC%98%88%EC%99%B8-%EC%B6%94%EC%83%81%ED%99%94-%EC%99%9C-%EC%96%B4%EB%96%A4-%EA%B1%B4-%EB%90%98%EA%B3%A0-%EC%96%B4%EB%96%A4-%EA%B1%B4-%EC%95%88-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Sun, 29 Mar 2026 08:14:19 GMT</pubDate>
            <description><![CDATA[<p>Spring의 큰 장점 중 하나는 JPA나 MyBatis 같은 특정 기술의 예외를 스프링 공통 예외(<code>DataAccessException</code>)로 추상화해준다는 것이다. 그런데 Spring Data JPA를 쓰다 보면, 분명 쿼리 문법이 틀렸는데도 기대했던 스프링 예외가 아닌 생소한 에러 로그를 마주할 때가 있다.</p>
<p>왜 어떤 상황에서는 예외 추상화가 적용되고, 어떤 상황에서는 날것의 에러가 터지는 걸까?</p>
<hr>
<h2 id="1-핵심은-에러-발생-시점과-프록시proxy">1. 핵심은 &#39;에러 발생 시점&#39;과 &#39;프록시(Proxy)&#39;</h2>
<p>결론부터 말하면, <strong>에러가 스프링의 프록시(Proxy)가 가로챌 수 있는 시점에 터졌느냐</strong>가 관건이다.</p>
<p>스프링의 예외 변환 메커니즘(<code>PersistenceExceptionTranslator</code>)은 <code>@Repository</code>가 붙은 빈의 메서드 호출을 <strong>AOP 프록시</strong>가 가로챌 때 작동한다. 즉, 프록시라는 그물망 안에서 에러가 터져야 스프링이 예쁜 예외로 포장해 줄 수 있다는 뜻이다.</p>
<br>

<h2 id="2-spring-data-jpa의-정적-쿼리-검증-시점">2. Spring Data JPA의 정적 쿼리 검증 시점</h2>
<p>Spring Data JPA는 서비스 운영 중 발생할 실수를 줄이기 위해 <strong>&quot;잘못된 쿼리는 앱을 띄우기 전에 미리 잡자&quot;</strong>는 전략을 취한다.</p>
<p>예를 들어 <code>@Query</code>에 작성한 JPQL에 오타가 있거나, 메서드 이름 기반 쿼리에서 존재하지 않는 엔티티 필드명을 사용한 경우, 스프링은 애플리케이션 로딩 단계(빈 초기화)에서 인터페이스를 하나하나 스캔하며 쿼리의 유효성을 미리 파싱하고 검증하려고 시도한다.</p>
<p>여기서 중요한 점은 이 에러가 터지는 <strong>&#39;위치&#39;</strong>다. 이 시점은 리포지토리 프록시가 생성되어 실제 메서드 메서드 호출을 가로채는 실행 단계가 아니기 때문에, 에러가 <strong>스프링의 예외 추상화 그물망(Proxy) 바깥</strong>에서 발생한 것으로 간주된다. 결과적으로 스프링의 예외 추상화가 적용되지 않고 <code>BeanCreationException</code>이나 <code>IllegalArgumentException</code> 같은 날것의 에러가 그대로 노출되며 애플리케이션 시작 자체가 실패하게 된다.</p>
<br>

<h2 id="3-순수-jpa나-런타임-오류가-추상화되는-시점">3. 순수 JPA나 런타임 오류가 추상화되는 시점</h2>
<p>반면, 우리가 기대하는 <code>DataAccessException</code> 계열의 예외는 대개 <strong>런타임(Runtime)</strong>에 발생한다.</p>
<ul>
<li><strong>순수 JPA</strong>: 리포지토리를 직접 구현하면 앱 로딩 시점이 아니라, 실제 메서드가 호출되어 <code>createQuery()</code>가 실행되는 시점에 에러가 터진다. 이때는 프록시가 이미 작동 중이므로 에러를 가로채서 변환해 준다.</li>
<li><strong>런타임 DB 오류</strong>: Spring Data JPA를 쓰더라도 문법은 맞지만 DB 제약 조건 위반(Unique Key 중복, FK 위반 등)은 실제 쿼리가 날아가는 런타임에 발생한다. 이 에러들은 프록시를 통과하며 정상적으로 스프링 예외로 변환된다.</li>
</ul>
<br>

<h2 id="4-startup-검증의-사각지대">4. Startup 검증의 사각지대</h2>
<p>Spring Data JPA를 쓰더라도 모든 쿼리 오류가 애플리케이션 시작 시점에 잡히는 것은 아니다. 다음과 같은 상황에서는 검증이 <strong>실제 실행 시점(Runtime)</strong>으로 밀려나기도 한다.</p>
<ul>
<li><strong><code>@Query(nativeQuery = true)</code></strong>: JPQL과 달리 사전 검증이 충분히 이루어지지 않아, 실제 메서드 호출 시점에 SQL 오류가 발생한다.</li>
<li><strong>동적 쿼리 사용</strong>: Criteria API, QueryDSL, 혹은<code>EntityManager.createQuery</code>를 직접 사용하는 경우, 로직이 실행될 때 쿼리가 생성되므로 런타임에 에러가 발생한다.</li>
<li><strong>일부 파라미터 바인딩 오류</strong>: 쿼리 문법은 맞지만, 런타임에 넘겨준 파라미터 값이 타입 불일치 등으로 문제를 일으키는 경우다.</li>
</ul>
<p>이 상황들은 모두 애플리케이션이 이미 뜬 상태에서 <strong>프록시를 통과하는 시점</strong>에 예외가 발생한다. 따라서 이때는 우리가 기대했던 대로 스프링의 예외 추상화가 적용되어 <code>DataAccessException</code> 계열의 예외를 마주하게 된다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 핵심은 <strong>에러가 터지는 시점</strong>과 <strong>프록시(Proxy)의 경계선</strong>이다.</p>
<ol>
<li><strong>준비 단계(startup)</strong>에서 터지면? -&gt; 프록시 호출 흐름 바깥이라서 <strong>추상화 안됨</strong> (정적 쿼리 검증 등)</li>
<li><strong>실행 단계(Runtime)</strong>에서 터지면? -&gt; 프록시가 가로채서 <strong>추상화 됨</strong> (순수 JPA, DB 제약 조건 위반 등)</li>
</ol>
<p>&quot;왜 예외 추상화가 적용 안 되지?&quot;라고 고민하기 전에, 에러 로그가 앱이 뜨는 중에 찍혔는지 아니면 실제 메서드를 실행할 때 찍혔는지부터 확인하자. 그 시점이 스프링이 개입할 수 있는지 없는지를 결정하는 명확한 기준이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] JPA 리포지토리를 직접 상속받지 않고 주입 받아 쓰는 이유(DIP와 어댑터 패턴)]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-JPA-%EB%A6%AC%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC%EB%A5%BC-%EC%A7%81%EC%A0%91-%EC%83%81%EC%86%8D%EB%B0%9B%EC%A7%80-%EC%95%8A%EA%B3%A0-%EC%A3%BC%EC%9E%85-%EB%B0%9B%EC%95%84-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0DIP%EC%99%80-%EC%96%B4%EB%8C%91%ED%84%B0-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@yu-jin-song/Spring-JPA-%EB%A6%AC%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC%EB%A5%BC-%EC%A7%81%EC%A0%91-%EC%83%81%EC%86%8D%EB%B0%9B%EC%A7%80-%EC%95%8A%EA%B3%A0-%EC%A3%BC%EC%9E%85-%EB%B0%9B%EC%95%84-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0DIP%EC%99%80-%EC%96%B4%EB%8C%91%ED%84%B0-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Sat, 28 Mar 2026 12:18:27 GMT</pubDate>
            <description><![CDATA[<p>스프링 데이터 JPA를 사용하다 보면 아래와 같은 구조의 코드를 자주 마주하게 된다. </p>
<pre><code class="language-java">// 1. 우리가 만든 순수 인터페이스 (역할)
public interface ItemRepository {
    Item save(Item item);
    Optional&lt;Item&gt; findById(Long id);
}

// 2. 스프링 데이터 JPA 전용 인터페이스 (기술)
public interface SpringDataJpaItemRepository extends JpaRepository&lt;Item, Long&gt; {}

// 3. 실제 구현체 (어댑터)
@Repository
@RequiredArgsConstructor
public class JpaItemRepository implements ItemRepository {
    // 왜 상속(implements)받지 않고 주입을 받을까?
    private final SpringDataJpaItemRepository repository;

    @Override
    public Item save(Item item) {
        return repository.save(item);
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        return repository.findById(id);
    }
}</code></pre>
<p>여기서 의문이 생긴다. <code>JpaItemRepository</code>가 <code>ItemRepository</code>와 <code>SpringDataJpaItemRepository</code>를 <strong>둘 다 직접 구현(implements)</strong>해버리면 코드도 줄고 편할 것 같은데, 왜 굳이 하나를 필드로 주입받아서 대신 일 시키는 구조를 가져가는 걸까?</p>
<hr>
<h2 id="1-기술적인-한계-수십-개의-메서드를-직접-구현할-것인가">1. 기술적인 한계: &quot;수십 개의 메서드를 직접 구현할 것인가?&quot;</h2>
<p>만약 <code>JpaItemRepository</code>가 <code>SpringDataJpaItemRepository</code>를 직접 구현(implements)한다고 가정해 보자. 여기서 발생하는 문제는 크게 두 가지다.</p>
<p>첫째는 <strong>&#39;구현의 늪&#39;</strong>에 빠진다는 점이다. <code>SpringDataJpaItemRepository</code>는 <code>JpaRepository</code>를 상속받고 있으며, <code>save()</code>, <code>findAll()</code>, <code>findById()</code> 등 수많은 메서드를 가지고 있다. 인터페이스를 직접 구현하는 순간, 우리는 쓰지도 않을 이 수십 개의 메서드를 클래스 내부에서 전부 오버라이드(Override)해야 하는 노가다를 마주하게 된다.</p>
<p>하지만 더 근본적인 문제는 <strong>스프링 데이터 JPA의 동작 방식</strong>에 있다. 스프링 데이터 JPA는 <strong>개발자가 인터페이스만 선언하면 런타임에 프록시 객체를 생성</strong>해서 모든 메서드의 구현을 대신 제공한다. 만약 우리가 직접 구현 클래스를 만들어 <code>JpaRepository</code>를 implements 해버리면, 스프링이 제공하는 이 자동 프록시 생성 메커니즘을 제대로 활용할 수 없게 된다.</p>
<p>결국 스프링이 대신 만들어주던 모든 CRUD 기능을 포기하고 내 손으로 직접 다 작성해야 하는 상황에 처하게 된다. 이는 스프링 데이터 JPA가 제공하는 가장 큰 장점인 <strong>&#39;자동 구현&#39;</strong>을 스스로 걷어차는 꼴이다.</p>
<br>

<h2 id="2-객체지향의-핵심-역할과-구현의-분리">2. 객체지향의 핵심: 역할과 구현의 분리</h2>
<p>여기서 우리가 놓치지 말아야 할 개념이 바로 <strong>역할과 구현의 분리</strong>다.</p>
<ul>
<li><strong>역할(ItemRepository)</strong>: 우리 서비스에서 필요한 &quot;상품 저장&quot;, &quot;조회&quot;라는 순수한 비즈니스 기능 명세서다.</li>
<li><strong>구현(SpringDataJpaItemRepository)</strong>: JPA라는 특정 기술에 종속된 구체적인 도구다.</li>
</ul>
<p>만약 이 둘을 하나로 합쳐버리면 서비스의 리포지토리 역할이 특정 기술(JPA)과 강하게 결합(Strong Coupling)된다. 기술이 바뀌면 비즈니스 로직까지 흔들리는 위험한 구조가 되는 것이다.</p>
<p>반면 <strong>주입(DI)</strong> 방식을 사용하면 <code>JpaItemRepository</code>가 중간에서 <strong>어댑터(Adaptor)</strong> 역할을 수행한다. 외부(Service)에는 <code>ItemRepository</code>라는 표준 인터페이스만 노출하고, 실제 동작은 내부의 JPA 리포지토리에게 위임하는 구조가 된다.</p>
<br>

<h2 id="3-왜-이렇게-번거롭게-설계할까-dip-원칙">3. 왜 이렇게 번거롭게 설계할까? (DIP 원칙)</h2>
<p>이 설계의 핵심 목적은 <strong>기술이 바뀌어도 내 비즈니스 로직(Service)은 최대한 흔들리지 않게 하기 위해서</strong>다. 이를 객체지향 원칙에서는 <strong>의존관계 역전 원칙(DIP)</strong>이라고 부른다.</p>
<p>만약 내일 &quot;JPA를 버리고 MyBatis나 Querydsl, 혹은 다른 ORM으로 갈아타자&quot;는 결정이 내려졌다고 치자. 이 구조라면 <code>ItemRepository</code> 인터페이스는 그대로 두고, 새로운 구현체(예: <code>MyBatisItemRepository</code>)만 만들어서 빈으로 교체하면 된다. 서비스 계층은 오직 인터페이스만 의존하고 있기 때문에, 거의 코드를 수정할 필요가 없다.</p>
<br>

<h2 id="4-필요한-것만-노출하는-은닉의-미학">4. 필요한 것만 노출하는 &#39;은닉&#39;의 미학</h2>
<p><code>JpaRepository</code>가 제공하는 수많은 메서드 중 실제 서비스에서 사용하는 건 보통 몇 개에 불과하다. 우리가 직접 정의한 <code>ItemRepository</code> 인터페이스를 통해 <strong>딱 필요한 기능만 깔끔하게 노출</strong>할 수 있다는 점도 큰 장점이다.</p>
<p>불필요한 기술적 세부사항(예: <code>Pageable</code>, 특정 JPA 전용 메서드 등)이 서비스 계층으로 새어나가는 것을 막아주며, 도메인/서비스 계층의 순수성을 지켜준다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>처음 이 구조를 보면 &quot;왜 이렇게 빙 돌아가지?&quot; 싶을 수 있다. 하지만 이건 단순히 코드를 짜는 문제가 아니라 <strong>설계</strong>의 문제다.</p>
<p><strong>역할과 구현을 철저히 분리</strong>하는 것. 시스템이 커지고 기술 스택이 변할 때, 이 사소해 보이는 구조 차이가 유지보수의 지옥과 천국을 가른다. &quot;동작하는 코드&quot;를 넘어 &quot;변화에 강한 코드&quot;를 만들고 싶다면 반드시 이해하고 넘어가야 할 지점이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] JdbcTemplate은 어떻게 람다식만 보고 RowMapper인 줄 알까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-JdbcTemplate%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%9E%8C%EB%8B%A4%EC%8B%9D%EB%A7%8C-%EB%B3%B4%EA%B3%A0-RowMapper%EC%9D%B8-%EC%A4%84-%EC%95%8C%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-JdbcTemplate%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%9E%8C%EB%8B%A4%EC%8B%9D%EB%A7%8C-%EB%B3%B4%EA%B3%A0-RowMapper%EC%9D%B8-%EC%A4%84-%EC%95%8C%EA%B9%8C</guid>
            <pubDate>Tue, 17 Mar 2026 13:56:52 GMT</pubDate>
            <description><![CDATA[<p>JdbcTemplate으로 조회 로직을 짜다 보면 이런 코드를 자주 작성하게 된다.</p>
<pre><code class="language-java">private RowMapper&lt;Item&gt; itemRowMapper() {
    return (rs, rowNum) -&gt; {
        Item item = new Item();
        item.setId(rs.getLong(&quot;id&quot;);
        item.setItemName(rs.getString(&quot;item_name&quot;);
        item.setPrice(rs.getInt(&quot;price&quot;);
        item.setQuantity(rs.getInt(&quot;quantity&quot;);
        return item;
    };
}</code></pre>
<p>여기서 문득 의문이 생긴다. JdbcTemplate은 어떻게 저 람다식을 보고 &quot;아, 이게 RowMapper 구나&quot;라고 인식하는 걸까? 그리고 <code>rs</code>, <code>rowNum</code> 같은 변수명은 반드시 이렇게 지어야 하는걸까?</p>
<hr>
<h2 id="1-jdbctemplate이-람다를-인식하는-이유">1. JdbcTemplate이 람다를 인식하는 이유</h2>
<p>결론부터 말하면, <code>RowMapper</code>가 <strong>함수형 인터페이스(<code>@FunctionalInterface</code>)</strong>이기 때문이다.</p>
<p><code>RowMapper</code> 인터페이스를 뜯어보면 메서드가 딱 하나뿐이다.</p>
<pre><code class="language-java">@FunctionalInterface
public interface RowMapper&lt;T&gt; {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}</code></pre>
<p>자바 8+에서는 구현해야 할 메서드가 하나뿐인 인터페이스를 람다식으로 대체할 수 있다.</p>
<p>여기서 중요한 포인트는, <strong>JdbcTemplate이 람다식을 직접 해석하는 것이 아니라는 점</strong>이다. 우리가 람다를 넘기면 <strong>자바 컴파일러</strong>가 이미 컴파일 시점에 이를 <strong>RowMapper 인터페이스를 구현한 객체</strong>로 변환해서 전달한다. 런타임에는 람다가 아니라 <strong>일반적인 RowMapper 구현체</strong>로 동작하는 셈이다.</p>
<p>JdbcTemplate은 쿼리 실행 후 ResultSet을 순회하면서 <strong>각 행마다 mapRow()를 호출</strong>하고, 우리가 람다로 작성한 로직이 그때 실행된다. 즉, <strong>껍데기(인터페이스)는 RowMapper로 고정</strong>하고, <strong>알맹이(mapRow 구현)는 람다로 채우는 것</strong>이다.</p>
<p>참고로, 람다 안에서는 <code>throws SQLException</code>을 명시적으로 쓰지 않아도 된다. 자바가 암묵적으로 처리해준다.</p>
<br>

<h2 id="2-파라미터-변수명은-내-마음대로-해도-될까">2. 파라미터 변수명은 내 마음대로 해도 될까?</h2>
<p>람다식에서 쓰는 <code>(rs, rowNum)</code>이라는 이름은 사실 <strong>아무렇게나 지어도 상관없다.</strong></p>
<p>자바 컴파일러와 스프링은 변수의 이름이 아니라 <strong>함수형 인터페이스의 메서드 시그니처(타입, 순서, 개수)</strong>를 기준으로 판단하기 때문이다.</p>
<pre><code class="language-java">// 변수명을 마음대로 바꿔도 똑같이 동작한다.
return (result, n) -&gt; {
    Item item = new Item();
    item.setId(result.getLong(&quot;id&quot;);
    ...
    return item;
};</code></pre>
<p>JdbcTemplate은 쿼리를 실행하고 한 줄씩 읽을 때마다 값을 던져주는데, 이때 첫 번째 파라미터에는 <code>ResultSet</code> 객체, 두 번째 파라미터에는 <code>int</code>(현재 행 번호)가 들어가도록 이미 약속되어 있다.</p>
<p>이름을 <code>a</code>, <code>b</code>로 짓든, <code>r</code>, <code>i</code>라고 짓든 이 <strong>타입과 순서만 맞으면</strong> 동일하게 동작한다.</p>
<br>

<h2 id="3-그럼-왜-대부분-rs-rownum이라고-쓸까">3. 그럼 왜 대부분 <code>rs</code>, <code>rowNum</code>이라고 쓸까?</h2>
<p>이름이 상관없음에도 대부분의 예제에서 <code>rs</code>와 <code>rowNum</code>을 사용하는 이유는 단순하다. 바로 <strong>관례(Convention)</strong> 때문이다.</p>
<ul>
<li><code>rs</code> → ResultSet의 약자</li>
<li><code>rowNum</code> → row number(현재 행 번호)</li>
</ul>
<p>다른 개발자나 미래의 내가 코드를 봤을 때 별도의 고민 없이 &quot;아, DB 결과구나&quot;라고 바로 이해할 수 있다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>JdbcTemplate의 람다식은 별다른 마법이 아니라, <strong>함수형 인터페이스의 명세(시그니처)를 따르는 자바 문법</strong>일 뿐이다. 변수명은 자유롭게 바꿀 수 있지만, 협업과 가독성을 생각하면 <code>rs</code>, <code>rowNum</code> 같은 관례를 따르는 편이 훨씬 낫다.</p>
<p>결국 핵심은 이름이 아니라 <strong>&quot;각 행의 데이터(ResultSet + 행번호)를 받아 객체로 변환한다&quot;는 흐름과 약속을 이해하는 것</strong>이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 존재하지 않는 Profile을 지정해도 에러 없이 실행되는 이유]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%A1%B4%EC%9E%AC%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-Profile%EC%9D%84-%EC%A7%80%EC%A0%95%ED%95%B4%EB%8F%84-%EC%97%90%EB%9F%AC-%EC%97%86%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%A1%B4%EC%9E%AC%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-Profile%EC%9D%84-%EC%A7%80%EC%A0%95%ED%95%B4%EB%8F%84-%EC%97%90%EB%9F%AC-%EC%97%86%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 15 Mar 2026 16:48:50 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트를 실행할 때 보통 <strong>active profile</strong>을 지정한다. 실행 옵션에 <code>--spring.profiles.active=prod</code>를 추가하거나 환경 변수를 설정하는 방식이 일반적이다. 그런데 만약 실수로 존재하지 않는 프로필 이름(<code>prodd</code>)을 넣으면 어떻게 될까?</p>
<p>애플리케이션은 <strong>&quot;그런 프로필 없는데?&quot;</strong> 라고 에러를 내지 않는다. 대부분의 경우 <strong>아무 문제 없이 정상 실행된다.</strong> 왜 이런 일이 발생할까?</p>
<hr>
<h2 id="1-실제로-일어나는-일-없으면-그냥-무시">1. 실제로 일어나는 일: &quot;없으면 그냥 무시&quot;</h2>
<p>프로젝트에 <code>application.yml</code>과 <code>application-prod.yml</code> 설정 파일이 있다고 가정해보자. 여기서 <code>--spring.profiles.active=prodd</code>로 실행하면 스프링은 내부적으로 다음과 같은 과정을 거친다.</p>
<ol>
<li>active profile을 <code>prodd</code>로 설정</li>
<li><code>application-prodd.yml</code> 파일을 찾음</li>
<li>파일이 없음 → 그냥 넘어감</li>
<li>결국 <strong><code>application.yml</code>만 로드된 상태로 애플리케이션 가동</strong>됨</li>
</ol>
<p>즉, 프로필 이름이 틀려도 <strong>기본 설정</strong>으로 그냥 실행된다.</p>
<br>

<h2 id="2-왜-에러를-내지-않을까">2. 왜 에러를 내지 않을까?</h2>
<p>핵심 이유는 간단하다. 스프링에서 <strong>프로필(Profile)은 &#39;필수 조건&#39;이 아니라 &#39;선택적 조건&#39;</strong>이기 때문이다.</p>
<p>예를 들어 <code>@Profile(&quot;prod&quot;)</code> 어노테이션이 붙은 빈이나 설정이 있다면, 스프링은 active profile 목록을 확인해서 <code>prod</code>가 있으면 해당 설정을 적용하고, 없으면 적용하지 않는다. 즉, 스프링의 판단 기준은 &quot;이 프로필이 존재하는가?&quot;가 아니라 <strong>&quot;현재 활성화된 프로필 목록에 이 키워드가 포함되어 있는가?&quot;</strong>일 뿐이다.</p>
<p>따라서 <code>prodd</code>처럼 존재하지 않는 이름을 넣어도, 그 이름과 매칭되는 파일이나 빈이 없다면 <strong>아무 설정도 추가하지 않은 채 기본 환경으로 실행</strong>할 뿐이다. 스프링 입장에서는 잘못된 설정을 넣은 게 아니라, 그저 &#39;추가 설정이 없는 환경&#39;을 실행한 것이기 때문이다.</p>
<br>

<h2 id="3-실무에서-주의할-점-조용한-장애">3. 실무에서 주의할 점: &quot;조용한 장애&quot;</h2>
<p>이런 유연한 동작은 실무에서 꽤 위험한 상황을 초래한다. 특히 운영 배포 스크립트에서 오타가 발생했을 때가 치명적이다. 애플리케이션은 에러 없이 정상 실행되지만, 실제로는 <strong>운영 전용 설정이 하나도 적용되지 않은 상태</strong>이기 때문이다.</p>
<p>운영 DB 접속 설정이 무시되어 엉뚱한 곳에 접속을 시도하거나, 보안 설정이 개발용으로 풀려버리는 등 서버는 정상 실행 상태지만 서비스는 의도하지 않은 설정으로 동작하는 <strong>&#39;조용한 장애&#39;</strong>가 발생할 수 있다. 그래서 스프링 서비스를 운영할 때는 기동 로그에 뜨는 다음 한 줄을 반드시 확인해야 한다.</p>
<pre><code class="language-bash">The following profiles are active: prod</code></pre>
<p>이 로그가 의도한대로 찍혔는지 확인하는 습관만으로도 수많은 배포 사고를 막을 수 있다.</p>
<br>

<h2 id="4-실무에서-쓰는-방지-대책들">4. 실무에서 쓰는 방지 대책들</h2>
<p>단순히 확인을 넘어 시스템적으로 방어하고 싶다면 몇 가지 전략을 쓸 수 있다.</p>
<h3 id="41-배포-파이프라인cicd-검증">4.1 배포 파이프라인(CI/CD) 검증</h3>
<p>애플리케이션이 실행되기 전, 배포 스크립트 단계에서 허용된 프로필인지 먼저 확인하는 방법이다. 가장 대중적으로 쓰인다.</p>
<pre><code class="language-bash">if [[ &quot;$SPRING_PROFILES_ACTIVE&quot; != &quot;prod&quot; &amp;&amp; &quot;$SPRING_PRIFILES_ACTIVE&quot; != &quot;staging&quot; ]]
    echo &quot;허용되지 않은 프로필입니다&quot; &gt;&amp;2
    exit 1
fi</code></pre>
<h3 id="42-코드-레벨에서-fail-fast-검증">4.2 코드 레벨에서 Fail Fast 검증</h3>
<p>환경이 특히 중요한 서비스라면 애플리케이션 기동 직후에 프로필을 직접 검증하여, 조건에 맞지 않으면 프로세스를 즉시 종료(Fail Fast)시킬 수 있다.</p>
<pre><code class="language-java">@Component
public class ProfileValidator implements ApplicationRunner {

    private final Environment env;
    
    public ProfileValidator(Environment env) {
        this.env = env;
    }
    
    @Override
    public void run(ApplicationArguments args) {
        String[] active = env.getActiveProfiles();
        
        if (Arrays.stream(active).noneMatch(&quot;prod&quot;::equals)) {
            throw new IllegalStateException(
                &quot;prod 프로필이 활성화되지 않았습니다. 현재 active profiles: &quot; + Arrays.toString(active));
            );
        }
    }
}</code></pre>
<h3 id="43-springprofilesvalidate-설정-유지">4.3 <code>spring.profiles.validate</code> 설정 유지</h3>
<p>Spring Boot 2.4부터는 프로필 표현식의 문법 오류를 검증하는 <code>spring.profiles.validate</code> 옵션이 추가되었다.</p>
<ul>
<li><strong>설정</strong>: <code>spring.profiles.validate: true</code>(기본값)</li>
<li><strong>효과</strong>: <code>prod &amp; test</code>나 <code>prod | dev</code> 같은 표현식에 문법적 오류가 있으면 앱 기동을 차단한다.</li>
</ul>
<p>비록 프로필의 이름의 존재 여부(오타)까지는 잡아주지 못하지만, 최소한의 문법 방어선이 되므로 이 설정을 굳이 <code>false</code>로 끄지 않는 것이 좋다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>Spring Profile은 유연한 설정 분리를 위해 프로필 이름의 존재 여부를 강제 검증하지 않는다. 이 유연함은 분명 편리하지만, 역설적으로 오타 한 번에 운영 환경 전체를 위험에 빠뜨리는 <strong>&#39;조용한 장애&#39;</strong>의 원인이 되기도 한다.</p>
<p>결국 시스템이 잡아주지 못하는 오타를 방어하는 가장 확실한 방법은 <strong>개발자의 확인 습관</strong>이다. 실무에서 가장 중요한 &#39;기본 태도&#39;는 크게 두 가지다.</p>
<ol>
<li><strong>로그 확인</strong>: 배포 후 서버가 뜨면 가장 먼저 로그 내 <code>The following profiles are active: [프로필명]</code> 한 줄을 눈으로 검증하자.</li>
<li><strong>설정 의심하기</strong>: 기능이 의도대로 동작하지 않을 때, <strong>코드를 고치기 전에 설정 파일(yml)이 제대로 로드됐는지</strong>부터 확인하자. 프로필 설정은 배포할 때마다 틀릴 수 있는 가장 약한 고리이기 때문이다.</li>
</ol>
<p>기술의 유연함은 양날의 검과 같다. 그 검에 베이지 않으려면, 시스템이 내뱉는 로그 한 줄을 무심히 넘기지 않는 <strong>디테일한 확인 습관</strong>이 무엇보다 중요하다. 지금 내 서비스가 의도한 프로필로 돌고 있는지 로그부터 다시 한번 살펴보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] 프로젝트를 열 때 폴더 대신 build.gradle을 선택하는 이유]]></title>
            <link>https://velog.io/@yu-jin-song/IntelliJ-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%97%B4-%EB%95%8C-%ED%8F%B4%EB%8D%94-%EB%8C%80%EC%8B%A0-build.gradle%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@yu-jin-song/IntelliJ-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%97%B4-%EB%95%8C-%ED%8F%B4%EB%8D%94-%EB%8C%80%EC%8B%A0-build.gradle%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 15 Mar 2026 10:13:27 GMT</pubDate>
            <description><![CDATA[<p>인텔리제이에서 새로운 프로젝트를 불러올 때, 단순히 프로젝트 폴더를 선택할 수도 있지만 더 정확하고 안정적인 방법은 <code>build.gradle</code>(또는 <code>pom.xml</code>) 파일을 기준으로 프로젝트를 여는 것이다.</p>
<p>왜 굳이 폴더 대신 이런 설정 파일을 선택해서 여는 걸까?
이는 <strong>인텔리제이가 프로젝트를 인식하고 구성하는 방식</strong>과 관련이 있다.</p>
<hr>
<h2 id="1-프로젝트의-중심은-빌드-도구에-있다">1. 프로젝트의 중심은 &#39;빌드 도구&#39;에 있다</h2>
<p>과거에는 IDE(IntelliJ, Eclipse 등)가 컴파일 경로와 라이브러리 관리를 직접 담당하는 경우가 많았다. 하지만 현대적인 개발 환경에서는 <strong>프로젝트의 빌드 설정과 의존성 관리가 빌드 도구(Gradle, Maven)에 의해 정의되는 구조</strong>로 바뀌었다.</p>
<p>Gradle 프로젝트에서는 <code>build.gradle</code>(그리고 필요에 따라 <code>settings.gradle</code>) 파일에 프로젝트 빌드 설정과 의존성, 플러그인 정보 등이 정의된다.</p>
<p>인텔리제이는 이 빌드 스크립트를 분석하여 IDE 내부의 <strong>Project Model</strong>을 생성한다.
즉, IDE가 스스로 프로젝트 구조를 결정하기보다는, <strong>Gradle이 정의한 설정을 읽어와 이를 IDE 환경에 반영하는 방식</strong>에 가깝다.</p>
<p>그래서 프로젝트를 열 때 <code>build.gradle</code>을 기준으로 import하면, 인텔리제이가 프로젝트 구조와 설정을 가장 명확하게 인식할 수 있는 것이다.</p>
<br>

<h2 id="2-인덱싱indexing과-프로젝트-인식">2. 인덱싱(Indexing)과 프로젝트 인식</h2>
<p>인텔리제이는 프로젝트를 열면 코드 분석과 자동완성을 위해 <strong>인덱싱(Indexing)</strong> 작업을 수행한다.</p>
<p>폴더 전체를 열 경우 인텔리제이는 먼저 프로젝트 구조를 스캔하면서 <code>build.gradle</code> 같은 빌드 파일을 감지하고, 이를 Gradle 프로젝트로 인식하려고 시도한다. 최신 버전에서는 이 과정을 자동으로 제안하기도 한다.</p>
<p>다만 이 과정은 내부 스캔 이후에 Gradle 프로젝트를 import하는 흐름이기 때문에, 처음부터 <code>build.gradle</code>을 기준으로 프로젝트를 여는 것보다 설정 인식이 늦어질 수 있다.</p>
<p>반면 <code>build.gradle</code> 파일을 직접 선택해 <strong>Import as Project</strong>를 수행하면 인텔리제이는 즉시 Gradle 프로젝트로 인식하고, 소스 경로(<code>src/main/java</code>)와 의존성 정보를 기반으로 프로젝트 환경을 구성한다.</p>
<br>

<h2 id="3-gradle뿐만-아니라-현대-개발의-공통-구조">3. Gradle뿐만 아니라 현대 개발의 공통 구조</h2>
<p>이 원리는 비단 Spring이나 Gradle 프로젝트에만 해당되는 이야기가 아니다. 현대적인 대부분의 빌드 도구 기반 프로젝트에서도 비슷한 구조를 볼 수 있다.</p>
<p>예를 들어 Maven 프로젝트에서는 <code>pom.xml</code>이 프로젝트의 의존성과 빌드 설정을 정의한다. IDE는 이 파일을 기반으로 프로젝트 구조와 라이브러리를 구성한다.</p>
<p>C#(.NET) 환경에서도 <code>.sln</code>이나 <code>.csproj</code> 파일이 프로젝트 구성과 참조 관계를 정의하며, IDE는 이 설정을 읽어 개발 환경을 구축한다.</p>
<p>Node.js 프로젝트에서도 <code>package.json</code>이 의존성과 실행 스크립트를 정의하는 중심 역할을 한다.</p>
<p>이처럼 <strong>프로젝트의 구조와 의존성은 빌드 도구나 설정 파일이 정의하고, IDE는 이를 읽어 개발 환경을 구성하는 방식</strong>은 현대 개발 생태계에서 널리 사용되는 패턴이다.</p>
<br>

<h2 id="4-그럼-그냥-폴더를-열면-안될까">4. 그럼 그냥 폴더를 열면 안될까?</h2>
<p>최신 인텔리제이는 폴더를 열어도 내부의 <code>build.gradle</code>을 감지해 <strong>Gradle 프로젝트로 import 할지 자동으로 제안</strong>하는 경우가 많다. 그래서 단순히 폴더를 열어도 정상적으로 프로젝트가 인식되는 경우도 많다.</p>
<p>다만 프로젝트 구조가 복잡하거나 멀티모듈 프로젝트에서 <code>settings.gradle</code>을 기준으로 모듈 구조를 인식해야 하는 경우에는 IDE가 이를 파악하는 과정에서 추가적인 확인 단계가 필요할 수 있다.</p>
<p>그래서 처음부터 <code>build.gradle</code>을 기준으로 <strong>&#39;Import as Project&#39;</strong>를 수행하면 프로젝트 구조를 보다 명확하게 인식시키고 초기 설정 과정을 안정적으로 진행할 수 있다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p><code>build.gradle</code>은 단순한 설정 파일이 아니라 <strong>프로젝트의 빌드 설정과 의존성을 정의하는 중심적인 파일</strong>이다. 인텔리제이는 이 파일을 분석해 프로젝트 모델을 만들고 동기화(Sync)한다.</p>
<p>이러한 구조는 Gradle뿐 아니라 다양한 빌드 도구에서도 공통적으로 사용되는 방식이다.</p>
<p>새로운 프로젝트를 열 때 폴더를 먼저 선택하기보다, 프로젝트의 설정 파일(<code>build.gradle</code> 또는 <code>pom.xml</code>)을 기준으로 열어보자. 인텔리제이가 프로젝트 구조를 더 명확하게 이해하고 개발 환경을 빠르게 구성하는 데 도움이 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 체크 예외 vs 언체크 예외]]></title>
            <link>https://velog.io/@yu-jin-song/Java-%EC%B2%B4%ED%81%AC-%EC%98%88%EC%99%B8-vs-%EC%96%B8%EC%B2%B4%ED%81%AC-%EC%98%88%EC%99%B8</link>
            <guid>https://velog.io/@yu-jin-song/Java-%EC%B2%B4%ED%81%AC-%EC%98%88%EC%99%B8-vs-%EC%96%B8%EC%B2%B4%ED%81%AC-%EC%98%88%EC%99%B8</guid>
            <pubDate>Tue, 10 Mar 2026 15:02:02 GMT</pubDate>
            <description><![CDATA[<p>자바 예외는 크게 두 가지로 나뉜다. 어떤 건 예외 처리를 안 하면 빨간 줄이 뜨고(체크), 어떤 건 아무 말 없다가 실행 중에 터진다(언체크). 이 둘을 나누는 기준은 단순하다. <strong>&quot;컴파일러가 간섭하느냐, 아니냐&quot;</strong>다.</p>
<hr>
<h2 id="1-컴파일러가-참견하는-체크-예외">1. 컴파일러가 참견하는 &#39;체크 예외&#39;</h2>
<p><strong>체크 예외(Checked Exception)</strong>는 <code>RuntimeException</code>을 상속받지 않은 예외들을 말한다. 이 예외들은 컴파일러가 &quot;너 이거 예외 터지면 어떻게 할 거야?&quot;라고 사사건건 간섭하기 때문에 반드시 <code>try-catch</code>로 잡거나 <code>throws</code>로 던져야 한다. 대책을 세우지 않으면 빌드조차 안 되는 강제성이 있다.</p>
<p>이렇게까지 하는 이유는 이 예외들이 주로 &#39;외부 사고&#39;에서 오기 때문이다. 파일이 없거나 DB 연결이 끊기는 건 내 코드 실수가 아니라 어쩔 수 없는 외부 요인이다. 자바는 이런 사고가 났을 때 프로그램이 그냥 죽게 내버려 두지 말고, 어떻게든 복구 대책을 세우라는 의도로 예외 처리를 강제한다. 즉, <strong>코드 차원에서의 안전장치</strong>인 셈이다.</p>
<br>

<h2 id="2-컴파일러가-모른-척하는-언체크-예외">2. 컴파일러가 모른 척하는 &#39;언체크 예외&#39;</h2>
<p>반대로 <strong>언체크 예외(Unchecked Exception)</strong>는 <code>RuntimeException</code>을 상속받은 예외들이다. 이름처럼 컴파일러가 예외 처리 여부를 확인하지 않아서 코드상으로는 조용하다가 실행(Runtime) 중에 펑 터진다.</p>
<p>이건 주로 &#39;개발자의 실수&#39;에서 온다. 0으로 나누거나 빈 객체를 참조하는 건 복구 대책을 세울 일이 아니라, 애초에 코드를 똑바로 짜서 고쳐야 할 문제다. 그래서 자바는 굳이 예외 처리를 강요하지 않는다. 예외 처리를 하기 전에 로직을 수정하라는 뜻이다.</p>
<br>

<h2 id="3-실무에서는-어떻게-다를까">3. 실무에서는 어떻게 다를까?</h2>
<p>이 철학적 차이는 스프링의 <strong>트랜잭션(@Transactional) 롤백 정책</strong>에 그대로 반영되어 있다.</p>
<p>체크 예외는 자바 설계상 &#39;복구 가능한 사고&#39;에 가깝다. 그래서 스프링도 이를 시스템이 멈춰야 할 장애가 아닌, <strong>비즈니스 과정에서 발생한 예외적인 시나리오</strong>로 보고 일단 커밋을 진행한다. 반면에 언체크 예외는 &#39;명백한 개발자의 실수&#39;다. 스프링은 이를 데이터 정합성을 깨뜨릴 수 있는 <strong>치명적인 장애</strong>로 판단하고 즉시 롤백을 수행한다.</p>
<p>하지만 최근 실무 트렌드는 모든 예외를 언체크 예외로 던지는 쪽으로 기울고 있다. 어차피 DB가 꺼지는 사고는 코드 수준에서 복구하기 어렵기 때문이다. 굳이 모든 메서드에 <code>throws</code>를 달아 코드를 지저분하게 만들기보다, 체크 예외를 언체크 예외로 감싸서(Wrapping) 던지고 공통 에러 핸들러에서 한 번에 처리하는 방식이 훨씬 깔끔하다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 체크 예외와 언체크 예외의 구분은 <strong>프로그램의 안정성(체크)</strong>과 <strong>코드의 생산성(언체크)</strong> 사이에서 균형을 잡기 위한 자바의 설계 장치다.</p>
<p>단순히 빨간 줄을 없애는 게 목적이 아니라, 지금 발생한 문제가 <strong>사용자가 다시 시도하게 할 &#39;사고&#39;인지, 아니면 당장 버그를 잡아야 하는 &#39;실수&#39;인지</strong>를 먼저 고민해 보는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 리소스 반납은 왜 항상 '역순'일까?]]></title>
            <link>https://velog.io/@yu-jin-song/Java-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EB%B0%98%EB%82%A9%EC%9D%80-%EC%99%9C-%ED%95%AD%EC%83%81-%EC%97%AD%EC%88%9C%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Java-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EB%B0%98%EB%82%A9%EC%9D%80-%EC%99%9C-%ED%95%AD%EC%83%81-%EC%97%AD%EC%88%9C%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sun, 01 Mar 2026 09:01:15 GMT</pubDate>
            <description><![CDATA[<p>자바에서 외부 리소스(DB, 파일 I/O, 네트워크)를 다룰 때 가장 중요한 규칙 중 하나는 <strong>&quot;사용한 리소스는 반드시 닫아야(close) 한다.&quot;</strong>는 것이다. 그런데 여기에는 숨겨진 디테일이 하나 더 있다. 바로 <strong>&#39;생성된 순서의 반대로 닫아야 한다.&#39;</strong>는 원칙이다.</p>
<p>왜 굳이 역순이어야 할까? 단순히 관습일까? 아니면 기술적인 필연성 때문일까?</p>
<hr>
<h2 id="1-거울을-보는-듯한-대칭-생성과-소멸">1. 거울을 보는 듯한 대칭: 생성과 소멸</h2>
<p>자바에서 리소스 관리의 핵심은 <strong>&quot;나중에 생성된 것을 먼저 닫는 LIFO(Last-In, First-Out)&quot;</strong> 원칙에 있으며, 이는 마치 스택(Stack) 구조와 같은 형태로 리소스의 생명주기를 다룬다.</p>
<ul>
<li><strong>생성</strong>: A(통로) → B(도구) → C(데이터)</li>
<li><strong>소멸</strong>: C(데이터) → B(도구) → A(통로)</li>
</ul>
<p>이러한 &#39;역순 배치&#39;가 표준이 된 이유는 객체 간의 <strong>의존성(Dependency)</strong> 때문이다. 나중에 만들어진 객체(C)는 필연적으로 먼저 만들어진 객체(A, B)를 참조하거나 그 기반 위에서 동작하기 때문이다.</p>
<br>

<h2 id="2-현실-세계의-비유-수도꼭지와-호스">2. 현실 세계의 비유: 수도꼭지와 호스</h2>
<p>이 원리를 가장 쉽게 이해할 수 있는 비유는 <strong>수도꼭지와 호스</strong>다.</p>
<p>수도꼭지라는 원천 자원에 호스라는 도구를 끼우고 물통에 물을 받고 있는 상황을 상상해보자. 지금 이 순간에도 호스 안에는 물이 꽉 차서 흐르고 있으며, 호스는 수도꼭지에 연결되어 있어야만 그 물을 온전히 전달할 수 있다.</p>
<p>그런데 물을 다 받기도 전에 누군가 갑자기 수도꼭지부터 잠그거나 호스를 확 뽑아버리면 어떻게 될까? 호스 안에 남아있던 물이 사방으로 튀어 옷이 젖거나, 빠져나가지 못한 물이 고여 곰팡이가 생길 수 있다.</p>
<p>그래서 우리는 항상 물통에 물을 다 받고, 호스를 먼저 정리한 뒤, 마지막에 수도꼭지를 잠그는 순서를 지킨다. 이것이 현실 세계에서도 가장 안전하고 깔끔한 정리 방법이기 때문이다.</p>
<br>

<h2 id="3-자바의-세계로-치환하기">3. 자바의 세계로 치환하기</h2>
<p>이 현실의 원리는 자바의 세계에서도 그대로 적용된다. 수도꼭지가 잠겨 물이 튀는 것은 부모 객체가 먼저 닫혀 자식 객체가 에러를 던지는 것과 같고, 호스에 물이 고이는 것은 통로가 끊겨 미처 해제되지 못한 자원이 <strong>리소스 누수(Leak)</strong>를 일으키는 것과 같다.</p>
<p>자바는 이 &#39;역순 정리&#39;를 아예 언어 차원의 표준으로 명시했다. 우리가 쓰는 <code>try-with-resources</code> 구문을 보면 알 수 있다.</p>
<pre><code class="language-java">try (Resource A = ....;    // 1번 생성(수도꼭지)
     Statement stmt = ...;    // 2번 생성(호스)
     ResultSet rs = ...) {    // 3번 생성(물)
     // 로직 수행
}
// 자바가 보장하는 종료 순서: C -&gt; B -&gt; A</code></pre>
<p>나중에 생성된 리소스가 먼저 생성된 리소스에 의존하고 있을 확률이 높다는 전제하에, 자바는 가장 말단(C)부터 뿌리(A)까지 거슬러 올라가며 안전하게 자원을 회수한다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국, 리소스 반납이 역순인 이유는 <strong>나중에 만들어진 자원일수록 먼저 만들어진 자원에 의존하고 있기 때문</strong>이다.</p>
<p>자바에서 리소스를 닫는 행위는 단순히 문을 닫는 것이 아니라, <strong>안전하게 연결을 해제하는 과정</strong>이다. 나무의 뿌리가 뽑히면 가지가 말라 죽듯, 부모가 사라지면 자식은 고아가 된다. 가장 말단(자식)부터 정리하며 뿌리(부모)까지 거슬러 올라가는 &#39;역순 정리&#39;는 자바 개발자가 지켜야 할 기초적이고도 중요한 <strong>설계의 대칭성</strong>이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] 파일 자체를 DB에 저장하지 않고 '경로'만 저장하는 이유]]></title>
            <link>https://velog.io/@yu-jin-song/DB-%ED%8C%8C%EC%9D%BC-%EC%9E%90%EC%B2%B4%EB%A5%BC-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EC%A7%80-%EC%95%8A%EA%B3%A0-%EA%B2%BD%EB%A1%9C%EB%A7%8C-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@yu-jin-song/DB-%ED%8C%8C%EC%9D%BC-%EC%9E%90%EC%B2%B4%EB%A5%BC-DB%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EC%A7%80-%EC%95%8A%EA%B3%A0-%EA%B2%BD%EB%A1%9C%EB%A7%8C-%EC%A0%80%EC%9E%A5%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 20 Feb 2026 08:16:28 GMT</pubDate>
            <description><![CDATA[<p>파일 업로드 기능을 구현하다 보면 DB 설계 단계에서 한 번쯤 이러한 생각을 하게 된다.</p>
<blockquote>
<p>&quot;파일도 결국 데이터 아닌가? 그냥 DB에 바이너리(<code>BLOB</code>) 형태로 넣으면 안되나?&quot;</p>
</blockquote>
<p>하지만 실무에서는 <strong>파일은 스토리지에, DB에는 &#39;경로(Path)&#39;와 메타데이터만 저장</strong>한다. 이게 단순한 관습이 아니라 서비스 운영과 확장성에 직결되는 설계 원칙이기 때문이다.</p>
<hr>
<h2 id="1-db는-검색-전문이지-짐-보관-전문이-아니다">1. DB는 &#39;검색&#39; 전문이지 &#39;짐 보관&#39; 전문이 아니다</h2>
<p>DB의 핵심 존재 이유는 인덱스를 활용해 원하는 데이터를 빠르게 찾는 것이다. 파일 자체(바이너리)를 저장하면 DB의 본질적인 성능이 위협받는다.</p>
<h4 id="📌-문제-1-db의-비대화">📌 문제 1. DB의 비대화</h4>
<p>파일 자체를 DB에 넣기 시작하면 DB 파일 크기가 기하급수적으로 커진다. 이는 인덱스 성능 저하로 이어지고, 무엇보다 <strong>백업</strong>과 <strong>복구</strong>가 재앙 수준으로 느려진다. 10GB DB 백업과 1TB DB 백업의 운영 난이도 차이는 상상을 초월한다.</p>
<h4 id="📌-문제-2-저장-비용의-불균형">📌 문제 2. 저장 비용의 불균형</h4>
<p>DB 서버용 디스크는 고성능이라 비싼 반면에, S3 같은 오브젝트 스토리지는 저렴하고 용량이 무한대에 가깝다. 비싼 금고(DB)에 굳이 무거운 쌀가마니(파일)를 넣어둘 필요가 없는 것이다.</p>
<br>

<h2 id="2-서버-자원메모리의-효율적-활용">2. 서버 자원(메모리)의 효율적 활용</h2>
<p>사용자가 파일을 요청할 때, 서버가 DB에서 파일을 꺼내주는 과정을 상상해보자.</p>
<h4 id="❌-db-방식">❌ DB 방식</h4>
<pre><code>DB 접속 -&gt; 대용량 바이너리 읽기 -&gt; WAS 메모리에 적재 -&gt; 사용자 전송</code></pre><p>이 과정에서 WAS와 DB 서버의 메모리가 순식간에 요동치게 된다.</p>
<h4 id="⭕️-경로-방식">⭕️ 경로 방식</h4>
<p>DB에서는 아주 짧은 문자열(경로)만 읽어오면 끝이다. 실제 파일 전송은 Nginx 같은 웹 서버나 CDN(CloudFront)이 전용 통로로 처리하게 한다. 역할 분담을 통해 전체적인 시스템 부하를 낮추는 것이다.</p>
<br>

<h2 id="3-유연한-확장성과-메타데이터-관리">3. 유연한 확장성과 메타데이터 관리</h2>
<p>DB에는 경로 외에도 <strong>확장자, 크기, 원본 파일명</strong> 같은 &#39;메타데이터&#39;를 함께 저장한다.</p>
<h4 id="✅-장점-1-효율적인-조회">✅ 장점 1. 효율적인 조회</h4>
<p>실제 파일을 일일이 열어보지 않고도 &quot;지난달에 업로드된 10MB 이상의 이미지&quot; 같은 조건을 SQL 쿼리만으로 순식간에 찾아낼 수 있다.</p>
<h4 id="✅-장점-2-유연한-마이그레이션">✅ 장점 2. 유연한 마이그레이션</h4>
<p>저장 공간이 부족해져서 저장소를 A 서버에서 B 서버로, 혹은 S3로 옮겨야 할 때, 파일 자체를 옮기는 것보다 DB의 경로 문자열을 업데이트하는 것이 훨씬 안전하고 빠르다.</p>
<br>

<h2 id="4-그럼-db-저장은-무조건-나쁜가">4. 그럼 DB 저장은 무조건 나쁜가?</h2>
<p>물론 예외는 있다. 다음과 같은 특수한 상황에서는 DB 저장이 더 적절할 수 있다.</p>
<ul>
<li>10KB 이하의 아주 작은 썸네일/아이콘</li>
<li>파일이 트랜잭션과 강하게 묶여 데이터 무결성이 최우선인 경우</li>
<li>인프라 구축 비용이 더 큰 소규모 토이 프로젝트</li>
</ul>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>DB는 파일이 어디에 있는지(Path) 알려주는 &#39;이정표&#39; 역할을 하고, 실제 무거운 짐(File)은 저렴하고 튼튼한 &#39;전용 창고&#39;에 보관하는 것이 성능과 비용 면에서 압도적으로 유리하다.</p>
<p>결국 시스템 설계의 핵심은 <strong>&quot;각자 제일 잘하는 일에 집중하게 하는 것&quot;</strong>임을 다시 한번 깨달았다. DB는 주소록 역할만 할 때 가장 아름답다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] MultipartFile.transferTo: 왜 File과 Path 두 가지 방식을 다 제공할까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-MultipartFile.transferTo-%EC%99%9C-File%EA%B3%BC-Path-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EB%8B%A4-%EC%A0%9C%EA%B3%B5%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-MultipartFile.transferTo-%EC%99%9C-File%EA%B3%BC-Path-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EB%8B%A4-%EC%A0%9C%EA%B3%B5%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 17 Feb 2026 06:05:42 GMT</pubDate>
            <description><![CDATA[<p>파일 업로드를 구현하다 보면 <code>MultipartFile</code>의 <code>transferTo()</code> 메서드가 파라미터로 <code>File</code>도 받고 <code>Path</code>도 받는 걸 볼 수 있다. &quot;결국 저장하는 건 똑같은데 왜 굳이 두 개로 나눠놨지?&quot;라는 궁금증이 생겨, 실무에서 주로 쓰이는 방식과 함께 정리해보았다.</p>
<hr>
<h2 id="1-file-vs-path-뭐가-다른걸까">1. File vs Path, 뭐가 다른걸까?</h2>
<p>결론부터 말하면, 자바의 <strong>구형 방식(IO)</strong>과 <strong>신형 방식(NIO)</strong>을 모두 지원하기 위한 스프링의 배려다.</p>
<h4 id="✅-클래식한-file-방식">✅ 클래식한 <code>File</code> 방식</h4>
<p>가장 오래된 방식으로, 저장할 파일의 &#39;실체&#39;를 객체로 직접 만들어 전달한다.</p>
<pre><code class="language-java">file.transferTo(new File(fullPath));</code></pre>
<p>경로 유효성 검사 등을 <code>File</code> 객체 생성 시점에 미리 처리할 수 있지만, 코드가 다소 투박하다.</p>
<h4 id="✅-현대적인-path-방식-권장">✅ 현대적인 <code>Path</code> 방식 (권장)</h4>
<p>Java 7부터 도입된 NIO(New IO)를 활용한다. 파일 객체 대신 &#39;경로&#39; 정보만 전달한다.</p>
<pre><code class="language-java">file.transferTo(Paths.get(fullPath));</code></pre>
<p><code>new File()</code>을 직접 할 필요가 없다. 내부적으로 최신 자바 API를 사용하여 더 안전하고 효율적으로 파일을 저장한다.</p>
<br>

<h2 id="2-실무에서는-어떻게-저장할까-외부-스토리지의-등장">2. 실무에서는 어떻게 저장할까? (외부 스토리지의 등장)</h2>
<p>공부하다 보니 실무에서는 서버 로컬 디스크에 저장하는 위 방식들보다 <strong>AWS S3 같은 외부 스토리지</strong>를 훨씬 많이 쓴다는 걸 알게 되었다. 이때는 <code>transferTo</code>를 아예 쓰지 않는 &#39;반전&#39;이 있다.</p>
<h4 id="✅-스트림-전송">✅ 스트림 전송</h4>
<p>서버 디스크에 임시 저장하지 않고, <strong>입력 스트림(InputStream)</strong>을 열어 외부로 바로 쏴주는 방식이다.</p>
<pre><code class="language-java">public void uploadS3(MultipartFile file) throws IOException {
    // transferTo 대신 getInputStream()을 사용
    amazonS3.putObject(&quot;bucket-name&quot;, &quot;filename&quot;, file.getInputStream(), metadata);
}</code></pre>
<p>서버의 디스크 자원을 소모하지 않고 곧바로 클라우드에 저장할 수 있어 대규모 서비스에 유리하다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 <code>transferTo</code>가 두 가지인 이유는 <strong>&quot;그릇을 직접 빚어서 줄래(File), 아니면 놓을 자리만 알려줄래(Path)?&quot;</strong>의 차이였다. 최근에는 <code>Path</code> 방식이 표준이지만, 인프라 환경에 따라 아예 스트림을 직접 다루는 방식이 더 중요할 수 있다는 점이 흥미로웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] "당연히 true 아닌가요?" spring.servlet.multipart.enabled 옵션이 존재하는 이유]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EB%8B%B9%EC%97%B0%ED%9E%88-true-%EC%95%84%EB%8B%8C%EA%B0%80%EC%9A%94-spring.servlet.multipart.enabled-%EC%98%B5%EC%85%98%EC%9D%B4-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EB%8B%B9%EC%97%B0%ED%9E%88-true-%EC%95%84%EB%8B%8C%EA%B0%80%EC%9A%94-spring.servlet.multipart.enabled-%EC%98%B5%EC%85%98%EC%9D%B4-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 15 Feb 2026 16:20:43 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트로 파일 업로드를 구현하다 보면, 별다른 설정 없이도 <code>MultipartFile</code> 객체를 통해 파일을 받아낼 수 있다.</p>
<pre><code class="language-java">@PostMapping(&quot;/upload&quot;)
public String upload(@RequestParam MultipartFile file) {
    // 그냥 된다. 신기할 정도로...
    return file.getOriginalFilename();
}</code></pre>
<p>그런데 설정에 <code>spring.servlet.multipart.enabled</code>라는 옵션이 존재한다. 기본값은 <code>true</code>라지만, 문득 이런 의문이 든다.</p>
<blockquote>
<p><em><strong>&quot;아니, 멀티파트 요청이 들어오면 당연히 처리해줘야 하는거 아님? 이걸 왜 굳이 끄고 켤 수 있게 만든 거지?&quot;</strong></em></p>
</blockquote>
<p>오늘은 당연하게 생각했던 이 스위치 뒤에 숨겨진 이유를 정리해 보았다.</p>
<hr>
<h2 id="1-멀티파트-처리는-공짜가-아니다">1. 멀티파트 처리는 &#39;공짜&#39;가 아니다</h2>
<p>HTTP 요청이 들어올 때 이게 일반 데이터인지, 대용량 파일이 섞인 <code>Multipart</code> 요청인지 판단하고 파싱(Parsing)하는 과정은 서버 자원을 꽤 잡아먹는 작업이다.</p>
<ul>
<li><strong>옵션이 켜져 있는 경우(<code>true</code>)</strong>: 스프링의 <code>MultipartResolver</code>가 모든 요청을 일단 감시한다. 멀티파트 요청이면 <code>HttpServletRequest</code>를 <code>MultipartHttpServletRequest</code>로 변환해 우리가 쓰기 편하게 &#39;세팅&#39;해준다.</li>
<li><strong>옵션이 꺼져 있는 경우(<code>false</code>)</strong>: 이런 변환 과정을 통째로 건너뛴다. 서버는 멀티파트 데이터를 그냥 읽을 수 없는 바이너리 덩어리로 취급한다.</li>
</ul>
<br>

<h2 id="2-왜-굳이-off-스위치를-만들었을까">2. 왜 굳이 &#39;OFF&#39; 스위치를 만들었을까?</h2>
<p>스프링이 &quot;알아서 다 해줄게!&quot;라고 하지 않고 선택권을 준 이유는 크게 두 가지다.</p>
<h4 id="①-자원의-보호와-보안">① 자원의 보호와 보안</h4>
<p>모든 서비스가 파일 업로드를 사용하는 건 아니다. 만약 업로드 기능이 없는 서버인데, 누군가 악의적으로 10GB짜리 멀티파트 데이터를 계속 던진다고 가정해보자. 서버가 매번 이걸 파싱하려고 시도하는 것 자체가 CPU와 메모리 낭비이며, 서비스 장애(DDoS)로 이어질 수 있다. 이럴 땐 옵션을 꺼서 문을 아예 닫아버리는 게 상책이다.</p>
<h4 id="②-설계의-유연성">② 설계의 유연성</h4>
<p>스프링은 특정 라이브러리를 강제하지 않는다. 표준 서블릿을 쓸지, Apache Commons 같은 외부 라이브러리를 쓸지 개발자가 선택할 수 있게 하려고 &quot;기본 자동 설정을 끌 수 있는 스위치&quot;를 남겨준 것이다.</p>
<br>

<h2 id="3-이-옵션을-일부러-끄는-상황도-있다">3. 이 옵션을 &#39;일부러&#39; 끄는 상황도 있다?</h2>
<p>실제로 이 편리한 옵션을 <code>false</code>로 설정하는 건 아주 특수한 경우라고 한다. 하지만 <strong>&quot;스프링의 친절한 자동 처리가 오히려 방해가 되는 상황&quot;</strong>에서는 이 선택권이 필수적이라는 점을 알게 되었다.</p>
<h4 id="🚀-초고용량-파일-스트리밍이-필요할-때">🚀 초고용량 파일 스트리밍이 필요할 때</h4>
<p>스프링의 기본 멀티파트 처리는 파일을 받을 때 내부적으로 임시 디렉토리에 저장하거나 메모리에 올린다. 하지만 10GB 이상의 초고용량 영상을 처리해야 한다면? 스프링이 이걸 중간에 가로채서 저장하려다 서버 메모리가 버티지 못할 수 있다.
이런 경우 옵션을 끄고, 개발자가 <strong>서블릿 입력 스트림(InputStream)</strong>을 직접 열어 데이터가 들어오는 대로 즉시 스토리지로 쏴버리는 방식을 택한다고 한다.</p>
<h4 id="🛡️-파싱-전-단계에서-정교하게-검증하고-싶을-때">🛡️ 파싱 전 단계에서 정교하게 검증하고 싶을 때</h4>
<p>스프링이 파싱을 시작하기 전(Filter 단계)에 HTTP 헤더만 보고 &quot;허용되지 않은 파일이니 파싱조차 안 하겠다&quot;라고 더 앞단에서 차단하고 싶을 때, 자동 설정을 끄고 직접 제어 로직을 구현하기도 한다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 <code>spring.servlet.multipart.enabled</code> 옵션이 존재하는 이유는 <strong>&quot;자동화의 편리함(True)과 수동 제어의 세밀함(False) 사이의 선택권&quot;</strong>을 주기 위해서였다.</p>
<ul>
<li><strong>일반적인 상황(True)</strong>: 스프링이 제공하는 <code>MultipartResolver</code>의 편리함을 누리면 된다.</li>
<li><strong>특수한 상황(False)</strong>&quot;: 초대용량 스트리밍 처리나 정교한 보안 검증이 필요할 때, 개발자가 직접 제어봉을 잡을 수 있게 길을 열어둔 것이다.</li>
</ul>
<p>&quot;Multipart 처리를 꺼야 하는 이유가 있나?&quot;라는 단순한 의문에서 시작했지만, 공부를 마칠 때쯤엔 스프링이 개발자의 자유도를 얼마나 깊게 고려하고 있는지 체감할 수 있었다. 역시 모든 설정에는 다 이유가 있고, 그 이면을 이해할 때 비로소 프레임워크를 제대로 다룰 수 있게 되는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 컨버전 서비스, 파라미터가 2개인데 왜 "편하다"고 할까? (ISP 관점)]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%BB%A8%EB%B2%84%EC%A0%84-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EA%B0%80-2%EA%B0%9C%EC%9D%B8%EB%8D%B0-%EC%99%9C-%ED%8E%B8%ED%95%98%EB%8B%A4%EA%B3%A0-%ED%95%A0%EA%B9%8C-ISP-%EA%B4%80%EC%A0%90</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%BB%A8%EB%B2%84%EC%A0%84-%EC%84%9C%EB%B9%84%EC%8A%A4-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0%EA%B0%80-2%EA%B0%9C%EC%9D%B8%EB%8D%B0-%EC%99%9C-%ED%8E%B8%ED%95%98%EB%8B%A4%EA%B3%A0-%ED%95%A0%EA%B9%8C-ISP-%EA%B4%80%EC%A0%90</guid>
            <pubDate>Tue, 10 Feb 2026 14:44:49 GMT</pubDate>
            <description><![CDATA[<p>스프링의 컨버전 서비스를 공부하다 보면 의문이 생긴다. 개별 컨버터를 직접 사용하면 파라미터 하나만 던지면 되는데, 컨버전 서비스는 데이터와 반환 타입까지 굳이 두 개를 적어야 한다.</p>
<pre><code class="language-java">// 1. 컨버터 직접 사용 (파라미터 1개)
Integer result = integerToStringConverter.convert(data);

// 2. 컨버전 서비스 사용 (파라미터 2개)
Integer result = conversionService.convert(data, Integer.class);</code></pre>
<p>코드만 보면 2번이 더 길고 불편해 보인다. 그런데 왜 스프링은 이 방식을 권장하고, 다들 이게 더 &quot;편하다&quot;고 말하는 걸까? 그 이면에 숨겨진 <strong>ISP(인터페이스 분리 원칙)</strong>의 실체를 정리해 보았다.</p>
<hr>
<h2 id="1-내가-요리사를-외울-것인가-메뉴판을-볼-것인가">1. &quot;내가 요리사를 외울 것인가, 메뉴판을 볼 것인가&quot;</h2>
<p>가장 큰 차이는 <strong>사용자가 알아야 할 정보의 양</strong>이다.</p>
<ul>
<li><strong>직접 호출</strong>: 숫자를 바꿀 땐 <code>A컨버터</code>, 문자를 바꿀 땐 <code>B컨버터</code>... 사용하는 쪽에서 모든 컨버터의 존재와 이름을 다 알고 있어야 한다. (식당 요리사 이름을 일일이 외워야 주문이 가능한 상황)</li>
<li><strong>컨버전 서비스</strong>: 나는 어떤 컨버터가 있는지 모른다. 그냥 &quot;이 데이터(source)를 이 타입(target)으로 바꿔줘&quot;라고 <strong>목적</strong>만 말하면 된다. (메뉴판만 보고 주문하는 상황)</li>
</ul>
<p>컨버전 서비스의 파라미터가 2개가 된 이유는, 수많은 컨버터를 <strong>단 하나의 인터페이스(convert)로 통합</strong>했기 때문이다. 구체적인 대상을 지정하는 대신 목적지(Target Type)을 알려줘야 하기 때문에 파라미터가 추가된 것이다.</p>
<br>

<h2 id="2-isp인터페이스-분리-원칙가-주는-진짜-편리함">2. ISP(인터페이스 분리 원칙)가 주는 &quot;진짜&quot; 편리함</h2>
<p>여기서 <strong>ISP(Interface Segregation Principle)</strong> 개념이 등장한다. ISP의 핵심은 &quot;사용자는 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다&quot;는 것이다.</p>
<p>컨버전 서비스를 쓰면 클라이언트(컨트롤러 등)는 수많은 컨버터 구현체들을 몰라도 된다. 오직 <strong><code>ConversionService</code>라는 단일 창구</strong>만 바라보면 된다.</p>
<h3 id="왜-이게-결국-더-편할까">왜 이게 결국 더 편할까?</h3>
<ol>
<li><strong>의존성 단순화</strong>: 컨버터가 100개여도 내 코드에는 <code>ConversionService</code> 하나만 주입(DI)받으면 끝난다.</li>
<li><strong>변경에 강함</strong>: 내부에서 컨버터 로직이 바뀌거나 새로운 컨버터가 추가되어도, 사용하는 쪽의 코드는 단 한 줄도 바뀌지 않는다. (구현체와 사용자의 완전한 분리)</li>
<li><strong>스프링과의 협업</strong>: 사실 우리가 <code>convert()</code>를 직접 호출할 일도 별로 없다. 스프링이 내부적으로 컨버전 서비스를 사용하기 때문에, 우리는 등록만 해두면 <code>@RequestParam</code> 등에서 자동 변환 혜택을 누릴 수 있다.</li>
</ol>
<br>

<h2 id="3-결론-불편함은-보험료다">3. 결론: 불편함은 &quot;보험료&quot;다</h2>
<p>파라미터 하나를 더 적는 수고로움은 <strong>나중에 수십 개의 컨트롤러 코드를 일일이 수정해야 할 대참사를 막기 위한 일종의 보험료</strong>와 같다.</p>
<ul>
<li><strong>직접 호출</strong>: 파라미터는 적지만, 결합도가 높아 시스템이 커질수록 관리 지옥이 된다.</li>
<li><strong>컨버전 서비스</strong>: 파라미터는 늘었지만, 내부 구조를 감춤으로써 시스템의 유연함과 확장성을 얻었다.</li>
</ul>
<blockquote>
<p><em><strong>&quot;사용자(클라이언트)를 바보로 만들수록 시스템은 더 견고해진다.&quot;</strong></em></p>
</blockquote>
<p>이것이 컨버전 서비스가 파라미터를 두 개나 받으면서도 &quot;편리한 도구&quot;라고 불리는 진짜 이유였다.</p>
<hr>
<h2 id="📚-요약">📚 요약</h2>
<table>
<thead>
<tr>
<th align="left">방식</th>
<th align="left">파라미터</th>
<th align="left">의존성</th>
<th align="left">유연성</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>개별 컨버터</strong></td>
<td align="left">1개 (데이터)</td>
<td align="left">모든 컨버터 구현체를 알아야 함</td>
<td align="left">낮음 (교체 시 코드 수정 필요)</td>
</tr>
<tr>
<td align="left"><strong>컨버전 서비스</strong></td>
<td align="left">2개 (데이터 + 타입)</td>
<td align="left">컨버전 서비스 인터페이스만 알면 됨</td>
<td align="left">높음 (구현체 숨김, ISP/DIP  준수)</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] @ExceptionHandler에 여러 예외를 묶을 때의 파라미터 선언 규칙]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-ExceptionHandler%EC%97%90-%EC%97%AC%EB%9F%AC-%EC%98%88%EC%99%B8%EB%A5%BC-%EB%AC%B6%EC%9D%84-%EB%95%8C%EC%9D%98-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%84%A0%EC%96%B8-%EA%B7%9C%EC%B9%99</link>
            <guid>https://velog.io/@yu-jin-song/Spring-ExceptionHandler%EC%97%90-%EC%97%AC%EB%9F%AC-%EC%98%88%EC%99%B8%EB%A5%BC-%EB%AC%B6%EC%9D%84-%EB%95%8C%EC%9D%98-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%84%A0%EC%96%B8-%EA%B7%9C%EC%B9%99</guid>
            <pubDate>Sun, 08 Feb 2026 13:40:42 GMT</pubDate>
            <description><![CDATA[<p><code>@ExceptionHandler</code>를 사용하면 다음과 같이 여러 예외를 배열로 묶어 한꺼번에 처리할 수 있다.</p>
<pre><code class="language-java">@ExceptionHandler({AException.class, BException.class})
public String ex(Exception e) {
    log.info(&quot;exception e&quot;, e);
    return &quot;error&quot;;
}</code></pre>
<p>그런데 코드를 작성하다 보니 문득 의문이 생겼다. 처리해야 할 예외는 <code>A</code>와 <code>B</code> 두 개인데, <strong>파라미터는 왜 굳이 상위 타입인 Exception으로 선언해야 할까?</strong> 혹시 파라미터를 아예 <strong>생략</strong>해도 되는 건지, 아니면 반드시 둘을 아우르는 부모 클래스를 적어야만 하는 건지 궁금해졌다.</p>
<p>결론부터 말하자면 <strong>&quot;둘 다 가능하지만, 선택의 기준은 명확하다&quot;</strong>는 것이다.</p>
<hr>
<h2 id="1-파라미터-생략하기-정보가-필요-없다면-비워라">1. 파라미터 생략하기: &quot;정보가 필요 없다면 비워라&quot;</h2>
<p>스프링은 예외 객체 자체를 로직에서 쓸 일이 없다면 파라미터를 아예 생략하는 것을 허용한다.</p>
<pre><code class="language-java">@ExceptionHandler({AException.class, BException.class})
public ModelAndView ex() {
    // 예외 객체를 안 써도 된다면 이렇게만 써도 무방함
    log.info(&quot;A 또는 B 예외 발생&quot;);
    return new ModelAndView(&quot;error/4xx&quot;);
}</code></pre>
<p>구체적인 에러 메시지나 스택 트레이스를 로그로 남길 필요가 없는 단순한 에러 페이지 이동 등에 사용한다.</p>
<br>

<h2 id="2-상위-타입으로-받기-로그와-다형성">2. 상위 타입으로 받기: &quot;로그와 다형성&quot;</h2>
<p>실무에서 가장 권장되는 방식은 공통 부모 타입(<code>Exception</code>이나 <code>RuntimeException</code> 등)을 파라미터로 선언하는 것이다.</p>
<pre><code class="language-java">@ExceptionHandler({AException.class, BException.class})
public ErrorResult ex(Exception e) {
    // A나 B 중 무엇이 터져도 e로 들어옴 (다형성)
    log.error(&quot;[exceptionHandle] 발생한 예외: {}&quot;, e.getClass());
    return new ErrorResult(&quot;BAD_REQUEST&quot;, e.getMessage());
}</code></pre>
<p>어떤 예외가 들어왔는지 로그를 남겨야 하거나, 예외 객체에서 메시지를 추출해 공통 에러 응답 객체를 만들어야 할 때 사용한다.</p>
<br>

<h2 id="3-주의-특정-자식-타입을-적으면-안-되는-이유">3. 주의: 특정 자식 타입을 적으면 안 되는 이유</h2>
<p>만약 여러 예외를 묶어두고 파라미터만 특정 자식 타입인 <code>AException</code>으로 고정하면 문제가 생긴다.</p>
<pre><code class="language-java">@ExceptionHandler({AException.class, BException.class})
public String ex(AException e) { ... }    // 위험!</code></pre>
<p><code>AException</code>이 터지면 잘 작동하지만, <code>BException</code>이 터지면 <strong>파라미터 타입 불일치</strong> 문제가 발생한다. 스프링 내부에서 예외 해결에 실패하게 되므로, 여러 개를 묶을 때는 반드시 <strong>공통 조상</strong>을 쓰거나 <strong>생략</strong>해야 한다.</p>
<br>

<h2 id="4-실무적인-선택-기준-요약">4. 실무적인 선택 기준 요약</h2>
<h4 id="✅-로그를-남기거나-응답-메시지를-써야-한다면">✅ 로그를 남기거나 응답 메시지를 써야 한다면?</h4>
<p>공통 부모 타입(<code>Exception</code>, <code>RuntimeException</code> 등)을 파라미터로 선언한다. (가장 권장)</p>
<h4 id="✅-예외-정보와-상관없이-단순-처리만-하면-된다면">✅ 예외 정보와 상관없이 단순 처리만 하면 된다면?</h4>
<p>파라미터를 생략한다.</p>
<h4 id="✅-만약-예외마다-처리-로직이-완전히-달라야-한다면">✅ 만약 예외마다 처리 로직이 완전히 달라야 한다면?</h4>
<p>하나로 묶지 말고 메서드를 분리해서 각각의 구체적인 타입을 파라미터로 쓴다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>단순히 &quot;동작한다&quot;는 사실을 넘어, <strong>&quot;내가 예외 객체를 로직에서 사용할 것인가?&quot;</strong>에 따라 파라미터 선언 방식이 달라진다. 실무에서는 디버깅을 위해 로그를 남겨야 하는 경우가 대부분이므로, 다형성을 활용해 공통 부모 타입을 받아두는 것이 가장 유연한 대응이라고 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] "그냥 @ResponseStatus 쓰면 안 되나요?" — ResponseStatusException의 존재 이유]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EA%B7%B8%EB%83%A5-ResponseStatus-%EC%93%B0%EB%A9%B4-%EC%95%88-%EB%90%98%EB%82%98%EC%9A%94-ResponseStatusException%EC%9D%98-%EC%A1%B4%EC%9E%AC-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EA%B7%B8%EB%83%A5-ResponseStatus-%EC%93%B0%EB%A9%B4-%EC%95%88-%EB%90%98%EB%82%98%EC%9A%94-ResponseStatusException%EC%9D%98-%EC%A1%B4%EC%9E%AC-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 08 Feb 2026 12:55:23 GMT</pubDate>
            <description><![CDATA[<p>API 예외 처리를 위해 <code>ResponseStatusExceptionResolver</code>를 공부하다 보면 의문이 생긴다. <code>@ResponseStatus</code>라는 훨씬 간결한 애노테이션이 있는데, 왜 굳이 <code>throw new ResponseStatusException(...)</code>이라는 긴 코드를 직접 써야 할까?</p>
<p>글로만 봐서는 잘 와닿지 않았던 <strong>&#39;외부 라이브러리 제어&#39;</strong>와 <strong>&#39;동적 상태 코드 변경&#39;</strong> 상황을 코드로 정리해 보았다.</p>
<hr>
<h2 id="1-내가-고칠-수-없는-남의-코드를-다룰-때">1. 내가 고칠 수 없는 &#39;남의 코드&#39;를 다룰 때</h2>
<p>가장 대표적인 상황은 <strong>외부 라이브러리</strong>를 사용할 때다. 라이브러리 내부에서 터지는 예외 클래스는 우리가 직접 <code>@ResponseStatus</code> 애노테이션을 붙일 수 없다. 소스 코드를 수정할 권한이 없기 때문이다.</p>
<p>이럴 때 <code>catch</code>문 안에서 <code>ResponseStatusException</code>으로 감싸주면, 라이브러리 예외에도 원하는 상태 코드를 입혀줄 수 있다.</p>
<h4 id="💻-수정할-수-없는-외부-라이브러리-예외-애노테이션-부착-불가">💻 수정할 수 없는 외부 라이브러리 예외 (애노테이션 부착 불가)</h4>
<pre><code class="language-java">package com.external.auth;

public class InvalidTokenException extends RuntimeException { ...}</code></pre>
<h4 id="💻-서비스-로직에서의-활용">💻 서비스 로직에서의 활용</h4>
<pre><code class="language-java">public void validate(String token) {
    try {
    } catch (InvalidTokenException e) {
        // 라이브러리 예외를 잡아서 401(Unauthorized) 상태 코드를 부여한다.
        throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, &quot;토큰 정보가 유효하지 않습니다.&quot;, e);
    }
}</code></pre>
<br>

<h2 id="2-한-클래스에서-상황에-따라-다른-에러를-내보낼-때">2. 한 클래스에서 &#39;상황에 따라&#39; 다른 에러를 내보낼 때</h2>
<p><code>@ResponseStatus</code>는 예외 클래스 하나당 상태 코드가 1:1로 고정된다. 하지만 비즈니스 로직을 작성하다 보면 <strong>동일한 흐름 안에서 조건에 따라 404를 줄지, 403을 줄지 결정해야 하는 순간</strong>이 온다.</p>
<pre><code class="language-java">@GetMapping(&quot;/api/members/{id}&quot;)
public MemberResponse getMember(@PathVariable Long id) {
    Member member = repository.findById(id);

    // 상황 1: 찾는 회원이 없을 때는 404 (Not Found)
    if (member == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;존재하지 않는 회원입니다.&quot;);
    }

    // 상황 2: 회원은 있지만 정지된 상태라면 403 (Forbidden)
    if (member.isRestricted()) {
        throw new ResponseStatusException(HttpStatus.FORBIDDEN, &quot;접근 권한이 없는 사용자입니다.&quot;);
    }

    return new MemberResponse(member);
}</code></pre>
<br>

<h2 id="3-그래서-무엇을-선택해야-할까">3. 그래서 무엇을 선택해야 할까?</h2>
<ul>
<li><code>@ResponseStatus</code>: 내가 만든 예외 클래스에 <strong>고정된</strong> 상태 코드를 부여할 때 (정적)</li>
<li><code>ResponseStatusException</code>: 내가 못 고치는 예외를 처리하거나, 로직 안에서 <strong>상황별로</strong> 상태 코드를 바꿔야 할 때 (동적)</li>
</ul>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 <code>ResponseStatusException</code>을 쓰는 이유는 <strong>&#39;제어권&#39;</strong>과 <strong>&#39;유연함&#39;</strong> 때문이다. 애노테이션 방식이 주는 깔끔함도 좋지만, 내가 통제할 수 없는 예외를 다루거나 복잡한 비즈니스 조건에 대응해야 할 때는 이 방식이 훨씬 강력한 도구가 된다.</p>
<p>단순히 이론으로만 보던 내용들이 실제 코드에서 어떻게 동작하는지 정리하고 나니, 스프링이 왜 이 두 가지 방식을 모두 열어두었는지 명확해졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 요청 매핑과 에러 페이지의 관계]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%9A%94%EC%B2%AD-%EB%A7%A4%ED%95%91%EA%B3%BC-%EC%97%90%EB%9F%AC-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%9A%94%EC%B2%AD-%EB%A7%A4%ED%95%91%EA%B3%BC-%EC%97%90%EB%9F%AC-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Sun, 08 Feb 2026 07:39:38 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트가 제공하는 기본 예외 처리 기능을 사용하기 위해 <code>resources/templates/error</code> 디렉토리에 <code>404.html</code>과 <code>4xx.html</code>을 모두 만들어 두었다. 이 정도면 400번대 모든 에러에 철저히 대비했다고 생각했는데, 실습 중 <code>/error-401</code>에 접속하자 기대했던 <code>4xx.html</code> 화면 대신 <code>404.html</code>이 나타났다.</p>
<p>파일은 모두 준비되어 있는데, 왜 스프링은 하필 404를 선택했을까? 그 내부 동작의 우선순위를 정리해 보았다.</p>
<hr>
<h2 id="1-현상-준비된-4xxhtml-대신-404html이-출력됨">1. 현상: 준비된 4xx.html 대신 404.html이 출력됨</h2>
<p>분명 400번대 에러를 통합 처리하는 <code>4xx.html</code>이 있음에도 불고하고, 컨트롤러에 등록되지 않은 주소인 <code>/error-401</code>로 접속했을 때 브라우저는 <code>404.html</code>을 선택해서 보여주었다.</p>
<br>

<h2 id="2-원인-에러-코드-이전에-url-존재-여부가-우선이다">2. 원인: 에러 코드 이전에 &#39;URL 존재 여부&#39;가 우선이다</h2>
<p>이 현상의 핵심은 <strong>요청이 컨트롤러(핸들러)에 닿았느냐</strong>에 있다. 에러 페이지가 결정되는 과정은 단순히 에러 번호만 보는 것이 아니라, 다음과 같은 논리 구조를 가진다.</p>
<h4 id="✅-상황-1-애초에-없는-url인-경우-404-확정">✅ 상황 1. 애초에 없는 URL인 경우 (404 확정)</h4>
<ul>
<li>요청한 경로가 컨트롤러에 매핑되어 있지 않다면, 스프링은 &quot;이 요청을 처리할 수 있는 핸들러가 없다&quot;고 판단 한다.</li>
<li>이 시점에서는 내부 로직을 실행할 기회조차 없으므로 즉시 <strong>404(Not Found)</strong> 코드가 확정되고, 스프링은 <code>error</code> 디렉토리에서 가장 구체적인 <code>404.html</code>을 찾아 보여준다.</li>
</ul>
<h4 id="✅-상황-2-url은-맞는데-내부에서-에러가-난-경우-4xx-5xx-등">✅ 상황 2. URL은 맞는데 내부에서 에러가 난 경우 (4xx, 5xx 등)</h4>
<ul>
<li>일단 매핑된 컨트롤러에 진입해야 한다. 그 안에서 <code>response.sendError(401)</code> 처럼 명시적으로 에러 코드를 발생시켜야 비로소 우리가 의도한 401 응답이 확정된다.</li>
<li>이때 스프링은 <code>401.html</code>이 없으면 차선책인 <code>4xx.html</code>을 찾아 보여주게 된다.</li>
</ul>
<p>결국 내 실습에서 <code>404.html</code>이 뜬 이유는, <code>/error-401</code>이라는 주소 자체가 서버 입장에선 <strong>&quot;권한이 없는 페이지&quot;이기 이전에 &quot;없는 페이지&quot;</strong>였기 때문이다.</p>
<br>

<h2 id="3-결론-에러-페이지-호출의-메커니즘">3. 결론: 에러 페이지 호출의 메커니즘</h2>
<p>결론적으로 <code>resources/templates/error</code> 내의 파일들이 선택되는 기준은 다음과 같다.</p>
<ol>
<li><strong>존재하지 않는 URL</strong>: 매핑 실패 → <strong>404</strong> 발생 → <code>404.html</code> 호출</li>
<li><strong>존재하는 URL + 내부 오류</strong>: 매핑 성공 → 컨트롤러 내부 로직 실행 중 <strong>에러 코드(401, 500 등)</strong> 발생 → 해당 번호나 범위(<code>4xx</code>, <code>5xx</code>)의 html 호출</li>
</ol>
<p>이 원리는 400번대뿐만 아니라 모든 예외 처리 프로세스에 공통으로 적용된다. 특정 에러 화면을 테스트하고 싶다면, <strong>반드시 해당 URL이 매핑된 컨트롤러가 있어야 하며</strong>, 그 내부에서 에러 코드를 명시적으로 던져야 한다.</p>
<br>

<h2 id="💡-마치며">💡 마치며</h2>
<p>단순히 <strong>&quot;파일만 만들어 두면 에러 페이지가 알아서 매핑되겠지&quot;라는 추측에서 벗어나, 에러 코드 결정보다 &#39;URL 매칭&#39;이 선행된다는 스프링 MVC의 기본적인 우선순위를 정리해 보았다.</strong></p>
<p><code>404.html</code>과 <code>4xx.html</code>이 모두 준비되어 있더라도, 요청이 컨트롤러에 닿지 못하면 결국 404가 선택될 수밖에 없다.</p>
<p>이번 정리를 통해 에러 페이지가 호출되는 두 가지 갈래를 명확히 구분할 수 있게 되었고, 자동 설정 뒤에 숨은 핸들러 매핑의 논리를 다시 한 번 확인할 수 있었다.</p>
<hr>
<h3 id="📚-참고-자료">📚 참고 자료</h3>
<ul>
<li><strong>인프런 Q&amp;A</strong>, <em><a href="https://www.inflearn.com/courses/lecture?courseId=327260&amp;type=LECTURE&amp;unitId=83354&amp;tab=QnA&amp;category=questionDetail&amp;q=261539">401에러를 냈는데 404화면이 뜹니다.</a></em></li>
<li><strong>김영한</strong>, <em>스프링 MVC 2편 - 백엔드 웹 개발 활용 기술</em> (섹션 8. 예외 처리와 오류 페이지)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 필터와 인터셉터, 왜 예외 처리 메커니즘이 서로 다를까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%ED%95%84%ED%84%B0%EC%99%80-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0-%EC%99%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98%EC%9D%B4-%EC%84%9C%EB%A1%9C-%EB%8B%A4%EB%A5%BC%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%ED%95%84%ED%84%B0%EC%99%80-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0-%EC%99%9C-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98%EC%9D%B4-%EC%84%9C%EB%A1%9C-%EB%8B%A4%EB%A5%BC%EA%B9%8C</guid>
            <pubDate>Sun, 08 Feb 2026 06:07:08 GMT</pubDate>
            <description><![CDATA[<p>김영한 강사님의 스프링 MVC 2편 예외 처리 파트를 학습하다 보면, WAS가 오류 페이지 처리를 위해 내부적으로 재요청을 보낸다는 사실을 배운다. 이때 필터와 인터셉터 각각의 중복 호출 방지법을 실습하게 되는데, 여기서 한 가지 의문이 생겼다.</p>
<blockquote>
<p><em><strong>&quot;필터는 요청의 &#39;성격&#39;을 보고 판단하는데, 인터셉터는 왜 요청의 &#39;경로&#39;를 보고 판단해야 할까?&quot;</strong></em></p>
</blockquote>
<p>강의에서 배운 기술적 사실을 바탕으로, 두 도구가 예외라는 특수한 상황을 필터링하는 <strong>&#39;판단의 근거&#39;</strong>가 서로 다른지 그 설계 차이를 정리해봤다.</p>
<hr>
<h2 id="1-필터-요청의-성격type에-집중한다">1. 필터: 요청의 &quot;성격(Type)에 집중한다&quot;</h2>
<p>필터는 서블릿 표준 기술이다. 즉, 스프링보다 더 바깥쪽인 서블릿 컨테이너(WAS) 수준에서 동작한다. 그래서 WAS가 제공하는 요청의 성격, 즉 <strong><code>DispatchType</code></strong> 메타데이터를 직접 활용할 수 있다.</p>
<ul>
<li><strong>동작 원리 및 판단 근거</strong>: 요청이 들어올 때 이 요청이 일반적인 고객의 요청(<code>REQUEST</code>)인지, 에러 처리를 위한 서버 내부의 재요청(<code>ERROR</code>)인지 그 <strong>성격(Type)</strong>을 보고 실행 여부를 결정한다.</li>
<li><strong>학습 포인트(메커니즘)</strong>: 스프링 부트는 필터 등록 시 기본값을 <code>REQUEST</code>로 설정한다. 따라서 별도 설정을 하지 않으면 <code>ERROR</code> 타입의 요청은 필터 로직 근처에도 가지 못한다.</li>
<li><strong>결론</strong>: 필터는 서블릿 컨테이너가 제공하는 시스템 정보를 활용해 본인이 수행되어야 할 상황인지를 판단한다. 로직이 실행되기도 전, 시스템 설정에 의해 <strong>입구에서 컷(Cut)</strong> 당하는 방식을 취하는 것이다.</li>
</ul>
<br>

<h2 id="2-인터셉터-요청의-목적지path에-집중한다">2. 인터셉터: 요청의 &quot;목적지(Path)&quot;에 집중한다</h2>
<p>인터셉터는 스프링 MVC 내부 기술이다. 필터와 달리 서블릿 컨테이너가 관리하는 <code>DispatchType</code>이라는 메타데이터에 의존하지 않는다.</p>
<ul>
<li><strong>동작 원리 및 판단 근거</strong>: 인터셉터에게는 WAS가 보낸 재요청도 결국 DispatcherServlet을 거쳐가는 하나의 URL 요청일 뿐이다. 인터셉터는 요청의 성격을 파악하기보다 <strong>&quot;어디로 가는 요청인가(Path)&quot;</strong>에 더 집중한다.</li>
<li><strong>해결 방법(메커니즘)</strong>: 인터셉터는 이를 필터링할 &#39;시스템적 스위치&#39;가 없다. 그래서 에러 처리를 위한 전용 컨트롤러 경로(예: <code>/error-page/**</code>)를 <strong><code>excludePathPatterns</code></strong>에 직접 등록하여 차단한다.</li>
<li><strong>결론</strong>: 인터셉터는 시스템 정보보다 스프링이 관리하는 고도의 <strong>경로 매칭 로직</strong>을 활용해 호출 여부를 결정한다.</li>
</ul>
<br>

<h2 id="3-deep-dive-왜-설계-방식이-다를까">3. Deep Dive: 왜 설계 방식이 다를까?</h2>
<p>여기서 핵심적인 질문이 남는다. <em><strong>&quot;인터셉터도 DispatchType을 판단 근거로 삼으면 더 편하지 않았을까?&quot;</strong></em>
<a href="https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter">Baeldung의 아티클</a>과 스프링의 구조를 살펴보며 나름의 이유를 추론해 보았다.</p>
<ul>
<li><strong>필터의 관심사(Infrastructure/Low-level)</strong>: 웹 애플리케이션의 최전방에서 모든 요청에 공통 적용되는 인프라(보안, 인코딩)를 처리한다. 따라서 HTTP 요청의 시스템적 상태인 <code>DispatchType</code>을 활용하는 것이 가장 원자적이고 확실한 방법이다.</li>
<li><strong>인터셉터의 관심사(Application Context/High-level)</strong>: 컨트롤러(Handler)와 밀접하게 맞닿아 비즈니스 흐름을 제어한다. 인터셉터에게 중요한 것은 &quot;이 요청이 어떤 비즈니스 핸들러에게 도달하는가&quot;이다. 따라서 시스템 정보보다는 스프링이 고도화해둔 <strong>&#39;경로 패턴&#39;</strong>과 <strong>&#39;핸들러 정보&#39;</strong>에 집중하도록 설계된 것이다.</li>
</ul>
<p>결국, <strong>&quot;서 있는 위치가 다르면 판단의 근거(보이는 정도)도 다르다&quot;</strong>는 점이 두 도구의 예외 처리 메커니즘을 가른 근본적인 차이라고 생각한다.</p>
<br>

<h2 id="💡-마치며">💡 마치며</h2>
<p>단순히 오류 페이지 재요청 시 필터와 인터셉터가 중복 호출되는 문제를 설정으로 해결하는 데 그치지 않고, 왜 두 도구의 필터링 방식이 다른지 그 기저의 설계 의도를 정리해 보았다.</p>
<p>시스템 메타데이터(<code>DispatchType</code>)를 활용하는 필터와 애플리케이션 문맥(Path)을 활용하는 인터셉터의 차이는, 결국 각 도구가 서 있는 <strong>태생적 위치</strong>와 그에 따른 <strong>책임 영역</strong>이 어디인지 보여준다.</p>
<p>이번 정리를 통해 서블릿과 스프링 MVC가 요청을 바라보는 관점 차이를 조금 더 명확하게 구분할 수 있게 되었다.</p>
<hr>
<h3 id="📚-참고-자료">📚 참고 자료</h3>
<ul>
<li><strong>김영한</strong>, <em>스프링 MVC 2편 - 백엔드 웹 개발 활용 기술</em> (섹션 8. 예외 처리와 오류 페이지)</li>
<li><strong>Baeldung</strong>, <em><a href="https://www.baeldung.com/spring-mvc-handlerinterceptor-vs-filter">HandlerInterceptors vs. Filters in Spring MVC</a></em></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] @SessionAttribute는 왜 세션을 직접 만들지 못할까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-SessionAttribute%EB%8A%94-%EC%99%9C-%EC%84%B8%EC%85%98%EC%9D%84-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EC%A7%80-%EB%AA%BB%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-SessionAttribute%EB%8A%94-%EC%99%9C-%EC%84%B8%EC%85%98%EC%9D%84-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A4%EC%A7%80-%EB%AA%BB%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 27 Jan 2026 16:19:29 GMT</pubDate>
            <description><![CDATA[<p>스프링 MVC 강의를 듣던 중 <code>@SessionAttribute</code>에 대해 알게 됐다. 김영한 강사님은 이 기능을 소개하며 한 가지 특징을 명확히 짚어주셨다. <strong>&quot;이 어노테이션은 세션을 새로 생성하지 않는다.&quot;</strong></p>
<p>단순히 편의성만 생각했다면 생성 기능까지 있어도 좋았을 텐데, 왜 스프링은 이 기능을 굳이 빼놓았을까? 강의를 들으며 바로 꽂혔던 이 &#39;제약 사항&#39; 뒤에 숨겨진 이유와 설계 의도를 정리해 보려 한다.</p>
<hr>
<h2 id="1-기술적-팩트-세션-생성-불가능">1. 기술적 팩트: 세션 생성 불가능</h2>
<p><code>@SessionAttribute</code>는 세션이 존재하지 않으면 <code>null</code>을 반환하거나 예외를 던질 뿐, 결코 세션을 새로 생성하지 않는다.</p>
<p>보통 <code>request.getSession()</code>은 세션이 없으면 새로 만들어주는 옵션(<code>true</code>)이 기본이지만, <code>@SessionAttribute</code>는 내부적으로 <strong><code>request.getSession(false)</code></strong>를 고집한다. 즉, 이 어노테이션을 사용하는 순간 스프링은 &quot;이미 있는 세션에서 가져오되, 없으면 새로 만들지 마&quot;라는 스탠스를 취한다.</p>
<br>

<h2 id="2-왜-생성을-못-하게-막아뒀을까">2. 왜 생성을 못 하게 막아뒀을까?</h2>
<p>처음엔 &quot;생성 기능까지 있으면 더 편하지 않나?&quot;라는 생각이 들 수도 있지만, 조금 더 고민해보니 이건 스프링의 철저한 <strong>리소스 관리 전략</strong>이었다.</p>
<ul>
<li><strong>불필요한 세션 생성 방지</strong>: 로그인이 필요 없는 단순 조회 페이지인데, 실수로 세션 관련 파라미터를 넣었다고 해서 서버 메모리에 세션이 훅 생성되어 버리면 리소스 낭비가 심각해진다.</li>
<li><strong>권한과 책임의 분리</strong>: 세션을 생성하고 관리(로그인/로그아웃)하는 것은 시스템에서 매우 무거운 책임이다. 이런 권한을 단순히 파라미터를 주입받는 어노테이션에게까지 주지 않겠다는 의도로 읽힌다.</li>
</ul>
<br>

<h2 id="3-생성-불가능이-만든-조회-전용이라는-성격">3. &#39;생성 불가능&#39;이 만든 &#39;조회 전용&#39;이라는 성격</h2>
<p>세션을 직접 만들지 못한다는 기술적 제약은 자연스럽게 이 어노테이션을 <strong>&#39;조회 전용(Read-Only)&#39;</strong> 도구로 정의한다.</p>
<p>이미 생성된 데이터에 접근할 때만 사용 가능하기 때문에, 우리는 이 어노테이션을 보는 것만으로도 <strong>&quot;이 메서드는 세션 상태를 변경하지 않고 정보만 참고하겠구나&quot;</strong>라는 확신을 가질 수 있다. 기술적 제약이 오히려 코드의 의도를 명확하게 만들어주는 셈이다.</p>
<p>결국 수정이나 삭제 같은 강력한 권한은 <code>HttpSession</code>을 직접 다룰 때만 명시적으로 허용함으로써, 개발자의 실수로 세션 상태가 꼬이는 것을 방지한다.</p>
<br>

<h2 id="4-동작의-비밀-sessionattributemethodargumentresolver">4. 동작의 비밀: SessionAttributeMethodArgumentResolver</h2>
<p>스프링이 파라미터 한 줄로 이 모든 걸 처리해주는 비결은 <strong>ArgumentResolver</strong>에 있다.</p>
<p>내부를 들여다보면 <code>SessionAttributeMethodArguementResolver</code>가 우리가 수동으로 하던 <code>getAttribute()</code>와 <code>null</code> 체크, 그리고 타입 캐스팅까지 대신 수행한다. 우리가 반복 작업을 줄이기 위해 커스텀 어노테이션을 만들어 쓰듯, 스프링도 이 리졸버를 통해 <strong>&quot;안전하게 세션 값만 꺼내오는 로직&quot;</strong>을 표준화해서 제공하고 있다.</p>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>강의에서 배운 &quot;세션을 생성하지 않는다&quot;는 짧은 한 문장이 사실은 가독성 그 이상의 <strong>안전</strong>과 <strong>최적화</strong>를 담고 있다는 걸 알 수 있었다.</p>
<ol>
<li><strong>리소스 최적화</strong>: 불필요한 세션 생성 방지</li>
<li><strong>안전성</strong>: 조회 전용 접근으로 사이드 이펙트 차단</li>
<li><strong>추상화</strong>: 기술적 상세를 숨기고 도메인 데이터에 집중</li>
</ol>
<p>직접 어노테이션을 만들어 쓸 때도 마찬가지인 것 같다. 기능을 무조건 꽉꽉 채워 넣는 것보다, 때로는 <strong>정확한 제약</strong>을 걸어주는 것이 나중에 더 읽기 좋고 안전한 코드를 만드는 길이라는 것을 배웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 왜 바인딩에 실패한 필드는 Bean Validation을 수행하지 않을까?]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%99%9C-%EB%B0%94%EC%9D%B8%EB%94%A9%EC%97%90-%EC%8B%A4%ED%8C%A8%ED%95%9C-%ED%95%84%EB%93%9C%EB%8A%94-Bean-Validation%EC%9D%84-%EC%88%98%ED%96%89%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%99%9C-%EB%B0%94%EC%9D%B8%EB%94%A9%EC%97%90-%EC%8B%A4%ED%8C%A8%ED%95%9C-%ED%95%84%EB%93%9C%EB%8A%94-Bean-Validation%EC%9D%84-%EC%88%98%ED%96%89%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Sun, 18 Jan 2026 07:21:45 GMT</pubDate>
            <description><![CDATA[<p>Spring MVC로 개발을 하다 보면, 컨트롤러에서 <code>@Validated</code>나 <code>@valid</code>를 사용해 객체를 검증한다. 그런데 문득 이런 의문이 생긴다.</p>
<blockquote>
<p><em>&quot;타입이 안 맞아서 바인딩이 실패하면, 왜 뒤에 붙여놓은 <code>@Min</code>이나 <code>@NotBlank</code> 같은 검증은 실행되지 않을까?&quot;</em></p>
</blockquote>
<p>김영한 강사님의 강의에서도 <strong>&quot;타입 변환에 성공해서 바인딩에 성공한 필드여야 Bean Validation 적용이 의미가 있다&quot;</strong>는 설명이 나온다. 왜 그런지 기술적, 논리적 이유를 정리해 보았다.</p>
<hr>
<h2 id="1-spring의-데이터-처리-메커니즘-순서가-중요하다">1. Spring의 데이터 처리 메커니즘 (순서가 중요하다)</h2>
<p>Spring은 HTTP 요청 파라미터를 처리할 때 엄격한 <strong>단계(Phase)</strong>를 거친다.</p>
<ol>
<li><strong>데이터 바인딩(Data Binding)</strong>: 문자열로 들어온 요청값(Query Parameter, JSON 등)을 자바 객체의 필드 타입에 맞게 변환하여 주입한다.<ul>
<li>예: &quot;20&quot;(String) -&gt; 20(Integer)</li>
</ul>
</li>
<li><strong>Validator 호출(Bean Validation)</strong>: 바인딩이 완료된 객체의 필드에 대해 설정된 제약 조건(<code>@NotNull</code>, <code>@Max</code> 등)을 검사한다.</li>
</ol>
<p>여기서 핵심은 <strong>&quot;바인딩 실패 시 해당 필드는 값이 없다&quot;</strong>는 점이다.</p>
<br>

<h2 id="2-왜-바인딩-실패-시-검증을-생략할까">2. 왜 바인딩 실패 시 검증을 생략할까?</h2>
<h4 id="①-검증할-대상이-없다">① 검증할 &#39;대상&#39;이 없다</h4>
<p>만약 <code>Integer age</code> 필드에 &quot;abc&quot;라는 문자열이 들어왔다고 가정해 보자. 타입 변환에 실패했기 때문에 <code>age</code> 필드에는 정상적으로 담기지 못한다(보통 <code>null</code> 혹은 초기값). 값이 없는데 &quot;이 값이 10보다 큰가?&quot;(<code>@Min(10)</code>)를 묻는 검증 프로세스는 기술적으로 무의미하며, 제대로 된 결과를 낼 수 없다.</p>
<h4 id="②-잘못된-에러-메시지-방지-ux-관점">② 잘못된 에러 메시지 방지 (UX 관점)</h4>
<p>사용자가 나이 입력창에 &quot;abc&quot;를 입력했을 때, 시스템은 <strong>&quot;숫자를 입력해주세요&quot;</strong>라는 에러를 보여줘야 한다. 만약 바인딩 실패를 무시하고 검증을 강제한다면, 내부적으로 값이 <code>null</code>이라서 발생하는 <code>@NotNull</code> 오류까지 함께 발생하게 된다. 사용자는 &quot;나는 입력을 했는데 왜 값이 없다고 하지?&quot;라며 혼란을 느끼게 된다.</p>
<h4 id="③-우선순위의-논리">③ 우선순위의 논리</h4>
<ul>
<li><strong>바인딩 오류</strong>: &quot;데이터의 <strong>형식(Type)</strong> 자체가 틀림&quot; (더 근본적인 문제)</li>
<li><strong>검증 오류</strong>: &quot;형식은 맞지만 <strong>비즈니스 규칙</strong>에 어긋남&quot;</li>
</ul>
<p>더 큰 오류(타입 불일치)가 먼저 발견되었으므로, 세부적인 규칙 검사는 뒤로 미루는 것이 논리적으로 타당하다.</p>
<br>

<h2 id="3-spring의-처리-방식-요약">3. Spring의 처리 방식 요약</h2>
<ol>
<li>Spring은 바인딩을 시도한다.</li>
<li>타입 변환에 실패하면 <code>BindingResult</code>에 <code>FieldError</code>를 담는다.</li>
<li><strong>바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.</strong></li>
<li>바인딩에 성공한 필드만 모아서 Bean Validation을 수행한다.</li>
<li>모든 에러(바인딩 오류 + 검증 오류)를 합쳐서 컨트롤러에 넘겨준다.</li>
</ol>
<hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>결국 바인딩에 실패한 필드가 Bean Validation을 수행하지 않는 이유는, <strong>&quot;검사할 데이터가 제대로 준비되어야 검사도 할 수 있다&quot;</strong>는 아주 단순하고 명확한 원리 때문이다.
데이터 자체가 오염되었거나 존재하지 않는 상태에서의 검증은 논리적 모순을 낳기 때문에, &quot;바인딩 성공 후에만 검증이 의미가 있다&quot;는 말은 스프링이 제공하는 합리적인 배려라고 이해할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 오류 메시지 결정의 비밀(MessageCodesResolver vs MessageSource)]]></title>
            <link>https://velog.io/@yu-jin-song/Spring-%EC%98%A4%EB%A5%98-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B2%B0%EC%A0%95%EC%9D%98-%EB%B9%84%EB%B0%80MessageCodesResolver-vs-MessageSource</link>
            <guid>https://velog.io/@yu-jin-song/Spring-%EC%98%A4%EB%A5%98-%EB%A9%94%EC%8B%9C%EC%A7%80-%EA%B2%B0%EC%A0%95%EC%9D%98-%EB%B9%84%EB%B0%80MessageCodesResolver-vs-MessageSource</guid>
            <pubDate>Sat, 17 Jan 2026 11:28:55 GMT</pubDate>
            <description><![CDATA[<p>스프링에서 검증 오류가 발생했을 때, BindingResult에 담기는 수많은 에러 코드들은 도대체 어떻게 실제 화면에 출력되는 메시지로 변환되는 것일까?</p>
<p>단순히 &quot;에러가 나면 메시지를 출력한다&quot;는 과정 뒤에 숨겨진 <strong>전략(Strategy)</strong>과 <strong>조회(Lookup)</strong>의 분리 메커니즘을 정리해 보았다.</p>
<hr>
<h2 id="1-역할의-분리-누가-코드를-만들고-누가-찾을까">1. 역할의 분리: 누가 코드를 만들고, 누가 찾을까?</h2>
<p>가장 먼저 이해해야 할 핵심은 <strong>&quot;키(Key)를 생성하는 역할&quot;</strong>과 <strong>&quot;값(Value)을 찾는 역할&quot;</strong>이 철저히 분리되어 있다는 점이다.</p>
<table>
<thead>
<tr>
<th align="center">구분</th>
<th align="left">역할</th>
<th align="left">클래스/인터페이스</th>
<th align="left">특징</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>전략가(Generator)</strong></td>
<td align="left"><strong>메시지 코드 생성</strong></td>
<td align="left"><code>MessageCodesResolver</code></td>
<td align="left"><strong>검증 오류 발생 시</strong> 작동. 메시지 파일 유무와 상관없이 규칙에 따라 <strong>후보 키(Candidate Keys) 리스트</strong>를 생성함.</td>
</tr>
<tr>
<td align="center"><strong>조회자(Lookup)</strong></td>
<td align="left"><strong>메시지 내용 찾기</strong></td>
<td align="left"><code>MessageSource</code></td>
<td align="left">생성된 리스트를 들고 실제 <strong>메시지 설정 파일에서 값을 매칭</strong>함.</td>
</tr>
</tbody></table>
<br>

<h2 id="2-messagecodesresolver의-계층적-코드-생성-전략">2. MessageCodesResolver의 &quot;계층적 코드 생성 전략&quot;</h2>
<p>오류가 발생하면 리졸버는 가장 구체적인 코드부터 범용적인 코드까지 순서대로 생성한다. 예를 들어 <code>user</code> 객체의 <code>age</code> 필드(<code>int</code> 타입)에 잘못된 값이 들어왔다면, 리졸버는 내부 규칙에 따라 다음과 같은 배열을 만든다.</p>
<ol>
<li><code>코드.객체명.필드</code> (예: <code>typeMismatch.user.age</code>)</li>
<li><code>코드.필드명</code> (예: <code>typeMismatch.age</code>)</li>
<li><code>코드.필드타입</code> (예: <code>typeMismatch.java.lang.Integer</code>)</li>
<li><code>코드</code> (예: <code>typeMismatch</code>)</li>
</ol>
<p>이때 리졸버는 <code>messages.properties</code>을 확인하지 않는다. 오직 약속된 규칙대로 &quot;이 이름들로 한 번 찾아봐&quot;라며 <strong>조회용 키 배열</strong>만 만들어 에러 객체에 담아줄 뿐이다.</p>
<br>

<h2 id="3-messagesoruce의-순차-탐색과-basename">3. MessageSoruce의 &quot;순차 탐색&quot;과 Basename</h2>
<p>이제 <code>MessageSource</code>가 바통을 이어받는다. 리졸버가 만든 메시지 코드 배열을 들고 설정된 메시지 파일(Resource Bundle)을 위에서부터 훑는다.</p>
<h4 id="여기서-잠깐-어떤-파일을-뒤질까">여기서 잠깐! 어떤 파일을 뒤질까?</h4>
<p>스프링 부트의 기본 설정값(<code>basename</code>)은 <code>messages</code>이므로 보통 <code>src/main/resources/messages.properties</code>를 사용한다. 하지만 이는 설정이므로 변경 가능하다.</p>
<pre><code class="language-yaml"># application.properties 예시
spring.messages.basename=errors, common # errors.properties와 common.properties를 사용</code></pre>
<p>이처럼 <code>basename</code>을 다르게 설정했다면, <code>MessageSource</code>는 해당 설정 파일들을 대상으로 매칭을 시도한다.</p>
<ul>
<li><strong>1순위(<code>코드.객체명.필드</code>)</strong>가 파일에 있나? -&gt; 없으면 패스</li>
<li><strong>2순위(<code>코드.필드명</code>)</strong>가 파일에 있나? -&gt; 없으면 패스</li>
<li><strong>3순위(<code>코드.필드타입</code>)</strong>가 파일에 있나? -&gt; <strong>&quot;발견! 해당 메시지 반환&quot;</strong></li>
</ul>
<p>만약 우리가 설정 파일에 아래와 같이 적어두었다면, 3순위에서 매칭이 성공하여 해당 문구가 출력된다.</p>
<pre><code class="language-yaml"># errors.properties
typeMismatch.java.lang.Integer=숫자 형식으로 입력해야 합니다.</code></pre>
<br>

<h2 id="4-왜-코드--필드-타입-단계가-존재할까">4. 왜 &#39;코드 + 필드 타입&#39; 단계가 존재할까?</h2>
<p>이 계층 구조에서 <code>오류코드.필드타입</code> 단계가 존재하는 이유는 <strong>&quot;공통 처리의 유연성&quot;</strong> 때문이다.</p>
<ul>
<li><strong>개별 처리</strong>: <code>required.user.age</code>처럼 특정 필드에만 특화된 메시지를 제공할 수 있다.</li>
<li><strong>공통 처리</strong>: 모든 숫자 필드에 일일이 메시지를 적기 귀찮을 때, <code>typeMismatch.java.lang.Integer</code> 하나만 등록해두면 리졸버가 생성한 후보군 덕분에 모든 <code>int</code> 필드 오류가 이 메시지를 공유하게 된다.</li>
</ul>
<p>결국 이 구조는 <strong>&quot;구체적인 설정이 있으면 그걸 쓰고, 없으면 범용적인 설정을 Fallback으로 써라&quot;</strong>라는 스프링의 유연한 설계가 반영된 결과이다.</p>
<p><br><br></p>
<h2 id="5-만약-아무것도-정의하지-않았다면">5. 만약 아무것도 정의하지 않았다면?</h2>
<p>만약 개발자가 메시지 파일에 1~4순위 중 아무것도 적지 않았다면 어떻게 될까? <code>MessageSource</code>는 모든 검색에 실패하고, 최종적으로 <strong>스프링이 내부적으로 들고 있던 기본 메시지(영문)</strong>를 출력하게 된다.</p>
<pre><code>Failed to convert property value of type &#39;java.lang.String&#39; to required type &#39;Java.lang.Integer&#39;...</code></pre><p>우리가 메시지 파일에 키 값을 등록하는 이유는 바로 이 불친절한 기본 메시지를 친절한 한글 메시지로 <strong>재정의(Override)</strong>하기 위함이다.</p>
<br>

<h2 id="6-요약-및-결론">6. 요약 및 결론</h2>
<ol>
<li><code>MessageCodesResolver</code>: 검증 오류 발생 시 작동하며, 규칙에 따라 후보 키 리스트를 만든다.</li>
<li><code>MessageSource</code>: <code>basename</code>으로 설정한 메시지 파일을 뒤져서 후보 키 중 일치하는 첫 번째 메시지를 확정한다.</li>
<li><strong>타입 기반 코드(<code>typeMismatch.java.lang.Integer</code>)</strong>: 필드별 메시지가 없을 때, 특정 타입에 대해 공통 안내 문구를 처리하기 위한 효율적인 대안이다.</li>
</ol>
<blockquote>
<p><strong>한 줄 평</strong>: &quot;리졸버는 조회용 키 리스트를 설계하고, 소스는 설정된 파일에서 정답을 찾는다!&quot;</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IntelliJ] .properties 한글 깨짐 해결 (Encoding)]]></title>
            <link>https://velog.io/@yu-jin-song/IntelliJ-.properties-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%95%B4%EA%B2%B0-Encoding</link>
            <guid>https://velog.io/@yu-jin-song/IntelliJ-.properties-%ED%95%9C%EA%B8%80-%EA%B9%A8%EC%A7%90-%ED%95%B4%EA%B2%B0-Encoding</guid>
            <pubDate>Thu, 15 Jan 2026 15:01:24 GMT</pubDate>
            <description><![CDATA[<h1 id="❌-문제-상황">❌ 문제 상황</h1>
<p>Spring Boot 프로젝트에서 <code>messages.properties</code>를 활용해 메시지 관리 기능을 테스트하던 중, 실행 화면에서 한글이 정상적으로 출력되지 않고 <strong>&quot;?? ???&quot;</strong>와 같이 깨져서 보이는 현상이 발생했다.
<img src="https://velog.velcdn.com/images/yu-jin-song/post/3112deae-cdcc-471c-b11b-daa1fe2c2d34/image.png" alt=""></p>
<hr>
<h1 id="✅-해결-방법">✅ 해결 방법</h1>
<p>IntelliJ 설정에서 <code>.properties</code> 파일의 인코딩 방식을 <strong>UTF-8</strong>로 변경하고, 이를 자동으로 변환해주는 옵션을 활성화해야 한다.</p>
<ol>
<li><code>설정</code>(Windows: <code>Ctrl + Alt + S</code> / macOS: <code>Cmd + ,</code>) 창을 연다.</li>
<li><code>에디터</code> &gt; <code>파일 인코딩</code> 메뉴로 이동한다.</li>
<li>하단의 프로퍼티 파일 섹션을 다음과 같이 설정한다.<ul>
<li><code>프로퍼티 파일에 대한 디폴트 인코딩</code>: <code>UTF-8</code>로 변경</li>
<li><code>명확한 Native에서 ASCII로의 변환</code>: 체크박스 선택
<img src="https://velog.velcdn.com/images/yu-jin-song/post/507d021e-3fbf-45de-9742-438dce0e1073/image.png" alt=""></li>
</ul>
</li>
<li><code>적용</code> 및 <code>확인</code>을 눌러 설정을 저장한다.
<img src="https://velog.velcdn.com/images/yu-jin-song/post/f6366640-d6a3-4ead-8120-cf9e8031d3b8/image.png" alt=""></li>
</ol>
<hr>
<h1 id="💡-주의사항">💡 주의사항</h1>
<p>설정을 바꾸기 전에 이미 작성된 한글은 인코딩 변경 후에도 깨져 보일 수 있다.
<img src="https://velog.velcdn.com/images/yu-jin-song/post/5dd53b7c-f945-4183-aad6-03cae1dc0ea8/image.png" alt=""></p>
<p>이 경우, 해당 텍스트를 지우고 다시 한글로 입력한 뒤 저장하면 정상적으로 출력된다.
<img src="https://velog.velcdn.com/images/yu-jin-song/post/25950c68-bfe0-4082-8e0b-d0026381bd07/image.png" alt="">
<img src="https://velog.velcdn.com/images/yu-jin-song/post/5e4c9deb-2a23-44a7-aca3-c35f375b595c/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>