<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jjung.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 13 Jul 2025 15:17:27 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jjung.log</title>
            <url>https://velog.velcdn.com/images/j_hee/profile/7d5a372e-6470-44da-8a91-e8d6f24637f6/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jjung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/j_hee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[@Transactional 어디까지 붙이나요?: 프록시, 전파속성]]></title>
            <link>https://velog.io/@j_hee/%EC%8A%A4%ED%94%84%EB%A7%81-Transactional%EC%9D%98-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%A0%84%ED%8C%8C-%EC%86%8D%EC%84%B1</link>
            <guid>https://velog.io/@j_hee/%EC%8A%A4%ED%94%84%EB%A7%81-Transactional%EC%9D%98-%ED%94%84%EB%A1%9D%EC%8B%9C-%EC%9B%90%EB%A6%AC%EC%99%80-%EC%A0%84%ED%8C%8C-%EC%86%8D%EC%84%B1</guid>
            <pubDate>Sun, 13 Jul 2025 15:17:27 GMT</pubDate>
            <description><![CDATA[<h3 id="같은-메소드에-트랜잭션이-중복되어-있는데-왜-문제없이-동작하지-🤔">같은 메소드에 트랜잭션이 중복되어 있는데, 왜 문제없이 동작하지? 🤔</h3>
<p>최근 기존 프로젝트에 새로운 요구사항이 생겨 기능을 확장하게 되었습니다. 코드를 살펴보던 중 서비스 레이어와 레포지토리 레이어에 똑같이 @Transactional 어노테이션이 붙어 있는 메소드를 발견했는데요,</p>
<p>처음엔 이렇게 생각했습니다.</p>
<blockquote>
<p>“어? 이렇게 트랜잭션이 중첩되면 충돌이 나거나 오류가 생기는 거 아닌가?”</p>
</blockquote>
<p>하지만 실제로는 아무 문제 없이 잘 동작하고 있었고, 심지어 롤백도 정확히 되더군요. 알고 보니 트랜잭션의 전파 속성때문이였습니다. 이마를 탁..🤦‍♀️</p>
<p>이번 글에서는 다음 세 가지를 중심으로 트랜잭션의 동작 원리를 정리해봅니다.</p>
<ol>
<li><p>스프링에서 프록시 기반이란?</p>
</li>
<li><p>@Transactional이 어떻게 동작하는지 예제 코드</p>
</li>
<li><p>전파 속성</p>
</li>
<li><p>결론: 그래서 @Transactional 어디까지 붙이나요?</p>
</li>
</ol>
<br>

<h2 id="1-스프링에서-프록시-기반이란">1. 스프링에서 프록시 기반이란?</h2>
<p>스프링은 AOP(관점 지향 프로그래밍)를 활용해 트랜잭션 기능을 제공합니다. 이 AOP는 바로 <strong>프록시(proxy)</strong>를 통해 구현되는데요.</p>
<p>프록시는 원본 객체를 감싸는 껍데기 객체로, 호출을 가로채 필요한 부가 로직을 추가할 수 있는 구조입니다.</p>
<p>예를 들어, MyService 클래스가 있으면, Spring은 실제 객체 대신 다음과 같이 프록시 객체를 생성합니다.</p>
<blockquote>
<p>Client --&gt; MyServiceProxy --&gt; (Real) MyService</p>
</blockquote>
<p>클라이언트는 실제 서비스가 아니라 프록시 객체를 호출하게 되며, MyServiceProxy는 트랜잭션 시작/커밋/롤백 처리를 합니다.</p>
</br>

<h2 id="2-transactional-어노테이션은-어떻게-동작할까">2. @Transactional 어노테이션은 어떻게 동작할까?</h2>
<p><strong>&lt;예제 코드&gt;</strong></p>
<pre><code class="language-java">@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        orderRepository.save();
        dealServcie.changeStatus(); // 여기서 예외 발생 시 전체 롤백
    }
}</code></pre>
<ul>
<li><p>위 코드는 createOrder()가 호출되면 프록시가 트랜잭션을 시작합니다.</p>
</li>
<li><p>내부에서 예외가 발생하면 트랜잭션은 자동으로 롤백됩니다.</p>
</li>
<li><p>아무 문제 없이 끝나면 커밋됩니다.</p>
</li>
</ul>
</br>

<h2 id="3-전파속성">3. 전파속성</h2>
<p>트랜잭션 전파 속성은 현재 트랜잭션이 있는 상태에서 메서드가 호출될 때 어떤 방식으로 동작할지를 지정하는 설정입니다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>REQUIRED</code> (기본값)</td>
<td>현재 트랜잭션 있으면 참여, 없으면 새로 시작</td>
</tr>
<tr>
<td><code>REQUIRES_NEW</code></td>
<td>항상 새 트랜잭션 시작 (기존 트랜잭션은 일시 중단)</td>
</tr>
<tr>
<td><code>NESTED</code></td>
<td>트랜잭션 중첩 (savepoint 기반)</td>
</tr>
</tbody></table>
<p>보통 기본 @Transactional 어노테이션을 사용하면 기본값으로 동작하는데, 
기본적으로 가장 외부에서 호출된 메서드 기준으로 트랜잭션이 시작됩니다.</p>
<br>

<p><strong>&lt;예제 코드&gt;</strong></p>
<pre><code class="language-java">@Service
@Transactional // 여기에 붙음
public class MyService {
    public void doSomething() {
        repository.save(); // Repository에도 @Transactional 있음
    }
}

