<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>김코딩</title>
    <link>https://ddokyun.tistory.com/</link>
    <description>나는 할 수 있다!</description>
    <language>ko</language>
    <pubDate>Sun, 7 Jun 2026 21:38:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>김코딩딩</managingEditor>
    <item>
      <title>[Spring Batch] 4편. 실전 코드 패턴과 Reader/Processor/Writer</title>
      <link>https://ddokyun.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시리즈 마지막 편이다. 3편까지 읽었으면 Spring Batch의 기본 구조는 모두 정리한 셈이다. 이번 편에서는 3편의 간단한 예제보다 &lt;b&gt;한 걸음 더 실무에 가까운 코드&lt;/b&gt;를 다루면서, 운영에서 마주치는 대표적인 고민들을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서 다룰 내용은 네 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Processor가 포함된 실전 시나리오 코드&lt;/li&gt;
&lt;li&gt;Processor의 역할 (변환 vs 필터링)&lt;/li&gt;
&lt;li&gt;JPA Writer의 함정과 JdbcBatchItemWriter와의 비교&lt;/li&gt;
&lt;li&gt;실제로 배치를 실행하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;배치를 학습하면서 느낀 건, 프레임워크의 추상화를 맹목적으로 쓰면 오히려 성능 문제를 만들 수 있다는 점이다. 특히 Writer 쪽이 그렇다. &quot;saveAll 한 줄이면 끝이네&quot;라고 생각하면 나중에 운영에서 크게 당할 수 있다. 그 지점을 이번 편에서 풀어본다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3편 예제 vs 4편 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편에서는 &lt;b&gt;탈퇴 회원의 이메일을 익명화하는 배치&lt;/b&gt;를 만들었다. 단순 수정 작업이었기 때문에 Reader와 Writer만으로 충분했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서는 한 단계 더 복잡한 시나리오를 다룬다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어제 구매 금액이 10만원 이상인 회원의 등급을 VIP로 변경한다. 단, 이미 VIP 회원이라면 건너뛴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시나리오는 조건 필터링과 상태 변경이 함께 필요하다. Reader가 조회한 회원 중 일부만 저장해야 하고, 각 회원마다 도메인 로직(등급 변경)이 적용되어야 한다. 이럴 때 &lt;b&gt;Processor가 등장&lt;/b&gt;한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;883&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HC0LQ/dJMcadn9cEO/kgUyEu2QkePf551qTzRAqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HC0LQ/dJMcadn9cEO/kgUyEu2QkePf551qTzRAqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HC0LQ/dJMcadn9cEO/kgUyEu2QkePf551qTzRAqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHC0LQ%2FdJMcadn9cEO%2FkgUyEu2QkePf551qTzRAqk%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;1785&quot; height=&quot;883&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;883&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편 예제는 단순 수정이라 Reader &amp;rarr; Writer만으로 충분했다. 4편 예제는 도메인 로직과 필터링이 필요하므로 Processor가 중간에 들어간다. 이 차이를 염두에 두고 코드를 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Processor의 두 가지 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Processor는 두 가지 역할을 한다. 각각을 이해하면 언제 Processor를 넣고 언제 뺄지 감이 잡힌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GaxkT/dJMcag6hqn9/H0QO6mD8emzU1eEhWKQtV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GaxkT/dJMcag6hqn9/H0QO6mD8emzU1eEhWKQtV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GaxkT/dJMcag6hqn9/H0QO6mD8emzU1eEhWKQtV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGaxkT%2FdJMcag6hqn9%2FH0QO6mD8emzU1eEhWKQtV0%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;1785&quot; height=&quot;959&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할 1. 변환 (Transform)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader가 준 데이터를 가공해서 반환한다. 같은 타입으로 상태만 변경할 수도 있고, 완전히 다른 타입으로 바꿀 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;// Member의 등급을 VIP로 변경해서 같은 타입으로 반환
ItemProcessor&amp;lt;Member, Member&amp;gt; processor = member -&amp;gt; {
    member.upgradeToVip();
    return member;
};

