<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>차가운 개발노트</title>
    <link>https://chanyoungcoding.tistory.com/</link>
    <description>개발 공부를 하면서 개발자 분들에게 좀 더 쉽게 설명해주고 싶은 기술들을 정리한 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 01:05:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Chan Dev</managingEditor>
    <image>
      <title>차가운 개발노트</title>
      <url>https://tistory1.daumcdn.net/tistory/6743799/attach/453db31a5cde40f59634520de5cdf04d</url>
      <link>https://chanyoungcoding.tistory.com</link>
    </image>
    <item>
      <title>[ 주니어 탈출기 ] 고수준 컴포넌트와 저수준 컴포넌트</title>
      <link>https://chanyoungcoding.tistory.com/146</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 짜다가 아래와 같은 경험을 겪었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;외부 카카오톡 알림톡 API 가 버전업 되었는데, 서비스 레이어 코드까찌 싹 다 고쳐야 하네&quot;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;데이터베이스를 MySQL 에서 MongoDB 로 바꾸자고 하는데, 비즈니스 로직 전체를 뜯어야 하는구나&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저와 같은 고통을 겪고 있는 개발자분들은, 100% 고수준 컴포넌트와 저수준 컴포넌트의 관계가 잘못 설계되었기 때문일 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰는 목적은 두 개념을 완벽하게 마스터하고, 단단한 코드를 짜기 위해 정리할 겸 같은 고민을 겪고 있는 개발자들에게 조금이나마 도움이 될 것 같아 정리하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본론으로 들어가기 전에 '컴포넌트' 라는 단어부터 짚고 넘어가자면, 컴포넌트라고 하면 프론트엔드의 React 컴포넌트가 먼저 떠오를 수 있지만, 백엔드나 시스템 아키텍처에서는 더 넓은 의미로 쓰입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해 컴포넌트란 &quot;특징적인 기능을 수행하기 위해 독립적으로 구성된 소프트웨어 모듈&quot; 을 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 세계에서는 하나의 독립된 기능을 담당하는 객체(Bean)나 패키지 단위를 컴포넌트라고 이해하시면 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고수준 컴포넌트 vs 저수준 컴포넌트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 컴포넌트를 나누는 가장 중요한 기준은 바로 &quot;비즈니스의 핵심 정책을 담고 있는가, 아니면 구체적인 세부 기술을 담고 있는가?&quot; 입니다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고수준 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템의 핵심 정책과 비즈니스 로직을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 서비스가 무엇(What) 을 해야 하는가?&quot; 를 담당하며 비즈니스 모델이 바뀌지 않는 한 자주 바뀌지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 OrderServcice, MemberStatus, DiscountPolicy 등이 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저수준 컴포넌트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책을 실현하기 위한 구체적인 세부사항을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그 기능을 어떻게(How) 기술적으로 구현할 것인가?&quot; 를 담당하며 기술 트랜드나 외부 인프라에 따라 자주 바뀌게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 SmsSender, KakaoSender, MySqlRepository 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주니어들이 가장 많이 만드는 파멸의 아키텍처&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 세계에서 가장 이상적인 구조는 &quot;고수준은 저수준의 변경에 영향을 받지 않아야 한다&quot; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인턴이 엑셀을 쓰다가 구글 스프레드시트로 도구를 바꿨다고 해서, CEO 의 &quot;10% 적립해 준다&quot; 는 정책이 바뀌거나 CEO 가 일을 못 하게 되면 안되는 것처럼 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 제가 경험한 많은 주니어 개발자분들께서는 코드를 고수준 컴포넌트가 저수준 컴포넌트에 의존하는 형태로 짜는 모습을 많이 목격했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안좋은 예시: (고수준 &amp;rarr; 저수준)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문이 완료되면 '카카오톡 알림톡' 을 보내는 시스템 예시입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779086142455&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 저수준 컴포넌트: 카카오톡 API를 사용해 메시지를 보내는 구체적인 기술 클래스
public class KakaoTalkSender {
    public void sendKakao(String phone, String message) {
        // 카카오톡 외부 API 연동 라이브러리 코드...
        System.out.println(&quot;카카오톡 발송: &quot; + message);
    }
}

// 2. 고수준 컴포넌트: 주문이라는 핵심 비즈니스 정책을 다루는 클래스
public class OrderService {
    //   문제 발생: 고수준 컴포넌트가 구체적인 저수준 컴포넌트를 직접 참조(new)함
    private final KakaoTalkSender kakaoSender = new KakaoTalkSender();