@Repository
@Transactional
public class MyRepository {
    public void save() {
        // DB 저장 로직
    }
}</code></pre>
<ul>
<li><p>MyService.doSomething()이 호출되면서 트랜잭션 시작</p>
</li>
<li><p>내부적으로 MyRepository.save() 호출 → 이때는 이미 트랜잭션이 진행 중</p>
</li>
<li><p>따라서 레포지토리의 @Transactional은 새로운 트랜잭션을 만들지 않음</p>
</li>
</ul>
<blockquote>
<p><strong>즉, 트랜잭션 중첩이 아니라 상속/전이되는 구조입니다!</strong></p>
</blockquote>
</br>

<h2 id="4-그래서-transactional-어디까지-붙이나요">4. 그래서 @Transactional 어디까지 붙이나요?</h2>
<p>어차피 트랜잭션은 전파되는데, 각 레이어에 @Transactional을 모두 붙이는 게 과연 의미가 있을까요?
서비스 레이어에만 붙여도 충분할 것 같은데 말이죠… 🤔</p>
<p>사실, 각 레이어에 @Transactional을 명시하는 것도 분명한 장점이 있습니다:</p>
<ul>
<li><p>레포지토리를 단독으로 사용하는 경우에도 트랜잭션이 보장됩니다.
예: 테스트 코드, 배치 작업, 별도의 유틸성 컴포넌트 등 (레포지토리를 독립적으로 쓸 일이 있는가?”에 따른 전략)</p>
</li>
<li><p>예를 들어 readOnly = true를 설정하면, 해당 메서드는 조회 목적임을 명확히 드러낼 수 있어 성능 최적화나 의도 파악에 도움이 됩니다.</p>
</li>
</ul>
<br>