// Member를 MemberGradeHistory DTO로 변환
ItemProcessor&amp;lt;Member, MemberGradeHistory&amp;gt; processor = member -&amp;gt;
    new MemberGradeHistory(member.getId(), OLD, VIP, now());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할 2. 필터링 (Filter)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 만족하지 않는 데이터는 &lt;b&gt;&lt;code&gt;null&lt;/code&gt;을 반환&lt;/b&gt;한다. &lt;code&gt;null&lt;/code&gt;이 반환된 건은 &lt;b&gt;Writer로 전달되지 않는다&lt;/b&gt;.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If, while processing the item, it is determined that the item is not valid, returning null indicates that the item should not be written out.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;ItemProcessor&amp;lt;Member, Member&amp;gt; processor = member -&amp;gt; {
    // 이미 VIP면 건너뜀
    if (member.isVip()) {
        return null;
    }
    member.upgradeToVip();
    return member;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 팁: 언제 Processor를 넣나&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader와 Writer 사이에 &lt;b&gt;비즈니스 로직&lt;/b&gt;이나 &lt;b&gt;필터링&lt;/b&gt;이 필요하면 Processor를 추가한다. 반대로 &quot;DB에서 읽은 걸 그대로 다른 곳에 쓴다&quot; 같은 단순 이동 작업은 Processor 없이도 충분하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;처음에는 &quot;Processor를 쓰면 뭐가 좋지?&quot;라고 생각했다. Writer 안에서 if로 걸러내도 되잖아? 하지만 관점을 바꿔보면 명확하다. Reader는 &quot;데이터 가져오기&quot;, Processor는 &quot;비즈니스 로직&quot;, Writer는 &quot;저장&quot;이라는 단일 책임을 지킬 수 있다. 덕분에 각 Bean을 독립적으로 테스트하기도 쉬워진다. 결국 객체지향 설계 원칙을 Spring Batch 레벨로 확장한 것이라고 이해했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 코드: VIP 등급 변경 배치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 코드를 보자. 3편 예제와 구조가 거의 같지만 Processor가 추가되어 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class MemberVipBatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final MemberRepository memberRepository;

    // ① Job 정의
    @Bean
    public Job memberVipJob() {
        return new JobBuilder(&quot;memberVipJob&quot;, jobRepository)
                .start(upgradeVipStep())
                .build();
    }

    // ② Step 정의 (Processor 포함)
    @Bean
    public Step upgradeVipStep() {
        return new StepBuilder(&quot;upgradeVipStep&quot;, jobRepository)
                .&amp;lt;Member, Member&amp;gt;chunk(1000, transactionManager)
                .reader(yesterdayBuyerReader())
                .processor(vipUpgradeProcessor())  // Processor 추가
                .writer(memberWriter())
                .build();
    }

    // ③ Reader - 어제 구매 금액 10만원 이상 회원 조회
    @Bean
    public ItemReader&amp;lt;Member&amp;gt; yesterdayBuyerReader() {
        return new RepositoryItemReaderBuilder&amp;lt;Member&amp;gt;()
                .name(&quot;yesterdayBuyerReader&quot;)
                .repository(memberRepository)
                .methodName(&quot;findByYesterdayPurchaseOver&quot;)
                .arguments(List.of(100_000))
                .pageSize(1000)
                .sorts(Map.of(&quot;id&quot;, Sort.Direction.ASC))
                .build();
    }

    // ④ Processor - 이미 VIP면 필터링, 아니면 등급 변경
    @Bean
    public ItemProcessor&amp;lt;Member, Member&amp;gt; vipUpgradeProcessor() {
        return member -&amp;gt; {
            if (member.isVip()) {
                return null;  // 필터링: Writer로 전달 안 됨
            }
            member.upgradeToVip();  // 변환: 상태 변경
            return member;
        };
    }

    // ⑤ Writer - JPA saveAll
    @Bean
    public ItemWriter&amp;lt;Member&amp;gt; memberWriter() {
        return items -&amp;gt; memberRepository.saveAll(items);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3편 코드와 거의 같은 구조에, &lt;code&gt;vipUpgradeProcessor()&lt;/code&gt; 하나만 추가되었을 뿐이다. Step 빌더에 &lt;code&gt;.processor(...)&lt;/code&gt; 한 줄이 추가되었고, Processor 내부에서 필터링과 변환이 함께 일어난다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA Writer의 함정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점이 있다. 위 코드의 Writer는 &lt;code&gt;memberRepository.saveAll(items)&lt;/code&gt; 이지만, &lt;b&gt;이게 생각보다 느릴 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCGACN/dJMcaf0zCln/YeXxdI76qhiBbZl7Fvanpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCGACN/dJMcaf0zCln/YeXxdI76qhiBbZl7Fvanpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCGACN/dJMcaf0zCln/YeXxdI76qhiBbZl7Fvanpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCGACN%2FdJMcaf0zCln%2FYeXxdI76qhiBbZl7Fvanpk%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;1785&quot; height=&quot;1034&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;saveAll(items)&lt;/code&gt;는 이름만 보면 한 번에 전부 저장하는 &lt;b&gt;Bulk Insert&lt;/b&gt;처럼 보이지만, 실제로는 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 영속성 컨텍스트 안에서 각 엔티티의 변경 여부를 추적한다 (&lt;b&gt;Dirty Checking&lt;/b&gt;). 1,000건을 &lt;code&gt;saveAll&lt;/code&gt;에 넘기면 JPA는 각 엔티티를 하나씩 순회하면서 변경된 필드를 감지하고, 변경된 것에 대해 &lt;b&gt;개별 UPDATE 쿼리&lt;/b&gt;를 생성한다. 결과적으로 1,000번의 UPDATE 쿼리가 DB로 날아간다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;UPDATE member SET grade = 'VIP' WHERE id = 1;
UPDATE member SET grade = 'VIP' WHERE id = 2;
UPDATE member SET grade = 'VIP' WHERE id = 3;
-- ... (1,000번 반복)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 &lt;b&gt;JPA Writer의 함정&lt;/b&gt;이다. 코드 한 줄로 끝나는 것처럼 보이지만 내부에서는 개별 쿼리가 반복 실행된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;Flash Deal 프로젝트에서 TPS 최적화를 할 때도 비슷한 경험이 있었다. &quot;한 줄 코드&quot;라고 해서 &quot;한 번의 DB 작업&quot;이 되는 건 아니다. 특히 ORM을 쓸 때는 추상화 뒤에서 실제로 어떤 쿼리가 나가는지 반드시 확인해야 한다. show_sql=true 설정이나 p6spy 같은 도구가 필수인 이유가 여기에 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해결책: JdbcBatchItemWriter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 insert/update가 필요할 때는 &lt;code&gt;JdbcBatchItemWriter&lt;/code&gt;를 쓴다. JPA를 우회하고 JDBC 배치 업데이트로 한 번에 여러 건을 처리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cor4ph/dJMcacivGRx/x0L9Hv8bmvz5jEzv0ECgGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cor4ph/dJMcacivGRx/x0L9Hv8bmvz5jEzv0ECgGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cor4ph/dJMcacivGRx/x0L9Hv8bmvz5jEzv0ECgGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcor4ph%2FdJMcacivGRx%2Fx0L9Hv8bmvz5jEzv0ECgGK%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;1785&quot; height=&quot;959&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
public ItemWriter&amp;lt;Member&amp;gt; jdbcMemberWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder&amp;lt;Member&amp;gt;()
            .dataSource(dataSource)
            .sql(&quot;UPDATE member SET grade = :grade WHERE id = :id&quot;)
            .beanMapped()
            .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 JDBC 배치 업데이트가 적용되어, 1,000건을 처리하는 데 드는 쿼리 횟수가 획기적으로 줄어든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 Writer를 써야 할까&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 Writer는 &lt;b&gt;트레이드 오프 관계&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA Writer (&lt;code&gt;saveAll&lt;/code&gt;)를 쓰는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 양이 적고 성능이 크리티컬하지 않을 때&lt;/li&gt;
&lt;li&gt;엔티티의 라이프사이클(영속성 컨텍스트, 이벤트, 캐스케이드)이 중요할 때&lt;/li&gt;
&lt;li&gt;도메인 로직을 엔티티 메서드로 활용해야 할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;JdbcBatchItemWriter&lt;/code&gt;를 쓰는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 insert/update로 성능이 크리티컬할 때&lt;/li&gt;
&lt;li&gt;단순 데이터 이동 작업이라 엔티티 라이프사이클이 필요 없을 때&lt;/li&gt;
&lt;li&gt;메모리 사용량을 최소화해야 할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;이번 예제 코드에서는 편의상 saveAll을 썼지만, 실제 100만 건 처리 배치라면 JdbcBatchItemWriter를 쓰는 게 맞다. 실무에서는 &quot;간결한 코드&quot;와 &quot;실행 시간&quot;을 저울질해야 하는 상황이 많다. 특히 SLA가 있는 배치라면 더더욱.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배치 실행하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 Job을 Bean으로 정의하는 것까지만 다뤘다. 실제로 이 배치를 &lt;b&gt;어떻게 실행하는지&lt;/b&gt;는 다루지 않았는데, 이 부분으로 시리즈를 마무리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/W56kd/dJMcacivGRF/HnQ6NOwSKy2sSRz8FiKgOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/W56kd/dJMcacivGRF/HnQ6NOwSKy2sSRz8FiKgOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/W56kd/dJMcacivGRF/HnQ6NOwSKy2sSRz8FiKgOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FW56kd%2FdJMcacivGRF%2FHnQ6NOwSKy2sSRz8FiKgOk%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;1785&quot; height=&quot;1109&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1109&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 1. 스케줄러에서 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 방법은 &lt;code&gt;@Scheduled&lt;/code&gt;로 정해진 시간에 &lt;code&gt;JobLauncher&lt;/code&gt;를 호출하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class MemberVipScheduler {

    private final JobLauncher jobLauncher;
    private final Job memberVipJob;

    @Scheduled(cron = &quot;0 0 3 * * *&quot;)  // 매일 새벽 3시
    public void runVipJob() throws Exception {
        JobParameters params = new JobParametersBuilder()
                .addString(&quot;date&quot;, LocalDate.now().minusDays(1).toString())
                .addLong(&quot;timestamp&quot;, System.currentTimeMillis())
                .toJobParameters();

        jobLauncher.run(memberVipJob, params);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편에서 다룬 내용이 여기서 등장한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;date&lt;/code&gt; 파라미터로 &lt;b&gt;의미 있는 JobInstance 구분&lt;/b&gt; (어제 날짜 기준)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt; 파라미터는 &lt;b&gt;강제로 매번 다른 Instance를 만들기 위한 값&lt;/b&gt;. 개발/테스트 단계에서 유용하지만 운영에서는 주의해서 사용해야 한다 (중복 실행 방지 기능이 무력화되기 때문에)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 2. 수동 실행 (REST API 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중 수동으로 실행해야 할 때는 REST 컨트롤러를 열어두기도 한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class BatchController {

    private final JobLauncher jobLauncher;
    private final Job memberVipJob;

    @PostMapping(&quot;/admin/batch/vip&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; runVipJob(@RequestParam String date) throws Exception {
        JobParameters params = new JobParametersBuilder()
                .addString(&quot;date&quot;, date)
                .addLong(&quot;timestamp&quot;, System.currentTimeMillis())
                .toJobParameters();

        JobExecution execution = jobLauncher.run(memberVipJob, params);
        return ResponseEntity.ok(&quot;실행됨: &quot; + execution.getStatus());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행 결과 확인하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치가 끝나면 메타데이터 테이블에 결과가 기록된다. 2편에서 다룬 그 테이블들이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 이 Job의 최근 실행 이력
SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE
FROM BATCH_JOB_EXECUTION je
JOIN BATCH_JOB_INSTANCE ji ON je.JOB_INSTANCE_ID = ji.JOB_INSTANCE_ID
WHERE ji.JOB_NAME = 'memberVipJob'
ORDER BY START_TIME DESC
LIMIT 10;

-- 특정 실행의 Step별 처리 건수
SELECT STEP_NAME, STATUS, READ_COUNT, WRITE_COUNT, FILTER_COUNT, ROLLBACK_COUNT
FROM BATCH_STEP_EXECUTION
WHERE JOB_EXECUTION_ID = ?;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;FILTER_COUNT&lt;/code&gt;가 주목할 만한 지표다. &lt;b&gt;Processor가 &lt;code&gt;null&lt;/code&gt;을 반환해서 걸러낸 건수&lt;/b&gt;가 이 컬럼에 기록된다. VIP 배치라면 &quot;이미 VIP였던 회원&quot;이 몇 명이었는지를 이 값으로 확인할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;메타데이터 테이블의 위력이 이 지점에서 실감난다. 운영 중 &quot;어제 VIP 배치에서 얼마나 걸렀어?&quot;라는 질문에 쿼리 한 번으로 답할 수 있다. 로그를 뒤지는 것보다 훨씬 빠르다. 2편에서 메타데이터 테이블의 존재 이유를 다뤘을 때는 감이 잘 안 왔는데, 4편에서 실제 운영 쿼리를 써보니 왜 이 테이블들이 자동으로 만들어지는지 이해가 됐다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1편&lt;/b&gt;: Spring Batch가 해결하는 5가지 문제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2편&lt;/b&gt;: Job, JobInstance, JobExecution의 관계와 메타데이터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3편&lt;/b&gt;: Chunk 기반 처리의 원리, Job/Step 구조, Processor 없는 예제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4편&lt;/b&gt;: Processor가 포함된 실전 코드, JPA Writer의 함정, 실행 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 Spring Batch 전체를 다루는 가이드가 아니라, &lt;b&gt;실무 투입 전에 반드시 알아야 할 기초&lt;/b&gt;를 정리한 것이다. 이 위에 쌓아야 할 주제는 아직 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Retry / Skip 전략의 실전 활용&lt;/li&gt;
&lt;li&gt;JobParameters 검증과 &lt;code&gt;@JobScope&lt;/code&gt;, &lt;code&gt;@StepScope&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Reader 종류별 성능 비교 (JpaPagingItemReader vs JdbcCursorItemReader)&lt;/li&gt;
&lt;li&gt;Chunk size 튜닝, JDBC Writer 최적화&lt;/li&gt;
&lt;li&gt;조건부 Step Flow 제어&lt;/li&gt;
&lt;li&gt;Listener를 통한 모니터링 및 알림&lt;/li&gt;
&lt;li&gt;대용량 파티셔닝과 병렬 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;여기서 일단 멈추기로 했다. 위 주제들을 지금 다 정리하려면 분량도 방대하고, 아직 실무에서 부딪혀보지 않은 내용이 많아서 깊이가 부족해진다. 현업에서 실제로 Spring Batch를 돌려보고, 장애를 겪어보고, 성능 이슈를 마주친 뒤에 그 경험을 글로 정리하는 것이 나한테도 독자한테도 더 가치 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웨이트리프팅에서도 똑같다. 100kg 들어본 적 없는 사람이 150kg 스쿼트 팁을 정리할 수는 없다. 배운 만큼, 해본 만큼 기록한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주부터 실무에서 Spring Batch를 실제로 돌려보고, 거기서 나오는 문제와 해결 과정을 또 글로 남기려 한다. 시리즈는 여기서 마무리하지만, Spring Batch 학습은 계속된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing.html&quot;&gt;Spring Batch Reference: Chunk-oriented Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/readers-and-writers/item-writer.html&quot;&gt;Spring Batch Reference: ItemWriter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/readers-and-writers/database/jdbc-batch-item-writer.html&quot;&gt;Spring Batch Reference: JdbcBatchItemWriter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-batch-tasklet-chunk&quot;&gt;Baeldung: Spring Batch - Tasklets vs Chunks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Batch</category>
      <category>Spring Batch</category>
      <category>배치</category>
      <category>스케줄러</category>
      <category>스프링</category>
      <category>스프링 배치</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/92</guid>
      <comments>https://ddokyun.tistory.com/92#entry92comment</comments>
      <pubDate>Sat, 18 Apr 2026 20:30:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 3편. Chunk 기반 처리의 원리와 Job/Step 구조</title>
      <link>https://ddokyun.tistory.com/91</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서는 Spring Batch가 왜 필요한지, 2편에서는 Job/JobInstance/JobExecution의 관계를 다뤘다. 여기까지는 &quot;무엇을 위한 프레임워크인가&quot;에 대한 이야기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편부터는 드디어 실제 구조와 코드를 본다. 이 글에서는 Spring Batch의 &lt;b&gt;실행 단위인 Step&lt;/b&gt;이 어떻게 생겼는지, 그리고 &lt;b&gt;Chunk 기반 처리&lt;/b&gt;가 내부에서 어떻게 돌아가는지 정리한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;개념을 탄탄히 다진 뒤에 코드를 보니 이해 속도가 확실히 달랐다. 웨이트리프팅을 할 때도 기본 자세가 잡혀있으면 새로운 종목을 배울 때 훨씬 빠르게 익혀지는 것과 같은 감각이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job은 Step의 묶음이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 Job이라는 단어를 여러 번 썼지만, 실제로 &lt;b&gt;Job 안에는 하나 이상의 Step이 들어있다&lt;/b&gt;. Job 자체가 데이터를 처리하는 게 아니라, Step들이 순서대로 실행되면서 실제 작업을 수행한다.&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;1785&quot; data-origin-height=&quot;885&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kQbWn/dJMcajhAoKz/x0l4Tnw7JR0snLjFxUitKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kQbWn/dJMcajhAoKz/x0l4Tnw7JR0snLjFxUitKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kQbWn/dJMcajhAoKz/x0l4Tnw7JR0snLjFxUitKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkQbWn%2FdJMcajhAoKz%2Fx0l4Tnw7JR0snLjFxUitKK%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;1785&quot; height=&quot;885&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;885&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;저녁 준비&quot;라는 Job은 세 가지 Step으로 나뉜다. 장보기, 요리하기, 설거지. 만약 요리하기 단계에서 실수가 있었다면 장보기를 다시 할 필요는 없다. 이미 완료된 단계는 건너뛰고 실패한 단계부터 다시 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch도 똑같다. 일일 정산 Job을 &quot;거래 조회 &amp;rarr; 정산 계산 &amp;rarr; 결과 저장&quot; 같은 Step으로 나눠두면, 정산 계산에서 실패했을 때 &lt;b&gt;거래 조회를 다시 하지 않고 정산 계산부터 이어서 실행할 수 있다&lt;/b&gt;. 이 상태는 2편에서 다룬 &lt;code&gt;BATCH_STEP_EXECUTION&lt;/code&gt; 테이블에 기록된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;처음 이 구조를 봤을 때는 &quot;그냥 하나의 Step으로 다 하면 안 되나?&quot;라고 생각했다. 하지만 운영 중 장애를 떠올려보면 답이 나온다. 1시간짜리 배치에서 거의 끝날 때쯤 실패했는데 처음부터 다시 돌려야 한다면? 실패 포인트가 명확하게 구분되어 있을수록 운영 비용이 줄어든다. Step 분리는 &quot;언제든 장애가 날 수 있다&quot;는 전제 위에서 설계된 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Step의 두 가지 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Step을 구현하는 방식에는 두 가지가 있다. 이 구분이 중요한 이유는, 실무에서 &lt;b&gt;어떤 방식을 선택할지 고민하는 첫 번째 지점&lt;/b&gt;이기 때문이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oOWDy/dJMcafl0n8o/Xd2ohBj4zNfkYurDoSzD60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oOWDy/dJMcafl0n8o/Xd2ohBj4zNfkYurDoSzD60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oOWDy/dJMcafl0n8o/Xd2ohBj4zNfkYurDoSzD60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoOWDy%2FdJMcafl0n8o%2FXd2ohBj4zNfkYurDoSzD60%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;1785&quot; height=&quot;959&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;959&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chunk 기반 Step&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 일정 단위로 읽고, 가공하고, 저장하는 과정을 &lt;b&gt;반복&lt;/b&gt;하는 방식이다. Spring Batch를 쓰는 이유 대부분이 여기에 해당한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch uses a &quot;chunk-oriented&quot; processing style in its most common implementation.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing.html&quot;&gt;Spring Batch Reference: Chunk-oriented Processing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 100만 명의 등급을 재계산하거나, 거래 내역을 일괄 정산하거나, 로그 파일을 DB로 적재하는 등 &lt;b&gt;대량 데이터를 반복 처리&lt;/b&gt;해야 할 때 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tasklet 기반 Step&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 반복 없이 &lt;b&gt;한 번 실행하고 끝나는&lt;/b&gt; 단순 작업에 쓴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오래된 파일 삭제&lt;/li&gt;
&lt;li&gt;배치 시작 전 디렉토리 초기화&lt;/li&gt;
&lt;li&gt;외부 API 한 번만 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단발성 작업을 억지로 Chunk 방식으로 만들면 오히려 코드가 어색해진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;실무에서 마주칠 일의 대부분은 Chunk 기반일 것이다. Tasklet은 &quot;이런 것도 있구나&quot; 정도로 알아두고, 실제로 뭔가 준비/정리 성격의 전후 작업을 할 때 떠올리는 정도면 충분하다고 느꼈다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Chunk 기반 Step의 3요소&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk 기반 Step은 세 가지 구성 요소로 이루어진다. ItemReader, ItemProcessor, ItemWriter다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcckdR/dJMcai313zR/zLVKQP0YuzStkEUFoRqtsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcckdR/dJMcai313zR/zLVKQP0YuzStkEUFoRqtsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcckdR/dJMcai313zR/zLVKQP0YuzStkEUFoRqtsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcckdR%2FdJMcai313zR%2FzLVKQP0YuzStkEUFoRqtsK%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;1785&quot; height=&quot;1108&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소의 역할은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ItemReader - 1건씩 읽는다&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 소스(DB, 파일, API 등)에서 데이터를 가져오는 책임&lt;/li&gt;
&lt;li&gt;내부적으로는 페이징이나 커서로 조회하지만, &lt;b&gt;반환은 1건씩&lt;/b&gt; 한다&lt;/li&gt;
&lt;li&gt;이 덕분에 100만 건을 한 번에 메모리에 올리지 않는다 (OOM 방지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ItemProcessor - 1건씩 가공한다 (선택 사항)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reader가 준 데이터를 변환하거나 필터링한다&lt;/li&gt;
&lt;li&gt;변환: 입력 타입과 다른 타입으로 바꿔서 반환 가능&lt;/li&gt;
&lt;li&gt;필터: &lt;code&gt;null&lt;/code&gt;을 반환하면 해당 건은 Writer로 전달되지 않는다&lt;/li&gt;
&lt;li&gt;필요 없으면 생략할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ItemWriter - Chunk 단위로 저장한다&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Processor가 준 결과를 &lt;b&gt;모아서 한 번에&lt;/b&gt; 저장한다&lt;/li&gt;
&lt;li&gt;Chunk size가 1,000이면 1,000건짜리 &lt;code&gt;List&lt;/code&gt;를 받아서 &lt;code&gt;saveAll&lt;/code&gt;이나 bulk insert 수행&lt;/li&gt;
&lt;li&gt;여기가 &lt;b&gt;성능 최적화의 핵심 포인트&lt;/b&gt;다&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;이 설계가 정말 영리하다고 느꼈다. 읽기는 1건씩(메모리 안정성), 쓰기는 모아서(성능). 혼자서는 절대 한 번에 이런 구조로 설계하지 못했을 것이다. 현업에서 대용량 배치를 짜본 사람들의 노하우가 프레임워크 레벨로 녹아있는 느낌이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Chunk 처리 사이클&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위 3요소가 어떻게 돌아가는지 전체 흐름을 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pe9kQ/dJMcah5cgpy/T2wcBuo9boYJ5t7zBBeSQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pe9kQ/dJMcah5cgpy/T2wcBuo9boYJ5t7zBBeSQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pe9kQ/dJMcah5cgpy/T2wcBuo9boYJ5t7zBBeSQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpe9kQ%2FdJMcah5cgpy%2FT2wcBuo9boYJ5t7zBBeSQ0%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;1785&quot; height=&quot;1034&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chunk size가 1,000이라면 한 사이클은 다음과 같이 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Read 단계&lt;/b&gt; - Reader가 1건씩 1,000번 호출되어 1,000건을 모은다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Process 단계&lt;/b&gt; - Processor가 1건씩 1,000번 호출되어 1,000건을 가공한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Write 단계&lt;/b&gt; - Writer가 1,000건짜리 List를 받아서 한 번에 저장한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Commit&lt;/b&gt; - 트랜잭션을 커밋한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 단계가 하나의 Chunk를 이루고, 전체 데이터가 다 처리될 때까지 &lt;b&gt;반복&lt;/b&gt;된다. 공식 문서의 의사 코드로 보면 이 흐름이 더 명확하다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;List items = new ArrayList();
for (int i = 0; i &amp;lt; commitInterval; i++) {
    Object item = itemReader.read();
    if (item != null) {
        items.add(item);
    }
}

List processedItems = new ArrayList();
for (Object item : items) {
    Object processedItem = itemProcessor.process(item);
    if (processedItem != null) {
        processedItems.add(processedItem);
    }
}

itemWriter.write(processedItems);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 블록 전체가 &lt;b&gt;하나의 트랜잭션 안에서 실행&lt;/b&gt;된다. 즉 &lt;b&gt;Chunk size = 트랜잭션 단위&lt;/b&gt;라는 말이 여기서 확인된다. 1편에서 &quot;Chunk는 메모리 단위이자 트랜잭션 단위&quot;라고 했던 이야기가 바로 이 구조에서 나온 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드 구조 미리보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 이해했으면, Spring Batch 코드가 대략 어떻게 생겼는지 그림으로 그릴 수 있다. 구체적인 코드는 잠시 뒤에 보고, 먼저 &lt;b&gt;Bean들이 어떻게 연결되는지&lt;/b&gt; 구조부터 확인하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HaRLw/dJMcah5cgpJ/VPi0r0XAdc8Gmdypra9io0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HaRLw/dJMcah5cgpJ/VPi0r0XAdc8Gmdypra9io0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HaRLw/dJMcah5cgpJ/VPi0r0XAdc8Gmdypra9io0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHaRLw%2FdJMcah5cgpJ%2FVPi0r0XAdc8Gmdypra9io0%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;1785&quot; height=&quot;1184&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 &lt;code&gt;@Configuration&lt;/code&gt; 클래스 안에 네 개의 &lt;code&gt;@Bean&lt;/code&gt;이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Bean Job&lt;/code&gt;: 어떤 Step들을 순서대로 실행할지 정의&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Bean Step&lt;/code&gt;: Chunk size와 Reader, Writer를 연결&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Bean ItemReader&lt;/code&gt;: DB에서 1건씩 읽어오는 역할&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Bean ItemWriter&lt;/code&gt;: Chunk 단위로 저장하는 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(간단한 예제라 Processor는 생략했다. 필요하면 &lt;code&gt;@Bean ItemProcessor&lt;/code&gt;를 추가하면 된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조가 &lt;b&gt;Spring Batch 코드의 90%&lt;/b&gt;이다. 어떤 배치든 이 뼈대에서 Reader/Processor/Writer 부분의 내용만 달라진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;간단한 코드 예제&lt;/h2&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 새벽, 탈퇴한 회원의 이메일을 익명화한다. 탈퇴 시 개인정보를 즉시 삭제할 수 없는 법적 보관 기간이 있지만, 이메일은 바로 마스킹 처리해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 배치는 &lt;b&gt;Processor가 필요 없다&lt;/b&gt;. DB에서 탈퇴 회원을 조회해서, 이메일을 &lt;code&gt;deleted-{id}@masked.local&lt;/code&gt; 같은 형식으로 바꿔서 저장하면 끝이다. Reader와 Writer만으로 구현할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
@RequiredArgsConstructor
public class MemberAnonymizeBatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final MemberRepository memberRepository;

    // ① Job 정의
    @Bean
    public Job memberAnonymizeJob() {
        return new JobBuilder(&quot;memberAnonymizeJob&quot;, jobRepository)
                .start(anonymizeStep())
                .build();
    }

    // ② Step 정의 (Chunk 기반)
    @Bean
    public Step anonymizeStep() {
        return new StepBuilder(&quot;anonymizeStep&quot;, jobRepository)
                .&amp;lt;Member, Member&amp;gt;chunk(1000, transactionManager)
                .reader(withdrawnMemberReader())
                .writer(anonymizeWriter())
                .build();
    }

    // ③ Reader - 탈퇴한 회원 조회
    @Bean
    public ItemReader&amp;lt;Member&amp;gt; withdrawnMemberReader() {
        return new RepositoryItemReaderBuilder&amp;lt;Member&amp;gt;()
                .name(&quot;withdrawnMemberReader&quot;)
                .repository(memberRepository)
                .methodName(&quot;findByStatus&quot;)
                .arguments(List.of(MemberStatus.WITHDRAWN))
                .pageSize(1000)
                .sorts(Map.of(&quot;id&quot;, Sort.Direction.ASC))
                .build();
    }

    // ④ Writer - 이메일 마스킹 후 저장
    @Bean
    public ItemWriter&amp;lt;Member&amp;gt; anonymizeWriter() {
        return items -&amp;gt; {
            items.forEach(member -&amp;gt; member.anonymizeEmail());
            memberRepository.saveAll(items);
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 한 줄씩 해석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;① Job 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;new JobBuilder(&quot;memberAnonymizeJob&quot;, jobRepository)
    .start(anonymizeStep())
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2편에서 다뤘듯이 &lt;code&gt;&quot;memberAnonymizeJob&quot;&lt;/code&gt;이 Job 이름이고, 이 이름 + 파라미터 조합이 JobInstance의 유일성을 결정한다. &lt;code&gt;.start()&lt;/code&gt;로 첫 Step을 연결하고, 여러 Step이 있다면 &lt;code&gt;.next(step2()).next(step3())&lt;/code&gt; 식으로 체이닝한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② Step 정의&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;.&amp;lt;Member, Member&amp;gt;chunk(1000, transactionManager)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많은 게 설정된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;Member, Member&amp;gt;&lt;/code&gt;: Reader가 주는 타입 &amp;rarr; Writer가 받는 타입. (Processor가 있으면 Processor 입출력 타입이 중간에 들어간다.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1000&lt;/code&gt;: Chunk size. 1,000건마다 커밋된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transactionManager&lt;/code&gt;: Chunk 단위로 이 트랜잭션 매니저가 커밋을 담당한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ Reader&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RepositoryItemReaderBuilder
    .methodName(&quot;findByStatus&quot;)
    .arguments(List.of(MemberStatus.WITHDRAWN))
    .pageSize(1000)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;RepositoryItemReader&lt;/code&gt;는 Spring Data JPA Repository를 Reader로 감싸주는 구현체다. 내부적으로 &lt;code&gt;findByStatus(WITHDRAWN, PageRequest.of(0, 1000))&lt;/code&gt; 같은 페이징 쿼리를 반복 호출해서 1,000건씩 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pageSize와 chunkSize는 보통 같은 값으로 맞춘다.&lt;/b&gt; 다르게 해도 동작은 하지만, 쿼리 호출 횟수와 커밋 주기가 어긋나서 성능 측면에서 손해를 볼 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reader 종류(JpaPagingItemReader, JdbcCursorItemReader 등)별 차이점은 4편에서 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;④ Writer&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;items -&amp;gt; {
    items.forEach(member -&amp;gt; member.anonymizeEmail());
    memberRepository.saveAll(items);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Writer는 &lt;code&gt;ItemWriter&amp;lt;Member&amp;gt;&lt;/code&gt; 함수형 인터페이스를 람다로 구현한 것이다. 여기서 &lt;code&gt;items&lt;/code&gt;는 &lt;b&gt;Reader가 읽어온 1,000건짜리 List&lt;/b&gt;다. 각 회원의 이메일을 마스킹하고, &lt;code&gt;saveAll&lt;/code&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;withdrawnMemberReader&lt;/code&gt;가 탈퇴 회원 1번을 읽는다&lt;/li&gt;
&lt;li&gt;1,000건이 모일 때까지 Reader를 반복 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;anonymizeWriter&lt;/code&gt;가 1,000건짜리 List를 받아 이메일 마스킹 + &lt;code&gt;saveAll&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋&lt;/li&gt;
&lt;li&gt;다음 1,000건(1,001~2,000) 처리 &amp;rarr; 또 커밋&lt;/li&gt;
&lt;li&gt;탈퇴 회원이 더 없을 때까지 반복&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;실제 코드를 보니 개념 공부한 게 하나하나 연결되는 느낌이었다. &quot;Chunk size가 1,000이면 1,000건마다 커밋된다&quot;는 문장이 추상적이었는데, chunk(1000, transactionManager) 한 줄에 다 담겨있는 걸 보고 명확해졌다. 프레임워크가 좋은 추상화를 제공한다는 게 이런 뜻이구나 싶었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서 정리한 내용은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job은 Step의 묶음&lt;/b&gt;이고, Step 단위로 성공/실패가 기록된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chunk 기반 Step&lt;/b&gt;은 Read &amp;rarr; Process &amp;rarr; Write &amp;rarr; Commit 사이클을 반복한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chunk size = 트랜잭션 단위&lt;/b&gt;다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Reader는 1건씩 읽고, Writer는 모아서 저장&lt;/b&gt;해서 메모리 안정성과 성능을 동시에 잡는다&lt;/li&gt;
&lt;li&gt;Spring Batch 코드의 기본 구조는 &lt;code&gt;@Configuration&lt;/code&gt; 안의 Job/Step/Reader/Writer Bean 구성이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 이번 편의 간단한 예제보다 &lt;b&gt;더 실무에 가까운 코드&lt;/b&gt;를 본다. Processor가 포함된 구조와, 두 예제의 차이를 비교하면서 &quot;Processor가 왜 필요한가&quot;와 &quot;Writer 선택이 성능에 어떤 영향을 주는가&quot;를 정리할 예정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편에서는 어제 구매 금액 10만원 이상 회원의 등급을 VIP로 변경하는 배치를 만들어본다. 3편의 익명화 배치와 비교하면서 Processor의 역할, JPA Writer의 함정, 실행 방법까지 다룬다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing.html&quot;&gt;Spring Batch Reference: Chunk-oriented Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/configuring.html&quot;&gt;Spring Batch Reference: Configuring a Step&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/commit-interval.html&quot;&gt;Spring Batch Reference: The Commit Interval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-batch-tasklet-chunk&quot;&gt;Baeldung: Spring Batch - Tasklets vs Chunks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Batch</category>
      <category>Spring Batch</category>
      <category>배치</category>
      <category>스케줄러</category>
      <category>스프링</category>
      <category>스프링 배치</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/91</guid>
      <comments>https://ddokyun.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 18 Apr 2026 20:12:25 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 2편. Job, JobInstance, JobExecution의 관계</title>
      <link>https://ddokyun.tistory.com/90</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 Spring Batch가 왜 필요한지 알아보았다. 그중 &quot;실패 시 어디까지 처리했는지 기억하고 재시작할 수 있다&quot;는 장점이 있었는데, 이게 어떻게 가능한지 내부를 들여다보면 &lt;b&gt;Job, JobInstance, JobExecution&lt;/b&gt;이라는 세 가지 개념이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 용어는 이름이 비슷해서 처음엔 굉장히 헷갈린다. 하지만 한 번 제대로 정리해두면 Spring Batch의 재시작 메커니즘, 중복 실행 방지, 메타데이터 관리가 전부 한 줄기로 꿰어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 내가 학습하면서 가장 헷갈렸던 이 세 개념을 정리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Job이 곧 JobInstance일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 공식 문서를 봤을 때 가장 먼저 든 생각은 이거였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Job과 JobInstance가 이름도 비슷한데, 같은 거 아닌가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 &lt;b&gt;다르다&lt;/b&gt;. 그리고 이 둘의 관계는 Java의 클래스-객체 관계와 거의 동일하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;01-class-analogy.png&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcDaQG/dJMcadBC9Y3/E1ds3xhPH4uac86DirdUk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcDaQG/dJMcadBC9Y3/E1ds3xhPH4uac86DirdUk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcDaQG/dJMcadBC9Y3/E1ds3xhPH4uac86DirdUk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcDaQG%2FdJMcadBC9Y3%2FE1ds3xhPH4uac86DirdUk1%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;1785&quot; height=&quot;960&quot; data-filename=&quot;01-class-analogy.png&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;// Java에서 클래스는 설계도, 객체는 그 설계도에서 찍어낸 실체
class Member { ... }
Member m1 = new Member(&quot;철수&quot;, 28);  // 객체 1
Member m2 = new Member(&quot;영희&quot;, 27);  // 객체 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch도 똑같다. Job은 코드로 정의하는 &quot;설계도&quot;이고, 실행할 때마다 JobParameters와 조합되어 &quot;JobInstance&quot;라는 실체가 만들어진다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A Job defines what a job is and how it is to be executed, and a JobInstance is a purely organizational object to group executions together, primarily to enable correct restart semantics.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;이 비유를 스스로 찾아냈을 때 막혔던 퍼즐이 풀리는 느낌이었다. 공식 문서에서도 &quot;Job과 JobInstance는 다르다&quot;라고는 명확히 나와 있지만, 왜 나눴는지에 대한 직관적인 설명은 부족했다. Java 개발자에게는 클래스-객체 비유가 가장 빠른 이해 경로인 것 같다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JobInstance의 유일성은 어떻게 결정될까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job이 클래스고 JobInstance가 객체라면, 하나의 Job으로 여러 JobInstance를 만들 수 있다는 뜻이다. 그렇다면 &lt;b&gt;무엇이 각 JobInstance를 구분 짓는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 &lt;b&gt;JobParameters&lt;/b&gt;다. 공식 문서의 표현을 빌리면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thus, the contract can be defined as: JobInstance = Job + identifying JobParameters.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJuRZ7/dJMcaiQv74h/K9EsE2m8VkIxmLRhwMhrK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJuRZ7/dJMcaiQv74h/K9EsE2m8VkIxmLRhwMhrK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJuRZ7/dJMcaiQv74h/K9EsE2m8VkIxmLRhwMhrK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJuRZ7%2FdJMcaiQv74h%2FK9EsE2m8VkIxmLRhwMhrK1%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;1785&quot; height=&quot;1036&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;dailySettlementJob&lt;/code&gt;이라는 Job을 매일 다른 날짜 파라미터로 실행하면, 매일 다른 JobInstance가 생성된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 4/18 실행 - 4/17 데이터 정산
JobParameters params1 = new JobParametersBuilder()
    .addString(&quot;date&quot;, &quot;2026-04-17&quot;)
    .toJobParameters();

// 4/19 실행 - 4/18 데이터 정산
JobParameters params2 = new JobParametersBuilder()
    .addString(&quot;date&quot;, &quot;2026-04-18&quot;)
    .toJobParameters();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job 이름은 &lt;code&gt;dailySettlementJob&lt;/code&gt;으로 동일하지만 &lt;code&gt;date&lt;/code&gt; 파라미터 값이 다르므로, 두 실행은 서로 다른 JobInstance로 취급된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;&quot;파라미터 이름&quot;이 아니라 &quot;파라미터 값&quot;이 유일성을 결정한다는 점이 중요하다. 두 번 모두 date라는 동일한 키를 쓰지만, 값(2026-04-17 vs 2026-04-18)이 다르기 때문에 다른 JobInstance가 된다. 처음에는 이걸 잘못 이해해서 &quot;같은 파라미터 이름을 쓰면 같은 Instance인가?&quot; 라고 생각했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JobExecution은 또 뭘까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JobInstance가 &quot;해야 할 작업 1건&quot;이라면, JobExecution은 &lt;b&gt;그 작업을 실제로 돌린 시도&lt;/b&gt;다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A JobExecution refers to the technical concept of a single attempt to run a Job.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 둘로 나눴을까? 바로 &lt;b&gt;재시작 때문이다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 배치는 실패할 수 있다. 네트워크가 끊기거나, DB가 순간 포화되거나, 예상하지 못한 데이터가 들어올 수 있다. 그런 일이 일어났을 때 &quot;어떤 작업의 몇 번째 시도였는지&quot;를 추적할 구조가 필요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsROcv/dJMcafGi94P/mh77ZdYNKcLohZ8TkbtSUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsROcv/dJMcafGi94P/mh77ZdYNKcLohZ8TkbtSUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsROcv/dJMcafGi94P/mh77ZdYNKcLohZ8TkbtSUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsROcv%2FdJMcafGi94P%2Fmh77ZdYNKcLohZ8TkbtSUk%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;1785&quot; height=&quot;1036&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 보면 이해가 쉽다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Job&lt;/b&gt;: &lt;code&gt;dailySettlementJob&lt;/code&gt; 하나&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobInstance&lt;/b&gt;: 날짜별로 3개 (4/17, 4/18, 4/19)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JobExecution&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Instance A (4/17): 첫 시도에 성공 &amp;rarr; Execution 1개&lt;/li&gt;
&lt;li&gt;Instance B (4/18): 첫 시도 실패, 두 번째 시도에 성공 &amp;rarr; Execution 2개&lt;/li&gt;
&lt;li&gt;Instance C (4/19): 첫 시도에 성공 &amp;rarr; Execution 1개&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 건 &lt;b&gt;Instance B의 경우, 같은 JobInstance 아래에 두 개의 JobExecution이 쌓였다&lt;/b&gt;는 점이다. 첫 실패가 기록으로 남아있기 때문에 두 번째 시도에서 &quot;30만 건 처리 완료된 상태였지&quot; 하고 이어서 갈 수 있는 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;이 구조는 장애 복구 관점에서 굉장히 잘 설계되어 있다. 운영 중 장애가 나면 실패한 Execution의 이력을 보고 원인을 파악하고, 같은 Instance로 재실행하면 이어서 처리된다. 결제나 정산처럼 절대 중복 처리되면 안 되는 도메인에서 Spring Batch를 쓰는 이유가 여기에 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;같은 파라미터로 재실행하면 어떻게 될까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림에서 Instance B가 &quot;실패 &amp;rarr; 재시도&quot;로 Execution 2개를 가진 것을 보면, 이런 의문이 생긴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 이미 성공한 Instance A(4/17)를 실수로 또 실행하면 Execution이 2개 생기는 건가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 &lt;b&gt;아니다&lt;/b&gt;. Spring Batch는 이런 경우 아예 실행 자체를 차단한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baoOlR/dJMcadVYp7m/E4xWpY23y7b3midxWdzC50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baoOlR/dJMcadVYp7m/E4xWpY23y7b3midxWdzC50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baoOlR/dJMcadVYp7m/E4xWpY23y7b3midxWdzC50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaoOlR%2FdJMcadVYp7m%2FE4xWpY23y7b3midxWdzC50%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;1785&quot; height=&quot;1036&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙을 정리하면:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;이전 실행 상태&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;재실행 시도 결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;COMPLETED (성공)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;JobInstanceAlreadyCompleteException&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;발생, 실행 차단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;FAILED (실패)&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;같은 JobInstance에 새 JobExecution 생성, 이어서 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서의 표현은 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If it is run again with the same identifying job parameters as the first run, a new JobExecution is created. However, there is still only one JobInstance.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(위 문장은 실패 후 재실행 시나리오에 대한 설명이다. 성공한 Instance를 재실행하려 하면 예외가 발생한다.)&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;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새벽에 정산 배치가 성공적으로 끝났는데, 코드에 버그가 있어서 정산 금액이 잘못 계산됐다는 게 오전에 발견됐다. CTO가 &quot;어제 날짜로 다시 정산 돌려!&quot;라고 지시한다. 같은 파라미터로 재실행 버튼을 누르면?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답: &lt;code&gt;JobInstanceAlreadyCompleteException&lt;/code&gt;이 발생하며 실행이 차단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 때는 &lt;b&gt;파라미터에 식별자를 추가&lt;/b&gt;하여 새로운 JobInstance로 만들거나, &lt;b&gt;별도의 보정 Job을 작성&lt;/b&gt;해서 실행하는 방식으로 해결한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 방법 1: 파라미터에 식별자 추가
JobParameters params = new JobParametersBuilder()
    .addString(&quot;date&quot;, &quot;2026-04-17&quot;)
    .addString(&quot;reason&quot;, &quot;bugfix-2026-04-18&quot;)  // 새 Instance가 됨
    .toJobParameters();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;처음엔 &quot;왜 성공한 걸 다시 못 돌리게 막아놨지?&quot;라고 의아했다. 하지만 정산, 결제, 포인트 지급 같은 도메인을 생각해보면 당연한 설계다. 실수로 한 번 더 돌렸다가는 돈이 두 번 빠져나가는 사고가 날 수 있다. Spring Batch는 &quot;프레임워크가 개발자의 실수를 막아주는&quot; 사례라고 느꼈다. Saga 패턴에서 보상 트랜잭션(Compensating Transaction)을 별도로 설계하는 철학과도 비슷한 결이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 모든 상태는 어디에 저장될까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job, JobInstance, JobExecution의 상태 정보는 &lt;b&gt;어디엔가 영구적으로 저장되어야 한다&lt;/b&gt;. 메모리에만 있으면 프로세스가 죽는 순간 날아가서 재시작이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 이를 위해 &lt;b&gt;9개의 메타데이터 테이블&lt;/b&gt;을 자동으로 생성한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF1Jlu/dJMcafl0jPO/lijVyaPxv1X5kwl7CKgqM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF1Jlu/dJMcafl0jPO/lijVyaPxv1X5kwl7CKgqM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF1Jlu/dJMcafl0jPO/lijVyaPxv1X5kwl7CKgqM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF1Jlu%2FdJMcafl0jPO%2FlijVyaPxv1X5kwl7CKgqM1%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;1785&quot; height=&quot;1183&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테이블의 역할은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Job 레벨&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_INSTANCE&lt;/code&gt;: JobInstance 정보 (Job 이름, 파라미터 해시)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION&lt;/code&gt;: JobExecution 정보 (시작/종료 시간, 상태)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION_PARAMS&lt;/code&gt;: 전달받은 JobParameters의 실제 값&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION_CONTEXT&lt;/code&gt;: Job 단위 상태 저장소 (재시작용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 레벨&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BATCH_STEP_EXECUTION&lt;/code&gt;: Step 실행 정보 (read/write/skip/commit 카운트)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_STEP_EXECUTION_CONTEXT&lt;/code&gt;: Step 단위 상태 저장소 (재시작용, 예: 마지막 처리 ID)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시퀀스&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_INSTANCE_SEQ&lt;/code&gt;, &lt;code&gt;BATCH_JOB_EXECUTION_SEQ&lt;/code&gt;, &lt;code&gt;BATCH_STEP_EXECUTION_SEQ&lt;/code&gt;: 각 테이블의 PK 생성용 시퀀스&lt;/li&gt;
&lt;/ul&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;-- 최근 실행된 Job 이력 확인
SELECT
    ji.JOB_NAME,
    je.JOB_EXECUTION_ID,
    je.START_TIME,
    je.END_TIME,
    je.STATUS,
    je.EXIT_CODE
FROM BATCH_JOB_INSTANCE ji
JOIN BATCH_JOB_EXECUTION je ON ji.JOB_INSTANCE_ID = je.JOB_INSTANCE_ID
ORDER BY je.START_TIME DESC
LIMIT 20;

-- 특정 실행의 Step별 처리 건수 확인
SELECT
    STEP_NAME,
    STATUS,
    READ_COUNT,
    WRITE_COUNT,
    COMMIT_COUNT,
    ROLLBACK_COUNT,
    READ_SKIP_COUNT,
    WRITE_SKIP_COUNT
FROM BATCH_STEP_EXECUTION
WHERE JOB_EXECUTION_ID = ?;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;장애 대응 시 가장 먼저 이 테이블을 뒤진다고 한다. 로그보다 빠를 때가 많다는 얘기를 여러 곳에서 봤는데, 실제 구조를 보니 그럴 만하다고 느꼈다. &quot;read_count = 300,000, write_count = 299,998&quot; 같은 숫자만 보면 &quot;어디서 2건이 날아갔는지&quot; 즉시 파악할 수 있는 구조다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 편에서 정리한 내용을 한 문장으로 요약하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job은 작업의 설계도, JobInstance는 파라미터로 구체화된 작업 1건, JobExecution은 그 작업의 실제 시도다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 세 개념의 상태는 모두 &lt;b&gt;메타데이터 테이블 9개에 자동으로 기록&lt;/b&gt;된다. 이 기록이 Spring Batch의 재시작, 중복 실행 방지, 실행 이력 관리를 가능하게 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;1편을 쓸 때 &quot;Spring Batch는 대용량 데이터를 안전하고 재시작 가능하게 처리하는 프레임워크&quot;라고 정리했었다. 2편을 쓰면서 보니 &quot;안전성&quot;과 &quot;재시작성&quot;이 전부 Job-Instance-Execution 구조와 메타데이터 테이블에서 나온다는 걸 명확히 알게 됐다. 다음 편에서는 이제 드디어 실제 코드로 들어가서, Chunk 기반 처리와 Reader/Processor/Writer를 다룬다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 Spring Batch 실무에서 가장 많이 쓰는 &lt;b&gt;Chunk 기반 처리 구조&lt;/b&gt;를 다룬다. ItemReader, ItemProcessor, ItemWriter 세 요소가 각각 어떤 역할을 하고, 실제 코드가 어떻게 생겼는지 회원 등급 재계산 배치 예제로 알아본다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/4.2.4.RELEASE/reference/html/job.html&quot;&gt;Spring Batch Reference: Configuring and Running a Job&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-batch-tasklet-chunk&quot;&gt;Baeldung: Spring Batch - Tasklets vs Chunks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.toptal.com/developers/spring/spring-batch-tutorial&quot;&gt;Toptal: Spring Batch Tutorial&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Batch</category>
      <category>Chunk</category>
      <category>Spring Batch</category>
      <category>tasklet</category>
      <category>배치</category>
      <category>스케줄러</category>
      <category>스프링</category>
      <category>스프링 배치</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/90</guid>
      <comments>https://ddokyun.tistory.com/90#entry90comment</comments>
      <pubDate>Sat, 18 Apr 2026 16:36:04 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Batch] 1편. Spring Batch는 왜 필요할까?</title>
      <link>https://ddokyun.tistory.com/89</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 주부터 실무에서 Spring Batch 작업을 진행하게 되었다. 이름만 들어봤을 뿐 실제로 써본 적은 없어서, 실무 투입 전에 개념부터 차근차근 정리하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시리즈는 내가 Spring Batch를 학습하며 정리한 기록이다. 단순히 &quot;이렇게 쓰세요&quot;가 아니라, &lt;b&gt;왜 이 프레임워크가 필요한지&lt;/b&gt;부터 이해하고 싶었다. 그래야 나중에 실무에서 문제가 생겼을 때 원인을 제대로 파악할 수 있을 것 같았기 때문이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;새로운 기술을 배울 때 &quot;어떻게 쓰는지&quot;보다 &quot;왜 만들어졌는지&quot;를 먼저 이해하는 습관은 웨이트리프팅 루틴에서 배운 것 같다. 어떤 동작을 하든 근본 원리를 모르면 부상으로 이어지듯, 기술도 원리를 모르면 예상치 못한 지점에서 장애로 이어진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단순 스케줄러로는 왜 부족한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 요구사항이 들어왔다고 가정해보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 새벽 3시에 어제 가입한 회원 100만 명에게 환영 이메일을 발송하고, 발송 결과를 DB에 저장해주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot에 익숙한 개발자라면 &lt;code&gt;@Scheduled&lt;/code&gt; 어노테이션으로 이렇게 작성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Scheduled(cron = &quot;0 0 3 * * *&quot;)
public void sendWelcomeEmails() {
    List&amp;lt;Member&amp;gt; members = memberRepository.findYesterdayJoined();
    for (Member member : members) {
        emailService.send(member);
        logRepository.save(new EmailLog(member.getId(), &quot;SUCCESS&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기엔 문제없어 보인다. 하지만 실무에서 이 코드는 &lt;b&gt;최소 5가지 문제&lt;/b&gt;를 안고 있다. 하나씩 살펴보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1. 대용량 데이터를 한 번에 메모리에 올림 (OOM)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;findYesterdayJoined()&lt;/code&gt;는 100만 건의 Member 객체를 &lt;b&gt;한 번에 List로 반환한다&lt;/b&gt;. 이 데이터가 그대로 JVM 힙 메모리에 올라가는데, 회원 한 명당 1KB만 잡아도 1GB 이상이다. 실제 운영 환경에서는 회원 객체가 훨씬 무거우니 &lt;b&gt;OOM(OutOfMemoryError)&lt;/b&gt; 으로 배치 프로세스가 죽는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RvDAy/dJMcaarsXbQ/TwCbYs6lbC9PyDVkS03Cs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RvDAy/dJMcaarsXbQ/TwCbYs6lbC9PyDVkS03Cs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RvDAy/dJMcaarsXbQ/TwCbYs6lbC9PyDVkS03Cs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRvDAy%2FdJMcaarsXbQ%2FTwCbYs6lbC9PyDVkS03Cs1%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;1785&quot; height=&quot;808&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;개발 환경에서 1만 건으로 테스트하면 문제없이 통과한다. 문제는 운영 배포 후 실제 트래픽이 들어왔을 때 터진다는 것이다. Flash Deal 프로젝트에서 TPS 튜닝하던 시절에도 비슷한 경험이 있었다. 부하 테스트 없이 &quot;잘 되겠지&quot; 하고 배포하면 꼭 터진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch의 해결책&lt;/b&gt;: Chunk 단위로 데이터를 나눠서 처리한다. 예를 들어 Chunk size를 1,000으로 설정하면, 한 번에 1,000건만 읽고 처리하고 저장하고 &amp;rarr; 다음 1,000건을 처리하는 식으로 반복한다. 메모리 사용량이 일정하게 유지된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch uses a &quot;chunk-oriented&quot; processing style in its most common implementation. Chunk oriented processing refers to reading the data one at a time and creating 'chunks' that are written out within a transaction boundary.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing.html&quot;&gt;Spring Batch Reference: Chunk-oriented Processing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2. 실패 시 어디서 실패했는지 모름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 건 중 30만 번째에서 에러가 터져 프로세스가 죽었다고 하자. 위 코드는 &lt;b&gt;어디까지 처리했는지에 대한 정보를 아무 데도 남기지 않는다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 재실행하면 1번부터 다시 시작한다. 이 경우:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미 메일 받은 30만 명에게 &lt;b&gt;중복 발송&lt;/b&gt; (사용자 컴플레인)&lt;/li&gt;
&lt;li&gt;시간 2배 소요&lt;/li&gt;
&lt;li&gt;외부 API(이메일 서비스) 비용 2배&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wd6e4/dJMcafTQfIu/L9nSdWSTWwHiqE3Sl1ygO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wd6e4/dJMcafTQfIu/L9nSdWSTWwHiqE3Sl1ygO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wd6e4/dJMcafTQfIu/L9nSdWSTWwHiqE3Sl1ygO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwd6e4%2FdJMcafTQfIu%2FL9nSdWSTWwHiqE3Sl1ygO0%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;1785&quot; height=&quot;884&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch의 해결책&lt;/b&gt;: &lt;code&gt;JobRepository&lt;/code&gt;라는 메타데이터 저장소가 있다. DB의 &lt;code&gt;BATCH_JOB_EXECUTION&lt;/code&gt;, &lt;code&gt;BATCH_STEP_EXECUTION&lt;/code&gt; 테이블에 &lt;b&gt;언제, 몇 건까지, 어떤 상태로 처리했는지를 자동으로 기록한다&lt;/b&gt;. 덕분에 실패한 지점부터 이어서 재시작할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 개념(&lt;code&gt;JobInstance&lt;/code&gt;, &lt;code&gt;JobExecution&lt;/code&gt;)은 2편에서 자세히 다룰 예정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 3. 실패 처리 전략이 없음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 실패가 나면 &lt;code&gt;for&lt;/code&gt; 루프는 그냥 예외가 터지고 끝난다. 하지만 실무에서 실패는 두 가지 성격으로 나뉜다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMOdpF/dJMb99TBAp7/7PhNpKOV8gV2wJDvKCL0wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMOdpF/dJMb99TBAp7/7PhNpKOV8gV2wJDvKCL0wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMOdpF/dJMb99TBAp7/7PhNpKOV8gV2wJDvKCL0wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMOdpF%2FdJMb99TBAp7%2F7PhNpKOV8gV2wJDvKCL0wK%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;1785&quot; height=&quot;961&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;961&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(1) 일시적 실패 (Transient Failure)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 일시 끊김, DB 커넥션 순간 포화&lt;/li&gt;
&lt;li&gt;다시 시도하면 성공할 가능성이 높음 &amp;rarr; &lt;b&gt;재시도(Retry)가 답&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 영구적 실패 (Permanent Failure)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일 주소 형식 오류, 탈퇴한 회원&lt;/li&gt;
&lt;li&gt;몇 번을 다시 해도 실패 &amp;rarr; &lt;b&gt;건너뛰기(Skip)가 답&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 &lt;code&gt;for&lt;/code&gt; + &lt;code&gt;try-catch&lt;/code&gt;로 이 로직을 구현하려면 코드가 지저분해진다. 또한 재시도 횟수 추적, 스킵된 건수 집계 같은 부가 로직도 직접 다 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch의 해결책&lt;/b&gt;: 선언적으로 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;.faultTolerant()
.retry(NetworkException.class).retryLimit(3)
.skip(InvalidEmailException.class).skipLimit(100)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If a FlatFileParseException is encountered while reading, it is always thrown for that record. Resetting the ItemReader does not help. However, for other exceptions (such as a DeadlockLoserDataAccessException, which indicates that the current process has attempted to update a record that another process holds a lock on), waiting and trying again might result in success.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/reference/5.1/step/chunk-oriented-processing/retry-logic.html&quot;&gt;Spring Batch Reference: Configuring Retry Logic&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;재시도와 건너뛰기를 구분하는 관점이 흥미롭다. 기존에 API 개발할 때는 &quot;실패하면 예외 던지고 끝&quot;이었는데, 배치는 &quot;어떤 실패는 참고 넘어가고, 어떤 실패는 다시 해봐야 한다&quot;는 더 섬세한 제어가 필요하다. Circuit Breaker 패턴을 공부했을 때와 비슷한 결의 철학이라고 느꼈다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 4. 트랜잭션 경계가 극단적&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;b&gt;(1) 전체를 하나의 트랜잭션으로 묶는 경우&lt;/b&gt;: 999,999번째에서 실패 시 100만 건 &lt;b&gt;전부 롤백&lt;/b&gt;. 2시간 돌린 게 날아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(2) 트랜잭션 없이 건별로 커밋하는 경우&lt;/b&gt;: 중간에 서버가 죽으면 어디까지 반영됐는지 알 수 없고 정합성이 깨진다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1035&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kF6rr/dJMcabYcNwL/aws6rnugkNUkicsLEWVRc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kF6rr/dJMcabYcNwL/aws6rnugkNUkicsLEWVRc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kF6rr/dJMcabYcNwL/aws6rnugkNUkicsLEWVRc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkF6rr%2FdJMcabYcNwL%2Faws6rnugkNUkicsLEWVRc1%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;1785&quot; height=&quot;1035&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1035&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch의 해결책&lt;/b&gt;: Chunk 단위가 곧 트랜잭션 단위다. Chunk size가 1,000이면 1,000건마다 커밋된다. 중간에 실패해도 이전에 커밋된 건은 안전하게 보존되고, 현재 Chunk만 롤백된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Once the number of items read equals the commit interval, the entire chunk is written out by the ItemWriter, and then the transaction is committed.&lt;br /&gt;&amp;mdash; &lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/commit-interval.html&quot;&gt;Spring Batch Reference: The Commit Interval&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 5. 실행 이력이 남지 않음&lt;/h2&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;단순 스케줄러 방식에서는 이 질문에 답하려면 &lt;b&gt;애플리케이션 로그를 뒤져야 한다&lt;/b&gt;. 로그 레벨을 제대로 설정해두지 않았거나, 로그 롤링으로 이미 삭제되었다면 답할 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Batch의 해결책&lt;/b&gt;: 모든 실행 이력이 메타데이터 테이블에 자동 저장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch를 적용하면 다음 9개의 테이블이 자동으로 생성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_INSTANCE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION_PARAMS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION_CONTEXT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_STEP_EXECUTION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_STEP_EXECUTION_CONTEXT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_INSTANCE_SEQ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_JOB_EXECUTION_SEQ&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BATCH_STEP_EXECUTION_SEQ&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테이블에 언제 실행했는지, 몇 건 처리했는지, 어느 단계에서 실패했는지가 기록된다. SQL 한 번이면 이력 조회가 끝난다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;SELECT JOB_NAME, START_TIME, END_TIME, STATUS, EXIT_CODE
FROM BATCH_JOB_EXECUTION je
JOIN BATCH_JOB_INSTANCE ji ON je.JOB_INSTANCE_ID = ji.JOB_INSTANCE_ID
ORDER BY START_TIME DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테이블의 역할은 2편에서 자세히 다룰 예정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리: Spring Batch가 필요한 5가지 이유&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1033&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5A7vr/dJMcadIq0Yt/uEDbOpVEjBbrYY81h7Mrkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5A7vr/dJMcadIq0Yt/uEDbOpVEjBbrYY81h7Mrkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5A7vr/dJMcadIq0Yt/uEDbOpVEjBbrYY81h7Mrkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5A7vr%2FdJMcadIq0Yt%2FuEDbOpVEjBbrYY81h7Mrkk%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;1785&quot; height=&quot;1033&quot; data-origin-width=&quot;1785&quot; data-origin-height=&quot;1033&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 정리하면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Batch는 대용량 데이터를 안전하고 재시작 가능하게 처리하기 위한 프레임워크다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 기능을 배우든 &quot;이 기능이 안전성, 재시작성, 관찰 가능성 중 어디에 기여하는가?&quot;로 연결해서 이해하면 머리에 잘 들어온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  나의 생각&lt;br /&gt;처음에는 &quot;그냥 for 루프로 해도 되지 않나?&quot;라고 생각했다. 실제로 소규모 데이터나 단발성 작업은 그게 맞을 수도 있다. 하지만 데이터가 100만 건 이상, 외부 API 호출이 섞이고, 실패 시 금전적 손해가 생길 수 있는 환경이라면 이야기가 달라진다. 프레임워크의 존재 이유는 &quot;개발자가 이 모든 엣지 케이스를 매번 다시 짜지 않게&quot; 하는 것이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다음 편 예고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 Spring Batch의 핵심 개념인 &lt;b&gt;Job, JobInstance, JobExecution의 관계&lt;/b&gt;를 정리한다. 특히 &quot;같은 배치를 두 번 실행하면 어떻게 될까?&quot;라는 질문에 대한 답을 찾아가 볼 예정이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing.html&quot;&gt;Spring Batch Reference: Chunk-oriented Processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/step/chunk-oriented-processing/commit-interval.html&quot;&gt;Spring Batch Reference: The Commit Interval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/reference/5.1/step/chunk-oriented-processing/retry-logic.html&quot;&gt;Spring Batch Reference: Configuring Retry Logic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-batch/docs/current/reference/html/domain.html&quot;&gt;Spring Batch Reference: The Domain Language of Batch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-batch-tasklet-chunk&quot;&gt;Baeldung: Spring Batch - Tasklets vs Chunks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Batch</category>
      <category>Spring Batch</category>
      <category>배치</category>
      <category>스케줄러</category>
      <category>스프링</category>
      <category>스프링 배치</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/89</guid>
      <comments>https://ddokyun.tistory.com/89#entry89comment</comments>
      <pubDate>Sat, 18 Apr 2026 15:59:20 +0900</pubDate>
    </item>
    <item>
      <title>옵저버 패턴(Observer Pattern)</title>
      <link>https://ddokyun.tistory.com/88</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 마지막 주제는 &lt;b&gt;옵저버 패턴(Observer Pattern)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;템플릿 메서드 패턴&lt;/b&gt;이 &lt;b&gt;&quot;알고리즘의 흐름을 정의&quot;&lt;/b&gt;하는 거였다면, &lt;b&gt;옵저버 패턴&lt;/b&gt;은 &lt;b&gt;&quot;변화를 알려주는&quot;&lt;/b&gt; 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 유튜브를 생각해보자. 좋아하는 채널을 구독하면, 그 채널에 새 영상이 올라올 때마다 알림이 온다. 채널은 구독자들에게 &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;b&gt;옵저버 패턴&lt;/b&gt;은 이처럼 &lt;b&gt;한 객체의 상태가 변하면, 그것을 지켜보던(Observe) 다른 객체들에게 자동으로 알려주는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;옵저버 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;옵저버 패턴의 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵저버 패턴(Observer Pattern)&lt;/b&gt;은 &lt;b&gt;객체의 상태 변화를 관찰하는 관찰자(Observer)들의 목록을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 관찰자에게 통지하도록 하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;일대다 의존성&lt;/b&gt;을 정의한다&lt;/li&gt;
&lt;li&gt;주체(Subject)와 관찰자(Observer)로 구성된다&lt;/li&gt;
&lt;li&gt;느슨한 결합(Loose Coupling)을 유지한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행-구독(Publish-Subscribe)&lt;/b&gt; 모델이다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유튜브 구독 (채널이 새 영상 올리면 구독자들에게 알림)&lt;/li&gt;
&lt;li&gt;신문 구독 (신문사가 신문 발행하면 구독자들에게 배달)&lt;/li&gt;
&lt;li&gt;날씨 앱 (기상청이 날씨 변경하면 앱들에게 알림)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 옵저버 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날씨 정보를 제공하는 시스템을 개발한다고 가정해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 문제가 있는 코드

// 날씨 데이터
public class WeatherData {
    private float temperature;
    private float humidity;
    
    // 날씨 변경 시 모든 디스플레이를 직접 업데이트
    public void setMeasurements(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        
        // 문제: 디스플레이가 추가될 때마다 코드 수정 필요
        CurrentConditionsDisplay display1 = new CurrentConditionsDisplay();
        display1.update(temperature, humidity);
        
        StatisticsDisplay display2 = new StatisticsDisplay();
        display2.update(temperature, humidity);
        
        ForecastDisplay display3 = new ForecastDisplay();
        display3.update(temperature, humidity);
        
        // 새로운 디스플레이 추가하려면? 또 코드 수정...
    }
}

// 현재 날씨 디스플레이
public class CurrentConditionsDisplay {
    public void update(float temperature, float humidity) {
        System.out.println(&quot;현재 날씨: &quot; + temperature + &quot;도, 습도 &quot; + humidity + &quot;%&quot;);
    }
}

// 통계 디스플레이
public class StatisticsDisplay {
    public void update(float temperature, float humidity) {
        System.out.println(&quot;평균 온도: &quot; + temperature + &quot;도&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;강한 결합&lt;/b&gt;: WeatherData가 모든 디스플레이 클래스를 알아야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OCP 위반&lt;/b&gt;: 새로운 디스플레이 추가 시 WeatherData 수정 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성 부족&lt;/b&gt;: 런타임에 디스플레이를 추가/제거할 수 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 어려움&lt;/b&gt;: 디스플레이가 100개면? 코드가 엄청 길어짐&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵저버 패턴&lt;/b&gt;은 &lt;b&gt;&quot;관찰자들을 등록하고, 변화 시 자동으로 알림을 보내자&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 인터페이스 정의&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 주체(Subject) 인터페이스
public interface Subject {
    void registerObserver(Observer observer);    // 관찰자 등록
    void removeObserver(Observer observer);      // 관찰자 제거
    void notifyObservers();                      // 관찰자들에게 알림
}

// 관찰자(Observer) 인터페이스
public interface Observer {
    void update(float temperature, float humidity);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 주체(Subject) 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 날씨 데이터 (주체)
public class WeatherData implements Subject {
    private List&amp;lt;Observer&amp;gt; observers;
    private float temperature;
    private float humidity;
    
    public WeatherData() {
        this.observers = new ArrayList&amp;lt;&amp;gt;();
    }
    
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
        System.out.println(&quot;관찰자 등록됨&quot;);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
        System.out.println(&quot;관찰자 제거됨&quot;);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity);
        }
    }
    
    // 날씨 데이터 변경
    public void setMeasurements(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        notifyObservers();  // 자동으로 모든 관찰자에게 알림!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 관찰자(Observer) 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 현재 날씨 디스플레이
public class CurrentConditionsDisplay implements Observer {
    private String name;
    
    public CurrentConditionsDisplay(String name) {
        this.name = name;
    }
    
    @Override
    public void update(float temperature, float humidity) {
        System.out.println(&quot;[&quot; + name + &quot;] 현재 날씨: &quot; + temperature + &quot;도, 습도 &quot; + humidity + &quot;%&quot;);
    }
}

// 통계 디스플레이
public class StatisticsDisplay implements Observer {
    private float totalTemperature = 0;
    private int count = 0;
    
    @Override
    public void update(float temperature, float humidity) {
        totalTemperature += temperature;
        count++;
        float avg = totalTemperature / count;
        System.out.println(&quot;[통계] 평균 온도: &quot; + avg + &quot;도&quot;);
    }
}

// 예보 디스플레이
public class ForecastDisplay implements Observer {
    @Override
    public void update(float temperature, float humidity) {
        if (temperature &amp;gt; 25) {
            System.out.println(&quot;[예보] 날씨가 더워질 것 같습니다&quot;);
        } else {
            System.out.println(&quot;[예보] 날씨가 선선할 것 같습니다&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 주체 생성
        WeatherData weatherData = new WeatherData();
        
        // 관찰자 생성 및 등록
        Observer display1 = new CurrentConditionsDisplay(&quot;디스플레이1&quot;);
        Observer display2 = new StatisticsDisplay();
        Observer display3 = new ForecastDisplay();
        
        weatherData.registerObserver(display1);
        weatherData.registerObserver(display2);
        weatherData.registerObserver(display3);
        
        System.out.println(&quot;\n=== 첫 번째 날씨 업데이트 ===&quot;);
        weatherData.setMeasurements(25.5f, 65f);
        
        System.out.println(&quot;\n=== 두 번째 날씨 업데이트 ===&quot;);
        weatherData.setMeasurements(28.0f, 70f);
        
        System.out.println(&quot;\n=== 관찰자 하나 제거 ===&quot;);
        weatherData.removeObserver(display3);
        
        System.out.println(&quot;\n=== 세 번째 날씨 업데이트 ===&quot;);
        weatherData.setMeasurements(22.0f, 60f);
        
        System.out.println(&quot;\n=== 새로운 관찰자 추가 ===&quot;);
        Observer display4 = new CurrentConditionsDisplay(&quot;디스플레이4&quot;);
        weatherData.registerObserver(display4);
        
        System.out.println(&quot;\n=== 네 번째 날씨 업데이트 ===&quot;);
        weatherData.setMeasurements(30.0f, 75f);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;771&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nFcQy/dJMcadm9PH6/8GS4JasH89Wr78hIAMcGa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nFcQy/dJMcadm9PH6/8GS4JasH89Wr78hIAMcGa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nFcQy/dJMcadm9PH6/8GS4JasH89Wr78hIAMcGa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnFcQy%2FdJMcadm9PH6%2F8GS4JasH89Wr78hIAMcGa0%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;421&quot; height=&quot;771&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;771&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WeatherData는 구체적인 디스플레이 클래스를 몰라도 됨&lt;/li&gt;
&lt;li&gt;런타임에 관찰자 추가/제거 가능&lt;/li&gt;
&lt;li&gt;새로운 디스플레이 추가해도 WeatherData 수정 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 주식 가격 알림&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 주식 (주체)
public class Stock implements Subject {
    private List&amp;lt;Observer&amp;gt; observers;
    private String stockName;
    private int price;
    
    public Stock(String stockName, int initialPrice) {
        this.stockName = stockName;
        this.price = initialPrice;
        this.observers = new ArrayList&amp;lt;&amp;gt;();
    }
    
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(stockName, price);
        }
    }
    
    public void setPrice(int price) {
        System.out.println(&quot;\n  &quot; + stockName + &quot; 가격 변동: &quot; + this.price + &quot;원 &amp;rarr; &quot; + price + &quot;원&quot;);
        this.price = price;
        notifyObservers();
    }
}

// 투자자 (관찰자)
public class Investor implements Observer {
    private String name;
    private int targetPrice;
    
    public Investor(String name, int targetPrice) {
        this.name = name;
        this.targetPrice = targetPrice;
    }
    
    @Override
    public void update(String stockName, int price) {
        if (price &amp;gt;= targetPrice) {
            System.out.println(&quot;  [&quot; + name + &quot;] &quot; + stockName + &quot;이(가) 목표가(&quot; + targetPrice + &quot;원) 도달! 현재가: &quot; + price + &quot;원&quot;);
        } else {
            System.out.println(&quot;  [&quot; + name + &quot;] &quot; + stockName + &quot; 관찰 중... 현재가: &quot; + price + &quot;원&quot;);
        }
    }
}

// 알림 봇 (관찰자)
public class NotificationBot implements Observer {
    @Override
    public void update(String stockName, int price) {
        System.out.println(&quot;  [알림봇] &quot; + stockName + &quot; 가격 알림: &quot; + price + &quot;원&quot;);
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 삼성전자 주식
        Stock samsung = new Stock(&quot;삼성전자&quot;, 70000);
        
        // 투자자들 등록
        Investor investor1 = new Investor(&quot;김투자&quot;, 75000);
        Investor investor2 = new Investor(&quot;이부자&quot;, 80000);
        NotificationBot bot = new NotificationBot();
        
        samsung.registerObserver(investor1);
        samsung.registerObserver(investor2);
        samsung.registerObserver(bot);
        
        // 가격 변동
        samsung.setPrice(72000);
        samsung.setPrice(76000);
        samsung.setPrice(81000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 채팅방&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 채팅방 (주체)
public class ChatRoom implements Subject {
    private List&amp;lt;Observer&amp;gt; users;
    private String roomName;
    
    public ChatRoom(String roomName) {
        this.roomName = roomName;
        this.users = new ArrayList&amp;lt;&amp;gt;();
    }
    
    @Override
    public void registerObserver(Observer observer) {
        users.add(observer);
        System.out.println(&quot;  새로운 사용자가 [&quot; + roomName + &quot;]에 입장했습니다.&quot;);
    }
    
    @Override
    public void removeObserver(Observer observer) {
        users.remove(observer);
        System.out.println(&quot;  사용자가 [&quot; + roomName + &quot;]에서 퇴장했습니다.&quot;);
    }
    
    @Override
    public void notifyObservers(String message) {
        for (Observer observer : users) {
            observer.update(message);
        }
    }
    
    public void sendMessage(String sender, String message) {
        String fullMessage = &quot;[&quot; + sender + &quot;] &quot; + message;
        System.out.println(&quot;\n  메시지 전송: &quot; + fullMessage);
        notifyObservers(fullMessage);
    }
}

// 인터페이스 수정
interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String message);
}

interface Observer {
    void update(String message);
}

// 사용자 (관찰자)
public class User implements Observer {
    private String name;
    
    public User(String name) {
        this.name = name;
    }
    
    @Override
    public void update(String message) {
        System.out.println(&quot;  &amp;rarr; &quot; + name + &quot;님이 수신: &quot; + message);
    }
    
    public String getName() {
        return name;
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 채팅방 생성
        ChatRoom chatRoom = new ChatRoom(&quot;디자인패턴 스터디&quot;);
        
        // 사용자들
        User user1 = new User(&quot;김도균&quot;);
        User user2 = new User(&quot;이철수&quot;);
        User user3 = new User(&quot;박영희&quot;);
        
        // 입장
        chatRoom.registerObserver(user1);
        chatRoom.registerObserver(user2);
        chatRoom.registerObserver(user3);
        
        // 메시지 전송
        chatRoom.sendMessage(&quot;김도균&quot;, &quot;안녕하세요!&quot;);
        chatRoom.sendMessage(&quot;이철수&quot;, &quot;반갑습니다~&quot;);
        
        // 퇴장
        chatRoom.removeObserver(user3);
        
        // 메시지 전송
        chatRoom.sendMessage(&quot;김도균&quot;, &quot;오늘 스터디 열심히 해봐요!&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Java의 내장 옵저버 (Deprecated)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에는 Observer 인터페이스와 Observable 클래스가 있었지만, Java 9부터 &lt;b&gt;Deprecated&lt;/b&gt; 되었다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 예전 방식 (사용 비추천)
import java.util.Observable;
import java.util.Observer;

public class WeatherData extends Observable {
    private float temperature;
    
    public void setTemperature(float temperature) {
        this.temperature = temperature;
        setChanged();  // 변경 표시
        notifyObservers(temperature);  // 알림
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Deprecated 이유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Observable이 클래스라서 상속 제한&lt;/li&gt;
&lt;li&gt;스레드 안전하지 않음&lt;/li&gt;
&lt;li&gt;이벤트 모델이 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대신 사용:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 구현 (위 예시처럼)&lt;/li&gt;
&lt;li&gt;Spring의 ApplicationEvent&lt;/li&gt;
&lt;li&gt;RxJava, Flow API&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;옵저버 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;느슨한 결합&lt;/b&gt;: 주체와 관찰자가 독립적&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OCP 준수&lt;/b&gt;: 새로운 관찰자 추가 시 주체 수정 불필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동적 구독&lt;/b&gt;: 런타임에 관찰자 추가/제거 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브로드캐스트&lt;/b&gt;: 한 번에 여러 객체에게 알림&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;옵저버 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;알림 순서&lt;/b&gt;: 관찰자들의 알림 순서를 보장할 수 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메모리 누수&lt;/b&gt;: 관찰자를 제거하지 않으면 메모리 누수 발생 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예상치 못한 업데이트&lt;/b&gt;: 관찰자가 많으면 성능 저하&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순환 의존성&lt;/b&gt;: 관찰자가 주체를 업데이트하면 무한 루프&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 옵저버 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 &lt;b&gt;이벤트 시스템&lt;/b&gt;이 옵저버 패턴을 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 이벤트 정의
public class OrderCreatedEvent {
    private Long orderId;
    private int amount;
    
    public OrderCreatedEvent(Long orderId, int amount) {
        this.orderId = orderId;
        this.amount = amount;
    }
    
    public Long getOrderId() { return orderId; }
    public int getAmount() { return amount; }
}

// 이벤트 발행자 (주체)
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;
    
    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
    
    public void createOrder(Long orderId, int amount) {
        // 주문 생성 로직
        System.out.println(&quot;주문 생성: &quot; + orderId);
        
        // 이벤트 발행
        eventPublisher.publishEvent(new OrderCreatedEvent(orderId, amount));
    }
}

// 이벤트 리스너 (관찰자)
@Component
public class EmailNotificationListener {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println(&quot;  이메일 발송: 주문번호 &quot; + event.getOrderId());
    }
}

@Component
public class SmsNotificationListener {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        System.out.println(&quot;  SMS 발송: 주문번호 &quot; + event.getOrderId());
    }
}

@Component
public class PointRewardListener {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        int points = event.getAmount() / 100;
        System.out.println(&quot;⭐ 포인트 적립: &quot; + points + &quot;점&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        OrderService orderService = context.getBean(OrderService.class);
        
        orderService.createOrder(1L, 50000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한 객체의 변경이 다른 객체들에게 영향을 줄 때&lt;/li&gt;
&lt;li&gt;어떤 객체들이 영향을 받을지 미리 알 수 없을 때&lt;/li&gt;
&lt;li&gt;이벤트 기반 시스템을 만들 때&lt;/li&gt;
&lt;li&gt;GUI 프로그래밍 (버튼 클릭 이벤트 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관찰자가 하나뿐일 때&lt;/li&gt;
&lt;li&gt;실시간 알림이 필요 없을 때&lt;/li&gt;
&lt;li&gt;강한 결합이 문제되지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;한 객체의 변화를 여러 객체에게 자동으로 알려야 한다면 옵저버 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;일대다 관계&lt;/b&gt;를 정의한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;느슨한 결합&lt;/b&gt;을 유지한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행-구독&lt;/b&gt; 모델이다&lt;/li&gt;
&lt;li&gt;Spring의 &lt;b&gt;이벤트 시스템&lt;/b&gt;이 대표적&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임에 구독/해지&lt;/b&gt; 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것으로 디자인 패턴 시리즈를 마무리한다. 지금까지 배운 11가지 패턴은 실무에서 가장 많이 사용되는 핵심 패턴들이다. 모든 패턴을 외울 필요는 없지만, 각 패턴이 &lt;b&gt;어떤 문제를 해결하는지&lt;/b&gt; 이해하고, 적절한 상황에서 사용할 수 있다면 훨씬 더 나은 코드를 작성할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디자인 패턴 학습의 핵심:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패턴을 &lt;b&gt;암기하지 말고 이해&lt;/b&gt;하라&lt;/li&gt;
&lt;li&gt;무조건 사용하지 말고 &lt;b&gt;필요할 때만&lt;/b&gt; 사용하라&lt;/li&gt;
&lt;li&gt;간단한 문제에 복잡한 패턴을 쓰지 마라 (&lt;b&gt;오버엔지니어링 주의&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;실제 프로젝트에 &lt;b&gt;적용하며 배워&lt;/b&gt;라&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>디자인 패턴</category>
      <category>옵저버 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/88</guid>
      <comments>https://ddokyun.tistory.com/88#entry88comment</comments>
      <pubDate>Wed, 14 Jan 2026 13:02:04 +0900</pubDate>
    </item>
    <item>
      <title>템플릿 메서드 패턴(Template Method Pattern)</title>
      <link>https://ddokyun.tistory.com/87</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 열 번째 주제는 &lt;b&gt;템플릿 메서드 패턴(Template Method Pattern)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전략 패턴&lt;/b&gt;이 &lt;b&gt;&quot;알고리즘 전체를 교체&quot;&lt;/b&gt;하는 거였다면, &lt;b&gt;템플릿 메서드 패턴&lt;/b&gt;은 &lt;b&gt;&quot;알고리즘의 일부만 변경&quot;&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 라면을 끓이는 과정을 생각해보자. 물을 끓이고 &amp;rarr; 스프를 넣고 &amp;rarr; 면을 넣고 &amp;rarr; 끓이는 순서는 똑같다. 하지만 신라면은 매운 스프, 안성탕면은 짠 스프를 넣는다. 전체 흐름은 같지만 일부만 다른 것이다. &lt;b&gt;템플릿 메서드 패턴&lt;/b&gt;은 이처럼 &lt;b&gt;알고리즘의 &quot;뼈대(Template)&quot;는 정의하고, 세부 단계는 하위 클래스에서 구현하는 패턴&lt;/b&gt;이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;템플릿 메서드 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;템플릿 메서드 패턴의 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;템플릿 메서드 패턴(Template Method Pattern)&lt;/b&gt;은 &lt;b&gt;알고리즘의 골격을 정의하고, 일부 단계를 서브클래스에서 구현하도록 하는 패턴&lt;/b&gt;이다. 알고리즘의 구조는 변경하지 않으면서, 특정 단계만 재정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;알고리즘의 &lt;b&gt;골격&lt;/b&gt;을 정의한다&lt;/li&gt;
&lt;li&gt;일부 단계를 &lt;b&gt;서브클래스에 위임&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상속&lt;/b&gt;을 사용한다&lt;/li&gt;
&lt;li&gt;코드 &lt;b&gt;중복을 제거&lt;/b&gt;한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라면 끓이기 (전체 과정은 같지만 스프만 다름)&lt;/li&gt;
&lt;li&gt;문서 작성 (헤더/본문/푸터 구조는 같지만 내용만 다름)&lt;/li&gt;
&lt;li&gt;시험 보기 (입실&amp;rarr;시험&amp;rarr;퇴실은 같지만 과목만 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 템플릿 메서드 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음료를 만드는 카페 시스템을 개발한다고 가정해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 문제가 있는 코드

// 커피 만들기
public class Coffee {
    public void make() {
        System.out.println(&quot;1. 물을 끓인다&quot;);
        System.out.println(&quot;2. 커피를 우려낸다&quot;);
        System.out.println(&quot;3. 컵에 따른다&quot;);
        System.out.println(&quot;4. 설탕과 우유를 추가한다&quot;);
    }
}

// 홍차 만들기
public class Tea {
    public void make() {
        System.out.println(&quot;1. 물을 끓인다&quot;);      // 중복!
        System.out.println(&quot;2. 차를 우려낸다&quot;);
        System.out.println(&quot;3. 컵에 따른다&quot;);      // 중복!
        System.out.println(&quot;4. 레몬을 추가한다&quot;);
}

// 핫초코 만들기
public class HotChocolate {
    public void make() {
        System.out.println(&quot;1. 물을 끓인다&quot;);      // 중복!
        System.out.println(&quot;2. 초코를 녹인다&quot;);
        System.out.println(&quot;3. 컵에 따른다&quot;);      // 중복!
        System.out.println(&quot;4. 마시멜로를 추가한다&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 중복&lt;/b&gt;: &quot;물을 끓인다&quot;, &quot;컵에 따른다&quot;가 반복됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 어려움&lt;/b&gt;: 순서 변경 시 모든 클래스 수정 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성 없음&lt;/b&gt;: 각 클래스가 독립적으로 동작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 어려움&lt;/b&gt;: 새로운 음료 추가 시 또 중복 코드 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 메서드 패턴은 &lt;b&gt;&quot;공통 부분은 부모 클래스에, 다른 부분만 자식 클래스에&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 추상 클래스 정의 (템플릿 메서드)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 음료 만들기의 템플릿
public abstract class Beverage {
    
    // 템플릿 메서드: 알고리즘의 골격
    public final void make() {
        boilWater();        // 공통: 물 끓이기
        brew();             // 다름: 서브클래스에서 구현
        pourInCup();        // 공통: 컵에 따르기
        addCondiments();    // 다름: 서브클래스에서 구현
    }
    
    // 공통 메서드
    private void boilWater() {
        System.out.println(&quot;물을 끓인다&quot;);
    }
    
    private void pourInCup() {
        System.out.println(&quot;컵에 따른다&quot;);
    }
    
    // 추상 메서드: 서브클래스가 구현해야 함
    protected abstract void brew();
    protected abstract void addCondiments();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 구체적인 클래스 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 커피
public class Coffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println(&quot;커피를 우려낸다&quot;);
    }
    
    @Override
    protected void addCondiments() {
        System.out.println(&quot;설탕과 우유를 추가한다&quot;);
    }
}

// 홍차
public class Tea extends Beverage {
    @Override
    protected void brew() {
        System.out.println(&quot;차를 우려낸다&quot;);
    }
    
    @Override
    protected void addCondiments() {
        System.out.println(&quot;레몬을 추가한다&quot;);
    }
}

// 핫초코
public class HotChocolate extends Beverage {
    @Override
    protected void brew() {
        System.out.println(&quot;초코를 녹인다&quot;);
    }
    
    @Override
    protected void addCondiments() {
        System.out.println(&quot;마시멜로를 추가한다&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;=== 커피 만들기 ===&quot;);
        Beverage coffee = new Coffee();
        coffee.make();
        
        System.out.println(&quot;\n=== 홍차 만들기 ===&quot;);
        Beverage tea = new Tea();
        tea.make();
        
        System.out.println(&quot;\n=== 핫초코 만들기 ===&quot;);
        Beverage hotChocolate = new HotChocolate();
        hotChocolate.make();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVsEDc/dJMcaajISK8/Y35stRPvGTpYZk7jFtS8a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVsEDc/dJMcaajISK8/Y35stRPvGTpYZk7jFtS8a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVsEDc/dJMcaajISK8/Y35stRPvGTpYZk7jFtS8a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVsEDc%2FdJMcaajISK8%2FY35stRPvGTpYZk7jFtS8a0%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;373&quot; height=&quot;558&quot; data-origin-width=&quot;373&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 코드 제거됨&lt;/li&gt;
&lt;li&gt;알고리즘 구조를 한 곳에서 관리&lt;/li&gt;
&lt;li&gt;새로운 음료 추가 쉬움&lt;/li&gt;
&lt;li&gt;일관성 보장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Hook 메서드 (선택적 단계)&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 메서드에 &lt;b&gt;선택적 단계&lt;/b&gt;를 추가할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public abstract class Beverage {
    
    // 템플릿 메서드
    public final void make() {
        boilWater();
        brew();
        pourInCup();
        
        // Hook: 원하면 추가 재료를 넣을 수 있음
        if (wantsCondiments()) {
            addCondiments();
        }
    }
    
    private void boilWater() {
        System.out.println(&quot;물을 끓인다&quot;);
    }
    
    private void pourInCup() {
        System.out.println(&quot;컵에 따른다&quot;);
    }
    
    protected abstract void brew();
    protected abstract void addCondiments();
    
    // Hook 메서드: 기본 구현 제공, 필요시 오버라이드
    protected boolean wantsCondiments() {
        return true;  // 기본값
    }
}

// 블랙커피 (추가 재료 없음)
public class BlackCoffee extends Beverage {
    @Override
    protected void brew() {
        System.out.println(&quot;커피를 우려낸다&quot;);
    }
    
    @Override
    protected void addCondiments() {
        System.out.println(&quot;아무것도 추가하지 않는다&quot;);
    }
    
    @Override
    protected boolean wantsCondiments() {
        return false;  // 추가 재료 안 넣음
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;=== 블랙커피 만들기 ===&quot;);
        Beverage blackCoffee = new BlackCoffee();
        blackCoffee.make();
    }
}
```

**실행 결과:**
```
=== 블랙커피 만들기 ===
물을 끓인다
커피를 우려낸다
컵에 따른다
(추가 재료 단계 생략됨)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 게임 캐릭터&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 게임 캐릭터 템플릿
public abstract class GameCharacter {
    
    // 템플릿 메서드: 게임 플레이 흐름
    public final void play() {
        initialize();
        startPlaying();
        
        if (hasSpecialSkill()) {
            useSpecialSkill();
        }
        
        endPlaying();
    }
    
    // 공통 단계
    private void initialize() {
        System.out.println(&quot;  게임 시작&quot;);
    }
    
    private void endPlaying() {
        System.out.println(&quot;  게임 종료\n&quot;);
    }
    
    // 서브클래스가 구현
    protected abstract void startPlaying();
    
    // Hook 메서드
    protected boolean hasSpecialSkill() {
        return false;
    }
    
    protected void useSpecialSkill() {
        // 기본 구현 없음
    }
}

// 전사
public class Warrior extends GameCharacter {
    @Override
    protected void startPlaying() {
        System.out.println(&quot;⚔️ 전사: 근접 공격!&quot;);
    }
    
    @Override
    protected boolean hasSpecialSkill() {
        return true;
    }
    
    @Override
    protected void useSpecialSkill() {
        System.out.println(&quot;  전사 특수기: 광폭화!&quot;);
    }
}

// 마법사
public class Mage extends GameCharacter {
    @Override
    protected void startPlaying() {
        System.out.println(&quot;  마법사: 마법 공격!&quot;);
    }
    
    @Override
    protected boolean hasSpecialSkill() {
        return true;
    }
    
    @Override
    protected void useSpecialSkill() {
        System.out.println(&quot;✨ 마법사 특수기: 메테오!&quot;);
    }
}

// 궁수
public class Archer extends GameCharacter {
    @Override
    protected void startPlaying() {
        System.out.println(&quot;  궁수: 원거리 공격!&quot;);
    }
    
    // 특수기 없음 (기본값 사용)
}

// 사용
public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;=== 전사 플레이 ===&quot;);
        GameCharacter warrior = new Warrior();
        warrior.play();
        
        System.out.println(&quot;=== 마법사 플레이 ===&quot;);
        GameCharacter mage = new Mage();
        mage.play();
        
        System.out.println(&quot;=== 궁수 플레이 ===&quot;);
        GameCharacter archer = new Archer();
        archer.play();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;템플릿 메서드 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 재사용&lt;/b&gt;: 공통 로직을 한 곳에 모음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성&lt;/b&gt;: 알고리즘 구조를 강제&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 용이&lt;/b&gt;: 새로운 구현 추가가 쉬움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수&lt;/b&gt;: 공통 부분만 수정하면 전체 반영&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제어 반전&lt;/b&gt;: 프레임워크가 흐름을 제어&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;템플릿 메서드 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상속 의존&lt;/b&gt;: 상속을 강제함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성 제한&lt;/b&gt;: 알고리즘 구조를 바꿀 수 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리스코프 치환 원칙 위반 가능&lt;/b&gt;: 잘못 설계하면 문제 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;템플릿 메서드 복잡도&lt;/b&gt;: 단계가 많으면 이해하기 어려움&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 템플릿 메서드 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 많은 클래스가 템플릿 메서드 패턴을 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JdbcTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Spring의 JdbcTemplate가 템플릿 메서드 패턴 사용
@Repository
public class UserRepository {
    private final JdbcTemplate jdbcTemplate;
    
    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public List&amp;lt;User&amp;gt; findAll() {
        // JdbcTemplate이 공통 로직 처리:
        // 1. Connection 열기
        // 2. PreparedStatement 생성
        // 3. ResultSet 처리
        // 4. Exception 처리
        // 5. Connection 닫기
        
        return jdbcTemplate.query(
            &quot;SELECT * FROM users&quot;,
            (rs, rowNum) -&amp;gt; new User(  // 개발자는 이 부분만 구현
                rs.getLong(&quot;id&quot;),
                rs.getString(&quot;name&quot;)
            )
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RestTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// RestTemplate도 템플릿 메서드 패턴
@Service
public class ApiService {
    private final RestTemplate restTemplate;
    
    public String callApi() {
        // RestTemplate이 공통 로직 처리:
        // 1. HTTP Connection 생성
        // 2. Request 전송
        // 3. Response 수신
        // 4. Exception 처리
        // 5. Connection 닫기
        
        return restTemplate.getForObject(
            &quot;https://api.example.com/data&quot;,
            String.class
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TransactionTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// TransactionTemplate도 템플릿 메서드 패턴
@Service
public class OrderService {
    private final TransactionTemplate transactionTemplate;
    
    public void createOrder() {
        transactionTemplate.execute(status -&amp;gt; {
            // TransactionTemplate이 공통 로직 처리:
            // 1. Transaction 시작
            // 2. 비즈니스 로직 실행
            // 3. Commit/Rollback
            
            // 개발자는 비즈니스 로직만 작성
            saveOrder();
            updateStock();
            return null;
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 클래스가 비슷한 흐름을 가질 때&lt;/li&gt;
&lt;li&gt;알고리즘의 일부만 다를 때&lt;/li&gt;
&lt;li&gt;코드 중복이 많을 때&lt;/li&gt;
&lt;li&gt;프레임워크 개발 시&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알고리즘이 완전히 다를 때&lt;/li&gt;
&lt;li&gt;상속을 피하고 싶을 때&lt;/li&gt;
&lt;li&gt;유연한 구조가 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;알고리즘의 골격은 같고 일부만 다르다면 템플릿 메서드 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전략 패턴 vs 템플릿 메서드 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 전략 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 템플릿 메서드 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 방법&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;합성(Composition)&lt;/td&gt;
&lt;td&gt;상속(Inheritance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;변경 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;알고리즘 전체 교체&lt;/td&gt;
&lt;td&gt;알고리즘 일부만 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유연성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;높음 (런타임 변경)&lt;/td&gt;
&lt;td&gt;낮음 (컴파일 시 결정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;클래스 수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;많음&lt;/td&gt;
&lt;td&gt;적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;알고리즘이 완전히 다를 때&lt;/td&gt;
&lt;td&gt;알고리즘 구조는 같을 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 전략 패턴: 결제 방법 전체를 바꿈
service.setPaymentStrategy(new CardStrategy());  // 카드 결제
service.setPaymentStrategy(new CashStrategy());  // 현금 결제

// 템플릿 메서드: 음료 만드는 일부만 바꿈
Beverage coffee = new Coffee();  // 커피 우려내기
Beverage tea = new Tea();        // 차 우려내기
// 둘 다 &quot;물 끓이기 &amp;rarr; 우리기 &amp;rarr; 따르기&quot; 흐름은 동일&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알고리즘의 &lt;b&gt;골격을 정의&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상속&lt;/b&gt;을 사용한다&lt;/li&gt;
&lt;li&gt;코드 &lt;b&gt;중복을 제거&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;Spring의 &lt;b&gt;핵심 패턴&lt;/b&gt; 중 하나&lt;/li&gt;
&lt;li&gt;전략 패턴보다 &lt;b&gt;덜 유연&lt;/b&gt;하지만 &lt;b&gt;간단&lt;/b&gt;하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 객체 간의 의존성을 느슨하게 만드는 &lt;b&gt;옵저버 패턴&lt;/b&gt;을 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글 예고:&lt;/b&gt; 옵저버 패턴 - 객체의 상태 변화를 관찰하라&lt;/p&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>디자인 패턴</category>
      <category>템플릿 메서드 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/87</guid>
      <comments>https://ddokyun.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 14 Jan 2026 12:46:47 +0900</pubDate>
    </item>
    <item>
      <title>전략 패턴(Strategy Pattern)</title>
      <link>https://ddokyun.tistory.com/86</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 아홉 번째 주제는 &lt;b&gt;전략 패턴(Strategy Pattern)&lt;/b&gt;이다. 이번부터는 &lt;b&gt;행위 패턴&lt;/b&gt;으로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구조 패턴&lt;/b&gt;이 &lt;b&gt;&quot;객체들을 어떻게 조합할까?&quot;&lt;/b&gt;였다면, &lt;b&gt;행위 패턴&lt;/b&gt;은 &lt;b&gt;&quot;객체들이 어떻게 협력하고 책임을 분담할까?&quot;&lt;/b&gt;에 대한 해답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 내비게이션 앱을 생각해보자. 목적지까지 가는 방법은 여러 가지다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;자동차로 갈 수도 있고&lt;/li&gt;
&lt;li&gt;대중교통을 이용할 수도 있고&lt;/li&gt;
&lt;li&gt;도보로 갈 수도 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황에 따라 최적의 경로 찾기 &lt;b&gt;전략&lt;/b&gt;을 선택해야 한다. 전략 패턴은 이처럼 여러 알고리즘 중 하나를 상황에 맞게 선택할 수 있게 해주는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;전략 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전략 패턴의 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전략 패턴(Strategy Pattern)&lt;/b&gt;은 &lt;b&gt;알고리즘군을 정의하고 각각을 캡슐화하여, 이들을 상호 교환 가능하게 만드는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;알고리즘을 &lt;b&gt;캡슐화&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;실행 중에 알고리즘을 &lt;b&gt;선택&lt;/b&gt;할 수 있다&lt;/li&gt;
&lt;li&gt;알고리즘 사용 코드와 &lt;b&gt;분리&lt;/b&gt;된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;if-else 지옥&lt;/b&gt;을 제거한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내비게이션 경로 찾기 (자동차/대중교통/도보)&lt;/li&gt;
&lt;li&gt;결제 수단 선택 (카드/현금/포인트)&lt;/li&gt;
&lt;li&gt;압축 방식 선택 (ZIP/RAR/7Z)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 전략 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 쇼핑몰에서 결제 시스템을 개발한다고 가정해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 문제가 있는 코드
public class PaymentService {
    public void processPayment(String paymentType, int amount) {
        if (paymentType.equals(&quot;CARD&quot;)) {
            System.out.println(&quot;신용카드로 &quot; + amount + &quot;원 결제&quot;);
            System.out.println(&quot;카드 승인 요청...&quot;);
            System.out.println(&quot;영수증 발행&quot;);
        } else if (paymentType.equals(&quot;CASH&quot;)) {
            System.out.println(&quot;현금으로 &quot; + amount + &quot;원 결제&quot;);
            System.out.println(&quot;현금 영수증 발행&quot;);
        } else if (paymentType.equals(&quot;POINT&quot;)) {
            System.out.println(&quot;포인트로 &quot; + amount + &quot;원 결제&quot;);
            System.out.println(&quot;포인트 차감&quot;);
            System.out.println(&quot;포인트 내역 저장&quot;);
        } else if (paymentType.equals(&quot;KAKAO&quot;)) {
            System.out.println(&quot;카카오페이로 &quot; + amount + &quot;원 결제&quot;);
            System.out.println(&quot;카카오 API 호출...&quot;);
            System.out.println(&quot;결제 승인&quot;);
        } else if (paymentType.equals(&quot;NAVER&quot;)) {
            System.out.println(&quot;네이버페이로 &quot; + amount + &quot;원 결제&quot;);
            System.out.println(&quot;네이버 API 호출...&quot;);
            System.out.println(&quot;결제 승인&quot;);
        }
        // 새로운 결제 수단이 추가될 때마다 if-else 추가...
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();
        service.processPayment(&quot;CARD&quot;, 10000);
        service.processPayment(&quot;KAKAO&quot;, 20000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;if-else 지옥&lt;/b&gt;: 결제 수단이 늘어날수록 코드가 길어짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OCP 위반&lt;/b&gt;: 새로운 결제 수단 추가 시 기존 코드 수정 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SRP 위반&lt;/b&gt;: 하나의 클래스가 모든 결제 로직을 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 어려움&lt;/b&gt;: 특정 결제 수단만 테스트하기 어려움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 악몽&lt;/b&gt;: 결제 로직 변경 시 전체 메서드 수정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전략 패턴&lt;/b&gt;은 &lt;b&gt;&quot;각 결제 방법을 독립적인 전략으로 분리하자&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 전략 인터페이스 정의&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 결제 전략 인터페이스
public interface PaymentStrategy {
    void pay(int amount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 구체적인 전략 구현&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 신용카드 결제 전략
public class CardPaymentStrategy implements PaymentStrategy {
    private String cardNumber;
    
    public CardPaymentStrategy(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(&quot;  신용카드로 &quot; + amount + &quot;원 결제&quot;);
        System.out.println(&quot;카드번호: &quot; + cardNumber);
        System.out.println(&quot;카드 승인 완료\n&quot;);
    }
}

// 현금 결제 전략
public class CashPaymentStrategy implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println(&quot;  현금으로 &quot; + amount + &quot;원 결제&quot;);
        System.out.println(&quot;현금 영수증 발행\n&quot;);
    }
}

// 포인트 결제 전략
public class PointPaymentStrategy implements PaymentStrategy {
    private String memberId;
    
    public PointPaymentStrategy(String memberId) {
        this.memberId = memberId;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(&quot;⭐ 포인트로 &quot; + amount + &quot;원 결제&quot;);
        System.out.println(&quot;회원ID: &quot; + memberId);
        System.out.println(&quot;포인트 차감 완료\n&quot;);
    }
}

// 카카오페이 결제 전략
public class KakaoPayStrategy implements PaymentStrategy {
    private String kakaoId;
    
    public KakaoPayStrategy(String kakaoId) {
        this.kakaoId = kakaoId;
    }
    
    @Override
    public void pay(int amount) {
        System.out.println(&quot;  카카오페이로 &quot; + amount + &quot;원 결제&quot;);
        System.out.println(&quot;카카오ID: &quot; + kakaoId);
        System.out.println(&quot;카카오 API 호출 완료\n&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 컨텍스트 클래스 (전략 사용)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 결제 처리 클래스 (컨텍스트)
public class PaymentService {
    private PaymentStrategy paymentStrategy;
    
    // 전략을 선택할 수 있음
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    // 선택된 전략으로 결제 처리
    public void processPayment(int amount) {
        if (paymentStrategy == null) {
            throw new IllegalStateException(&quot;결제 수단을 선택해주세요&quot;);
        }
        paymentStrategy.pay(amount);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();
        
        // 신용카드로 결제
        System.out.println(&quot;=== 고객 1 ===&quot;);
        paymentService.setPaymentStrategy(new CardPaymentStrategy(&quot;1234-5678-9012&quot;));
        paymentService.processPayment(15000);
        
        // 포인트로 결제
        System.out.println(&quot;=== 고객 2 ===&quot;);
        paymentService.setPaymentStrategy(new PointPaymentStrategy(&quot;user123&quot;));
        paymentService.processPayment(5000);
        
        // 카카오페이로 결제
        System.out.println(&quot;=== 고객 3 ===&quot;);
        paymentService.setPaymentStrategy(new KakaoPayStrategy(&quot;kakao_user&quot;));
        paymentService.processPayment(25000);
        
        // 런타임에 전략 변경 가능
        System.out.println(&quot;=== 고객 3 결제 수단 변경 ===&quot;);
        paymentService.setPaymentStrategy(new CashPaymentStrategy());
        paymentService.processPayment(10000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxLtMh/dJMcajt8S6Z/nbI4g5NcjPPoNuuReDV6r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxLtMh/dJMcajt8S6Z/nbI4g5NcjPPoNuuReDV6r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxLtMh/dJMcajt8S6Z/nbI4g5NcjPPoNuuReDV6r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxLtMh%2FdJMcajt8S6Z%2FnbI4g5NcjPPoNuuReDV6r1%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;338&quot; height=&quot;569&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;if-else 제거됨&lt;/li&gt;
&lt;li&gt;새로운 결제 수단 추가 시 기존 코드 수정 불필요&lt;/li&gt;
&lt;li&gt;각 전략을 독립적으로 테스트 가능&lt;/li&gt;
&lt;li&gt;런타임에 전략 변경 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 할인 정책&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 할인 전략 인터페이스
public interface DiscountStrategy {
    int calculateDiscount(int price);
}

// 정액 할인
public class FixedDiscountStrategy implements DiscountStrategy {
    private int discountAmount;
    
    public FixedDiscountStrategy(int discountAmount) {
        this.discountAmount = discountAmount;
    }
    
    @Override
    public int calculateDiscount(int price) {
        return Math.max(0, price - discountAmount);
    }
}

// 정률 할인
public class PercentDiscountStrategy implements DiscountStrategy {
    private int discountPercent;
    
    public PercentDiscountStrategy(int discountPercent) {
        this.discountPercent = discountPercent;
    }
    
    @Override
    public int calculateDiscount(int price) {
        return price * (100 - discountPercent) / 100;
    }
}

// 할인 없음
public class NoDiscountStrategy implements DiscountStrategy {
    @Override
    public int calculateDiscount(int price) {
        return price;
    }
}

// VIP 할인 (정률 + 추가 할인)
public class VIPDiscountStrategy implements DiscountStrategy {
    @Override
    public int calculateDiscount(int price) {
        int percentDiscount = price * 80 / 100;  // 20% 할인
        return Math.max(0, percentDiscount - 5000);  // 추가 5000원 할인
    }
}

// 주문 처리
public class Order {
    private int price;
    private DiscountStrategy discountStrategy;
    
    public Order(int price) {
        this.price = price;
        this.discountStrategy = new NoDiscountStrategy();  // 기본값
    }
    
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }
    
    public int calculateFinalPrice() {
        return discountStrategy.calculateDiscount(price);
    }
    
    public void printOrder() {
        System.out.println(&quot;원가: &quot; + price + &quot;원&quot;);
        System.out.println(&quot;최종가: &quot; + calculateFinalPrice() + &quot;원\n&quot;);
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 일반 고객
        Order order1 = new Order(50000);
        order1.setDiscountStrategy(new NoDiscountStrategy());
        System.out.println(&quot;=== 일반 고객 ===&quot;);
        order1.printOrder();
        
        // 신규 고객 (10% 할인)
        Order order2 = new Order(50000);
        order2.setDiscountStrategy(new PercentDiscountStrategy(10));
        System.out.println(&quot;=== 신규 고객 (10% 할인) ===&quot;);
        order2.printOrder();
        
        // 프로모션 (5000원 할인)
        Order order3 = new Order(50000);
        order3.setDiscountStrategy(new FixedDiscountStrategy(5000));
        System.out.println(&quot;=== 프로모션 (5000원 할인) ===&quot;);
        order3.printOrder();
        
        // VIP 고객
        Order order4 = new Order(50000);
        order4.setDiscountStrategy(new VIPDiscountStrategy());
        System.out.println(&quot;=== VIP 고객 ===&quot;);
        order4.printOrder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전략 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;OCP 준수&lt;/b&gt;: 새로운 전략 추가 시 기존 코드 수정 불필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SRP 준수&lt;/b&gt;: 각 전략이 독립적인 책임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;if-else 제거&lt;/b&gt;: 깔끔한 코드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이&lt;/b&gt;: 각 전략을 독립적으로 테스트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: 런타임에 전략 변경 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성&lt;/b&gt;: 전략을 여러 곳에서 재사용&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전략 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 증가&lt;/b&gt;: 전략마다 클래스 생성 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트 인지&lt;/b&gt;: 클라이언트가 전략의 차이를 알아야 함&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨텍스트 복잡도&lt;/b&gt;: 전략이 많아지면 선택 로직이 복잡해질 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 전략 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 전략 패턴은 매우 자주 사용된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 결제 전략 인터페이스
public interface PaymentStrategy {
    boolean pay(int amount);
    String getPaymentType();
}

// 구체적인 전략들
@Component
public class CardPaymentStrategy implements PaymentStrategy {
    @Override
    public boolean pay(int amount) {
        System.out.println(&quot;카드 결제: &quot; + amount);
        return true;
    }
    
    @Override
    public String getPaymentType() {
        return &quot;CARD&quot;;
    }
}

@Component
public class KakaoPayStrategy implements PaymentStrategy {
    @Override
    public boolean pay(int amount) {
        System.out.println(&quot;카카오페이 결제: &quot; + amount);
        return true;
    }
    
    @Override
    public String getPaymentType() {
        return &quot;KAKAO&quot;;
    }
}

// 전략 팩토리 (전략 선택)
@Component
public class PaymentStrategyFactory {
    private final Map&amp;lt;String, PaymentStrategy&amp;gt; strategies;
    
    // Spring이 모든 PaymentStrategy 구현체를 주입
    public PaymentStrategyFactory(List&amp;lt;PaymentStrategy&amp;gt; strategyList) {
        this.strategies = strategyList.stream()
            .collect(Collectors.toMap(
                PaymentStrategy::getPaymentType,
                strategy -&amp;gt; strategy
            ));
    }
    
    public PaymentStrategy getStrategy(String paymentType) {
        PaymentStrategy strategy = strategies.get(paymentType);
        if (strategy == null) {
            throw new IllegalArgumentException(&quot;지원하지 않는 결제 수단: &quot; + paymentType);
        }
        return strategy;
    }
}

// 서비스에서 사용
@Service
public class OrderService {
    private final PaymentStrategyFactory strategyFactory;
    
    public OrderService(PaymentStrategyFactory strategyFactory) {
        this.strategyFactory = strategyFactory;
    }
    
    public void processOrder(String paymentType, int amount) {
        PaymentStrategy strategy = strategyFactory.getStrategy(paymentType);
        boolean success = strategy.pay(amount);
        
        if (success) {
            System.out.println(&quot;주문 완료!&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 목적의 여러 알고리즘이 있을 때&lt;/li&gt;
&lt;li&gt;if-else나 switch-case가 많을 때&lt;/li&gt;
&lt;li&gt;런타임에 알고리즘을 선택해야 할 때&lt;/li&gt;
&lt;li&gt;알고리즘이 자주 변경되거나 추가될 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알고리즘이 하나뿐일 때&lt;/li&gt;
&lt;li&gt;알고리즘이 거의 변하지 않을 때&lt;/li&gt;
&lt;li&gt;간단한 if-else로 충분할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;여러 알고리즘 중 하나를 선택해야 한다면 전략 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;div&gt;
&lt;div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전략 패턴 vs 팩토리 메서드 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 런타임에 선택한다는 공통점이 있지만, 목적이 다르다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 147px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;구분 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 전략 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 팩토리 메서드 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;패턴 분류&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;행위 패턴&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생성 패턴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;알고리즘(행동) 실행 방법을 선택&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 생성 방법을 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;초점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&amp;ldquo;&lt;b&gt;어떻게 할까?&lt;/b&gt;&amp;rdquo;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&amp;ldquo;&lt;b&gt;무엇을 만들까?&lt;/b&gt;&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;변경 가능성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;언제든지 전략(알고리즘) 교체 가능&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;생성 후에는 객체 타입이 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;사용 시점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;알고리즘 선택이 중요할 때&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 생성이 복잡할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;핵심 역할&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;여러 알고리즘을 캡슐화하여 교체 가능&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;객체 생성을 서브클래스에 위임&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드로 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팩토리 메서드 패턴:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;fsharp&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 목적: 어떤 결제 객체를 생성할까?
public class PaymentFactory {
    public Payment createPayment(String type) {
        if (type.equals(&quot;CARD&quot;)) {
            return new CardPayment();  // 객체 생성
        } else if (type.equals(&quot;KAKAO&quot;)) {
            return new KakaoPayment();  // 객체 생성
        }
        return null;
    }
}

// 사용
Payment payment = factory.createPayment(&quot;CARD&quot;);
payment.pay(10000);
// payment는 한 번 생성되면 타입 변경 불가&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전략 패턴:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 목적: 어떤 결제 방식으로 처리할까?
public class PaymentService {
    private PaymentStrategy strategy;
    
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;  // 전략 교체
    }
    
    public void pay(int amount) {
        strategy.pay(amount);  // 전략 실행
    }
}

// 사용
PaymentService service = new PaymentService();
service.setStrategy(new CardStrategy());
service.pay(10000);

// 같은 객체로 전략 변경 가능
service.setStrategy(new KakaoStrategy());
service.pay(20000);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실무에서는 함께 사용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 패턴을 조합하면 더욱 강력하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 팩토리로 전략 객체를 생성
@Component
public class PaymentStrategyFactory {
    public PaymentStrategy createStrategy(String type) {
        // 팩토리 메서드로 전략 생성
        switch (type) {
            case &quot;CARD&quot;: return new CardPaymentStrategy();
            case &quot;KAKAO&quot;: return new KakaoPaymentStrategy();
            case &quot;POINT&quot;: return new PointPaymentStrategy();
            default: throw new IllegalArgumentException(&quot;Unknown type&quot;);
        }
    }
}

// 서비스에서 사용
@Service
public class OrderService {
    private final PaymentStrategyFactory factory;
    private final PaymentService paymentService;
    
    public void checkout(String paymentType, int amount) {
        // 1. 팩토리로 전략 생성
        PaymentStrategy strategy = factory.createStrategy(paymentType);
        
        // 2. 전략 패턴으로 실행
        paymentService.setStrategy(strategy);
        paymentService.process(amount);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;팩토리&lt;/b&gt;: 객체를 어떻게 만들까?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전략&lt;/b&gt;: 알고리즘을 어떻게 실행할까?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;알고리즘을 캡슐화&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;if-else 지옥&lt;/b&gt;을 제거한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;런타임에 전략 변경&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;Spring에서 &lt;b&gt;매우 자주&lt;/b&gt; 사용된다&lt;/li&gt;
&lt;li&gt;새로운 전략 추가가 &lt;b&gt;쉽다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 알고리즘의 골격을 정의하는 &lt;b&gt;템플릿 메서드 패턴&lt;/b&gt;을 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글 예고:&lt;/b&gt; 템플릿 메서드 패턴 - 알고리즘의 뼈대를 정의하라&lt;/p&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>디자인 패턴</category>
      <category>전략 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/86</guid>
      <comments>https://ddokyun.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 14 Jan 2026 10:57:21 +0900</pubDate>
    </item>
    <item>
      <title>프록시 패턴(Proxy Pattern)</title>
      <link>https://ddokyun.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 여덟 번째 주제는 &lt;b&gt;프록시 패턴(Proxy Pattern)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데코레이터 패턴&lt;/b&gt;이 &lt;b&gt;&quot;기능을 추가&quot;&lt;/b&gt;하는 거였다면, &lt;b&gt;프록시 패턴&lt;/b&gt;은 &lt;b&gt;&quot;접근을 제어&quot;&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 회사에 중요한 서버가 있다고 생각해보자. 모든 사람이 직접 서버에 접근하면 위험하다. 그래서 보안 담당자가 중간에서 신원을 확인하고, 권한이 있는 사람만 서버에 접근하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프록시 패턴&lt;/b&gt;은 이처럼 &lt;b&gt;실제 객체 앞에 &quot;대리인(Proxy)&quot;을 두어 접근을 제어하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;프록시 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프록시 패턴의 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프록시 패턴(Proxy Pattern)&lt;/b&gt;은 &lt;b&gt;다른 객체에 대한 접근을 제어하기 위해 대리자(Proxy)를 제공하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 객체를 직접 호출하는 대신, 프록시를 통해 간접적으로 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실제 객체 앞에 &lt;b&gt;대리인&lt;/b&gt;을 둔다&lt;/li&gt;
&lt;li&gt;실제 객체와 &lt;b&gt;동일한 인터페이스&lt;/b&gt;를 제공한다&lt;/li&gt;
&lt;li&gt;접근을 &lt;b&gt;제어하거나 추가 작업&lt;/b&gt;을 수행한다&lt;/li&gt;
&lt;li&gt;클라이언트는 프록시인지 &lt;b&gt;구분하지 못한다&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연예인의 매니저 (팬이 연예인에게 직접 접근 못함)&lt;/li&gt;
&lt;li&gt;부동산 중개인 (집주인과 구매자 사이)&lt;/li&gt;
&lt;li&gt;VPN (실제 서버와 사용자 사이)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 프록시 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트 애플리케이션을 개발한다고 가정해보자. 여러 서비스 클래스가 있고, 각각 데이터베이스 연결이 필요하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 데이터베이스 연결 클래스
public class DatabaseConnection {
    private String host;
    private int port;
    
    public DatabaseConnection(String host, int port) {
        this.host = host;
        this.port = port;
        connect();  // 생성 즉시 연결
    }
    
    private void connect() {
        System.out.println(&quot;  &quot; + host + &quot;:&quot; + port + &quot; DB 연결 중...&quot;);
        try {
            Thread.sleep(2000);  // 연결 시간 시뮬레이션
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(&quot;✅ DB 연결 완료!&quot;);
    }
    
    public String query(String sql) {
        System.out.println(&quot;  쿼리 실행: &quot; + sql);
        return &quot;결과 데이터&quot;;
    }
}

// 사용자 서비스
public class UserService {
    private DatabaseConnection connection;
    
    public UserService() {
        connection = new DatabaseConnection(&quot;localhost&quot;, 5432);
    }
    
    public String getUser(Long id) {
        return connection.query(&quot;SELECT * FROM users WHERE id = &quot; + id);
    }
}

// 상품 서비스
public class ProductService {
    private DatabaseConnection connection;
    
    public ProductService() {
        connection = new DatabaseConnection(&quot;localhost&quot;, 5432);
    }
    
    public String getProduct(Long id) {
        return connection.query(&quot;SELECT * FROM products WHERE id = &quot; + id);
    }
}

// 주문 서비스
public class OrderService {
    private DatabaseConnection connection;
    
    public OrderService() {
        connection = new DatabaseConnection(&quot;localhost&quot;, 5432);
    }
    
    public String getOrder(Long id) {
        return connection.query(&quot;SELECT * FROM orders WHERE id = &quot; + id);
    }
}

// 애플리케이션 시작
public class Application {
    public static void main(String[] args) {
        System.out.println(&quot;  애플리케이션 시작\n&quot;);
        
        // 모든 서비스 초기화
        UserService userService = new UserService();
        ProductService productService = new ProductService();
        OrderService orderService = new OrderService();
        
        System.out.println(&quot;\n✨ 애플리케이션 준비 완료\n&quot;);
        
        // 실제로는 UserService만 사용
        System.out.println(&quot;--- 사용자 조회 ---&quot;);
        userService.getUser(1L);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;앱 시작하는 데 6초나 걸림 (DB 연결 3개 * 2초)&lt;/li&gt;
&lt;li&gt;ProductService, OrderService는 사용하지 않는데도 DB 연결됨&lt;/li&gt;
&lt;li&gt;DB 커넥션 풀 리소스 낭비&lt;/li&gt;
&lt;li&gt;서비스가 100개라면? 200초 대기...&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 패턴은 &lt;b&gt;&quot;실제로 필요할 때만 연결하자 (Lazy Loading)&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 공통 인터페이스 정의&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public interface Image {
    void display();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 실제 객체 (Real Subject)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public class RealImage implements Image {
    private String fileName;
    
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk();
    }
    
    private void loadFromDisk() {
        System.out.println(fileName + &quot; 디스크에서 로딩 중...&quot;);
        try {
            Thread.sleep(2000);  // 로딩 시뮬레이션
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(fileName + &quot; 로딩 완료!&quot;);
    }
    
    @Override
    public void display() {
        System.out.println(fileName + &quot; 화면에 표시\n&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 프록시 객체&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public class ImageProxy implements Image {
    private String fileName;
    private RealImage realImage;  // 실제 객체 (처음엔 null)
    
    public ImageProxy(String fileName) {
        this.fileName = fileName;
        // 실제 이미지는 생성하지 않음!
    }
    
    @Override
    public void display() {
        // 실제로 필요할 때만 로딩 (Lazy Loading)
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;=== 프록시 사용 ===&quot;);
        System.out.println(&quot;앱 시작&quot;);
        
        // 프록시 객체 생성 (즉시 로딩 안 함)
        Image image1 = new ImageProxy(&quot;사진1.jpg&quot;);
        Image image2 = new ImageProxy(&quot;사진2.jpg&quot;);
        Image image3 = new ImageProxy(&quot;사진3.jpg&quot;);
        
        System.out.println(&quot;앱 준비 완료 (즉시!)\n&quot;);
        
        // 실제로 사용할 때만 로딩
        System.out.println(&quot;--- 사진1 보기 ---&quot;);
        image1.display();
        
        System.out.println(&quot;--- 사진1 다시 보기 ---&quot;);
        image1.display();  // 이미 로딩됨, 즉시 표시
        
        System.out.println(&quot;--- 사진3 보기 ---&quot;);
        image3.display();
        
        // 사진2는 끝까지 로딩 안 됨!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eAX7y4/dJMcagEgh1g/Ci6ZQKmhwE2j0ZKgUMIxg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eAX7y4/dJMcagEgh1g/Ci6ZQKmhwE2j0ZKgUMIxg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eAX7y4/dJMcagEgh1g/Ci6ZQKmhwE2j0ZKgUMIxg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeAX7y4%2FdJMcagEgh1g%2FCi6ZQKmhwE2j0ZKgUMIxg1%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;381&quot; height=&quot;393&quot; data-origin-width=&quot;381&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앱이 즉시 시작됨&lt;/li&gt;
&lt;li&gt;필요한 이미지만 로딩됨&lt;/li&gt;
&lt;li&gt;한 번 로딩한 이미지는 재사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프록시 패턴의 종류&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 가상 프록시 (Virtual Proxy)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예시처럼 &lt;b&gt;무거운 객체의 생성을 지연&lt;/b&gt;시킨다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 비용이 큰 객체
public class ExpensiveObject implements Service {
    public ExpensiveObject() {
        System.out.println(&quot;무거운 객체 생성 중... (시간 소요)&quot;);
        // 복잡한 초기화 작업
    }
    
    @Override
    public void process() {
        System.out.println(&quot;작업 처리&quot;);
    }
}

// 가상 프록시
public class VirtualProxy implements Service {
    private ExpensiveObject expensiveObject;
    
    @Override
    public void process() {
        if (expensiveObject == null) {
            expensiveObject = new ExpensiveObject();  // 처음 사용 시 생성
        }
        expensiveObject.process();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 보호 프록시 (Protection Proxy)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;접근 권한을 제어&lt;/b&gt;한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 문서 인터페이스
public interface Document {
    void read();
    void write(String content);
}

// 실제 문서
public class SecretDocument implements Document {
    private String content = &quot;극비 문서 내용&quot;;
    
    @Override
    public void read() {
        System.out.println(&quot;문서 읽기: &quot; + content);
    }
    
    @Override
    public void write(String content) {
        this.content = content;
        System.out.println(&quot;문서 수정 완료&quot;);
    }
}

// 보호 프록시
public class DocumentProxy implements Document {
    private SecretDocument document;
    private String userRole;
    
    public DocumentProxy(String userRole) {
        this.userRole = userRole;
        this.document = new SecretDocument();
    }
    
    @Override
    public void read() {
        // 모든 사용자가 읽기 가능
        document.read();
    }
    
    @Override
    public void write(String content) {
        // 관리자만 쓰기 가능
        if (userRole.equals(&quot;ADMIN&quot;)) {
            document.write(content);
        } else {
            System.out.println(&quot;❌ 권한 없음: 관리자만 수정 가능합니다&quot;);
        }
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 일반 사용자
        Document userDoc = new DocumentProxy(&quot;USER&quot;);
        userDoc.read();
        userDoc.write(&quot;변경 시도&quot;);
        
        System.out.println();
        
        // 관리자
        Document adminDoc = new DocumentProxy(&quot;ADMIN&quot;);
        adminDoc.read();
        adminDoc.write(&quot;관리자가 수정한 내용&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 원격 프록시 (Remote Proxy)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 서버에 있는 객체를 로컬처럼&lt;/b&gt; 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 서비스 인터페이스
public interface PaymentService {
    boolean pay(int amount);
}

// 실제 원격 서비스 (다른 서버에 있다고 가정)
public class RemotePaymentService implements PaymentService {
    @Override
    public boolean pay(int amount) {
        System.out.println(&quot;원격 서버에서 &quot; + amount + &quot;원 결제 처리&quot;);
        return true;
    }
}

// 원격 프록시
public class PaymentServiceProxy implements PaymentService {
    private RemotePaymentService remoteService;
    
    @Override
    public boolean pay(int amount) {
        System.out.println(&quot;네트워크 연결 중...&quot;);
        
        if (remoteService == null) {
            remoteService = new RemotePaymentService();
        }
        
        // 원격 호출
        boolean result = remoteService.pay(amount);
        
        System.out.println(&quot;연결 종료&quot;);
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4. 캐싱 프록시 (Caching Proxy)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과를 캐싱&lt;/b&gt;하여 성능을 개선한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 데이터베이스 서비스
public interface Database {
    String query(String sql);
}

// 실제 데이터베이스
public class RealDatabase implements Database {
    @Override
    public String query(String sql) {
        System.out.println(&quot;  DB 쿼리 실행: &quot; + sql + &quot; (느림)&quot;);
        try {
            Thread.sleep(1000);  // DB 조회 시간
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return &quot;결과 데이터&quot;;
    }
}

// 캐싱 프록시
public class CachingDatabaseProxy implements Database {
    private RealDatabase database;
    private Map&amp;lt;String, String&amp;gt; cache;
    
    public CachingDatabaseProxy() {
        this.database = new RealDatabase();
        this.cache = new HashMap&amp;lt;&amp;gt;();
    }
    
    @Override
    public String query(String sql) {
        // 캐시에 있으면 즉시 반환
        if (cache.containsKey(sql)) {
            System.out.println(&quot;✨ 캐시에서 반환: &quot; + sql + &quot; (빠름)&quot;);
            return cache.get(sql);
        }
        
        // 없으면 DB 조회 후 캐싱
        String result = database.query(sql);
        cache.put(sql, result);
        return result;
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        Database db = new CachingDatabaseProxy();
        
        System.out.println(&quot;첫 번째 조회:&quot;);
        db.query(&quot;SELECT * FROM users&quot;);
        
        System.out.println(&quot;\n두 번째 조회 (같은 쿼리):&quot;);
        db.query(&quot;SELECT * FROM users&quot;);
        
        System.out.println(&quot;\n세 번째 조회 (다른 쿼리):&quot;);
        db.query(&quot;SELECT * FROM products&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 로깅 프록시&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 사용자 서비스 인터페이스
public interface UserService {
    void createUser(String name);
    void deleteUser(String name);
}

// 실제 사용자 서비스
public class UserServiceImpl implements UserService {
    @Override
    public void createUser(String name) {
        System.out.println(&quot;✅ 사용자 생성: &quot; + name);
    }
    
    @Override
    public void deleteUser(String name) {
        System.out.println(&quot; ️ 사용자 삭제: &quot; + name);
    }
}

// 로깅 프록시
public class LoggingUserServiceProxy implements UserService {
    private UserService userService;
    
    public LoggingUserServiceProxy(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    public void createUser(String name) {
        System.out.println(&quot;  [로그] createUser 호출됨&quot;);
        long startTime = System.currentTimeMillis();
        
        userService.createUser(name);
        
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;⏱️ [로그] 실행 시간: &quot; + (endTime - startTime) + &quot;ms\n&quot;);
    }
    
    @Override
    public void deleteUser(String name) {
        System.out.println(&quot;  [로그] deleteUser 호출됨&quot;);
        long startTime = System.currentTimeMillis();
        
        userService.deleteUser(name);
        
        long endTime = System.currentTimeMillis();
        System.out.println(&quot;⏱️ [로그] 실행 시간: &quot; + (endTime - startTime) + &quot;ms\n&quot;);
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();
        UserService proxy = new LoggingUserServiceProxy(userService);
        
        proxy.createUser(&quot;김도균&quot;);
        proxy.deleteUser(&quot;이철수&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프록시 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개방-폐쇄 원칙&lt;/b&gt;: 실제 객체를 수정하지 않고 제어 로직 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 책임 원칙&lt;/b&gt;: 접근 제어를 별도 클래스로 분리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 향상&lt;/b&gt;: Lazy Loading, 캐싱으로 성능 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: 접근 권한 제어&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로깅/모니터링&lt;/b&gt;: 추가 작업 수행 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프록시 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡도 증가&lt;/b&gt;: 새로운 클래스가 추가됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 지연&lt;/b&gt;: 프록시를 거치면서 약간의 지연 발생&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코드 복잡&lt;/b&gt;: 프록시 체인이 길어질 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 프록시 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서 프록시 패턴이 가장 많이 사용되는 곳은 바로 &lt;b&gt;JPA&lt;/b&gt;다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;JPA의 지연 로딩 (Lazy Loading)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 엔티티
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    // 지연 로딩 설정
    @OneToMany(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY)
    private List&amp;lt;Order&amp;gt; orders;  // 프록시 객체!
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String productName;
    private int price;
    
    @ManyToOne
    @JoinColumn(name = &quot;user_id&quot;)
    private User user;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 서비스
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public void printUser(Long userId) {
        // User 조회 시 orders는 프록시 객체로 들어옴
        User user = userRepository.findById(userId).orElseThrow();
        
        System.out.println(&quot;사용자: &quot; + user.getName());
        // 여기까지는 orders 테이블 조회 안 함!
        
        // 실제로 사용할 때 쿼리 실행
        System.out.println(&quot;주문 개수: &quot; + user.getOrders().size());
        // 이 시점에 SELECT * FROM orders WHERE user_id = ? 실행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 로그:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;-- User 조회
SELECT * FROM user WHERE id = 1

-- user.getName() 호출 (orders는 아직 조회 안 함)

-- user.getOrders().size() 호출 시점에 쿼리 실행
SELECT * FROM orders WHERE user_id = 1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User만 필요하면 Order 조회 안 함 (성능 향상)&lt;/li&gt;
&lt;li&gt;필요할 때만 쿼리 실행 (Lazy Loading)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;즉시 로딩 vs 지연 로딩&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    // 즉시 로딩 - User 조회 시 Order도 함께 조회
    @OneToMany(fetch = FetchType.EAGER)
    private List&amp;lt;Order&amp;gt; orders;
}

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    // 지연 로딩 - 프록시 패턴 사용!
    @OneToMany(fetch = FetchType.LAZY)  
    private List&amp;lt;Order&amp;gt; orders;  // HibernateProxy 객체
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 테스트
@SpringBootTest
public class LazyLoadingTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void lazyLoadingTest() {
        User user = userRepository.findById(1L).orElseThrow();
        
        // orders는 실제 List가 아닌 프록시 객체
        System.out.println(user.getOrders().getClass());
        // 출력: class org.hibernate.collection.internal.PersistentBag
        
        // 실제로 사용할 때 쿼리 실행
        for (Order order : user.getOrders()) {
            System.out.println(order.getProductName());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;N+1 문제와 프록시&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// N+1 문제가 발생하는 코드
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    public void printAllUsersOrders() {
        // 1. User 전체 조회 (1번 쿼리)
        List&amp;lt;User&amp;gt; users = userRepository.findAll();
        
        // 2. 각 User마다 orders 조회 (N번 쿼리)
        for (User user : users) {
            System.out.println(user.getName());
            System.out.println(&quot;주문 개수: &quot; + user.getOrders().size());
            // User가 100명이면 100번의 추가 쿼리 발생!
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결책: Fetch Join, &lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;@EntityGraph, batch size 조절하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    // Fetch Join으로 한 번에 조회
    @Query(&quot;SELECT u FROM User u JOIN FETCH u.orders&quot;)
    List&amp;lt;User&amp;gt; findAllWithOrders();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring의 프록시 종류&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. JDK Dynamic Proxy (인터페이스 기반)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public interface UserService {
    void createUser(String name);
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void createUser(String name) {
        System.out.println(&quot;사용자 생성: &quot; + name);
    }
}

// Spring이 JDK Dynamic Proxy 생성
// UserService(인터페이스) &amp;rarr; Proxy &amp;rarr; UserServiceImpl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. CGLIB Proxy (클래스 기반)&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class ProductService {  // 인터페이스 없이 바로 클래스
    public void createProduct(String name) {
        System.out.println(&quot;상품 생성: &quot; + name);
    }
}

// Spring이 CGLIB Proxy 생성
// ProductService &amp;rarr; CGLIBProxy &amp;rarr; 실제 ProductService&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 프록시 확인
@SpringBootTest
public class ProxyTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void checkProxy() {
        User user = userRepository.findById(1L).orElseThrow();
        
        // orders는 프록시 객체
        System.out.println(&quot;Orders 타입: &quot; + user.getOrders().getClass());
        // 출력: class org.hibernate.collection.internal.PersistentBag
        
        // 프록시 여부 확인
        System.out.println(&quot;프록시인가? &quot; + 
            Hibernate.isInitialized(user.getOrders()));  // false
        
        // 사용 후
        user.getOrders().size();
        System.out.println(&quot;프록시인가? &quot; + 
            Hibernate.isInitialized(user.getOrders()));  // true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무거운 객체의 생성을 지연시킬 때&lt;/li&gt;
&lt;li&gt;접근 권한을 제어해야 할 때&lt;/li&gt;
&lt;li&gt;원격 객체를 로컬처럼 사용할 때&lt;/li&gt;
&lt;li&gt;캐싱이 필요할 때&lt;/li&gt;
&lt;li&gt;로깅, 모니터링이 필요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 가벼울 때&lt;/li&gt;
&lt;li&gt;추가 제어가 필요 없을 때&lt;/li&gt;
&lt;li&gt;성능이 중요하고 오버헤드를 피해야 할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;객체 접근을 제어하거나 추가 작업이 필요하다면 프록시 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데코레이터 vs 프록시&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 데코레이터 &lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt; 프록시 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;목적&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;기능 추가&lt;/td&gt;
&lt;td&gt;접근 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;생성 시점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;클라이언트가 조합&lt;/td&gt;
&lt;td&gt;프록시가 생성 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;개수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;여러 개 조합 가능&lt;/td&gt;
&lt;td&gt;보통 하나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;초점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;무엇을 할까?&lt;/td&gt;
&lt;td&gt;언제 할까?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데코레이터: 커피에 토핑 추가 (기능 추가)&lt;/li&gt;
&lt;li&gt;프록시: 이미지 필요할 때만 로딩 (접근 제어)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체 앞에 &lt;b&gt;대리인&lt;/b&gt;을 둔다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Lazy Loading&lt;/b&gt;으로 성능 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;접근 제어&lt;/b&gt;로 보안 강화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;으로 효율성 향상&lt;/li&gt;
&lt;li&gt;Spring AOP의 핵심 기술&lt;/li&gt;
&lt;li&gt;데코레이터와 비슷하지만 &lt;b&gt;목적이 다르다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 알고리즘을 캡슐화하는 &lt;b&gt;전략 패턴&lt;/b&gt;을 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글 예고:&lt;/b&gt; 전략 패턴 - 알고리즘을 캡슐화하라&lt;/p&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>디자인 패턴</category>
      <category>프록시 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/85</guid>
      <comments>https://ddokyun.tistory.com/85#entry85comment</comments>
      <pubDate>Tue, 13 Jan 2026 22:54:17 +0900</pubDate>
    </item>
    <item>
      <title>데코레이터 패턴(Decorator Pattern)</title>
      <link>https://ddokyun.tistory.com/84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 일곱 번째 주제는 &lt;b&gt;데코레이터 패턴(Decorator Pattern)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어댑터 패턴&lt;/b&gt;이 &lt;b&gt;&quot;인터페이스를 변환&quot;&lt;/b&gt;하는 거였다면, &lt;b&gt;데코레이터 패턴&lt;/b&gt;은 &lt;b&gt;&quot;기능을 추가&quot;&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 커피숍에서 아메리카노를 주문한다고 생각해보자. 기본 아메리카노에 샷 추가, 휘핑크림 추가, 시럽 추가... 이렇게 옵션을 덧붙이면 가격도 올라가고 기능도 추가된다. &lt;b&gt;데코레이터 패턴&lt;/b&gt;은 이처럼 &lt;b&gt;객체에 기능을 장식(Decorate)하듯이 추가하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;데코레이터 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데코레이터 패턴의 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데코레이터 패턴(Decorator Pattern)&lt;/b&gt;은 &lt;b&gt;객체에 추가적인 기능을 동적으로 첨가하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드를 수정하지 않고, 객체를 감싸서 새로운 기능을 추가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;객체에 &lt;b&gt;동적으로&lt;/b&gt; 기능을 추가한다&lt;/li&gt;
&lt;li&gt;기존 코드를 &lt;b&gt;수정하지 않는다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기능을 &lt;b&gt;조합&lt;/b&gt;할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;래퍼(Wrapper)&lt;/b&gt;처럼 객체를 감싼다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커피에 토핑 추가하기&lt;/li&gt;
&lt;li&gt;피자에 토핑 추가하기&lt;/li&gt;
&lt;li&gt;선물 포장하기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 데코레이터 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커피숍 주문 시스템을 개발한다고 가정해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;방법 1: 상속 사용 (문제가 많음)&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 기본 커피
public class Coffee {
    public int getCost() {
        return 3000;
    }
    
    public String getDescription() {
        return &quot;아메리카노&quot;;
    }
}

// 샷 추가 커피
public class CoffeeWithShot extends Coffee {
    @Override
    public int getCost() {
        return 3000 + 500;
    }
    
    @Override
    public String getDescription() {
        return &quot;아메리카노 + 샷&quot;;
    }
}

// 휘핑 추가 커피
public class CoffeeWithWhip extends Coffee {
    @Override
    public int getCost() {
        return 3000 + 700;
    }
    
    @Override
    public String getDescription() {
        return &quot;아메리카노 + 휘핑&quot;;
    }
}

// 샷 + 휘핑 추가 커피
public class CoffeeWithShotAndWhip extends Coffee {
    @Override
    public int getCost() {
        return 3000 + 500 + 700;
    }
    
    @Override
    public String getDescription() {
        return &quot;아메리카노 + 샷 + 휘핑&quot;;
    }
}

// 샷 + 시럽 추가 커피...
// 휘핑 + 시럽 추가 커피...
// 샷 + 휘핑 + 시럽 추가 커피...
// 끝이 없다!!!&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;조합이 늘어날수록 클래스가 폭발적으로 증가 (클래스 폭발)&lt;/li&gt;
&lt;li&gt;동적으로 기능을 추가/제거할 수 없음&lt;/li&gt;
&lt;li&gt;코드 중복이 심함&lt;/li&gt;
&lt;li&gt;유지보수가 불가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데코레이터 패턴&lt;/b&gt;은 &lt;b&gt;&quot;객체를 감싸서 기능을 추가하자&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 컴포넌트 인터페이스 정의&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 커피 인터페이스
public interface Coffee {
    int getCost();
    String getDescription();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 구체적인 컴포넌트 (기본 커피)&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 기본 아메리카노
public class Americano implements Coffee {
    @Override
    public int getCost() {
        return 3000;
    }
    
    @Override
    public String getDescription() {
        return &quot;아메리카노&quot;;
    }
}

// 기본 라떼
public class Latte implements Coffee {
    @Override
    public int getCost() {
        return 4000;
    }
    
    @Override
    public String getDescription() {
        return &quot;라떼&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 데코레이터 추상 클래스&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 추가 옵션의 기본 클래스
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;  // 감쌀 객체
    
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    
    @Override
    public int getCost() {
        return coffee.getCost();
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 구체적인 데코레이터들&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 샷 추가
public class ShotDecorator extends CoffeeDecorator {
    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public int getCost() {
        return coffee.getCost() + 500;  // 기존 가격 + 샷 가격
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + &quot; + 샷&quot;;
    }
}

// 휘핑크림 추가
public class WhipDecorator extends CoffeeDecorator {
    public WhipDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public int getCost() {
        return coffee.getCost() + 700;
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + &quot; + 휘핑&quot;;
    }
}

// 시럽 추가
public class SyrupDecorator extends CoffeeDecorator {
    public SyrupDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public int getCost() {
        return coffee.getCost() + 300;
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + &quot; + 시럽&quot;;
    }
}

// 우유 추가
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public int getCost() {
        return coffee.getCost() + 500;
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + &quot; + 우유&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5단계: 사용 예시&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        // 기본 아메리카노
        Coffee coffee1 = new Americano();
        System.out.println(coffee1.getDescription() + &quot; = &quot; + coffee1.getCost() + &quot;원&quot;);
        
        // 아메리카노 + 샷
        Coffee coffee2 = new ShotDecorator(new Americano());
        System.out.println(coffee2.getDescription() + &quot; = &quot; + coffee2.getCost() + &quot;원&quot;);
        
        // 아메리카노 + 샷 + 휘핑
        Coffee coffee3 = new WhipDecorator(new ShotDecorator(new Americano()));
        System.out.println(coffee3.getDescription() + &quot; = &quot; + coffee3.getCost() + &quot;원&quot;);
        
        // 라떼 + 샷 + 시럽 + 휘핑
        Coffee coffee4 = new Latte();
        coffee4 = new ShotDecorator(coffee4);
        coffee4 = new SyrupDecorator(coffee4);
        coffee4 = new WhipDecorator(coffee4);
        System.out.println(coffee4.getDescription() + &quot; = &quot; + coffee4.getCost() + &quot;원&quot;);
        
        // 아메리카노 + 샷 2번 + 우유
        Coffee coffee5 = new Americano();
        coffee5 = new ShotDecorator(coffee5);
        coffee5 = new ShotDecorator(coffee5);  // 샷을 2번 추가!
        coffee5 = new MilkDecorator(coffee5);
        System.out.println(coffee5.getDescription() + &quot; = &quot; + coffee5.getCost() + &quot;원&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rHVr3/dJMcachwWiA/kBD2y7KvHlIppHRkQzdIO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rHVr3/dJMcachwWiA/kBD2y7KvHlIppHRkQzdIO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rHVr3/dJMcachwWiA/kBD2y7KvHlIppHRkQzdIO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrHVr3%2FdJMcachwWiA%2FkBD2y7KvHlIppHRkQzdIO0%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;328&quot; height=&quot;269&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 조합을 만들 때 클래스를 추가할 필요 없음&lt;/li&gt;
&lt;li&gt;동적으로 기능을 추가/제거 가능&lt;/li&gt;
&lt;li&gt;같은 데코레이터를 여러 번 적용 가능 (샷 2번)&lt;/li&gt;
&lt;li&gt;단일 책임 원칙 준수&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 텍스트 에디터&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 실무적인 예시를 보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 텍스트 인터페이스
public interface Text {
    String getContent();
}

// 기본 텍스트
public class PlainText implements Text {
    private String content;
    
    public PlainText(String content) {
        this.content = content;
    }
    
    @Override
    public String getContent() {
        return content;
    }
}

// 텍스트 데코레이터
public abstract class TextDecorator implements Text {
    protected Text text;
    
    public TextDecorator(Text text) {
        this.text = text;
    }
    
    @Override
    public String getContent() {
        return text.getContent();
    }
}

// 볼드 데코레이터
public class BoldDecorator extends TextDecorator {
    public BoldDecorator(Text text) {
        super(text);
    }
    
    @Override
    public String getContent() {
        return &quot;&amp;lt;b&amp;gt;&quot; + text.getContent() + &quot;&amp;lt;/b&amp;gt;&quot;;
    }
}

// 이탤릭 데코레이터
public class ItalicDecorator extends TextDecorator {
    public ItalicDecorator(Text text) {
        super(text);
    }
    
    @Override
    public String getContent() {
        return &quot;&amp;lt;i&amp;gt;&quot; + text.getContent() + &quot;&amp;lt;/i&amp;gt;&quot;;
    }
}

// 밑줄 데코레이터
public class UnderlineDecorator extends TextDecorator {
    public UnderlineDecorator(Text text) {
        super(text);
    }
    
    @Override
    public String getContent() {
        return &quot;&amp;lt;u&amp;gt;&quot; + text.getContent() + &quot;&amp;lt;/u&amp;gt;&quot;;
    }
}

// 색상 데코레이터
public class ColorDecorator extends TextDecorator {
    private String color;
    
    public ColorDecorator(Text text, String color) {
        super(text);
        this.color = color;
    }
    
    @Override
    public String getContent() {
        return &quot;&amp;lt;color=&quot; + color + &quot;&amp;gt;&quot; + text.getContent() + &quot;&amp;lt;/color&amp;gt;&quot;;
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 기본 텍스트
        Text text1 = new PlainText(&quot;Hello World&quot;);
        System.out.println(text1.getContent());
        
        // 볼드 텍스트
        Text text2 = new BoldDecorator(new PlainText(&quot;Hello World&quot;));
        System.out.println(text2.getContent());
        
        // 볼드 + 이탤릭
        Text text3 = new ItalicDecorator(new BoldDecorator(new PlainText(&quot;Hello World&quot;)));
        System.out.println(text3.getContent());
        
        // 볼드 + 이탤릭 + 밑줄 + 색상
        Text text4 = new PlainText(&quot;Hello World&quot;);
        text4 = new BoldDecorator(text4);
        text4 = new ItalicDecorator(text4);
        text4 = new UnderlineDecorator(text4);
        text4 = new ColorDecorator(text4, &quot;red&quot;);
        System.out.println(text4.getContent());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 알림 시스템&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 알림 인터페이스
public interface Notifier {
    void send(String message);
}

// 기본 이메일 알림
public class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println(&quot;  이메일 전송: &quot; + message);
    }
}

// 알림 데코레이터
public abstract class NotifierDecorator implements Notifier {
    protected Notifier notifier;
    
    public NotifierDecorator(Notifier notifier) {
        this.notifier = notifier;
    }
    
    @Override
    public void send(String message) {
        notifier.send(message);
    }
}

// SMS 알림 추가
public class SMSDecorator extends NotifierDecorator {
    public SMSDecorator(Notifier notifier) {
        super(notifier);
    }
    
    @Override
    public void send(String message) {
        super.send(message);  // 기존 알림도 보냄
        System.out.println(&quot;  SMS 전송: &quot; + message);
    }
}

// 카카오톡 알림 추가
public class KakaoDecorator extends NotifierDecorator {
    public KakaoDecorator(Notifier notifier) {
        super(notifier);
    }
    
    @Override
    public void send(String message) {
        super.send(message);
        System.out.println(&quot;  카카오톡 전송: &quot; + message);
    }
}

// 슬랙 알림 추가
public class SlackDecorator extends NotifierDecorator {
    public SlackDecorator(Notifier notifier) {
        super(notifier);
    }
    
    @Override
    public void send(String message) {
        super.send(message);
        System.out.println(&quot;  슬랙 전송: &quot; + message);
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 이메일만
        Notifier notifier1 = new EmailNotifier();
        notifier1.send(&quot;회원가입 완료&quot;);
        
        System.out.println(&quot;\n--- VIP 고객 ---&quot;);
        // 이메일 + SMS
        Notifier notifier2 = new SMSDecorator(new EmailNotifier());
        notifier2.send(&quot;특별 할인 안내&quot;);
        
        System.out.println(&quot;\n--- 임직원 ---&quot;);
        // 이메일 + 카카오톡 + 슬랙
        Notifier notifier3 = new EmailNotifier();
        notifier3 = new KakaoDecorator(notifier3);
        notifier3 = new SlackDecorator(notifier3);
        notifier3.send(&quot;긴급 공지사항&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데코레이터 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개방-폐쇄 원칙&lt;/b&gt;: 새로운 기능 추가 시 기존 코드 수정 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 책임 원칙&lt;/b&gt;: 각 데코레이터는 하나의 기능만 담당&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: 런타임에 동적으로 기능 추가/제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조합 가능&lt;/b&gt;: 여러 데코레이터를 자유롭게 조합&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상속의 대안&lt;/b&gt;: 클래스 폭발 문제 해결&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데코레이터 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡도 증가&lt;/b&gt;: 작은 객체들이 많이 생성됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 의존성&lt;/b&gt;: 데코레이터 적용 순서가 중요할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅 어려움&lt;/b&gt;: 여러 겹으로 감싸져 있어 추적이 어려움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특정 데코레이터 제거 어려움&lt;/b&gt;: 중간 데코레이터만 제거하기 힘듦&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Java에서의 데코레이터 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 I/O 클래스가 대표적인 데코레이터 패턴 예시다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Java I/O의 데코레이터 패턴
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        // 기본 파일 입력
        FileInputStream fis = new FileInputStream(&quot;file.txt&quot;);
        
        // 버퍼링 기능 추가
        BufferedInputStream bis = new BufferedInputStream(fis);
        
        // 데이터 타입 변환 기능 추가
        DataInputStream dis = new DataInputStream(bis);
        
        // 모두 InputStream을 구현하지만 각자 다른 기능을 추가!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 데코레이터 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Spring의 TransactionTemplate 등이 데코레이터 패턴 활용
@Service
public class UserService {
    private final UserRepository userRepository;
    
    // 트랜잭션 데코레이터처럼 동작
    @Transactional  
    public void createUser(User user) {
        userRepository.save(user);
    }
}

// 캐싱 데코레이터
@Cacheable(&quot;users&quot;)
public User getUser(Long id) {
    return userRepository.findById(id);
}

// 로깅 데코레이터 (AOP)
@Aspect
@Component
public class LoggingAspect {
    @Around(&quot;execution(* com.example.service.*.*(..))&quot;)
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println(&quot;메서드 실행 전&quot;);
        Object result = joinPoint.proceed();  // 원본 메서드 실행
        System.out.println(&quot;메서드 실행 후&quot;);
        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체에 동적으로 기능을 추가/제거해야 할 때&lt;/li&gt;
&lt;li&gt;기능의 조합이 많을 때&lt;/li&gt;
&lt;li&gt;상속으로 인한 클래스 폭발을 피하고 싶을 때&lt;/li&gt;
&lt;li&gt;런타임에 기능을 선택해야 할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기능 조합이 고정되어 있을 때&lt;/li&gt;
&lt;li&gt;간단한 기능 추가만 필요할 때&lt;/li&gt;
&lt;li&gt;데코레이터 순서가 중요하지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;기능을 동적으로 조합해야 한다면 데코레이터 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;데코레이터 vs 어댑터 vs 프록시&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 84px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 목적 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 인터페이스 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt; 기능 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;데코레이터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;기능 추가&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;동일&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;원본 + 추가 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;어댑터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인터페이스 변환&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;다름&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인터페이스만 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;프록시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;접근 제어&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;동일&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;제어 + 원본 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 &lt;b&gt;감싸서&lt;/b&gt; 기능을 추가한다&lt;/li&gt;
&lt;li&gt;기존 코드를 &lt;b&gt;수정하지 않는다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;기능을 &lt;b&gt;자유롭게 조합&lt;/b&gt;할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상속의 좋은 대안&lt;/b&gt;이다&lt;/li&gt;
&lt;li&gt;Java I/O, Spring AOP 등에서 활발히 사용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 객체에 대한 접근을 제어하는 &lt;b&gt;프록시 패턴&lt;/b&gt;을 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글 예고:&lt;/b&gt; 프록시 패턴 - 객체 접근을 제어하라&lt;/p&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>데코레이터 패턴</category>
      <category>디자인 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/84</guid>
      <comments>https://ddokyun.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 13 Jan 2026 21:52:38 +0900</pubDate>
    </item>
    <item>
      <title>어댑터 패턴(Adapter Pattern)</title>
      <link>https://ddokyun.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;디자인 패턴 시리즈의 여섯 번째 주제는 &lt;b&gt;어댑터 패턴(Adapter Pattern)&lt;/b&gt;이다. 이번부터는 &lt;b&gt;생성 패턴에서 구조 패턴&lt;/b&gt;으로 넘어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;생성 패턴&lt;/b&gt;이 &lt;b&gt;&quot;객체를 어떻게 만들까?&quot;&lt;/b&gt;였다면, &lt;b&gt;구조 패턴&lt;/b&gt;은 &lt;b&gt;&quot;객체들을 어떻게 조합할까?&quot;&lt;/b&gt;에 대한 해답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 한국에서 쓰던 220V 전자기기를 미국에 가져가면 110V라서 사용할 수 없다. 이때 필요한 게 바로 '변압기(어댑터)'다.&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;b&gt;어댑터 패턴&lt;/b&gt;이 &lt;b&gt;무엇인지, 왜 필요한지, 어떻게 구현하는지&lt;/b&gt; 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어댑터 패턴의 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어댑터 패턴(Adapter Pattern)&lt;/b&gt;은 호환되지 않는 인터페이스를 가진 객체들이 함께 동작할 수 있도록 중간에서 변환해주는 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 특징:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존 클래스를 &lt;b&gt;수정하지 않고&lt;/b&gt; 재사용한다&lt;/li&gt;
&lt;li&gt;서로 다른 인터페이스를 &lt;b&gt;중간에서 변환&lt;/b&gt;한다&lt;/li&gt;
&lt;li&gt;클라이언트와 서비스 사이의 &lt;b&gt;호환성&lt;/b&gt;을 제공한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;래퍼(Wrapper)&lt;/b&gt; 역할을 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현실 비유:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;220V &amp;rarr; 110V 변압기&lt;/li&gt;
&lt;li&gt;USB-C &amp;rarr; USB-A 변환 젠더&lt;/li&gt;
&lt;li&gt;멀티탭 (여러 플러그를 한 곳에)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;왜 어댑터 패턴이 필요할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음악 플레이어를 만드는데, 기존에 MP3만 재생하는 라이브러리가 있다. 그런데 고객이 MP4, VLC 파일도 재생하고 싶다고 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 기존 MP3 플레이어 (이미 만들어져 있어서 수정할 수 없음)
public class MP3Player {
    public void playMP3(String fileName) {
        System.out.println(&quot;MP3 재생: &quot; + fileName);
    }
}

// 새로운 요구사항: MP4, VLC도 재생해야 함
// 하지만 기존 코드는 MP3만 지원...

// 클라이언트 코드
public class MusicPlayer {
    public void play(String fileType, String fileName) {
        if (fileType.equals(&quot;mp3&quot;)) {
            MP3Player player = new MP3Player();
            player.playMP3(fileName);
        } else if (fileType.equals(&quot;mp4&quot;)) {
            // MP4는 어떻게 재생하지?
        } else if (fileType.equals(&quot;vlc&quot;)) {
            // VLC는 어떻게 재생하지?
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존 MP3Player는 수정할 수 없다 (라이브러리나 레거시 코드)&lt;/li&gt;
&lt;li&gt;MP4Player, VLCPlayer는 다른 인터페이스를 가지고 있다&lt;/li&gt;
&lt;li&gt;매번 새로운 플레이어를 추가하면 클라이언트 코드를 수정해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑터 패턴은 &lt;b&gt;&quot;중간에서 변환해주는 어댑터를 만들자&quot;&lt;/b&gt;로 이 문제를 해결한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순수 Java로 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1단계: 타겟 인터페이스 정의&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 사용할 통일된 인터페이스를 만든다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;typescript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 미디어 플레이어 인터페이스 (타겟 인터페이스)
public interface MediaPlayer {
    void play(String fileType, String fileName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2단계: 어댑티(Adaptee) - 적응시킬 클래스들&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 존재하는 클래스들 (수정 불가능하다고 가정)&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// MP4 전용 플레이어 (어댑티)
public class MP4Player {
    public void playMP4(String fileName) {
        System.out.println(&quot;MP4 재생: &quot; + fileName);
    }
}

// VLC 전용 플레이어 (어댑티)
public class VLCPlayer {
    public void playVLC(String fileName) {
        System.out.println(&quot;VLC 재생: &quot; + fileName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3단계: 어댑터 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어댑티를 타겟 인터페이스에 맞게 변환한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 미디어 어댑터
public class MediaAdapter implements MediaPlayer {
    private MP4Player mp4Player;
    private VLCPlayer vlcPlayer;
    
    public MediaAdapter(String fileType) {
        if (fileType.equals(&quot;mp4&quot;)) {
            mp4Player = new MP4Player();
        } else if (fileType.equals(&quot;vlc&quot;)) {
            vlcPlayer = new VLCPlayer();
        }
    }
    
    @Override
    public void play(String fileType, String fileName) {
        if (fileType.equals(&quot;mp4&quot;)) {
            mp4Player.playMP4(fileName);
        } else if (fileType.equals(&quot;vlc&quot;)) {
            vlcPlayer.playVLC(fileName);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4단계: 클라이언트 코드&lt;/b&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 오디오 플레이어 (클라이언트)
public class AudioPlayer implements MediaPlayer {
    private MediaAdapter mediaAdapter;
    
    @Override
    public void play(String fileType, String fileName) {
        // MP3는 기본 지원
        if (fileType.equals(&quot;mp3&quot;)) {
            System.out.println(&quot;MP3 재생: &quot; + fileName);
        }
        // MP4, VLC는 어댑터를 통해 재생
        else if (fileType.equals(&quot;mp4&quot;) || fileType.equals(&quot;vlc&quot;)) {
            mediaAdapter = new MediaAdapter(fileType);
            mediaAdapter.play(fileType, fileName);
        }
        else {
            System.out.println(&quot;지원하지 않는 형식: &quot; + fileType);
        }
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        AudioPlayer player = new AudioPlayer();
        
        player.play(&quot;mp3&quot;, &quot;노래.mp3&quot;);
        player.play(&quot;mp4&quot;, &quot;뮤직비디오.mp4&quot;);
        player.play(&quot;vlc&quot;, &quot;영화.vlc&quot;);
        player.play(&quot;avi&quot;, &quot;비디오.avi&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;401&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wbCoo/dJMcacICjqh/VXzlIaoToDcK8qLKKup1zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wbCoo/dJMcacICjqh/VXzlIaoToDcK8qLKKup1zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wbCoo/dJMcacICjqh/VXzlIaoToDcK8qLKKup1zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwbCoo%2FdJMcacICjqh%2FVXzlIaoToDcK8qLKKup1zK%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;401&quot; height=&quot;248&quot; data-origin-width=&quot;401&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 MP4Player, VLCPlayer를 수정하지 않음&lt;/li&gt;
&lt;li&gt;클라이언트는 통일된 인터페이스만 사용&lt;/li&gt;
&lt;li&gt;새로운 플레이어 추가도 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실전 예제: 결제 시스템 통합&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 실무적인 예시를 보자. 여러 결제 시스템을 하나의 인터페이스로 통합하는 경우다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 우리가 사용할 통일된 결제 인터페이스
public interface PaymentProcessor {
    boolean processPayment(int amount);
    String getPaymentStatus();
}

// 기존 토스페이 라이브러리 (수정 불가)
public class TossPaymentService {
    public void sendPayment(int won) {
        System.out.println(&quot;토스페이로 &quot; + won + &quot;원 결제 요청&quot;);
    }
    
    public String checkStatus() {
        return &quot;토스 결제 완료&quot;;
    }
}

// 기존 카카오페이 라이브러리 (수정 불가)
public class KakaoPayService {
    public boolean pay(int amount) {
        System.out.println(&quot;카카오페이로 &quot; + amount + &quot;원 결제&quot;);
        return true;
    }
    
    public String status() {
        return &quot;카카오 결제 성공&quot;;
    }
}

// 토스페이 어댑터
public class TossPaymentAdapter implements PaymentProcessor {
    private TossPaymentService tossService;
    
    public TossPaymentAdapter() {
        this.tossService = new TossPaymentService();
    }
    
    @Override
    public boolean processPayment(int amount) {
        tossService.sendPayment(amount);
        return true;
    }
    
    @Override
    public String getPaymentStatus() {
        return tossService.checkStatus();
    }
}

// 카카오페이 어댑터
public class KakaoPaymentAdapter implements PaymentProcessor {
    private KakaoPayService kakaoService;
    
    public KakaoPaymentAdapter() {
        this.kakaoService = new KakaoPayService();
    }
    
    @Override
    public boolean processPayment(int amount) {
        return kakaoService.pay(amount);
    }
    
    @Override
    public String getPaymentStatus() {
        return kakaoService.status();
    }
}

// 주문 서비스 (클라이언트)
public class OrderService {
    private PaymentProcessor paymentProcessor;
    
    public OrderService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
    
    public void checkout(int amount) {
        System.out.println(&quot;=== 결제 시작 ===&quot;);
        boolean success = paymentProcessor.processPayment(amount);
        
        if (success) {
            System.out.println(&quot;상태: &quot; + paymentProcessor.getPaymentStatus());
            System.out.println(&quot;=== 주문 완료 ===\n&quot;);
        }
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        // 토스페이로 결제
        OrderService orderService1 = new OrderService(new TossPaymentAdapter());
        orderService1.checkout(15000);
        
        // 카카오페이로 결제
        OrderService orderService2 = new OrderService(new KakaoPaymentAdapter());
        orderService2.checkout(25000);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 결제 라이브러리의 다른 메서드명을 통일된 인터페이스로 사용&lt;/li&gt;
&lt;li&gt;새로운 결제 수단 추가 시 어댑터만 만들면 됨&lt;/li&gt;
&lt;li&gt;클라이언트 코드(OrderService)는 변경 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;클래스 어댑터 vs 객체 어댑터&lt;/b&gt;&lt;/h2&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;b&gt;합성(Composition)&lt;/b&gt;으로 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public class MediaAdapter implements MediaPlayer {
    private MP4Player mp4Player;  // 객체를 가지고 있음
    
    @Override
    public void play(String fileType, String fileName) {
        mp4Player.playMP4(fileName);  // 위임
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유연하다&lt;/li&gt;
&lt;li&gt;여러 어댑티를 동시에 사용 가능&lt;/li&gt;
&lt;li&gt;Java에서 권장되는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 어댑터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다중 상속&lt;/b&gt;을 사용한다. (Java는 인터페이스로만 가능)&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Java에서는 인터페이스로만 가능
public interface MP4Playable {
    void playMP4(String fileName);
}

public class MediaAdapter implements MediaPlayer, MP4Playable {
    @Override
    public void play(String fileType, String fileName) {
        playMP4(fileName);  // 직접 호출
    }
    
    @Override
    public void playMP4(String fileName) {
        System.out.println(&quot;MP4 재생: &quot; + fileName);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java는 다중 상속이 안 됨 (인터페이스만 가능)&lt;/li&gt;
&lt;li&gt;덜 유연함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어댑터 패턴의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;개방-폐쇄 원칙&lt;/b&gt;: 기존 코드를 수정하지 않고 새로운 어댑터 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단일 책임 원칙&lt;/b&gt;: 인터페이스 변환 로직을 분리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성&lt;/b&gt;: 기존 클래스를 그대로 재사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호환성&lt;/b&gt;: 서로 다른 인터페이스를 함께 사용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어댑터 패턴의 단점&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡도 증가&lt;/b&gt;: 새로운 클래스들이 추가됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;간접 호출&lt;/b&gt;: 어댑터를 거쳐서 호출하므로 약간의 오버헤드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과도한 사용&lt;/b&gt;: 간단한 경우 오히려 복잡해질 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Spring에서의 어댑터 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서도 어댑터 패턴이 많이 사용된다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// Spring의 HandlerAdapter가 대표적인 예시
public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request, 
                       HttpServletResponse response, 
                       Object handler);
}

// 우리 프로젝트에서 사용 예시
public interface NotificationService {
    void send(String message);
}

// 기존 이메일 서비스 (수정 불가)
@Component
public class EmailService {
    public void sendEmail(String to, String subject, String body) {
        System.out.println(&quot;이메일 발송: &quot; + body);
    }
}

// 기존 SMS 서비스 (수정 불가)
@Component
public class SMSService {
    public boolean sendSMS(String phoneNumber, String text) {
        System.out.println(&quot;SMS 발송: &quot; + text);
        return true;
    }
}

// 이메일 어댑터
@Component
public class EmailNotificationAdapter implements NotificationService {
    private final EmailService emailService;
    
    public EmailNotificationAdapter(EmailService emailService) {
        this.emailService = emailService;
    }
    
    @Override
    public void send(String message) {
        emailService.sendEmail(&quot;user@email.com&quot;, &quot;알림&quot;, message);
    }
}

// SMS 어댑터
@Component
public class SMSNotificationAdapter implements NotificationService {
    private final SMSService smsService;
    
    public SMSNotificationAdapter(SMSService smsService) {
        this.smsService = smsService;
    }
    
    @Override
    public void send(String message) {
        smsService.sendSMS(&quot;010-1234-5678&quot;, message);
    }
}

// 사용
@Service
public class NotificationManager {
    private final List&amp;lt;NotificationService&amp;gt; notificationServices;
    
    public NotificationManager(List&amp;lt;NotificationService&amp;gt; notificationServices) {
        this.notificationServices = notificationServices;
    }
    
    public void notifyAll(String message) {
        for (NotificationService service : notificationServices) {
            service.send(message);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 언제 사용할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용하면 좋은 경우:&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 클래스를 수정할 수 없을 때&lt;/li&gt;
&lt;li&gt;서로 다른 인터페이스를 가진 클래스들을 통합할 때&lt;/li&gt;
&lt;li&gt;레거시 시스템과 새 시스템을 연결할 때&lt;/li&gt;
&lt;li&gt;외부 라이브러리나 API를 통합할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사용하지 않아도 되는 경우:&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인터페이스를 직접 수정할 수 있을 때&lt;/li&gt;
&lt;li&gt;변환 로직이 매우 간단할 때&lt;/li&gt;
&lt;li&gt;한 번만 사용할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 원칙:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;호환되지 않는 인터페이스를 연결해야 한다면 어댑터 패턴을 고려하라&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;어댑터 vs 다른 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 104px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; 패턴 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; 목적 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt; 차이점 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;어댑터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인터페이스 변환&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;기존 인터페이스를 다른 인터페이스로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;데코레이터&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;기능 추가&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인터페이스는 같고 기능만 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;프록시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;접근 제어&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;인터페이스는 같고 접근을 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;&lt;b&gt;퍼사드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;단순화&lt;/td&gt;
&lt;td style=&quot;height: 21px;&quot;&gt;복잡한 시스템을 간단한 인터페이스로&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&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;&lt;b&gt;기억해야 할 포인트:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;변압기&lt;/b&gt;처럼 인터페이스를 변환한다&lt;/li&gt;
&lt;li&gt;기존 코드를 &lt;b&gt;수정하지 않고&lt;/b&gt; 재사용한다&lt;/li&gt;
&lt;li&gt;서로 다른 시스템을 &lt;b&gt;통합&lt;/b&gt;할 때 유용하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체 어댑터&lt;/b&gt;(합성)를 주로 사용한다&lt;/li&gt;
&lt;li&gt;레거시 시스템 통합에 필수적이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 기존 객체에 새로운 기능을 추가하는 &lt;b&gt;데코레이터 패턴&lt;/b&gt;을 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다음 글 예고:&lt;/b&gt; 데코레이터 패턴 - 객체에 동적으로 기능을 추가하라&lt;/p&gt;</description>
      <category>디자인 패턴</category>
      <category>GoF 23</category>
      <category>디자인 패턴</category>
      <category>어댑터 패턴</category>
      <author>김코딩딩</author>
      <guid isPermaLink="true">https://ddokyun.tistory.com/83</guid>
      <comments>https://ddokyun.tistory.com/83#entry83comment</comments>
      <pubDate>Tue, 13 Jan 2026 21:12:29 +0900</pubDate>
    </item>
  </channel>
</rss>