    public void completeOrder(Long orderId) {
        // 1. 주문 완료 처리 로직 (비즈니스 정책)
        System.out.println(orderId + &quot;번 주문이 완료되었습니다.&quot;);

        // 2. 알림 발송 (세부 구현 기술에 의존)
        kakaoSender.sendKakao(&quot;010-1234-5678&quot;, &quot;주문이 완료되었습니다!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이 구조가 시한폭탄인 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 달 뒤, 경영진이 &quot;카카오톡 비용이 너무 비싸니, 비용이 저렴한 일반 SMS 문자로 알림 시스템을 바꾸세요!&quot; 라고 요구하면 개발자는 SmsSender 라는 새로운 저수준 클래스를 만든 뒤, 핵심 비즈니스 로직이 들어있는 OrderService 코드를 열어서 직접 뜯어 고쳐야 합니다. 아래와 같이 말입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779086226381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderService {
    // private final KakaoTalkSender kakaoSender = new KakaoTalkSender(); // 삭제
    private final SmsSender smsSender = new SmsSender(); // 추가 (결국 코드 수정 유발)

    public void completeOrder(Long orderId) {
        System.out.println(orderId + &quot;번 주문이 완료되었습니다.&quot;);
        // kakaoSender.sendKakao(...);
        smsSender.sendSms(...); // 비즈니스 로직과 상관없는 기술 변경 때문에 코드가 바뀜!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로 &quot;저수준 컴포넌트(알림 기술) 의 변경이 고수준 컴포넌트(주문 정책) 를 흔드는 아키텍처&quot; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 커지면 이런 기술적 변경이 하루에도 수십 번씩 일어나는데, 그때마다 핵심 비즈니스 코드가 출렁거린다면 버그가 걷잡을 수 없이 터지게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결방법: DIP(의존성 역전 원칙)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;고수준 컴포넌트는 저수준 컴포넌트에 의존하면 안 된다. 양쪽 모두 '추상화'에 의존해야 한다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 추상화를 구현하는 가장 대표적인 도구는 인터페이스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고수준 컴포넌트와 저수준 컴포넌트 사이에 인터페이스라는 장막을 쳐서 의존성의 방향을 뒤틀어버리는것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779149465836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 고수준 영역에 위치한 '추상화 인터페이스'
// &quot;우리는 알림을 보낼 건데, 구체적으로 어떻게 보낼지는 나중에 구현해라&quot;
public interface NotificationSender {
    void send(String target, String message);
}

// 고수준 컴포넌트: 이제 오직 인터페이스(추상화)에만 의존합니다.
public class OrderService {
    private final NotificationSender notificationSender;

    // 의존성 주입(DI)을 받도록 설계
    public OrderService(NotificationSender notificationSender) {
        this.notificationSender = notificationSender;
    }

    public void completeOrder(Long orderId) {
        System.out.println(orderId + &quot;번 주문이 완료되었습니다.&quot;);
        
        // 카카오톡인지 SMS인지 이 코드는 전혀 알 바가 아님!
        notificationSender.send(&quot;010-1234-5678&quot;, &quot;주문 완료!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 저수준 컴포넌트들은 이 고수준의 인터페이스를 구현하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779149518274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 저수준 컴포넌트들: 고수준이 정의한 규칙을 따르는 플러그인들
public class KakaoTalkSender implements NotificationSender {
    @Override
    public void send(String target, String message) {
        System.out.println(&quot;카카오톡으로 발송: &quot; + message);
    }
}

public class SmsSender implements NotificationSender {
    @Override
    public void send(String target, String message) {
        System.out.println(&quot;일반 문자로 발송: &quot; + message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 의존성 '역전' 일까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 개발 방식에서는 고수준 컴포넌트에서 저수준 컴포넌트로 화살표가 흘러갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 DIP 를 적용하면 저수준 컴포넌트에서 고수준 인터페이스로, 마지막은 고수준 컴포넌트 형태로 화살표가 모입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 저수준 컴포넌트가 고수준 컴포넌트가 만들어 놓은 규칙에 대고 의존하게 되므로 의존성의 방향이 거꾸로 뒤집혔다고 해서 역전이라고 부르는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 알림 수단을 카카오톡에서 SMS 로 바꾸고 싶다면, OrderService 코드는 단 한 줄도 건드리지 않고 그저 조립기인 Spring Context 에서 SmsSender 를 갈아 끼워 주기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 매일 쓰는 스프링 프레임워크의 핵심 기능은 바로 IoC(제어의 역전) 와 DI(의존성 주입) 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 주니어 개발자들이 면접을 위해 &quot;DI 는 객체를 스프링이 대신 생성해서 주입해 주는 것입니다.&quot; 라고 외우지만, &quot;왜?&quot; 대신 해주는지는 체감하지 못합니다. 스프링이 대신 객체를 생성하고 조립해 주는 진짜 이유는 바로 고수준 컴포넌트와 저수준 컴포넌트를 완벽하게 분리하여 엔지니어가 DIP 를 쉽게 구현할 수 있도록 돕기 위해서입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 컨테이너가 없다면 우리는 어딘가에서 아래와 같이 구체적인 저수준 객체를 직접 new 해서 조립해야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779149787131&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 스프링이 없다면 직접 해야 하는 조립 과정
NotificationSender sender = new KakaoTalkSender(); 
OrderService orderService = new OrderService(sender);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 스프링에서는 @Component 나 @Configuration 을 통해 스프링에게 조립 책임을 떠넘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 우리의 고수준 비즈니스 로직은 외부의 지저분한 저수준 기술(DB, 파일 시스템등) 로부터 완전히 독립하여 청정 구역을 유지할 수 있게 되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Java</category>
      <category>고수준 컴포넌트</category>
      <category>저수준 컴포넌트</category>
      <category>주니어 탈출기</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/146</guid>
      <comments>https://chanyoungcoding.tistory.com/146#entry146comment</comments>
      <pubDate>Tue, 2 Jun 2026 15:36:55 +0900</pubDate>
    </item>
    <item>
      <title>실시간 대시보드 성능 장애 해결기</title>
      <link>https://chanyoungcoding.tistory.com/145</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 코드의 문제점과 원인 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 자체 개발하고 배포하고 있는 기존 시스템은 20초마다 차트를 실시간으로 갱신하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시간이 지날수록 대시보드가 느려지고 서버가 버벅이는 고질적인 문제가 발생하면서 이러한 문제점을 발견하고 고치는 부분을 기록한 블로그이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;문제의 코드&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779948872640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data.setsTime(data.getsTime().substring(0,8) + &quot;0800&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 20초마다 &quot;최근 1분 전부터 현재까지&quot; 의 신규 데이터만 요청을 하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 백에서 이 시작 시간을 강제로 아침 08:00 으로 고정하고 있는 코드를 발견하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 최근의 1분전부터 현재까지의 데이터만 필요하지만 오후 17:00 에 차트를 갱신하게 되면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터를 재사용하지 않고 9시간 치 누적 데이터를 20초마다 매번 통재로 다시 조회하는 문제점이 발생했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해당 코드를 삭제하고 불필요한 로직들도 리팩토링 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DgtxQ/dJMcabdfcRz/uLQTYmwZrm8mn2rHOmVeUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DgtxQ/dJMcabdfcRz/uLQTYmwZrm8mn2rHOmVeUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DgtxQ/dJMcabdfcRz/uLQTYmwZrm8mn2rHOmVeUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDgtxQ%2FdJMcabdfcRz%2FuLQTYmwZrm8mn2rHOmVeUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1386&quot; height=&quot;347&quot; data-origin-width=&quot;1386&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 쓰지 않는 데이터까지 움켜지는 코드를 발견하게 되었다.&lt;/p&gt;
&lt;pre id=&quot;code_1779949082584&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT ... FROM (SELECT A.PRCS, A.BRCD, A.BRNM, A.TRM ... FROM TB_DQS A ...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드를 분석해보니 최종적으로 화면에 그릴 때 필요한 데이터는 단지 시간, 상태, 건수 뿐이였지만 내부 서브쿼리에서 부점명등 무겁고 사용하지 않는 컬럼들까지 수만 건씩 전부 메모리에 로드하고 있는 상황을 발견하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 DB 의 메모리 낭비와 Disk I/O 부하가 극심해져, 쿼리 자체에 실행 속도가 뚝 떨어지는 현상을 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 내부 코드를 모두 블로그에 정리할 수는 없지만 해당 코드를 삭제하고 과거 변경 데이터가 있는 경우 1분전 데이터만 불러오는 로직과 SQL 문을 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjo3ed/dJMb99T4rnC/zfv1jTKc7Xw7R7eesEemO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjo3ed/dJMb99T4rnC/zfv1jTKc7Xw7R7eesEemO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjo3ed/dJMb99T4rnC/zfv1jTKc7Xw7R7eesEemO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbjo3ed%2FdJMb99T4rnC%2Fzfv1jTKc7Xw7R7eesEemO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1203&quot; height=&quot;398&quot; data-origin-width=&quot;1203&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20초마다 수만 건씩 풀 스캔하던 무식한 방식에서 딱 최근 1분간 발생한 수십 건의 데이터만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콕 집어 가져오는 정상적인 실시간 아키텍처로 변경하였다. 결과적으로 DB 쿼리 수행 시간이 초 단위에서 0.012초 대로 낮아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 서버가 과도한 JSON 객체를 생성하지 않아도 되고, 브라우저 역시 대량의 중복 데이터를 받아 재연산할 필요가 없어져 하루 종일 대시보드를 켜두어도 메모리 누수 없이 작동할 수 있게 만들엇으며 불필요한 SQL 서브쿼리를 제거함으로써 DB 내부 메모리 낭비를 막고, 대시보드 때문에 다른 메인 비즈니스 쿼리들이 대기하는 병목 현상을 방지하였다.&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Java</category>
      <category>Spring 성능 개선</category>
      <category>Spring 성능 장애 해결</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/145</guid>
      <comments>https://chanyoungcoding.tistory.com/145#entry145comment</comments>
      <pubDate>Thu, 28 May 2026 15:35:01 +0900</pubDate>
    </item>
    <item>
      <title>[ 주니어 탈출기 ] &amp;quot;인터페이스랑 뭐가 다를까?&amp;quot; - abstract 의 활용법</title>
      <link>https://chanyoungcoding.tistory.com/144</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문득 퇴근 후 개발 공부를 하다가 'interface 가 있는데 abstract 가 왜 필요할까?' 라는 궁금증에 정리해보았습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;문법은 알겠고, abstract 붙이면 상속받아서 구현해야 하고, new 로 객체 생성을 못하고... 그러면 인터페이스랑 똑같네?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같은 의문이 드셨으면 제가 이번에 정리한 글을 보시면 왜 abstract 를 사용하는지 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 통해 abstract 의 문법적 기초부터, 실무에서 인터페이스 대신 추상 클래스를 선택해야 하는 경우를 정리 해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추상의 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미술에서 추상화(Abstract Art) 는 피카소의 그림처럼 형태를 알아보기 힘들게 핵심만 남겨두는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어에서도 똑같습니다. 추상화란 &quot;지엽적이고 구체적인 세부 사항은 숨기고, 핵심적인 개념과 공통적인 특징만 뽑아내는 것&quot; 을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서 abstract 키워드는 &quot;이 클래스는 아직 미완성이니, 직접 객체로 만들지 말고 나를 상속받아서 구체적으로 완성해라&quot; 라는 일종의 가이드라인이자 강제제약입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 사용할까?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;그냥 일반 클래스로 만들고 자식들이 알아서 오버라이딩하게 두면 안되나요? 왜 굳이 미완성으로 만들어서 제약을 거나요?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스를 사용하는 진짜 이유는 크게 두가지입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실수를 방지하는 강제성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 일반 클래스에 껍데기만 있는 메서드를 만들어두면, 동료 개발자가 이를 상속받았을 때 실수로 메서드를 구현하지 않고 그냥 넘어갈 수 있습니다. 이 경우 런타임에 아무일도 일어나지 않거나 엉뚱한 버그가 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 abstract 를 붙여 추상 메서드로 선언하면, 자식 클래스에서 이를 구현하지 않았을 때 컴파일러가 에러를 뿜어냅니다. 시스템의 안전성을 극대화 시켜 주는 것입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중복 코드 제거와 공통 규격 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 클래스를 만들다 보면 반드시 공통으로 들어가는 중복 코드가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스는 &quot;공통적인 실행 흐름과 데이터(상태) 는 내가 다 가지고 있을 테니, 너희 자식들은 진짜 달라지는 핵심 알맹이만 구현해&quot; 할 때 최고의 효율을 발휘하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 코드(템플릿 메서드 패턴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 웹 개발에서 가장 많이 쓰이는 구조인 '백화점 상품 결제 시스템' 을 예시로 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 시스템에는 신용카드 결제(CardPayment) 와 네이버페이 결제(NaverPayPayment) 두 가지 방식이 있다고 가정해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추상 클래스를 활용한 결제 시스템 설계&lt;/p&gt;
&lt;pre id=&quot;code_1779175627061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결제 틀 정의
public abstract class PaymentProcessor {

    // 공통 상태를 가질 수 있음 (인터페이스와의 큰 차이점!)
    protected String companyName = &quot;우리 백화점&quot;;

    // 템플릿 메서드: 결제의 전체적인 흐름을 제어함 (final로 흐름 변조 방지)
    public final void processPayment(int amount) {
        validate(amount);      
        executePayment(amount);
        logResult();        
    }

    // 공통 메서드
    private void validate(int amount) {
        if (amount &amp;lt;= 0) throw new IllegalArgumentException(&quot;금액이 올바르지 않습니다.&quot;);
        System.out.println(&quot;[&quot; + companyName + &quot;] 결제 검증 완료: &quot; + amount + &quot;원&quot;);
    }

    // 추상 메서드
    protected abstract void executePayment(int amount);

    // 공통 메서드
    private void logResult() {
        System.out.println(&quot;결제 완료 로그를 DB에 저장하고 알림을 보냅니다.\n&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이 미완성 설계도를 상속받아 구체적인 결제 수단들을 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779175770011&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 신용카드 결제 구현체
public class CardPayment extends PaymentProcessor {
    @Override
    protected void executePayment(int amount) {
        // 신용카드 API 연동 핵심 로직만 작성
        System.out.println(&quot;[신용카드] PG사 요청 전송 -&amp;gt; &quot; + amount + &quot;원 승인 완료&quot;);
    }
}

// 3. 네이버페이 결제 구현체
public class NaverPayPayment extends PaymentProcessor {
    @Override
    protected void executePayment(int amount) {
        // 네이버페이 API 연동 핵심 로직만 작성
        System.out.println(&quot;[네이버페이] 인증 토큰 확인 -&amp;gt; &quot; + amount + &quot;원 포인트 차감 완료&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1779175789415&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // PaymentProcessor processor = new PaymentProcessor(); // 에러! 추상 클래스는 new 불가

        PaymentProcessor card = new CardPayment();
        card.processPayment(50000);

        PaymentProcessor naver = new NaverPayPayment();
        naver.processPayment(30000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 새로운 결제 수단인 '카카오페이 결제' 가 추가되어도, 개발자는 전체흐름을 새로 짤 필요가 전혀 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 PaymentProcessor 를 상속받아 executePayment 메서드 안에 카카오페이 API 호출 코드 딱 한 단락만 채워 넣으면 끝납니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 설계 방식을 디자인 패턴에서는 템플릿 메서드 패턴이라고도 부릅니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;interface 에 default 가 있는데?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8 부터 인터페이스에도 default 메서드가 들어가서 위 코드처럼 바디( {} ) 가 있는 메서드를 넣을 수 있으니 abstract 는 필요없는거 아니냐? 라는 의문이 들 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말씀드리자면, 둘은 태생적인 목적과 존재 이유가 완전히 다릅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abstract 는 상속(Is-A) 관계를 통한 확장 및 코드 재사용이 용이하며 가장 큰 특징인 인스턴스 변수를 가질수가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 자바 특성상 단일 상속만 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 인터페이스는 행위(Can-Do) 에 대한 규격과 계약만을 정의하며 변수를 가질 수가 없습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;interface 는 public static final 의 상수만 정의할 수 있습니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;상태&quot; 를 가질 수 있는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 8, 9 를 거치며 인터페이스가 아무리 발전했어도 인터페이스는 '인스턴스 변수' 를 가질 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 결제 예시 코드에서 protected String companyName; 같은 상태값을 저장하고 자식들이 이 필드를 공유하거나 변경하게 하려면 오직 추상 클래스만 가능합니다. 인터페이스는 상태를 가질 수 없으므로 상태를 제어하는 공통 로직을 온전히 담기가 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소스코드 뜯어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바 소스코드를 뜯어보면 Collection 인터페이스가 있고, 그 아래에 공통 로직을 모아둔 AbstractCollection, AbstractList 추상 클래스가 있으며, 이를 상속받아 우리가 매일 쓰는 ArrayList 가 구현되어 있습니다. 자바 언어의 창시자들도 인터페이스와 추상 클래스를 융합해서 최고의 효율을 내도록 설계해 둔 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;abstract 키워드는 단순히 '생성 못하는 클래스' 가 아닙니다. 동료 개발자들과 협업에서 &quot;이 프로세스 흐름은 절대 건드리지 말고, 너희는 요 알맹이만 안전하게 구현해줘&quot; 라고 코드로 대화하는 강력한 소통 도구이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>abstract</category>
      <category>Java</category>
      <category>추상 클래스</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/144</guid>
      <comments>https://chanyoungcoding.tistory.com/144#entry144comment</comments>
      <pubDate>Thu, 28 May 2026 10:19:15 +0900</pubDate>
    </item>
    <item>
      <title>1급 컬렉션</title>
      <link>https://chanyoungcoding.tistory.com/143</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어를 만들 때 우리는 필연적으로 '여러 개의 데이터' 를 다루게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 아무생각 없이 아래와 같은 코드를 짤 수가 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779178124439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;OrderItem&amp;gt; orderItems = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Car&amp;gt; cars = new ArrayList&amp;lt;&amp;gt;();
List&amp;lt;Integer&amp;gt; lottoNumbers = new ArrayList&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 컬렉션(List, Map, Set 등) 을 아무런 보호 장치 없이 서비스 레이어나 여기저기에 날것으로 노출하는 순간, 코드는 서서히 스파게티로 변하기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1급 컬렉션이란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;소트웍스 앤솔러지라는 책의 '객체지향 생활 체조 원칙' 에서 처음 제안하였습니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;컬렉션을 포함한 클래스는 반드시 다른 멤버 변수를 가지지 않아야 한다&quot;&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, List 나 Map 같은 컬렉션을 하나의 클래스로 꽁꽁 감싸고, 그 클래스 안에는 그 컬렉션 '단 하나' 만 존재하게 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한눈에 보기&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일반적인 방식&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order 클래스 안에 Long id, String orderNo, List&amp;lt;OrderItem&amp;gt; items 등이 다 섞여 있음.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1급 컬렉션을 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;List&amp;lt;OrderItem&amp;gt; 만을 전담으로 관리하는 OrderItems 라는 별도의 클래스를 만듦.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Order 클래스는 이 OrderItems 객체를 멤버 변수로 가짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 써야할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 List&amp;lt;OrderItem&amp;gt; 을 바로 쓰면 편한데, 굳이 클래스로 한 번 더 감싸서 코드를 복잡하게 맏드는 이유가 뭘까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스 로직과 검증의 응집도 극대화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 장바구니(Cart) 기능을 만드는데, 다음과 같은 기획 조건이 있다고 가정해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건1: 장바구니에는 최대 5개의 상품만 담을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건2: 장바구니에 담긴 상품들의 총금액을 계산할 수 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1급 컬렉션이 아닌 경우&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건들을 검증하고 계산하는 로직이 온통 CartService 같은 외부 서비스 레이어에 흩어지게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779178471332&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CartService {
    // 사용자의 장바구니 상품 목록이 날것의 List로 흘러다님
    public void addCartItem(List&amp;lt;CartItem&amp;gt; currentCart, CartItem newItem) {
        // 비즈니스 검증 로직이 서비스에 노출됨
        if (currentCart.size() &amp;gt;= 5) {
            throw new IllegalArgumentException(&quot;장바구니에는 최대 5개까지만 담을 수 있습니다.&quot;);
        }
        currentCart.add(newItem);
    }

    public int calculateTotalPrice(List&amp;lt;CartItem&amp;gt; currentCart) {
        // 총금액 계산 로직이 서비스에 매번 구현됨 (코드 중복 위험)
        return currentCart.stream()
                .mapToInt(CartItem::getPrice)
                .sum();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 서비스(AdminCartService) 에서 상품을 추가할 때 이 if 문 검증을 빼먹는다면 시스템에 구멍이 뚤리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터와 로직이 따로 노는 전형적인 절차지향 코드가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1급 컬렉션을 적용하는 경우&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1779178585887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Cart {
    // 외부에서 접근 불가능한 단 하나의 멤버 변수
    private final List&amp;lt;CartItem&amp;gt; items;

    public Cart(List&amp;lt;CartItem&amp;gt; items) {
        // 생성 시점에 스스로 검증 -&amp;gt; 5개가 넘는 장바구니는 아예 세상에 태어날 수 없음!
        if (items.size() &amp;gt; 5) {
            throw new IllegalArgumentException(&quot;장바구니에는 최대 5개까지만 담을 수 있습니다.&quot;);
        }
        this.items = new ArrayList&amp;lt;&amp;gt;(items);
    }

    // 장바구니에 상품을 추가하는 비즈니스 행위도 스스로 제어
    public void add(CartItem newItem) {
        if (items.size() &amp;gt;= 5) {
            throw new IllegalArgumentException(&quot;장바구니에는 최대 5개까지만 담을 수 있습니다.&quot;);
        }
        items.add(newItem);
    }

    // 총금액 계산 로직도 내부에서 처리 (응집도 최고)
    public int calculateTotalPrice() {
        return items.stream()
                .mapToInt(CartItem::getPrice)
                .sum();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 CartService 는 복잡한 조건문이나 계산식을 알 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그저 cart.add(newItem) 이나 cart.calulateTotalPrice() 를 호출하기만 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1급 컬렉션을 구현할 때의 자바 실무 팁&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1급 컬렉션을 만들 때 주니어들이 가장 많이 하는 실수가 바로 '얕은 복사' 로 인한 데이터 오염입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779178949473&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RacingCars {
    private final List&amp;lt;Car&amp;gt; cars;

    public RacingCars(List&amp;lt;Car&amp;gt; cars) {
        this.cars = cars; // 위험! 외부에서 전달된 List의 주소값을 그대로 참조함
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 생성자를 위와 같이 짜면, 외부에서 다음과 같은 크리티컬한 버그를 유발할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779178971892&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Car&amp;gt; originalList = new ArrayList&amp;lt;&amp;gt;();
originalList.add(new Car(&quot;포르쉐&quot;));

RacingCars racingCars = new RacingCars(originalList); // 1급 컬렉션 생성

// 외부에서 원래 리스트를 조작했는데, 1급 컬렉션 내부 데이터까지 바뀜!
originalList.add(new Car(&quot;모닝&quot;));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 반드시 생성자 단계에서 새로운 리스트로 복사하여 연결 고리를 끊어내야 안전합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779179017488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RacingCars {
    private final List&amp;lt;Car&amp;gt; cars;

    public RacingCars(List&amp;lt;Car&amp;gt; cars) {
        // 새로운 ArrayList로 감싸서 외부 리스트와 주소값을 끊어냄
        this.cars = new ArrayList&amp;lt;&amp;gt;(cars); 
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>1급 컬렉션</category>
      <category>Collection</category>
      <category>java collection</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/143</guid>
      <comments>https://chanyoungcoding.tistory.com/143#entry143comment</comments>
      <pubDate>Tue, 26 May 2026 14:59:19 +0900</pubDate>
    </item>
    <item>
      <title>[ 주니어 탈출기 ] 도메인(Domain) 개념 완벽 정리</title>
      <link>https://chanyoungcoding.tistory.com/142</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;color: #f3c000;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다른 개발자분께서 도메인이 무엇이냐 라고 물어볼 때 정확히&lt;br /&gt;답변을 하지 못해 공부하면서 다른 개발자분들에게도&amp;nbsp;&lt;br /&gt;&lt;/span&gt;알려주기 위해 정리하였습니다.&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 개발자들과 대화하거나 아키텍처 관련 책을 읽을 때 &quot;도메인&quot; 이라는 단어를 자주 들엇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;도메인 레이어를 분리해야 합니다.&quot;, &quot;도메인 모델에 비즈니스 로직을 모으세요.&quot;, &quot;그건 도메인이 아니라 인프라 영역입니다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 '비즈니스 로직이 들어가는 곳이네..' 하고 넘어가기는 했지만, 최근에 누군가 &quot;그래서 도메인이 정확히 무엇인데?&quot; 라고 물어보면 말문이 턱 막혔습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 오늘 이 글을 통해 소프트웨어 공학에서 말하는 도메인(Domain) 의 진짜 의미부터 시작해서, 왜 이 개념을 뼛속까지 이해해야 '지속 가능한 좋은 코드' 를 짤 수 있는지 아주 깊게 정리해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도메인(Domain) 의 진짜 정의 - URL 이 아니라!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 공학에서의 도메인은 가장 먼저 떠오르는 주소창 도메인(naver.com) 이 아니라, 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;소프트웨어가 해결하고자 하는 현실 세계의 비즈니스 문제 영역&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해, &quot;당신이 지금 만들고 있는 프로그램이 현실의 어떤 문제를 해결하기 위한 것인가?&quot; 에 대한 답이 바로 도메인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현실 세계의 예시로 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 매일 사용하는 서비스들을 도메인 관점으로 쪼개보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 배달의 민족&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실 세계의 '음식 주문, 가게 관리, 배달 라이더 매칭, 결제' 라는 문제를 해결합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 배달의민족 개발자들에게 도메인은 주문, 결제, 정산, 라이더, 메뉴 등이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 토스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실 세계의 '금융/자산 관리, 복잡한 송금' 문제를 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들의 도메인은 송금, 계좌 ,신용도, 펀드, 대출이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 네이버 웹툰&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실 세계의 '컨텐츠 소비, 작가 정산, 독자 관리' 문제를 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이들의 도메인은 웹툰, 회차, 유료쿠키, 작가, 댓글이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 개발자들이 출근해서 인텔리제이( 혹은 이클립스 ) 를 켜고 작성하는 코드 중에서 &quot;우리 회사가 돈을 벌기 위해 정의한 비즈니스 규칙&quot; 을 다루는 영역이 모두 도메인에 해당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 도메인이 소프트웨어의 중심이어야 할까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그냥 기능만 잘 돌아가면 되지, 왜 굳이 '도메인' 이라는 단어를 써가며 강조할까요?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이해하려면 과거 우리가 코딩했던 방식(데이터 중심 설계) 의 한계를 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과거의 방식: 데이터베이스(DB) 중심 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날(혹은 지금도 많은 곳에서) 서비스를 만들 때 가장 먼저 하는 일은 &quot;DB 테이블 설계&quot; 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ORDERS 테이블을 만들고, ORDER_STATUS 컬럼 만들고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스프링에서 OrderVO 나 OrderDto 같은 데이터 껍데기를 만든 뒤..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. OrderService 에서 모든 비즈니스 로직을 if-else 문으로 길게 짜고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 치명적인 단점은 비즈니스가 복잡해지면서 코드가 썩어 들어간다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획서에 &quot;주문 취소는 배송 전에만 가능하다&quot; 라는 규칙이 추가되면, OrderService 에 수백 줄짜리 스파게티 코드가 생기게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 지나면 기획자가 &quot;우리 서비스 로직이 지금 어떻게 되어 있나요?&quot; 라고 물었을 때, 아무도 대답하지 못하고 쿼리문과 서비스 코드 수천 줄을 헤집고 다녀야 하는 비극이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;현대의 방식: 도메인 중심 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현대 아키텍처(DDD) 는 외치게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;DB 나 프레임워크는 껍데기일 뿐이다. 소프트웨어의 본질은 도메인(비즈니스)이다!!&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어가 복잡해져도 살아남으려면, 기획자가 말하는 비즈니스 개념과 규칙이 코드(도메인 모델) 에 그대로 반영되어 있어야 합니다. 기획자가 &quot;배송 후에는 취소 불가&quot; 라고 하면, 코드에서도 Order.cancle() 메서드 안에 그 규칙이 정확히 한 곳에 명시되어 있어야 한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인(Domain) vs 인프라(Infrastructure)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인을 선명하게 이해하는 가장 좋은 방법은 &quot;도메인이 아닌 것&quot; 과 비교해 보는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어는 크게 도메인 영역과 인프라 기술 영역으로 나뉩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 영역(Domain)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 질문 : 우리 비즈니스의 규칙이 무엇인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사 : 주문, 결제, 환불, 회원상태, 할인 조건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화 : 비즈니스 정책 변경( 할인율 10% &amp;rarr; 할인율 15% )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프라 영역(Infrastructure)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 질문 : 그 규칙을 어떤 기술로 실현할 것인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관심사 : Spring, MySQL, Redis, AWS, HTTP&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화의 원인 : 기술적 필요 ( Oracle 에서 MySQL 로 마이그레이션 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;만약 내일 당장 스프링 플러그인을 다 뽑아버리고, DB 도 안 쓰고 메모리에만 데이터를 저장하도록 프로그램을 바꾼다면, 내 코드에서 살아남아야 하는 핵심 로직은 무엇인가?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때도 여전히 살아남아 수식과 조건을 계산하고 있어야 하는 코드, 그게 바로 도메인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 스프링 프렝미워크 JPA, Controller, 쿼리문 등은 도메인을 도와주는 인프라에 불과합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도메인을 더 깊게: 3가지 도메인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인은 단 한 덩어리로 존재하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 커지면 도메인도 성격에 따라 쪼개지는데, 시니어들과 아키텍처를 논할 때 필수적인 개념이 바로 하위 도메인의 3가지 분류입니다. 배달 앱 서비스를 만든다고 가정하고 아래와 같이 3가지로 분석하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 도메인(Core Domain)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 회사의 가장 경쟁력 있고, 가장 돈을 잘 벌어다 주는 핵심 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절대 외주를 주면 안되고, 사내에서 가장 뛰어난 개발자들이 투입되어야 하는 곳이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배달 앱에서의 '가게 추천 알고리즘', '효율적인 라이더 매칭 시스템' 이 해당될 수 있다고 봅니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원 도메인(Supporting Domain)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 도메인을 도와주기 위해 필수적이긴 하지만, 그 자체로 차별성을 가지지는 않는 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 정체성에 꼭 필요하므로 직접 구현하는 경우가 많습니다. 예를 들어 '메뉴판 등록/관리' 를 말할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 도메인(Generic Domain)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스에 꼭 필요하지만, 이미 세상에 좋은 솔루션이 널리 있어서 우리가 밤새워 직접 개발할 필요가 없는 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스를 쓰거나 외부 SaaS 서비스를 사서 쓰는게 이득인 영역이기도 합니다. '본인인증', '알림톡 발송' 등이 해당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자바 코드로 보는 도메인의 변화: 빈약 vs 풍부&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 공부는 직접 코드를 짜고 봐야한다는 말이 백문이 불여일견이라고 백엔드 개발자라면 코드로 봐야 완벽히 이해가 갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 생각하기에 주니어들이 가장 많이 실수하는 패턴과 이를 도메인 중심으로 리팩토링한 코드를 비교해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안좋은 예시: 빈약한 도메인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 주니어들이 JPA @Entity 를 만들때 아무 생각 없이 Getter/Setter 를 기계적으로 생성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 그랬었습니다. 이로 인해 비즈니스 로직이 서비스 레이어에 절절 흐르는 절차지향적 코드가 완성되게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779070682144&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 도메인 객체가 그저 '데이터 껍데기' 역할만 함 (빈약한 모델)
@Entity
@Getter @Setter
public class Order {
    @Id @GeneratedValue
    private Long id;
    private OrderStatus status; // START, SHIPPED, CANCELED 등
    private int totalAmount;
}

// 2. 서비스 레이어 (뚱뚱한 서비스 - 모든 비즈니스 로직을 감당함)
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;

    @Transactional
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;주문이 없습니다.&quot;));

        //   비즈니스 규칙이 도메인이 아닌 서비스에 들어있음!
        // &quot;이미 배송된 주문은 취소할 수 없다&quot;는 중요 규칙이 여기에 숨겨져 있음.
        if (order.getStatus() == OrderStatus.SHIPPED) {
            throw new IllegalStateException(&quot;이미 배송된 주문은 취소할 수 없습니다.&quot;);
        }

        order.setStatus(OrderStatus.CANCELED); // Setter를 통해 외부에서 상태를 막 바꿈
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;무엇이 문제일까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order 객체는 스스로 할 수 있는 게 아무것도 없는 수동적인 존재입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;배송된 주문은 취소 불가&quot; 라는 비즈니스 규칙을 알고 싶으면 OrderService 코드를 다 뒤져야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서비스(AdminOrderService) 에서 취소 기능을 만들면 똑같은 if 문이 복사/붙여넣기 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 도메인의 응집도가 깨진 상태라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 예시: 풍부한 도메인 모델&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 규칙을 도메인 객체 내부로 꽁꽁 숨겨(캡슐화) 객체지향적인 도메인 모델을 만든 코드입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779070847172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. 스스로 비즈니스 규칙을 통제하는 '풍부한 도메인 모델'
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
    @Id @GeneratedValue
    private Long id;
    private OrderStatus status;
    private int totalAmount;

    //   Setter를 폐쇄하고 비즈니스 의도가 드러나는 생성자/메서드만 노출
    public void cancel() {
        // ✨ &quot;내 상태는 내가 관리한다!&quot; 도메인 규칙을 스스로 검증
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException(&quot;이미 배송된 주문은 취소할 수 없습니다.&quot;);
        }
        this.status = OrderStatus.CANCELED;
    }
}

// 2. 서비스 레이어 (얇은 서비스 - 오케스트레이터 역할만 수행)
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;

    @Transactional
    public void cancelOrder(Long orderId) {
        // 1. 데이터 로드
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;주문이 없습니다.&quot;));

        // 2. 도메인 객체에게 핵심 비즈니스 로직(메시지)을 위임
        order.cancel(); 
        
        // 3. 서비스는 흐름만 제어할 뿐, 비즈니스 규칙이 어떻게 되는지는 알 필요가 없음!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리팩토링 후 얻은 이점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 최소와 관련된 핵심 규칙이 Order 클래스 딱 한 곳에 모여있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획이 바뀌면 이 클래스만 수정하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 order.setStatus() 로 상태를 마음대로 조작할 수 없으므로 버그가 예방됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리하면서..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 공학에서 도메인이란 배달, 금융, 쇼핑몰처럼 우리가 소프트웨어로 해결하고자 하는 현실 세계의 문제 영역 그 자체를 의미한다고 볼 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크가 제공하는 멋진 기능들(@Autowired, @Transactional) 은 결국 이 모데인 로직이 안전하고 편리하게 돌아가도록 뒤에서 받쳐주는 '기술 인프라' 라고 생각합니다. 기술의 화려함에 매몰되어 정작 본질인 비즈니스 도메인을 허술하게 짜게 된다면, 그 시스템은 금방 무너지게 될 것입니다. 앞으로 코드를 짤 때 &quot;이 코드는 순수한 비즈니스(도메인) 인가, 아니면 기술(인프라) 인가?&quot; 를 끊임없이 질문하고 이 질문에 스스로 답할 수 있게 되는 순간, 주니어를 넘어 시니어의 길로 접어들수 있다고 생각하며 그러한 개발자가 되기 위해 저 포함 다들 노력하면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>domain</category>
      <category>Infra</category>
      <category>Java Developer</category>
      <category>주니어 개발자</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/142</guid>
      <comments>https://chanyoungcoding.tistory.com/142#entry142comment</comments>
      <pubDate>Mon, 18 May 2026 11:37:58 +0900</pubDate>
    </item>
    <item>
      <title>하네스 엔지니어링에 대해서</title>
      <link>https://chanyoungcoding.tistory.com/141</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #f89009;&quot;&gt;회사에서 하네스 엔지니어링 스터디가 있어 개념을 정리했습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하네스 엔지니어링을 간단하게 말하자면 AI 에이전트가 일을 잘하게 만드는 바깥 시스템을 설계하는 일을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AI 가 업무를 끝까지 안정적으로 해낼 수 있게 환경을 설계하는 작업입니다. 단순히 프롬프트를 잘 쓰는 수준을 넘어서, 에이전트가 어떤 도구를 쓰고, 무엇을 참고해 어디서 검증을 하고, 실패하면 어떻게 복구할지 AI 의 작업 전반을 아우르는 실행 환경을 이용자가 구성하고 통제하는 개념이기 때문입니다. 이 개념은 본질을 AI 가 그 일을 안정적으로 수행하도록 환경과 규칙을 구성하는 것이라고도 할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;더 쉽게&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AI 가 작업하면서 길을 잃지 않도록 '안전한 놀이터' 와 '규칙' 을 만들어 주는 일 이라고 이해하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;인공지능에게 '이거 해볼래?' 라고만 하는 게 아니라, 어디까지 하고, 틀리면 어떻게 고치고, 누가 확인할지까지 미리 정해주는 것입니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4622&quot; data-origin-height=&quot;2222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TzwHf/dJMcaaecuLJ/wfnpdOIqGKOLvpbgVaGr3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TzwHf/dJMcaaecuLJ/wfnpdOIqGKOLvpbgVaGr3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TzwHf/dJMcaaecuLJ/wfnpdOIqGKOLvpbgVaGr3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTzwHf%2FdJMcaaecuLJ%2FwfnpdOIqGKOLvpbgVaGr3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;660&quot; height=&quot;317&quot; data-origin-width=&quot;4622&quot; data-origin-height=&quot;2222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하네스(harness)란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말의 가슴과 몸통에 채우는 마구를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 통해 말의 움직임을 통제합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에 적용을 하자면, 우리는 이 하네스라는 AI 를 둘러싼 운영 프레임을 통해 AI 를 안정적으로 움직인다고 생각하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프롬프트, 하네스, 에이전틱&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부를 하다 보면 하네스 엔지니어링과 비슷한 개념이 몇개 나오게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 엔지니어링, 하네스 엔지니어링, 에이전틱 엔지니어링 등 다양한 부분을 접하게 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;프롬프트 엔지니어링 : &quot;AI 에게 어떻게 말해야 할까?&quot;&lt;br /&gt;하네스 엔지니어링 : &quot;AI 를 어떻게 통제해야 할까?&quot;&lt;br /&gt;에이전틱 엔지니어링 : &quot;AI 에게 어떻게 일을 맡길까?&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 엔지니어링과의 차이를 먼저 살펴 보자면, 하네스 엔지니어링은 단순한 문장 개선이 아니라 업무 프로세스 설계에 더 가깝습니다. 반면 하네스 엔지니어링과 에이전틱 엔지니어링의 개념은 언뜻 보면 서로 겹치는 영역이라고 생각할 수 있지만 완전히 같지는 않습니다. 에이전틱 엔지니어링은 AI 가 서로 협력하며 일을 해내도록 워크플로우에 집중하고, 하네스는 이 에이전트들이 사고치지 않도록 울타리를 치는 느낌입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하네스 엔지니어링이 뜨는 이유&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 이유는 AI 가 단순히 답변 생성기를 넘어서 실제 업무를 직접 수행하는 단계로 들어왔기 때문이라고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈AI 에 관련된 정보를 찾아보면 사람이 코드를 직접 쓰지 않고도 소프트웨어 제품을 만들고 운영하는 실험을 했는데, 그 과정에서 가장 걸리는 부분이 작업 환경 설계와 피드백 루프라고 합니다. AI 모델의 성능보다 모델이 일을 끝까지 잘 해네게 만드는 시스템이 중요해진 것입니다. 앞서 프롬프트 엔지니어링이 뜨던 시기에는 챗봇 위주의 AI 사용뿐이였지만, 이제는 AI 의 역할이 광범위해지고 에이전틱 AI 의 권한이 확장되어 스스로 일할 수 있게 됐습니다. 더 똑똑해진 AI 에게 일을 거의 완전히 맡길 수 있게 된 만큼, AI 의 작업을 통제하고 설꼐해서 이용자가 원하는 결과를 잘 얻어낼 수 있는지가 더 중요해졌습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 제 개인적인 생각으로 하네스 엔지니어링이 급부상한 이유는 AI 가 이용자의 의도대로 작업하지 않는 원인이 대부분 AI 모델 지능의 문제가 아니고 오히려 이용자가 요구한 작업 과정이 복잡하거나 프롬프트가 제대로 입력되지 않아서 혹은 검증 단계를 거치지 않은 이유도 있다고 생각합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4622&quot; data-origin-height=&quot;1888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cigJw7/dJMcacwkbsN/hbUN1VHFDZD4jeM1CZykUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cigJw7/dJMcacwkbsN/hbUN1VHFDZD4jeM1CZykUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cigJw7/dJMcacwkbsN/hbUN1VHFDZD4jeM1CZykUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcigJw7%2FdJMcacwkbsN%2FhbUN1VHFDZD4jeM1CZykUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;660&quot; height=&quot;270&quot; data-origin-width=&quot;4622&quot; data-origin-height=&quot;1888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하네스 엔지니어링을 공부해야 하는 이유&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 하네스 엔지니어링의 핵심은 인공지능이라는 야생마를 제어할 수 있는 인간의 설계 능력에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 지능이 높아질수록 우리가 집중해야 할 것은 역설적으로 AI 외부의 시스템입니다. 이제는 단순히 질문을 잘 던지는 수준을 넘어서, AI 가 사고치지 않고 목적지까지 완주할 수 있도록 정교한 울타리를 처야 하는 시대가 온 것입니다. 끊임없이 발전하는 AI 성능에 따라가지 못하고 뒤쳐지는 것 보다 하네스 엔지니어링을 공부함으로써 탄탄한 하네스를 설계할 수 있는 사람이 된다면 한단계 더 높은 개발자로 성장할 수 있다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS</category>
      <category>하네스 엔지니어링</category>
      <category>하네스 엔지니어링에 개념</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/141</guid>
      <comments>https://chanyoungcoding.tistory.com/141#entry141comment</comments>
      <pubDate>Mon, 11 May 2026 17:13:10 +0900</pubDate>
    </item>
    <item>
      <title>IntelliJ 디버거</title>
      <link>https://chanyoungcoding.tistory.com/140</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍을 하다 보면 점점 새로운 기능을 만드는 시간보다 버그를 찾는 시간이 더 많아집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 System.out.println , console.log 를 찍는 등 원시적인 방법으로 해도 충분하지만 점점 복잡한 프로그램과 복잡한 버그들을 다루면서 이런 단순한 기능으로는 충분하지 않게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때 버그를 찾기 위해 편한 여러 기능을 제공해 준느 걸 디버거 라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통은 IDE 에 내장 돼 있고, 흔히 제공되는 기능들이 정해져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 내가 디버깅을 하고 싶은 코드로 가고 거기에 BreakPoint 를 추가하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디버깅을 하는 방법은 아래와 같이 2가지로 나누게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Line 번호가 있는 부분 마우스로 클릭하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Command + F8 단축키 사용&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJfe4p/dJMcaiwqE4K/ZHQTBQwKQpmXe0vuzb8qe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJfe4p/dJMcaiwqE4K/ZHQTBQwKQpmXe0vuzb8qe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJfe4p/dJMcaiwqE4K/ZHQTBQwKQpmXe0vuzb8qe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJfe4p%2FdJMcaiwqE4K%2FZHQTBQwKQpmXe0vuzb8qe0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;909&quot; height=&quot;383&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BreakPoint 를 추가한 뒤 프로그램을 실행하면 BreakPoint 가 추가된 줄을 실행할 때 프로그램이 멈추고, 밑에 부분에 디버깅용 창이 나타납니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;만약 창이 나타나지 않을 경우 Command + 5 를 입력하면 됩니다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;901&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Yh1w4/dJMcacJNIKx/Rfg195Tn4V96kTmWS7IxBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Yh1w4/dJMcacJNIKx/Rfg195Tn4V96kTmWS7IxBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Yh1w4/dJMcacJNIKx/Rfg195Tn4V96kTmWS7IxBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYh1w4%2FdJMcacJNIKx%2FRfg195Tn4V96kTmWS7IxBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1117&quot; height=&quot;901&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;901&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 디버깅 창을 살펴보면 크게 4가지 부분이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SMVd8/dJMcafsWXLe/atY88w98gEdOMCDYk9Mlx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SMVd8/dJMcafsWXLe/atY88w98gEdOMCDYk9Mlx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SMVd8/dJMcafsWXLe/atY88w98gEdOMCDYk9Mlx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSMVd8%2FdJMcafsWXLe%2FatY88w98gEdOMCDYk9Mlx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;612&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Call Stack&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Java 프로그램을 처음 실행하게 된다면 main Method 를 실행하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 main Method 는 우리가 만든 많은 Method/Class 를 호출하게 되는데 이 모든 과정을 한 눈으로 확인할 수 있는게 Call Stack 입니다. 프로그램의 전반적인 흐름을 볼때 유용하며, 어떤 메서드와 어떤 클래스가 호출되었는지 한 눈에 볼&amp;nbsp; 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관련 변수들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로그램은 실행하기 위해 각종 데이터를 변수에 저장하고 이 변수들에 연산을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 디버깅을 한다고 하면 이 각각의 변수에 내가 의도한 대로 값이 들어가 있는지, 예상하지 못 한 값이 들어가 있다면 그 원인은 무엇인지 보면서 시작하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 내가 원하는 값이 없다면 위의 칸에 보고 싶은 값을 넣고 엔터를 누르면 변수 목록에 추가가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qHBBC/dJMcaaZtKWq/1pqYi75F4nyyi6eEPjDxq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qHBBC/dJMcaaZtKWq/1pqYi75F4nyyi6eEPjDxq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qHBBC/dJMcaaZtKWq/1pqYi75F4nyyi6eEPjDxq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqHBBC%2FdJMcaaZtKWq%2F1pqYi75F4nyyi6eEPjDxq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;520&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 외에 동작들&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MZNnN/dJMcagrRzXY/oIS33gHzXMvaQrVJrpgMrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MZNnN/dJMcagrRzXY/oIS33gHzXMvaQrVJrpgMrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MZNnN/dJMcagrRzXY/oIS33gHzXMvaQrVJrpgMrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMZNnN%2FdJMcagrRzXY%2FoIS33gHzXMvaQrVJrpgMrK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;452&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8NPiN/dJMcahK5WfR/UOWSnzMOx3KCxSis2P7m70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8NPiN/dJMcahK5WfR/UOWSnzMOx3KCxSis2P7m70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8NPiN/dJMcahK5WfR/UOWSnzMOx3KCxSis2P7m70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8NPiN%2FdJMcahK5WfR%2FUOWSnzMOx3KCxSis2P7m70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;883&quot; height=&quot;613&quot; data-origin-width=&quot;883&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Spring</category>
      <category>ide</category>
      <category>IntelliJ</category>
      <category>Java</category>
      <category>디버거</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/140</guid>
      <comments>https://chanyoungcoding.tistory.com/140#entry140comment</comments>
      <pubDate>Wed, 6 May 2026 14:22:09 +0900</pubDate>
    </item>
    <item>
      <title>좌충우돌 원격 서버 배포 및 Jenkins CI/CD 구축기</title>
      <link>https://chanyoungcoding.tistory.com/139</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #009a87;&quot;&gt;이 글은 단순한 작업 기록을 넘어, 비슷한 문제를 겪을 동료들에게 가이드가 될 수 있도록 작성하였습니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &quot;배포에 성공했다&quot; 는 결과보다, 그 과정에서 마주친 수많은 'Permission Denied' 와 '접속불가' 메시지들을 어떻게 논리적으로 해결했는지 초점을 맞췄습니다. 저와 같은 주니어 개발자분들에게 이 글이 도움이 됐으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대두분을 로컬 환경에서 테스트를 하고 Git 에 배포하는 과정만 하다가 실제 서비스를 24시간 꺼지지 않는 별도의 서버에 돌아가게 만들어보라는 요청에 Maven 과 Spring 기반의 프로젝트를 회사 원격 서버에 올리는 임무를 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Terminus 를 통해 서버에 접속하여 보안상 공용 계정이 아닌 내 전용 계정을 생성하여 작업을 진행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣을 설치하고, 빌드된 ---.war 파일을 톰캣의 webapps 폴더에 넣는 것까지는 순조로웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;너 권한 있어?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣을 실행하기 위해 bin 폴더로 이동해 ./startup.sh 를 입력하였지만 아래와 같은 오류가 발생했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777340771480&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-bash: ./startup.sh: Permission denied&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리눅스는 다중 사용자 시스템입니다. 파일마다 읽기(r), 쓰기(w), 실행(x) 권한이 소유자, 그룹, 기타 사용자별로 엄격히 나뉩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 root 계정으로 접속하여 sudo 명령어를 사용해 권한을 주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777340872191&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. root 계정으로 전환 (또는 관리자 권한 사용)
sudo su - 

# 2. 톰캣 폴더 전체의 소유권을 내 계정(my-user)으로 변경
# -R 옵션은 하위 폴더와 파일까지 모두 적용한다는 뜻입니다.
chown -R my-user:my-user /usr/local/lib/apache-tomcat-9.0

# 3. 실행 파일(.sh)들에 실행 권한 부여
chmod +x /usr/local/lib/apache-tomcat-9.0/bin/*.sh

# 4. 다시 내 계정으로 돌아와 실행!
./startup.sh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 톰캣은 8080 포트를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이미 회사에서는 다른 서비스가 해당 포트를 사용하고 있기도 했고 보안 및 관리상의 이유로 포트를 변경해야 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 포트를 4080 으로 변경하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server.xml 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;톰캣의 모든 핵심 설정은 conf/server.xml 에 담겨 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777340968748&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Connector port=&quot;8080&quot; protocol=&quot;HTTP/1.1&quot; ... /&amp;gt;

&amp;lt;Connector port=&quot;4080&quot; protocol=&quot;HTTP/1.1&quot;
           connectionTimeout=&quot;20000&quot;
           redirectPort=&quot;8443&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위와 같이 수정을 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 위와 같이 설정 후에 리눅스 자체 방화벽에서도 4080 포트를 허용해 주어야 했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777341020541&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo firewall-cmd --permanent --add-port=4080/tcp
sudo firewall-cmd --reload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트포워딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방화벽까지 열었는데도 외부에서 접속이 되지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 해보니 서버 구조에 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버 내부에 Oracle VM VirtualBox 를 통해 가상 머신(VM) 이 돌아가고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;포트포워딩을 사용하는 이유&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 회사에서는 실제 주소(공인 IP) 가 하나인데, 그 안에 여러 개의 가상 컴퓨터가 살고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 데이터가 들어오면 호스트서버에 도착을 하게 됩니다. 하지만 호스트를 이러한 데이터가 어떤 VM 으로 가야 하는지 모르고 있습니다. 따라서 60600 번 포트로 오는 건, 내부 VM 의 4080 포트로 전달해야 한다 라는 규칙을 정하는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;당연한 말이지만 60600 이라는 포트도 호스트 서버의 방화벽에서 열어주어야 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 를 이용한 CI/CD 자동화&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원격 서버에 최종 배포를 완료하고, CI/CD 를 자동화 하기 위해 Jenkins 를 이용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jenkins 설정에서 호스트주소를 제대로 입력했음에도 배포가 되지 않아 찾아보니 톰캣에서 거부를 하고 있었습니다. Jenkins 가 톰캣의 Manager App 에 접근하여 파일을 업로드 하려면, 권한을 가진 계정 정보가 톰캣 설정에 등록이 되어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1777341338281&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;tomcat-users&amp;gt;
  &amp;lt;role rolename=&quot;manager-gui&quot;/&amp;gt;
  &amp;lt;role rolename=&quot;manager-script&quot;/&amp;gt;
  &amp;lt;user username=&quot;my-user&quot; password=&quot;my-password&quot; roles=&quot;manager-gui,manager-script&quot;/&amp;gt;
&amp;lt;/tomcat-users&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 tomcat-users.xml 에 계정 정보에 대한 설정을 완료하여 해결을 하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 리눅스는 권한으로 시작해서 권한으로 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 네트워크 층이 있다. 호스트 방화벽뿐만 아니라 VM 의 포트포워딩 구조를 이해해야 서비스가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. CI/CD 는 양방향 약속이다. Jenkins 뿐만 아니라 받는 쪽(Tomcat) 에서도 허용을 해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <category>CI/CD</category>
      <category>Jenkins</category>
      <category>자동배포</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/139</guid>
      <comments>https://chanyoungcoding.tistory.com/139#entry139comment</comments>
      <pubDate>Tue, 28 Apr 2026 10:59:03 +0900</pubDate>
    </item>
    <item>
      <title>빈 검증에 대하여</title>
      <link>https://chanyoungcoding.tistory.com/138</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;Bean Validation&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;검증 애노테이션과 여러 인터페이스의 모음이며 마치 JPA 가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다. &lt;br /&gt;&lt;br /&gt;Bean Validation 을 사용하려면 다음과 같은 의존관계를 추가해야 한다.&lt;br /&gt;&lt;br /&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1773638680859&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-validation'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773638700914&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
	private Long id;

	@NotBlank
	private String itemName;

	@NotNull
	@Range(min = 1000, max = 100000)
	private Integer price;

	@NotNull
	@Max(9999)
	private Integer quantity;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위에 코드가 기본적으로 빈 검증을 사용하는 코드이다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;javax.validation.constraints.NotNull;&lt;br /&gt;org.hibernate.validator.constraints.Range;&lt;br /&gt;&lt;br /&gt;javax.validation 으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이며,&lt;br /&gt;org.hibernate.validator 로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증기능이다.&lt;br /&gt;실무에서는 대부분 하이버네이트 validator 를 사용하므로 자유롭게 사용해도 된다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;Bean Validation 적용 &lt;/h3&gt;
&lt;pre id=&quot;code_1773638751779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/add&quot;)
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttribute redirectAttribute) {
	
    if(BindingResult.hasErrors()) {
		return &quot;validation/v3/addForm&quot;;
	}

	Item savedItem = itemRepository.save(item);
	redirecAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
	redirecAttributes.addAttribute(&quot;status&quot;, true);

	return &quot;redirect:/validation/v3/items/{itemId}&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;스프링 MVC 는 Bean Validator 를 자동으로 등록해 주는데 스프링 부트가 spring-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator 를 인지하고 스프링에 통합하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;바인딩에 성공한 필드만 Bean Validation 에 적용&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;쉽게 이야기 하자면 아래와 같다.&lt;br /&gt;itemName 에 문자 &amp;ldquo;A&quot; 입력 -&amp;gt; 타입변환성공 -&amp;gt; itemName 필드에 BeanVaildation 적용&lt;br /&gt;price 에 문자 &amp;rdquo;B&quot; 입력 -&amp;gt; &amp;ldquo;B&quot; 를 숫자 타입 변환 시도 실패 -&amp;gt; typeMismatch FieldError 발생 -&amp;gt; price 필드는 BeanValidation 적용 X &lt;br /&gt;&lt;br /&gt;따라서 만약 price 에 @NotNull, @Min(1000) 을 등록을 해놔도 해당 Bean Validation 은 아예 실행조차 되지 않는다. &lt;br /&gt;따라서 타입 변환 에러가 출력되어 스프링에서 기본 제공해주는 메시지만 출력이 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;숫자를 입력해 주세요.&lt;/blockquote&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;기본적인 Bean Validation 에러 코드&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Bean Validation 에서 제공하는 에러 코드는 애노테이션 이름으로 등록이 된다.&lt;br /&gt;아래 코드를 보면 이해가 쉽다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773638819715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Item {
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 10000)
    private Integer price;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 등록을 하게 되면 스프링은 자등으로 아래아 같이 오류 코드를 만들어 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1773638834368&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NotBlank.item.itemName;
NotBlank.itemName;
NotBlank.java.lang.String
NotBlank

Range.item.price
Range.price
Range.java.lang.Integer
Range&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 등록된 오류코드는 메시지에서 활용할 수가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773638846124&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NotBlank.item.itemName=특정상품
NotBlank.itemName=바나나
NotBlank=값이없습니다.

Range.item.price={0} 가격이 {1} ~ {2} 사이입니다.

price=상품&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 {0} 는 필드명이며 {1}, {2} 는 애노테이션 속성 값이다.&lt;br /&gt;위에 코드를 보면 필드명이 price 로 되어 있고, Rage 의 속성 값으로 1000, 10000 이 있다.&lt;br /&gt;따라서 price 가격이 1000 ~ 10000 사이 입니다. 라고 나오게 된다.&lt;br /&gt;추가적으로 위에 코드를 보면 price 를 상품 이라고 지정해 주었는데 이렇게 설정하면 {0} 에 상품 이라고 등록이&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;되므로 상품 가격이 1000 ~ 10000 사이 입니다. 라고 출력이 된다.&lt;br /&gt;&lt;br /&gt;전체흐름&lt;br /&gt;@NotBlank 실패 -&amp;gt; MessageCodesResolver 가 코드를 여러 개 생성 -&amp;gt; messages 또는 errors.properties 에서 순서대로 탐색 -&amp;gt; NotBlank 발견 -&amp;gt; &amp;ldquo;값이 없습니다.&amp;rdquo; 를 bindingResult 에 적재 후 실행 &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;BeanValidation 이 메시지를 찾는 순서&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. 생성된 메시지 코드 순서대로 messageSource(messages.properties) 에서 메시지를 찾기 &lt;br /&gt;2. 애노테이션의 message 속성을 사용 ( @NotBlank(message = &quot;공백&amp;ldquo;) )&lt;br /&gt;3. 라이브러리가 제공하는 기본 값 사용(공백일 수가 없습니다.)&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;GlobalError 추가 &lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;@Validated 를 사용하지 못하는 2개 이상의 오류 사항들은 아래와 같이 사용하면 된다.&lt;br /&gt;즉, 회원가입 할 때 이름이랑 비밀번호가 값이 들어오지 않았을 경우에 사용 할 수가 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773638895387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bindingResult.reject(&quot;totalPriceMin&quot;, new Object[]{10000, resultPrice}, null);

// 위와 같이 설정하면 messages.properties 에서 아래 정보를 찾게 된다.
totalPriceMin=총 금액은 {0}원 이상이어야 합니다. (현재: {1})&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773638906279&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;p th:each=&quot;err : ${#fields.globalErrors()}&quot; th:text=&quot;${err}&quot;&amp;gt;&amp;lt;/p&amp;gt;
// 총 금액은 10000원 이상이어야 합니다. (현재: 8000)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;에러 코드는 totalPriceMin 이며 메시지에 사용할 파라미터는 100000, resultPrice 이고 기본 메시지는 없음(null) 로 설정한 부분입니다.&lt;br /&gt;&lt;br /&gt;즉, 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1773638920512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void reject(String errorCode, Object[] errorArgs, String defaultMessage)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;errorCode : 메시지 코드 &lt;br /&gt;errorArgs : 메시지에서 사용할 치환 값 &lt;br /&gt;defaultMessage : 메시지를 못 찾았을 때 쓸 기본 메시지 &lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;수정시 요구사항이 다를 경우&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 등록, 수정을 할 때 동일한 UserDTO 를 사용하는 경우 &lt;br /&gt;데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.&lt;br /&gt;&lt;br /&gt;등록할 때는 UserDTO 의 ID 값이 필요하지만 수정할 때는 ID 값이 필요가 없을 수도 있다.&lt;br /&gt;하지만 @NotNull 로 ID 를 등록해 놓으면 수정할 때 무조건 값을 넣어야 하는 불편함이 생길 수 있다.&lt;br /&gt;&lt;br /&gt;동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법은 2가지가 있다.&lt;br /&gt;1. BeanValidation 의 groups 기능을 사용 &lt;br /&gt;2. 동일한 모델을 사용하지 않고 파생된 2가지의 모델로 쪼개어 객체를 만들어 사용 &lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실무에서는 2번째 방법을 가장 많이 사용하게 된다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;첫번째 groups 기능을 사용하는 경우는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1773638959766&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface SaveCheck {}

public interface UpdateCheck {}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;위와 같이 두가지 인터페이스를 만든 뒤 각각의 애노테이션에 적용해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1773638980739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Data
public class Item {
    @NotNull(groups = UpdateCheck.class) // 수정시에만 적용
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) // 수정, 등록에 모두 적용
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773638991240&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/add&quot;)
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute ...) {...}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 적용하고 싶은 부분에 대하여 설정만 해주면 된다.&lt;br /&gt;&lt;br /&gt;하지만 코드를 짜면 알겠지만 코드가 복잡해지고 유지보수 하기가 힘들어진다.&lt;br /&gt;따라서 2번째 방법처럼 각각의 전용 모델을 만들어 구분해 주면 더 쉽게 검증을 적용하고 안전성을 높일 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Bean Validation</category>
      <category>Java</category>
      <category>validation</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/138</guid>
      <comments>https://chanyoungcoding.tistory.com/138#entry138comment</comments>
      <pubDate>Mon, 23 Mar 2026 09:06:12 +0900</pubDate>
    </item>
    <item>
      <title>필터와 인터셉터</title>
      <link>https://chanyoungcoding.tistory.com/137</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Filter 와 Interceptor 가 필요한 이유는 모든 로직에 대하여 인증 절차를 처리하는 Logic 이 있다면, 하나하나 등록을 하는 것 보다 한번에 처리할 수 있는 기능이 있으면 유지보수 하기 쉬워 사용하는 것이다.&lt;br /&gt;&lt;br /&gt;이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사(Cross-cutting concern) 이라고 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;필터 (Filter)&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.&lt;br /&gt;그래서 모든 고객의 요청 로그를 남기는 요구사항이 존재한다면 필터를 사용하면 된다.&lt;br /&gt;&lt;br /&gt;HTTP 요청 -&amp;gt; WAS -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 컨트롤러 &lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고로 스프링을 사용하는 경우 여기서 말하는 서블릿은 스프링의 디스패터 서블릿으로 생각하면 된다.&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;필터 체인 (Filter Chain)&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다.&lt;br /&gt;예를 들어서 로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.&lt;br /&gt;&lt;br /&gt;HTTP 요청 -&amp;gt; WAS -&amp;gt; Filter1 -&amp;gt; Filter2 -&amp;gt; Filter3 -&amp;gt; 서블릿 -&amp;gt; 컨트롤러 &lt;br /&gt;&lt;br /&gt;필터 인터페이스는 다음과 같이 구성되어 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773634614439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Filter {
	public default void init(FilterConfig filterConfig) throws ServletException {}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {}

	public default void destory() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;필터 인터페이스를 구현하고 등록을 하게 되면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고,관리하게 된다.&lt;br /&gt;&lt;br /&gt;init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.&lt;br /&gt;doFilter() : 고객의 요청이 올 때마다 해당 메서드가 호출, 필터의 로직을 구현하면 된다.&lt;br /&gt;desotry() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;사용&lt;/h3&gt;
&lt;pre id=&quot;code_1773634671081&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class LogFIlter implements Filter {

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		log.info(&quot;log filter Start&quot;);
	}

	@Override
	public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
		try {
			log.info(&quot;doFIlter Start&quot;);
			chain.doFilter(request, response); // 가장 중요 (  다음 필터 호출 )
		} catch(Exception e) {
			throw e;
		}
	}

	@Override
	public default void destory() {
		log.info(&quot;doFilter Destory&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;chain.doFilter(request, response) 라는 부분이 제일 중요한데, 다음 필터가 있으면 필터를 호출하고 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지가 않는다.&lt;br /&gt;&lt;br /&gt;이렇게 나만의 FIlter 를 만들었다면 필터를 설정하여 등록을 해주면 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773634706725&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig {

	@Bean
	public FilterRegistrationBean logFilter() {
		FilterRegistrationBean&amp;lt;Filter&amp;gt; filterRegistrationBean = new FilterRegistrationbean&amp;lt;&amp;gt;();

		filterRegistrationBean.setFilter(new LogFilter());
		filterRegistrationBean.setOrder(1);
		filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);

		return filterRegistrationBean;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;필터를 등록하는 방법은 여러가지가 있지만, SpringBoot 를 사용한다면 FilterRegistrationBean 을 사용해서 등록을 해주면 된다.&lt;br /&gt;&lt;br /&gt;setFilter(new LogFilter()) : 등록할 필터를 지정한다.&lt;br /&gt;setOrder(1) : 필터는 체인으로 동작을 한다. 따라서 순서가 필요하다.&lt;br /&gt;addUrlPatterns(&quot;/*&quot;) : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정 할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;서블릿 필터를 이용한 인증 체크&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 &lt;br /&gt;미래에 개발될 페이지에도 접근하지 못하도록 막는 요건이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773634869114&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class LoginCheckFilter implements Filter {
	private static final String[] whiteList = {&quot;/&quot;, &quot;/members/add&quot;, &quot;/login&quot;, &quot;/logout&quot;, &quot;/css/*&quot;};

	@Override
	public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		String requestURI = httpRequest.getRequestURI();

		HttpServletResponse httpResponse = (HttpServletResponse) response;

		try {
			log.info(&quot;인증 체크 필터 시작 {}&quot;, requestURI);
			
            		if(isLoginCheckPath(requestURI)) {
				log.info(&quot;인증 체크 로직 실행 {}&quot;, requestURI);
				HttpSession session = httpRequest.getSession(false);
				
                		if(session == null || session.getAttribute(&quot;member&quot;) == null) {
					log.info(&quot;미인증 사용자 요청 {}&quot;, requestURI);
					httpResponse.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);
					return; // 미인증 사용자는 다음으로 진행하지 않고 끝
				}
			}
            
			chain.doFilter(request, response);
            
		} catch(Exception e) {
			throw e;
		} finally {
			log.info(&quot;인증 체크 필터 종료 {}&quot;, requestURI);
		}
	}


	// 화이트 리스트의 경우 인증 체크 X
	private boolean isLoginCheckPath(Strng requestURI) {
		return !PatternMatchUtils.simpleMatch(whilteList, requestURI);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;httpResponse.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다. 따라서 현재 요청한 경로인 requestURI 를 /login&amp;nbsp;&amp;nbsp;에 쿼리 파라미터로 함께 전달한다. 물론 Controller 에서 이를 받아서 후처리를 해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;return&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;필터를 더는 진행하지 않는다.&lt;br /&gt;이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다.&lt;br /&gt;앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝나게 된다.&lt;br /&gt;&lt;br /&gt;이렇게 등록된 Filter 을 사용할 수 있게 등록을 해주어야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773634980839&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public FilterRegistrationBean loginCheckFilter() {

	@Bean
	public FilterRegistrationBean loginCheckFilter() {
		FilterRegistrationBean&amp;lt;Filter&amp;gt; filterRegistrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;();

		filterRegistrationBean.setFilter(new LoginCheckFilter());
		filterRegistrationBean.getOrder(2);
		filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);

		return filterRegistrationBean;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;마지막으로 로그인 인증 Controller&amp;nbsp;&amp;nbsp;를 개발하면 된다.&lt;br /&gt;즉, 로그인에 성공하면 처음 요청한 URL 로 이동하는 기능이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773635025715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/login&quot;)
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, @RequestParam(defaultValue = &quot;/&quot;) String redirectURL, HttpServletRequest request) {
	if(bindingResult.hasErrors()) {
		return &quot;login/loginForm&quot;;
	}

	Member loginMember = loginService.login(form.getLoginId(), form.getPassword());

	if(loginMember == null) {
		bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다.&quot;);
		return &quot;login/loginForm&quot;;
	}

	// 로그인 성공 처리
	HttpSession session = request.getSession();
	session.setAttribute(&quot;login&quot;, loginMember);

	return &quot;redirect:&quot; + redirectURL;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;서블릿 필터를 잘 사용한 덕분에 로그인 하지 않은 사용자는 나머지 경로에 들어갈 수 없게 되었다.&lt;br /&gt;공통 관심사를 서블릿 필터를 사용해서 해결한 덕분에 향후 로그인 관련 정책이 변경되어도 이 부분만 변경하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;스프링 인터셉터 (Spring Interceptor)&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;스프링 인터셉터도 섭르릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.&lt;br /&gt;서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC 가 제공하는 기술이다. &lt;br /&gt;둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.&lt;br /&gt;&lt;br /&gt;스프링 인터셉터의 흐름 &lt;br /&gt;HTTP -&amp;gt; WAS -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 스프링 인터셉터 -&amp;gt; 컨트롤러 &lt;br /&gt;&lt;br /&gt;서블릿 인터셉터도 필터처럼 체인 기능을 제공한다.&lt;br /&gt;&lt;br /&gt;스프링 인터셉터 체인 &lt;br /&gt;HTTP -&amp;gt; WAS -&amp;gt; 필터 -&amp;gt; 서블릿 -&amp;gt; 스프링 인터셉터1 -&amp;gt; 스프링 인터셉터2 -&amp;gt; 컨트롤러 &lt;br /&gt;&lt;br /&gt;이러한 인터셉터 체인을 이용하여 로그르 남기는 인터셉터를 먼저 적용하고,&lt;br /&gt;그 다음에 로그인 여부를 체그하는 인터셉터를 만들 수 있다.&lt;br /&gt;&lt;br /&gt;| 스프링 인터셉터는 서블릿 필터보다 편리하고, 더 정교하고 다양한 기능을 제공한다.&lt;br /&gt;| 따라서 스프링 인터셉터를 지원하는 프로젝트는 인터셉터를 이용하는 것이 좋다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;스프링 인터셉터 인터페이스&lt;/h3&gt;
&lt;pre id=&quot;code_1773635075732&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object Handler, @Nullable ModelAndView modelAndView) throws Exception {}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;서블릿 필터의 경우 단순하게 doFilter() 하나만 제공디지만, 인터셉터는 컨트롤러 호출 전(preHandle), 컨트롤러 호출 후(PostHandle), 요청 완료 이후(afterCompletion) 아 같이 단계적으로 잘 세분화 되어 있다.&lt;br /&gt;&lt;br /&gt;인터셉터에 경우 또한 어떤 컨트롤러(Obejct handler)가 호출되는지 호출 정보를 받을 수 있다.&lt;br /&gt;그리고 어떤 modelAndView 가 반한되는지 응답 정보도 받아 볼 수 있다.&lt;br /&gt;&lt;br /&gt;- preHandle : 컨트롤러 호출 전에 호출 &lt;br /&gt;- postHandle : 컨트롤러 호출 후에 호출 &lt;br /&gt;- afterCompletion : 뷰가 렌더링 된 이후 호출 &lt;br /&gt;&lt;br /&gt;예외가 발생하면&lt;br /&gt;- preHandle : 컨트롤러 호출 전에 호출 &lt;br /&gt;- postHandle :&amp;nbsp;&amp;nbsp;컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않음 &lt;br /&gt;- afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외(ex) 를 파라미터로 받아서&amp;nbsp;&amp;nbsp;어떤 예외가 발생했는지 로그로 출력이 가능하다.&lt;br /&gt;&lt;br /&gt;afterCompletion 은 어떤 경우에도 호출되기 때문에, 예외가 발생하면 postHandle 은 호출되지 않으므로 예이와 무관하게 공통 처리를 하고 싶다면 afterCompletion 을 사용하면 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;등록 &lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;서블릿 인터셉터를 등록하기 위해서는 필터처럼 따로 @Configuration 어노테이션을 이용하여 WebMvcConfigurer 인터페이스를 상속받아 등록을 해주면 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1773635119521&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new LogInterceptor())
		.order(1)
		.addPathPatterns(&quot;/**&quot;)
		.excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>Java</category>
      <category>Filter</category>
      <category>Interceptor</category>
      <category>Java</category>
      <author>Chan Dev</author>
      <guid isPermaLink="true">https://chanyoungcoding.tistory.com/137</guid>
      <comments>https://chanyoungcoding.tistory.com/137#entry137comment</comments>
      <pubDate>Mon, 16 Mar 2026 13:26:55 +0900</pubDate>
    </item>
  </channel>
</rss>