<h2 id="✅-결론">✅ 결론!</h2>
<p>비즈니스 로직 단위로는 서비스 레이어에서 @Transactional을 사용하는 것이 기본입니다.</p>
<p>레포지토리에는 필요한 경우에만 최소한의 트랜잭션 설정(readOnly 등)을 적용하는 방식이 좋은것 같습니다.</p>
<p>다만, 무엇보다 중요한 건 팀 내에서의 일관된 기준과 명확한 의도 표현입니다. 상황에 맞게 유연하게, 그러나 일관성 있게 적용하는 것이 가장 이상적인 전략인것 같습니다~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Async와 ThreadPoolTaskExecutor를 활용한 Spring 비동기 처리]]></title>
            <link>https://velog.io/@j_hee/Async%EC%99%80-ThreadPoolTaskExecutor%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Spring-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@j_hee/Async%EC%99%80-ThreadPoolTaskExecutor%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Spring-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 29 May 2025 11:10:44 GMT</pubDate>
            <description><![CDATA[<p>Spring에서는 @Async 어노테이션을 사용하면 간단하게 비동기 작업을 구현할 수 있습니다. 하지만 아무런 설정 없이 기본값만 사용할 경우, 쓰레드가 과도하게 생성되어 시스템 리소스를 급격히 소모하고, 최악의 경우 애플리케이션이 다운될 위험도 있습니다.</p>
<p>이러한 문제를 방지하고 안정적이며 효율적인 비동기 처리를 구현하기 위해, Spring에서는 ThreadPoolTaskExecutor를 활용한 별도의 스레드 풀 설정을 지원합니다.</p>
<p>ThreadPoolTaskExecutor는 Spring에서 제공하는 TaskExecutor의 구현체로, Java의 java.util.concurrent.ThreadPoolExecutor를 감싸서 Spring 친화적으로 사용할 수 있도록 만든 클래스입니다.</p>
<p>모든 비동기 작업에 하나의 executor만 사용하는 것은 지양하는 것이 좋습니다. 업무 성격에 따라 executor를 나누면 더욱 유연하게 운영할 수 있습니다. 저는 비즈니스 로직과 알림 처리를 분리하기 위해 비즈니스용과 알림용 두 가지 ThreadPool을 설정해 사용하고 있습니다. 아래는 전체 설정 코드입니다.</p>
<pre><code class="language-java">
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
@EnableAsync
public class AsyncConfig {

    private static final int coreCount = Runtime.getRuntime().availableProcessors();

    @Bean(name = &quot;businessExecutor&quot;)
    public ThreadPoolTaskExecutor taskAsyncExecutor() {
        var executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(coreCount);
        executor.setMaxPoolSize(coreCount * 2);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix(&quot;BusinessAsyncExecutor-&quot;);
        executor.setKeepAliveSeconds(60);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        executor.initialize();
        return executor;
    }

    @Bean(name = &quot;alarmExecutor&quot;)
    public ThreadPoolTaskExecutor alarmAsyncExecutor() {
        var executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(coreCount);
        executor.setMaxPoolSize(coreCount * 2);
        executor.setQueueCapacity(250);
        executor.setThreadNamePrefix(&quot;AlarmAsyncExecutor-&quot;);
        executor.setKeepAliveSeconds(30);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

        executor.initialize();
        return executor;
    }

}</code></pre>
</br>

<h3 id="코드설명">코드설명</h3>
<p>*<em>1. 시스템 코어 수 기반 설정
*</em></p>
<pre><code class="language-java">    private static final int coreCount = Runtime.getRuntime().availableProcessors();
</code></pre>
<ul>
<li>현재 시스템에서 사용 가능한 CPU 코어 수를 기준으로 쓰레드 풀 크기를 동적으로 설정합니다.</li>
</ul>
<br>

<p>*<em>2. 비즈니스 로직 처리용 Executor (businessExecutor)
*</em></p>
<pre><code class="language-java">@Bean(name = &quot;businessExecutor&quot;)
public ThreadPoolTaskExecutor taskAsyncExecutor() {
    var executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(coreCount);
    executor.setMaxPoolSize(coreCount * 2);
    executor.setQueueCapacity(500);
    executor.setThreadNamePrefix(&quot;BusinessAsyncExecutor-&quot;);
    executor.setKeepAliveSeconds(60);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setAwaitTerminationSeconds(60);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

    executor.initialize();
    return executor;
}</code></pre>
<ul>
<li><p>corePoolSize: 최소 유지할 쓰레드 수. CPU 코어 수 만큼 설정하여 CPU 사용을 최적화 합니다.</p>
</li>
<li><p>maxPoolSize: 최대 쓰레드 수. 일반적으로 CPU는 코어 수 이상으로 스레드를 실행할 수 있지만, 너무 많으면 오히려 컨텍스트 스위칭 비용(스레드 전환 비용)이 증가해 성능이 저하됩니다. coreCount * 2는 병렬성과 유연성을 모두 확보하면서 과도한 스레드 경쟁을 피할 수 있는 안정적인 상한선입니다.</p>
</li>
<li><p>ueueCapacity: 작업 큐 크기 (500개의 작업을 큐에서 대기 가능) 유휴 쓰레드가 없으면 큐에서 대기합니다.</p>
</li>
<li><p>keepAliveSeconds: 유휴 쓰레드의 생존 시간 (기본은 60초)</p>
</li>
<li><p>threadNamePrefix:    디버깅에 유용한 쓰레드 이름 프리픽스</p>
</li>
<li><p>waitForTasksToCompleteOnShutdown:애플리케이션 종료 시 남은 작업이 끝날 때까지 대기 여부</p>
</li>
<li><p>awaitTerminationSeconds: 종료 대기 최대 시간</p>
</li>
<li><p>rejectedExecutionHandler: 작업 거부 시 정책 (여기서는 AbortPolicy → 예외 발생)</p>
</li>
</ul>
<br>


<p>*<em>3. 알림 처리용 Executor (alarmExecutor)
*</em></p>
<pre><code class="language-java">@Bean(name = &quot;alarmExecutor&quot;)
public ThreadPoolTaskExecutor alarmAsyncExecutor() {
    var executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(coreCount);
    executor.setMaxPoolSize(coreCount * 2);
    executor.setQueueCapacity(250);
    executor.setThreadNamePrefix(&quot;AlarmAsyncExecutor-&quot;);
    executor.setKeepAliveSeconds(30);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setAwaitTerminationSeconds(30);
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

    executor.initialize();
    return executor;
}</code></pre>
<ul>
<li>alarmExecutor는 알림 등 비교적 짧고 빈번한 작업 처리에 적합한 설정입니다.</li>
<li>queueCapacity와 keepAliveSeconds 값을 businessExecutor보다 작게 설정하여 자원 점유를 줄입니다.</li>
</ul>
<p>*<em>4. 실제 사용 예시
*</em></p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class StaffActionLogHelper {

    private final StaffActionLogProvider staffActionProvider;

    @Async(&quot;businessExecutor&quot;)
    public void create(StaffActionLog request) {
        staffActionProvider.save(request);
    }

}
</code></pre>
<p>@Async(&quot;businessExecutor&quot;)를 통해 지정한 executor에서 비동기 작업이 수행됩니다.</p>
<p>executor 이름은 @Bean(name = &quot;...&quot;)에서 정의한 이름과 일치해야 합니다.</p>
<br>

<h3 id="결론">결론</h3>
<p>@EnableAsync와 ThreadPoolTaskExecutor를 함께 사용하면 Spring 애플리케이션에서 효율적이고 안정적인 비동기 처리가 가능합니다.</p>
<p>특히, 서로 다른 executor를 나누어 구성하면 작업 간의 간섭을 방지하고, 한 executor가 과부하로 인해 실패하더라도 다른 executor로의 장애 전파를 차단할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring Security] Filter가 두 번 호출되는 이슈를 해결해보자]]></title>
            <link>https://velog.io/@j_hee/Spring-Security-JwtAuthenticationFilter-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@j_hee/Spring-Security-JwtAuthenticationFilter-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Tue, 29 Apr 2025 08:33:07 GMT</pubDate>
            <description><![CDATA[<p>최근 디버깅을 하던중에 Security에서 Filter가 같은 요청인데도 중복으로 실행되는 이슈를 발견했습니다.</p>
<p>처음엔 코드가 꼬인 줄 알았는데, 알고 보니 필터 등록 방식의 차이 때문이었습니다.</p>
<h2 id="1-문제-상황-요약">1. 문제 상황 요약</h2>
<p>Spring Security 설정에서 .addFilterBefore로 선언해 주면서 아래처럼 커스텀 필터를 수동으로 등록해놓았습니다.</p>
<pre><code class="language-java">@Bean
public SecurityFilterChain filterChain(HttpSecurity http, PasswordAuthenticationFilter passwordAuthenticationFilter) throws Exception {
    http
        .addFilterAt(passwordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        .addFilterBefore(new JwtAuthenticationFilter(tokenProvider, tokenValidator), UsernamePasswordAuthenticationFilter.class);
    return http.build();
}
</code></pre>
<p>그런데 해당 필터 클래스에 @Component를 추가한 상태였습니다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    // ...
}
</code></pre>
<p>이렇게 두 군데 모두 등록되어 있었고, 그 결과… 필터가 두 번 실행되는 현상이 발생했습니다. </p>
<br>

<h2 id="2-왜-두-번-호출될까">2. 왜 두 번 호출될까?</h2>
<p>이유는 <strong>Spring Security</strong>와 <strong>Servlet Container</strong>의 필터 등록 방식이 다르기 때문입니다.</p>
<h4 id="security-filter-chain">Security Filter Chain</h4>
<ul>
<li>우리가 .addFilterBefore()로 등록한 필터는 Spring Security 체인에서 동작합니다.</li>
<li>이 체인은 FilterChainProxy를 통해 호출되며, 보안 관련 필터들이 여기에 포함됩니다.</li>
</ul>
<h4 id="servlet-filter-chain">Servlet Filter Chain</h4>
<ul>
<li>반면, @Component나 @WebFilter로 등록하면 <strong>Servlet Container</strong>에서 관리하는 필터로 등록됩니다.</li>
<li>이 필터는 Spring Security와는 별개로 동작하며, 애플리케이션 전체 요청을 가로챕니다.</li>
</ul>
<p>결국 하나의 요청에 대해 두 필터 체인 모두 JwtAuthenticationFilter를 호출하게 되는 것!</p>
<br>

<h2 id="3-디버깅에서-본-실제-체인">3. 디버깅에서 본 실제 체인</h2>
<p>디버깅 화면을 보면 아래처럼 동작 순서를 확인할 수 있습니다.</p>
<p>🔍 ObservationFilterChain
Spring Security가 관찰(observation) 기능을 활성화하면 등장
내부적으로는 FilterChainProxy → JwtAuthenticationFilter 순으로 동작
<img src="https://velog.velcdn.com/images/j_hee/post/d3c4e23c-4a37-4f4c-b95d-23ecad1dd389/image.png" alt=""></p>
<p>🔍 ApplicationFilterChain
Servlet 기본 필터 체인
@Component로 등록된 필터가 여기에 포함되어 동작
<img src="https://velog.velcdn.com/images/j_hee/post/d86b320c-cd08-4919-9c1f-360c82d88c64/image.png" alt=""></p>
<p>같은 필터가 두 체인에 모두 포함되어 있기 때문에 결과적으로 중복 실행됐습니다.</p>
<br>

<p>&lt;요청흐름&gt;</p>
<ol>
<li>클라이언트 요청</li>
<li>Servlet 필터 체인 (ApplicationFilterChain) ← @Component 등록된 필터들 여기 포함</li>
<li>Spring Security 필터 체인 (FilterChainProxy)</li>
<li>DispatcherServlet</li>
<li>컨트롤러 &amp; 서비스 로직</li>
</ol>
<p>이 순서를 보면, Servlet 필터 체인은 Spring Security보다 바깥쪽에 있다는 걸 알 수 있습니다.</p>
<br>

<h2 id="4-해결-방법">4. 해결 방법</h2>
<p>결론은 @Component 제거해서 아주 간단하게 해결했습니다</p>
<p>JwtAuthenticationFilter는 수동으로 등록했기 때문에 스프링 컨테이너가 자동으로 등록하지 않도록 해야하는게 맞는것 같습니다.</p>
<br>

<p>[참고글]
<a href="https://velog.io/@im_h_jo/Spring-Security-%EC%93%B0%EB%8A%94%EB%8D%B0-%ED%95%84%ED%84%B0%EA%B0%80-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88">https://velog.io/@im_h_jo/Spring-Security-%EC%93%B0%EB%8A%94%EB%8D%B0-%ED%95%84%ED%84%B0%EA%B0%80-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</a></p>
<p><a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">https://docs.spring.io/spring-security/reference/servlet/architecture.html</a>
<img src="https://velog.velcdn.com/images/j_hee/post/261ddb8c-da3b-44bb-9280-e2eab26b6863/image.png" alt="">
[Architecture ::  Spring Security 발췌]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[IF문 리팩토링(전략패턴과 템플릿 메서드)]]></title>
            <link>https://velog.io/@j_hee/IF%EB%AC%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%EC%84%9C%EB%93%9C</link>
            <guid>https://velog.io/@j_hee/IF%EB%AC%B8-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%EC%84%9C%EB%93%9C</guid>
            <pubDate>Thu, 19 Sep 2024 14:02:22 GMT</pubDate>
            <description><![CDATA[<p>특정 이벤트는 QR코드로 출석하는 시스템을 개발하면서, 타입을 구분하는 switch문을 사용했다. 처음에는 간단하게 사용했지만 타입이 늘어날수록 코드는 유지보수와 멀어지고 점점 길어져만 갔다.</p>
<p>리팩토링한 과정을 기록해보려고한다.</p>
<blockquote>
<h2 id="as-is">AS-IS</h2>
</blockquote>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class QrAttendanceServiceImpl implements QrAttendanceService {

    private final SubmissionsClient submissionsClient;
    private final CourseClient courseClient;
    private final AssignmentGroupClient assignmentGroupClient;
    private final QrAttendanceRepository qrAttendanceRepository;

    @Transactional
    @Override
     switch (qrAttendanceCreateDto.getQrType()) {
            case FESTIVAL:
                // 1-1. DB에 있는지 조회
                Optional&lt;QrAttendance&gt; qrAttendance = qrAttendanceRepository.findAllByLoginIdAndQrTypeAndBoothNumber(
                    qrAttendanceCreateDto.getLoginId(),
                    qrAttendanceCreateDto.getQrType().getNumber(), 
                    qrAttendanceCreateDto.getBoothNumber()
                );

                // 1-2. 있다면 에러 발생, 없으면 저장
                checkIsPresent(qrAttendance, qrAttendanceCreateDto);

                // 1-3. 부스 번호가 3개 이상이면 과제 제출
                List&lt;QrAttendance&gt; qrAttendanceList = qrAttendanceRepository.findAllByLoginIdAndQrType(
                    qrAttendanceCreateDto.getLoginId(), 
                    qrAttendanceCreateDto.getQrType().getNumber()
                );
                if (qrAttendanceList.size() &gt;= 3) {
                    return submitQrAttendance(lmsUserId, &quot;[1주차] 축제 부스 방문&quot;);
                }
                break;

            case MEETING:
                // 2-1. DB에 있는지 조회
                qrAttendance = qrAttendanceRepository.findByLoginIdAndQrType(
                    qrAttendanceCreateDto.getLoginId(),
                    qrAttendanceCreateDto.getQrType().getNumber()
                );

                // 2-2. 있다면 에러 발생, 없으면 저장
                checkIsPresent(qrAttendance, qrAttendanceCreateDto);

                // 2-3. 과제 제출
                return submitQrAttendance(lmsUserId, &quot;[2주차] 간담회 참석&quot;);

            case CAREER_1:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, &quot;[4주차 1차시EXPO 부스 방문(야외)&quot;);

            case CAREER_2:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, &quot;[4주차 2차시] 커리어 페스티발 행정부스 방문하기(체육관)&quot;);

            case CAREER_3:
                return saveCareerQrAttendance(qrAttendanceCreateDto, lmsUserId, &quot;[4주차 3차시] 커리어 페스티발 이벤트, 학과부스 방문하기(체육관)&quot;);

            default:
                throw new BusinessException(ErrorCode.INVALID_QR_TYPE);
        }

     private QrSuccessResponse saveCareerQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId, String eventName) {
        Optional&lt;QrAttendance&gt; qrAttendance = qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
        checkIsPresent(qrAttendance, qrAttendanceCreateDto);
        return submitQrAttendance(lmsUserId, eventName);
    }

    private void checkIsPresent(Optional&lt;QrAttendance&gt; qrAttendance, QrAttendanceCreateDto qrAttendanceCreateDto) {
        if (qrAttendance.isPresent()) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        } else {
            qrAttendanceRepository.save(QrAttendance.of(qrAttendanceCreateDto));
        }
    }

    // 외부 API 호출
    private QrSuccessResponse submitQrAttendance(Long lmsUserId, String eventName) {
        List&lt;QrCourseFeignDto&gt; courseList = courseClient.getQrCourseList(lmsUserId).stream()
            .filter(course -&gt; course.getWorkflowState().equals(&quot;available&quot;) || course.getWorkflowState().equals(&quot;published&quot;))
            .toList();

        Optional&lt;QrAssignmentFeignResponse&gt; qrAssignmentOptional = courseList.stream()
            .map(course -&gt; assignmentGroupClient.findQrAssignment(lmsUserId, course.getId()))
            .flatMap(List::stream)
            .filter(assignment -&gt; assignment.getName().trim().equals(eventName.trim()))
            .filter(assignment -&gt; assignment.getSubmissionTypes().contains(&quot;on_paper&quot;))
            .filter(assignment -&gt; DateTimeUtil.addHoursToLocalDateTime(assignment.getDueAt()).isAfter(LocalDateTime.now()))
            .findFirst();

        QrAssignmentFeignResponse qrAssignment = qrAssignmentOptional.orElseThrow(
            () -&gt; new BusinessException(ErrorCode.NOT_FOUND_QR_ASSIGNMENT));

        submissionsClient.updateAssignmentComplete(
            qrAssignment.getCourseId(),
            qrAssignment.getId(),
            lmsUserId,
            &quot;complete&quot;
        );

        return new QrSuccessResponse(&quot;success&quot;, &quot;출석이 완료되었습니다.&quot;);
    }

}</code></pre>
<br>

<p>QrType 이넘클래스</p>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum QrType {

    FESTIVAL,
    MEETING(&quot;간담회&quot;, 1),
    CAREER_1(&quot;커리어 페스티발 1차시&quot;, 2),
    CAREER_2(&quot;커리어 페스티발 2차시&quot;, 3),
    CAREER_3(&quot;커리어 페스티발 3차시&quot;, 4);


    private String name;
    private Integer number;

  }
</code></pre>
<p>기존 코드의 문제점은 새로운 요구사항이 추가되거나, 새로운 타입이 생길때 마다 계속해서 수정해야 한다는 점이다. 확장성이 유연하지 않으며 코드가 중복되고 가독성도 떨어진다. 이 레거시코드를 전략패턴과 탬플릿 메소드 패턴으로 리팩토링 해보려고 한다.</p>
<br>

<blockquote>
<h2 id="리팩토링">리팩토링</h2>
</blockquote>
<ul>
<li><p>탬플릿 메소드 패턴</p>
<ul>
<li><p>로직의 기본적인 처리 흐름이 동일하지만, 세부적인 로직이 타입에 따라 달라지는 경우가 있다.</p>
</li>
<li><p>이때, 패턴을 통해 공통적인 처리 흐름은 부모 클래스에서 관리하고, 타입에 따라 다른 동작은 자식 클래스에서 오버라이딩 할 수 있도록 해준다.</p>
</li>
<li><p>이를 통해 중복코드가 줄어들고, 변경이 필요한 부분만 자식 클래스에서 수정하면 되므로 확장성이 높아진다.</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>전략패턴<ul>
<li>타입에 따라 다른 로직이 필요할때, ApplicationContext를 통해 적절한 구현체를 동적으로 주입받아 처리할 수 있다.</li>
<li>이때 조건문(switch, if문 등)의 남용을 피할 수 있다.</li>
<li>타입이 추가되더라도 코드의 변경없이 새로운 전략을 추가해 확장할 수 있다.</li>
<li>이는 개방-폐쇄원칙(OCP)을 지킬 수 있으며 유연성이 높아진다.</li>
</ul>
</li>
</ul>
<pre><code>&lt;br&gt;</code></pre><p>이런 장점들로 탬플릿 메소드 패턴과 전략패턴을 활용해서 아래의 순서대로 리팩토링을 해봤다. </p>
<ol>
<li>출석 과제명을 이넘으로 관리</li>
<li>템플릿 메소드 패턴으로 부모 클래스에서 호출되는 단계 메소드를 정의하고, 일부 메소드는 자식 클래스에서 구현</li>
<li>전략패턴으로 qrType에 따른 로직을 ApplicationContext를 통해서 동적으로 빈을 주입 </li>
</ol>
<br>

<h3 id="출석-과제명을-이넘으로-관리">출석 과제명을 이넘으로 관리</h3>
<p>출석처리 할때 필요한 과제를 이넘으로 관리하게끔 리팩토링했다.</p>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Getter
public enum QrType {

    FESTIVAL(QrAttendanceFestival.class, &quot;[10주차 1,2,3차시] 대학 축제 부스 방문&quot;, 0),
    MEETING(QrAttendanceMeeting.class, &quot;[15주차 1차시] 간담회 참석&quot;, 1),
    CAREER_1(QrAttendanceCareer.class, &quot;[4주차 1차시] KBU 건강복지 EXPO 부스 방문(야외)&quot;, 2),
    CAREER_2(QrAttendanceCareer.class,&quot;[4주차 2차시] 커리어 페스티발 행정부스 방문하기(체육관)&quot;, 3),
    CAREER_3(QrAttendanceCareer.class,&quot;[4주차 3차시] 커리어 페스티발 이벤트, 학과부스 방문하기(체육관)&quot;, 4);

    private Class&lt;? extends QrAttendanceSaver&gt; bean;
    private String name;
    private Integer number;


 }</code></pre>
<br>

<h3 id="템플릿-메소드-패턴-적용">템플릿 메소드 패턴 적용</h3>
<p>QrAttendanceSaver는 부모 클래스로, 전체 로직의 탬플릿을 만들었다. 
getQrAttendance 메소드를 호출하여 QR 출석 정보를 가져온다. 이 메소드는 추상 메소드로, 서브 클래스에서 구현해야 한다.
출석 정보가 이미 존재하는지 확인하고(checkIsPresent), 이미 존재하면 예외를 던진다.
추가적인 검증(checkOverItems)을 수행합니다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public abstract class QrAttendanceSaver {

    private final SubmissionsClient submissionsClient;
    private final CourseClient courseClient;
    private final AssignmentGroupClient assignmentGroupClient;
    private final QrAttendanceRepository qrAttendanceRepository;

    @Transactional
    public QrSuccessResponse saveStudentQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId) {
        Optional&lt;QrAttendance&gt; qrAttendance = getQrAttendance(qrAttendanceCreateDto);
        checkIsPresent(qrAttendance, qrAttendanceCreateDto);
        checkOverItems(qrAttendanceCreateDto);
        return submitQrAttendance(lmsUserId, qrAttendanceCreateDto.getQrType().getName());
    }

    public abstract Optional&lt;QrAttendance&gt; getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto);

    public void checkOverItems(QrAttendanceCreateDto qrAttendanceCreateDto) throws BusinessException {

    }

    private void checkIsPresent(Optional&lt;QrAttendance&gt; qrAttendance, QrAttendanceCreateDto qrAttendanceCreateDto) {
        if (qrAttendance.isPresent()) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        } else {
            qrAttendanceRepository.save(QrAttendance.of(qrAttendanceCreateDto));
        }
    }

    private QrSuccessResponse submitQrAttendance(Long lmsUserId, String eventName) {
        List&lt;QrCourseFeignDto&gt; courseList = courseClient.getQrCourseList(lmsUserId).stream()
            .filter(course -&gt; course.getWorkflowState().equals(&quot;available&quot;) || course.getWorkflowState().equals(&quot;published&quot;))
            .toList();

        Optional&lt;QrAssignmentFeignResponse&gt; qrAssignmentOptional = courseList.stream()
            .map(course -&gt; assignmentGroupClient.findQrAssignment(lmsUserId, course.getId()))
            .flatMap(List::stream)
            .filter(assignment -&gt; assignment.getName().trim().equals(eventName.trim()))
            .filter(assignment -&gt; assignment.getSubmissionTypes().contains(&quot;on_paper&quot;))
            .filter(assignment -&gt; DateTimeUtil.addHoursToLocalDateTime(assignment.getDueAt()).isAfter(LocalDateTime.now()))
            .findFirst();

        QrAssignmentFeignResponse qrAssignment = qrAssignmentOptional.orElseThrow(
            () -&gt; new BusinessException(ErrorCode.NOT_FOUND_QR_ASSIGNMENT));

        submissionsClient.updateAssignmentComplete(
            qrAssignment.getCourseId(),
            qrAssignment.getId(),
            lmsUserId,
            &quot;complete&quot;
        );

        return new QrSuccessResponse(&quot;success&quot;, &quot;출석이 모두 완료되었습니다.&quot;);
    }

}</code></pre>
<br>

<h3 id="전략-패턴-적용">전략 패턴 적용</h3>
<p>탬플릿 메소드 자식 클래스로 전략 패턴을 구현했다. QrAttendanceSaver를 상속받는다.
총 career, meeting, festival 세가지의 전략인데, career경우 과제명은 다르지만 로직은 같기 때문에 하나의 QrAttendanceCareer 클래스로 구현했다.</p>
<pre><code class="language-java">@Service
public class QrAttendanceCareer extends QrAttendanceSaver {

    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceCareer(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional&lt;QrAttendance&gt; getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
    }

}</code></pre>
<pre><code class="language-java">@Service
public class QrAttendanceFestival extends QrAttendanceSaver {

    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceFestival(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional&lt;QrAttendance&gt; getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findAllByLoginIdAndQrTypeAndBoothNumber(
            qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber(), qrAttendanceCreateDto.getBoothNumber());

    }

    public void checkOverItems(QrAttendanceCreateDto qrAttendanceCreateDto) throws BusinessException {
        List&lt;QrAttendance&gt; qrAttendanceList = qrAttendanceRepository.findAllByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
        if (qrAttendanceList.size() &gt; 3) {
            throw new BusinessException(ErrorCode.ALREADY_EXIST_QR_ATTENDANCE);
        }
    }

}</code></pre>
<pre><code class="language-java">@Service
public class QrAttendanceMeeting extends QrAttendanceSaver {

    private final QrAttendanceRepository qrAttendanceRepository;

    public QrAttendanceMeeting(SubmissionsClient submissionsClient, CourseClient courseClient,
        AssignmentGroupClient assignmentGroupClient,
        QrAttendanceRepository qrAttendanceRepository) {
        super(submissionsClient, courseClient, assignmentGroupClient, qrAttendanceRepository);
        this.qrAttendanceRepository = qrAttendanceRepository;
    }

    @Override
    public Optional&lt;QrAttendance&gt; getQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto) {
        return qrAttendanceRepository.findByLoginIdAndQrType(qrAttendanceCreateDto.getLoginId(),
            qrAttendanceCreateDto.getQrType().getNumber());
    }

}</code></pre>
<br>

<h3 id="서비스레이어-코드">서비스레이어 코드</h3>
<p>전략 패턴을 활용하여 qrType에 따라 다른 QrAttendanceSaver 빈을 사용한다. 즉, 여러 QR 처리 전략 중 하나를 동적으로 선택해 실행한다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class QrAttendanceService {

    private final ApplicationContext applicationContext;

    @Override
    public QrSuccessResponse saveStudentQrAttendance(QrAttendanceCreateDto qrAttendanceCreateDto, Long lmsUserId) {
        QrAttendanceSaver saver = applicationContext.getBean(qrAttendanceCreateDto.getQrType().getBean());
        return saver.saveStudentQrAttendance(qrAttendanceCreateDto, lmsUserId);
    }

}</code></pre>
<blockquote>
<h2 id="결론">결론</h2>
</blockquote>
<p>이렇게 리팩토링을 통해서 코드의 유지보수성과 확장성을 높여봤다. 하지만 클래스가 많아질 때 복잡해질 수 있다는 단점을 고려해서, 적절한 수준의 추상화와 분리를 통해 복잡도를 관리하는 것도 중요할것 같다는 생각이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파사드패턴으로 클-린한 코드 만들기]]></title>
            <link>https://velog.io/@j_hee/%ED%8C%8C%EC%82%AC%EB%93%9C%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%ED%81%B4-%EB%A6%B0%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@j_hee/%ED%8C%8C%EC%82%AC%EB%93%9C%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%ED%81%B4-%EB%A6%B0%ED%95%9C-%EC%BD%94%EB%93%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Wed, 11 Sep 2024 06:06:53 GMT</pubDate>
            <description><![CDATA[<p>로직이 복잡한 프로젝트인데, 서비스 레이어 간의 참조로 인해 순환 참조가 발생할 가능성이 높아졌습니다. 순환 참조는 클래스 간 의존 관계가 얽혀 복잡성을 더하고, 결과적으로 유지보수를 어렵게 만드는 문제가 있습니다. 그리고 순환참조 에러도 터지겠죠.
또한, 서비스 레이어에서 비즈니스 로직이 명확하게 드러나야하는데, 세부 구현 로직까지 함께 섞이면서 로직을 한눈에 파악하기 힘들어졌습니다. </p>
<p>이러다가 큰일나겠다 싶어서 팀원들과 고민을 나누던 중 제미니님의 <a href="https://geminikims.medium.com/%EC%A7%80%EC%86%8D-%EC%84%B1%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94-%EB%B0%A9%EB%B2%95-97844c5dab63">지속 성장 가능한 소프트웨어를 만들어가는 방법</a> 글을 추천받았습니다.
이 글을 읽고 <strong>파사드 패턴</strong>을 도입하면 제가 고민하던 부분이 해결될 것이라는 결론을 내렸습니다.</p>
<br>



<blockquote>
<h2 id="파사드패턴">파사드패턴</h2>
</blockquote>
<p>파사드 패턴은 복잡한 서브시스템을 단순화하여 클라이언트가 서브시스템 내부의 복잡성을 알지 못해도 쉽게 사용할 수 있게 도와주는 패턴입니다.
이를 통해 <strong>서비스 간 참조를 줄이고</strong>, <strong>비즈니스 로직과 구현로직을 분리</strong>할 수 있습니다.</p>
<br>

<p>[장점]</p>
<ul>
<li><p><strong>의존성 감소</strong>
  여러 서비스 레이어에서 서로 직접적으로 참조하지 않고, 파사드를 통해 간접적으로 의존성을 관리하므로 순환 참조 문제를 피할 수 있습니다.</p>
</li>
<li><p><strong>코드 가독성 및 유지보수성 향상</strong>
  비즈니스 로직을 파사드 클래스에 모아놓고, 세부 구현 로직은 각 책임에 맞는 클래스에 위임함으로써 비즈니스 흐름이 더 명확해집니다. 이로 인해 코드의 가독성이 향상되고, 각 역할이 명확히 분리되므로 유지보수도 쉬워집니다.</p>
</li>
<li><p><strong>클라이언트 코드의 단순화</strong>
  복잡한 로직을 파사드에 감싸 처리하므로, 클라이언트 코드에서 하위 서비스들의 복잡한 호출을 신경쓰지 않아도 됩니다.</p>
</li>
</ul>
<br>

<p>[단점]</p>
<ul>
<li><strong>추가적인 레이어 도입</strong>
  파사드패턴을 적용하면 새로운 레이어가 생기기 때문에 코드의 전체 구조가 박잡해질 수 있습니다. 잘못 설계되면 오히려 과도한 추상화로 인해 가독성이 떨어질 수 있습니다.<ul>
<li><strong>확장성 제한</strong>
 파사드에서 비즈니스 로직의 흐름을 단일 진입점으로 처리하다 보면, 특정 기능을 확장하거나 변경할 때 유연성이 떨어질 수 있습니다.</li>
</ul>
</li>
</ul>
<br>    
장점과 단점을 고민해봤지만, 저의 상황에는 장점이 더 커서 적용했습니다.
<br>    




<blockquote>
<h2 id="코드에-적용">코드에 적용</h2>
</blockquote>
<p>우선 가장 기본적인 회원가입부분 부터 적용해보겠습니다.
코치가 회원가입하는 경우에는 코치테이블, 코치 이미지 테이블, 코치 해쉬테크 테이블, 코치 지역 테이블 총 4개의 테이블에 저장해야합니다. </p>
<p>아래의 repository 레이어를 사용하여 구현한 코드는 가독성이 떨어지고, 로직을 한눈에 파악하기 어렵습니다.</p>
<h3 id="as-is">AS-IS</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final CoachRepository coachRepository;
    private final CoachImageRepository coachImageRepository;
    private final CoachTagRepository coachTagRepository;
    private final CoachRegionRepository coachRegionRepository;


    @Transactional
    public void coachSignup(CoachSignupRequest request) throws ServiceException {
        // 유저 정보를 조회 후 코치로 롤 업데이트 한다. 그리고 코치 정보를 저장한다.
        User user = userRepository.findByUid(request.getUid()).orElseThrow(() -&gt; new ServiceException(ErrorCode.USER_NOT_FOUND));
        user.updateRole(Role.COACH);
        Coach coach = coachRepository.save(request.toEntity(user.getId()));

        // 코치 이미지 정보를 저장한다.
        coachImageRepository.save(CoachImage.create(coach.getId(), request.getProfileImgUrl(), CoachImageType.COACH_PROFILE));
        request.getCertificateImgUrlList()
            .forEach(url -&gt; coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.CERTIFICATE)));
        request.getAwardsImgUrlList().forEach(url -&gt; coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.AWARDS)));
        request.getLicenseImgUrlList().forEach(url -&gt; coachImageRepository.save(CoachImage.create(coach.getId(), url, CoachImageType.LICENSE)));

        // 코치 태그 정보를 저장한다.
        request.getTagIdList().forEach(tagId -&gt; {
            coachTagRepository.save(
                CoachTagCreateRequest.builder()
                    .tagId(tagId)
                    .coachId(coach.getId())
                    .build()
                    .toEntity());
        });

        // 코치 지역 정보를 저장한다.
        String region1 = (request.getRegionList().size() &gt; 0) ? request.getRegionList().get(0) : null;
        String region2 = (request.getRegionList().size() &gt; 1) ? request.getRegionList().get(1) : null;
        coachRegionRepository.save(
            CoachRegion.builder()
                .coachId(coach.getId())
                .region1(region1)
                .region2(region2)
                .build()
        );
    }

  }
</code></pre>
<p>이 코드는 너무 복잡하므로, 레포지토리를 서비스 레이어로 감싸서 리팩토링해보겠습니다.</p>
<br>

<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final CoachRepository coachRepository;
    private final CoachImageService coachImageService;
    private final TagService tagService;
    private final CoachRegionService coachRegionService;


    @Transactional
    public void coachSignup(CoachSignupRequest request) throws ServiceException {
        User user = userRepository.findByUid(request.getUid()).orElseThrow(() -&gt; new ServiceException(ErrorCode.USER_NOT_FOUND));
        user.updateRole(Role.COACH);
        Coach coach = coachRepository.save(request.toEntity(user.getId()));
        coachImageService.save(request, coach.getId());
        tagService.save(request.getTagIdList(), coach.getId());
        coachRegionService.save(coach.getId(), request.getRegionList());
    }

 }</code></pre>
<p>코드가 훨씬 깔끔해졌습니다. 하지만, 순환 참조 문제가 발생합니다.</p>
<p>파사드 패턴을 도입하여 리팩토링해보겠습니다. insert, update가 발생하는 서비스는 Appender 클래스로, read가 발생하는 서비스는 Finder 클래스로 구분했습니다. 이 로직에서는 읽기가 없으므로 Appender만 사용했습니다.
<br></p>
<h3 id="최종코드">최종코드</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class UserService {

    private final CoachAppender coachAppender;
    private final TagAppender tagAppender;
    private final CoachImageAppender coachImageAppender;
    private final CoachRegionAppender coachRegionAppender;

    @Transactional
    public void coachSignup(CoachSignupRequest request) throws ServiceException {
        Coach coach = coachAppender.signup(request);
        coachImageAppender.save(request, coach.getId());
        tagAppender.save(request.getTagIdList(), coach.getId());
        coachRegionAppender.save(coach.getId(), request.getRegionList());
    }

 }</code></pre>
<br>

<p>Appender Class는 @Component로 등록해서 사용합니다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class CoachAppender {

    private final UserRepository userRepository;
    private final CoachRepository coachRepository;

    public Coach signup(CoachSignupRequest request) throws ServiceException {
        User user = userRepository.findByUid(request.getUid()).orElseThrow(() -&gt; new ServiceException(ErrorCode.USER_NOT_FOUND));
        user.updateRole(Role.COACH);
        return coachRepository.save(request.toEntity(user.getId()));
    }

 }</code></pre>
<br>

<blockquote>
<h2 id="결과">결과</h2>
</blockquote>
<p>파사드 패턴으로 리팩토링하여 서비스 간 참조를 줄이고, 비즈니스 로직과 구현 로직을 분리했습니다. 
이 패턴을 적용함으로써 코드의 가독성이 크게 향상된 것을 확인할 수 있습니다.</p>
]]></description>
        </item>
    </channel>
</rss>