<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>밤둘레</title>
    <link>https://bamdule.tistory.com/</link>
    <description>꾸준히 걷는 개발자</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 01:19:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Bamdule</managingEditor>
    <image>
      <title>밤둘레</title>
      <url>https://tistory1.daumcdn.net/tistory/3069221/attach/c3cb21e0145b4f94951f8ebe713e925c</url>
      <link>https://bamdule.tistory.com</link>
    </image>
    <item>
      <title>UnexpectedRollbackException 예외가 발생하는 이유</title>
      <link>https://bamdule.tistory.com/286</link>
      <description>&lt;h2 id=&quot;들어가며&amp;hellip;.&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size26&quot;&gt;들어가며&amp;hellip;.&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 트랜잭션을 사용하다 보면, &amp;ldquo;예외를 잡았는데 왜 커밋이 안 되고 롤백이 될까?&amp;rdquo; 하는 상황을 만나게 됩니다.&lt;br /&gt;특히 org.springframework.transaction.UnexpectedRollbackException은 회원 가입, 주문 처리처럼 주요 로직에 부가 기능(로그, 이력 저장 등)을 붙이다 보면 한 번쯤 경험하게 되는 예외입니다.&lt;br /&gt;이번 글에서는 이 예외가 왜 발생하는지, 그리고 어떻게 해결할 수 있는지 살펴보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;260&quot; data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;267&quot; data-ke-size=&quot;size16&quot;&gt;회원 가입 시 다음과 같은 두 가지 동작을 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;회원 저장&lt;/li&gt;
&lt;li&gt;회원 가입 이력 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;323&quot; data-ke-size=&quot;size16&quot;&gt;이력 저장은 부가 기능이므로, 실패하더라도 회원 저장은 성공하기를 기대했습니다.&lt;br /&gt;그래서 이력 저장 로직을 try-catch로 감싸 두었죠.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;402&quot; data-ke-size=&quot;size16&quot;&gt;하지만 결과는 예상과 달랐습니다.&lt;br /&gt;&lt;b&gt;회원 저장도 실패하면서 &lt;/b&gt;UnexpectedRollbackException&lt;b&gt;이 발생했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;472&quot; data-ke-size=&quot;size16&quot;&gt;기대 vs 실제를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&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; UnexpectedRollbackException 발생 &amp;rarr; 회원도 저장되지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;코드-예시&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;580&quot; data-ke-size=&quot;size26&quot;&gt;코드 예시&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;Member-엔티티&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;587&quot; data-ke-size=&quot;size23&quot;&gt;Member 엔티티&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1757662922236&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = &quot;member&quot;)
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private LocalDateTime createDateTime;
  // ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;MemberSignupHistory-엔티티&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;857&quot; data-ke-size=&quot;size23&quot;&gt;MemberSignupHistory 엔티티&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757662956981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = &quot;member_signup_history&quot;)
@Entity
public class MemberSignupHistory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long userId;
    private LocalDateTime createDateTime;
    public MemberSignupHistory(Member member) {
        this.userId = member.getId();
        this.createDateTime = LocalDateTime.now();
    }
  // ... 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;MemberSignupHistoryService---회원가입-이력-저장-서비스&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1337&quot; data-ke-size=&quot;size23&quot;&gt;MemberSignupHistoryService - 회원가입 이력 저장 서비스&lt;/h3&gt;
&lt;pre id=&quot;code_1757663032488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Service
public class MemberSignupHistoryService {
    private final MemberRegistrationHistoryRepository memberRegistrationHistoryRepository;
    @Transactional
    public void saveSignupHistory(Member member) {
        memberRegistrationHistoryRepository.save(new MemberSignupHistory(member));
        if (true) {
            throw new RuntimeException(&quot;회원가입 이력 저장 시 예외 발생&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;h3 id=&quot;MemberSignupService---회원가입-서비스&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1814&quot; data-ke-size=&quot;size23&quot;&gt;MemberSignupService - 회원가입 서비스&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1757663068525&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Service
public class MemberSignupService {
    private final MemberRepository memberRepository;
    private final MemberSignupHistoryService memberSignupHistoryService;
    @Transactional
    public Long signupMember() {
        // 회원 저장
        Member member = memberRepository.save(new Member());
        try {
            // 로그 저장을 실패하더라도 진행
            memberSignupHistoryService.saveSignupHistory(member);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return member.getId(); // UnexpectedRollbackException 발생
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;왜-이런일이-발생할까?&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2449&quot; data-ke-size=&quot;size26&quot;&gt;왜 이런일이 발생할까?&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2463&quot; data-ke-size=&quot;size16&quot;&gt;문제의 원인은 @Transactional&lt;b&gt;의 동작 원리&lt;/b&gt;에 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2501&quot; data-ke-size=&quot;size16&quot;&gt;스프링 트랜잭션은 &lt;b&gt;트랜잭션 경계 안에서 예외가 던져지면 rollback-only 마킹을 해둡니다.&lt;/b&gt;&lt;br /&gt;이 상태에서는 커밋이 불가능합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2577&quot; data-ke-size=&quot;size16&quot;&gt;즉, try-catch로 예외를 잡더라도 이미 트랜잭션은 rollback-only 상태인 것이죠.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2633&quot; data-ke-size=&quot;size16&quot;&gt;최종적으로 커밋을 시도할 때 스프링은 이렇게 반응합니다:&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2666&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;rollback-only인데 커밋하려 하네? &amp;rarr; UnexpectedRollbackException 발생!&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2728&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce4a6;&quot; data-background-custom-color=&quot;#fedec8&quot; data-renderer-mark=&quot;true&quot;&gt;핵심: 예외를 잡았다고 해서 트랜잭션이 살아나는 것은 아닙니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h2 id=&quot;해결-방법&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2765&quot; data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h4 id=&quot;방법-1.-이력-저장을-별도-트랜잭션으로-실행&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2772&quot; data-ke-size=&quot;size20&quot;&gt;방법 1. 이력 저장을 별도 트랜잭션으로 실행&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1757663119536&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveSignupHistory(Member member) {
    memberRegistrationHistoryRepository.save(new MemberSignupHistory(member));
    if (true) {
        throw new RuntimeException(&quot;회원가입 이력 저장 시 예외 발생&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REQUIRES_NEW는 새로운 트랜잭션을 시작합니다.&lt;/li&gt;
&lt;li&gt;따라서 실패하더라도 메인 트랜잭션에는 영향을 주지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;방법-2.-이력-저장을-try&amp;hellip;catch-처리&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3136&quot; data-ke-size=&quot;size20&quot;&gt;방법 2. 이력 저장을 try&amp;hellip;catch 처리&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1757663136688&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void saveSignupHistory(Member member) {
    try {
      memberRegistrationHistoryRepository.save(new MemberSignupHistory(member));
      if (true) {
          throw new RuntimeException(&quot;회원가입 이력 저장 시 예외 발생&quot;);
      }
    } catch (Exception e) {
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이력 저장 과정에서 발생한 예외를 &lt;b&gt;내부에서 처리&lt;/b&gt;하고 외부로 던지지 않습니다.&lt;/li&gt;
&lt;li&gt;따라서 외부 트랜잭션은 rollback-only 마킹되지 않고 정상적으로 커밋됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;방법-3.-비동기-처리-(이력-저장을-메시지-큐나-이벤트로-처리)&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3550&quot; data-ke-size=&quot;size20&quot;&gt;방법 3. 비동기 처리 (이력 저장을 메시지 큐나 이벤트로 처리)&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원 저장이 끝난 후 이벤트를 발행합니다.&lt;/li&gt;
&lt;li&gt;별도 Consumer나 비동기 핸들러에서 이력 저장을 처리합니다.&lt;/li&gt;
&lt;li&gt;실패하더라도 메인 트랜잭션에는 영향을 주지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;방법-4.-@Transactional-어노테이션-제거&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3690&quot; data-ke-size=&quot;size20&quot;&gt;방법 4. @Transactional 어노테이션 제거&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot;&gt;
&lt;pre id=&quot;code_1757663155572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void saveSignupHistory(Member member) {
    try {
      memberRegistrationHistoryRepository.save(new MemberSignupHistory(member));
      if (true) {
          throw new RuntimeException(&quot;회원가입 이력 저장 시 예외 발생&quot;);
      }
    } catch (Exception e) {
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@Transactional 어노테이션이 제거되면 MemberSignupHistoryService.saveSignupHistory(&amp;hellip;) 호출 시 트랜잭션이 전파되지 않기 때문에 예외가 발생하더라도 rollback-only 마킹이 되지 않습니다.&lt;/li&gt;
&lt;li&gt;rollback-only 마킹의 핵심은 트랜잭션 경계 안에서 예외가 전파될 경우입니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;정리&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;4188&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;span&gt;&lt;span style=&quot;color: #505258;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 트랜잭션은 내부에서 예외가 발생하면 rollback-only로 마킹합니다.&lt;/li&gt;
&lt;li&gt;try-catch로 잡아도 이미 트랜잭션은 커밋할 수 없습니다.&lt;/li&gt;
&lt;li&gt;따라서 보조 기능(로그, 이력 등)이 실패해도 메인 로직을 살리고 싶다면
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;별도 트랜잭션(&lt;/b&gt;REQUIRES_NEW&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;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fce4a6;&quot; data-background-custom-color=&quot;#fedec8&quot; data-renderer-mark=&quot;true&quot;&gt;중요 : rollback-only는 트랜잭션 경계 안에서 예외가 전파될 때 발생&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/286</guid>
      <comments>https://bamdule.tistory.com/286#entry286comment</comments>
      <pubDate>Fri, 12 Sep 2025 16:46:52 +0900</pubDate>
    </item>
    <item>
      <title>스프링 프레임워크와 서블릿 컨테이너의 관계</title>
      <link>https://bamdule.tistory.com/285</link>
      <description>&lt;p data-ke-size=&quot;size14&quot;&gt;스프링 프레임워크와 서블릿 컨테이너의 관계에 대해서 정리해보려고 한다. &lt;br /&gt;기초적인 이야기지만 창피하게도 필자는 정확히 알지 못했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이야기를 시작하기 앞서서 스프링 프레임워크에서 서블릿은 필요한가?&lt;br /&gt;필요하다면 서블릿 컨테이너와 스프링 컨테이너는 어떤 차이가 있는가? &lt;br /&gt;디스패처 서블릿은 무엇이고 왜 서블릿이라는 용어가 뒤에 붙는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이 질문들에 대해서 답하지 못한다면 스프링의 기초적인 내용을 놓치고 있는 것이다.&lt;br /&gt;이번 글에서 아래와 같은 내용에 대해서 간단히 정리해보겠다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 스프링프레임워크 &lt;br /&gt;2. 서블릿 컨테이너&lt;br /&gt;3. 디스패처 서블릿&lt;br /&gt;4. 디스패처 서블릿 동작 흐름&lt;br /&gt;5. 스프링부트 애플리케이션 실행 흐름&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 스프링 프레임워크란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 말해서 스프링 프레임워크(Spring Framework)는 자바(Java) 기반의 애플리케이션 개발을 지원하는 오픈 소스 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 복잡한 애플리케이션 개발을 간소화하고, 개발 생산성과 코드의 유연성을 높이는 데 도움을 준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;POJO (Plain Old Java Object) 기반 개발&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;순수 Java 객체를 이용해 비즈니스 로직을 구현&lt;/li&gt;
&lt;li&gt;POJO를 지향하는 이유는 옛날에 사용하던 EJB(Enterprise JavaBean)는 굉장히 무거웠고 복잡했으며 객체 간 의존성이 높았기 때문에 유지보수 및 개발이 어려웠고 이 때문에 순수 POJO 방식 개발이 선호되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; 의존성 주입(Dependency Injection, DI)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;객체 간의 의존 관계를 명시적으로 설정하고 &lt;b&gt;스프링 컨테이너&lt;/b&gt;가 이를 관리하도록 한다.&lt;/li&gt;
&lt;li&gt;이를 통해서 코드의 결합도를 낮추고, 유지 보수가 쉬워졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;스프링 컨테이너(Spring Container)&lt;/b&gt;는 스프링 프레임워크의 핵심으로, 객체를 생성하고, 관리하며, 필요한 의존성을 주입하는 역할을 한다. 스프링 컨테이너는 애플리케이션의 객체들을 Bean이라고 부르며, 이 Bean들을 생성, 초기화, 소멸, 의존성 주입 등을 담당한다. &lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt; AOP(Aspect-Oriented Programming)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션의 핵심 로직과 부가적인 기능(예: 로깅, 트랜잭션 관리)을 분리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;테스트 용이성&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DI와 POJO 기반 개발을 통해 단위 테스트와 통합 테스트를 쉽게 작성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; 모듈화된 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다양한 모듈로 구성되어 있으며, 필요한 부분만 선택적으로 사용할 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Core&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;DI 및 IoC(제어의 역전)를 지원하는 핵심 모듈.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring MVC&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;웹 애플리케이션 개발을 위한 Model-View-Controller 아키텍처를 지원.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Data&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스 작업을 쉽게 처리하기 위한 모듈.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Security&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인증과 권한 부여를 다루는 보안 모듈.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;여담이지만 스프링 프레임워크는 자바 진형에서 축복같은 존재다. 스프링 프레임워크의 철학으로 인해 객체지향 개발이 이토록 보편화될 수 있었던 것 같다!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; 서블릿 컨테이너 (Servlet Container)란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너란 무엇일까? 스프링 개발 시 필요한가?&lt;br /&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;결론부터 말하자면, &lt;/span&gt;&lt;b&gt;필요하다&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서블릿 컨테이너&lt;/b&gt;는 Java EE(현재는 Jakarta EE) 표준의 일부로, 서블릿과 JSP(JavaServer Pages)를 실행하는 환경을 제공하는 소프트웨어다. 이 컨테이너는 웹 애플리케이션에서 HTTP 요청을 처리하고, 응답을 반환하는 주요 역할을 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 서블릿 컨테이너가 무엇인지 간단하게 알아보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP 요청 처리&lt;/b&gt;: 클라이언트로부터 받은 HTTP 요청을 서블릿에 전달하여 처리한다.&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;JSP 파일 처리&lt;/b&gt;: JSP 파일을 서블릿으로 변환하여 실행한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 반환&lt;/b&gt;: 처리한 결과를 클라이언트로 HTTP 응답으로 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서블릿 컨테이너와 WAS&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너 환경을 구축하려면 WAS(Web Application Server)를 설치해야 한다. WAS는 서블릿 컨테이너와 더불어 웹 애플리케이션을 실행하고 관리하는 서버 소프트웨어이다.&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;b&gt;Tomcat&lt;/b&gt;, &lt;b&gt;Jetty&lt;/b&gt;, &lt;b&gt;Undertow&lt;/b&gt;)를 제공하여, 별도의 외부 서블릿 컨테이너 설치 없이 애플리케이션을 실행할 수 있게 되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;그래서 뭔데!?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서블릿 컨테이너와 스프링 컨테이너가 무슨 관계가 있는데요!? 라는 질문에 대해서 디스패처 서블릿이 그 답을 줄 것이다.&lt;br /&gt;디스패처 서블릿은 스프링 MVC 매커니즘의 핵심요소이니 잘 이해해야한다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; 디스패처 서블릿(Dispatcher Servlet)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크에서 디스패처 서블릿(DispatcherServlet)은 HTTP 통신의 핵심 역할을 수행하는 서블릿이다.&lt;br /&gt;스프링 애플리케이션 실행 시 디스패처 서블릿은 &lt;span data-token-index=&quot;1&quot;&gt;서블릿 컨테이너&lt;/span&gt;에 등록되며, 클라이언트의 모든 HTTP 요청을 처리하는 진입점 역할한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;왜 서블릿이라는 명칭이 붙는가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스패처 서블릿은 &lt;b&gt;서블릿 컨테이너&lt;/b&gt;와 협력하기 위해 설계되었기 때문에 서블릿이라는 이름이 붙었다.&lt;br /&gt;(디스패처 서블릿도 서블릿 컨테이너 입장에서는 단순히 서블릿이기 때문이다!)&lt;br /&gt;스프링 프레임워크는 HTTP 요청 및 응답 처리를 자체적으로 구현하지 않고, 이러한 역할을 &lt;b&gt;서블릿 컨테이너&lt;/b&gt;에 위임했다.&lt;br /&gt;그렇다. 이제 답이 나왔다.&lt;br /&gt;스프링 프레임워크는 HTTP 통신을 위해 서블릿 컨테이너와 협업을 해야한다. 그러므로 서블릿 컨테이너가 필요하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 프레임워크에서 서블릿 컨테이너를 의존하는 방법은 다음과 같다&lt;/p&gt;
&lt;pre id=&quot;code_1735032520401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# gradle.build
...

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    내장 톰캣만 사용하고 싶다면 아래 의존성을 가지면 된다. 
    implementation 'org.springframework.boot:spring-boot-starter-tomcat'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;디스패처 서블릿의 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;HTTP 요청 진입점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트의 모든 HTTP 요청이 디스패처 서블릿으로 전달되며, 이를 통해 애플리케이션의 각 구성 요소(Controller, Service 등)로 적절히 연결된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연한 확장성 제공&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;디스패처 서블릿은 스프링 MVC의 핵심 인터페이스를 기반으로 설계되어, 개발자가 작성한 &lt;b&gt;Controller&lt;/b&gt;, &lt;b&gt;AOP&lt;/b&gt;, &lt;b&gt;Interceptor&lt;/b&gt; 등의 컴포넌트와 유연하게 연동할 수 있는 메커니즘을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 디스패처 서블릿의 동작 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQEr22/btsLvVzE6R4/dezTL5tjXquT0FgXbaONFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQEr22/btsLvVzE6R4/dezTL5tjXquT0FgXbaONFk/img.png&quot; data-alt=&quot;https://terasolunaorg.github.io/guideline/5.0.1.RELEASE/en/Overview/SpringMVCOverview.html#overview-of-spring-mvc-processing-sequence&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQEr22/btsLvVzE6R4/dezTL5tjXquT0FgXbaONFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQEr22%2FbtsLvVzE6R4%2FdezTL5tjXquT0FgXbaONFk%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;1280&quot; height=&quot;836&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://terasolunaorg.github.io/guideline/5.0.1.RELEASE/en/Overview/SpringMVCOverview.html#overview-of-spring-mvc-processing-sequence&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트 요청&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 HTTP 요청을 전송한다 (예: /hello)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DispatcherServlet이 요청 수신&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서블릿 컨테이너가 요청을 받아 DispatcherServlet에 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Handler Mapping&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 URL에 따라 어떤 컨트롤러가 요청을 처리할지 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Handler Adapter&lt;/b&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨트롤러 실행&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청을 처리한 후, 결과 데이터(모델)와 뷰 이름(View Name)을 DispatcherServlet에 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;View Resolver&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;뷰 이름을 기반으로 렌더링할 뷰를 결정한다. (예: JSP, Thymeleaf, JSON 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답 반환&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;렌더링된 뷰나 데이터(JSON, XML 등)를 클라이언트에 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;span data-token-index=&quot;1&quot;&gt;서블릿 컨테이너&lt;/span&gt;(Tomcat, Jetty)가 제공하는 스레드 풀(Thread Pool)과 멀티 스레드 환경을 활용한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;스프링의 요청 처리 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링의 요청 처리 구조는 기본적으로 &lt;b&gt;Thread-Per-Request&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서 서블릿 컨테이너가 제공하는 스레드 풀 관련 설정은 다음과 같이 할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1735032828663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  tomcat:
    threads:
      min-spare: 10
      max: 200
      max-threads: 200
      accept-count: 100
    connection-timeout: 20000   # 밀리초 단위
    keep-alive-timeout: 10000   # 밀리초 단위&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;min-spare&lt;/b&gt;: 서블릿 컨테이너가 유지할 최소 유휴 스레드 수이다. 이 값은 시스템 부하가 적을 때도 최소한의 스레드를 유지하도록 설정하는 데 사용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max&lt;/b&gt; : 서블릿 컨테이너에서 동시에 &lt;b&gt;허용할 수 있는 최대 커넥션 수&lt;/b&gt;를 설정한다. (최대 커넥션 수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max-threads&lt;/b&gt;: 서블릿 컨테이너가 처리할 수 있는 최대 스레드 수이다. 이 값이 초과하면 요청은 대기열에 대기하게 된다. (최대 스레드 수)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;accept-count&lt;/b&gt;: &lt;b&gt;대기 큐 크기&lt;/b&gt;로, 최대 스레드 수를 초과하는 요청이 들어왔을 때 대기열에서 처리할 수 있는 요청 수이다. 이 값이 초과하면 새로운 연결은 거부된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;connection-timeout&lt;/b&gt;: 클라이언트와의 연결이 시간이 초과되면 연결을 종료하는 시간 설정이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;keep-alive-timeout&lt;/b&gt;: 연결을 유지할 수 있는 최대 시간 설정이다. 이 시간 동안 클라이언트와의 연결을 재사용할 수 있다.&lt;/li&gt;
&lt;/ul&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;: HTTP 요청을 처리하는 데 사용된다. 요청이 들어올 때마다 서블릿 컨테이너는 이 풀에서 스레드를 할당하여 해당 요청을 처리한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비동기 스레드 풀&lt;/b&gt;: 비동기 작업을 처리하기 위한 별도의 스레드 풀이다. &lt;b&gt;@Async&lt;/b&gt; 어노테이션을 사용하거나 TaskExecutor를 통해 별도로 관리할 수 있다. 이 스레드 풀은 비동기 작업이 요청을 처리하는 주 스레드와는 다른 스레드에서 실행되도록 도와준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; &lt;span data-token-index=&quot;0&quot;&gt;스프링부트 애플리케이션 실행 흐름&lt;/span&gt; &lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1) 스프링 애플리케이션 컨텍스트 실행&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SpringApplication.run(MyApplication.class, args)가 호출되면, 스프링 부트 애플리케이션이 시작된다.&lt;/li&gt;
&lt;li&gt;run() 메서드는 애플리케이션 컨텍스트(ApplicationContext)를 생성하고, 애플리케이션을 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2) &lt;/b&gt;&lt;b&gt;애플리케이션 컨텍스트 초기화 및 &lt;/b&gt;&lt;b&gt;빈 등록 및 의존성 주입&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;스프링은 애플리케이션 컨텍스트 초기화 시 @ComponentScan을 통해 빈을 자동으로 검색한다&lt;/li&gt;
&lt;li&gt;기본적으로 &lt;b&gt;@SpringBootApplication&lt;/b&gt; 어노테이션이 있는 클래스가 위치한 패키지와 그 하위 패키지들에서 빈을 스캔한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Component, @Service, @Repository, @Controller&lt;/b&gt;와 같은 어노테이션을 가진 클래스들이 자동으로 빈으로 등록되고 초기화되고 빈 간의 &lt;b&gt;의존성 주입&lt;/b&gt;을 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3 ) 내장 서블릿 컨테이너 실행&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;b&gt;서블릿 컨테이너&lt;/b&gt;(예: &lt;b&gt;Tomcat&lt;/b&gt;, &lt;b&gt;Jetty&lt;/b&gt;, &lt;b&gt;Undertow&lt;/b&gt;)가 자동으로 초기화된다. 이는 외부 WAS를 설정하지 않고 애플리케이션이 독립적으로 실행될 수 있게 한다.&lt;/li&gt;
&lt;li&gt;또한 이 과정에서 &lt;b&gt;DispatcherServlet&lt;/b&gt;이 등록된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IT/Spring</category>
      <category>Dispatcher Servlet</category>
      <category>Servlet Container</category>
      <category>Spring Framework</category>
      <category>디스패처 서블릿</category>
      <category>서블릿 컨테이너</category>
      <category>스프링 프레임워크</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/285</guid>
      <comments>https://bamdule.tistory.com/285#entry285comment</comments>
      <pubDate>Tue, 24 Dec 2024 18:53:05 +0900</pubDate>
    </item>
    <item>
      <title>도메인 주도 설계에 대한 간단한 지식</title>
      <link>https://bamdule.tistory.com/284</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도메인 주도 설계 (Domain Driven Design)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;기존에는 프로젝트 설계 및 개발 시 도메인 전문가와 상의하지 않고 기능 구현에 초점을 두었다.&lt;br /&gt;그리고 고객의 요구사항은 개발 중간에 자주 변경되고, 개발이 완료돼도 결과물을 받은 고객들이 만족하지 못하는 경우가 많았다. &lt;br /&gt;이는 프로젝트의 실패로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;이러한 문제가 발생하는 이유는 프로젝트 이해관계자들이 도메인을 제대로 파악하지 못한채 기능 구현에 몰입했기 때문이다.&lt;br /&gt;이를 해결하기 위해 고안된 방법론이 도메인 주도 설계이다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;도메인 주도 설계는 도메인 전문가와 상의해서 비즈니스 영역을 파악하고 도메인 모델을 중심으로 설계하는 기법이다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;여기서 중요한 것은 도메인 주도 설계에 참가하는 이해관계자들이 모두 도메인 전문가 수준의 이해도가 바탕이 되어야하는 점이다.&lt;br /&gt;설계 단계에서 도메인 전문가가 참여하고 의사소통을 통해 개발자와 도메인 전문가가 모두 이해할 수 있는 도메인 모델을 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;초기에 도메인 모델을 잘 설계하려면 도메인 전문가와 이해관계자들이 모여서 이벤트 스토밍을 진행해야한다.&lt;br /&gt;이벤트 스토밍을 통해서 도메인, 어그리거트, 바운디드 컨텍스트, 도메인 간 협력 등 전략적 설계에 필요한 문서들이 도출된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도메인 (Domain)&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;도메인은 엔티티, 값 객체, 어그리거트, 레파지토리, 도메인 서비스, 팩토리, 명세로 나뉘어 질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;엔티티 (Entity)&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;시스템에서 고유한 식별자(ID)를 가지고 있으며, 그 식별자를 통해 동일성을 판단할 수 있는 객체다.&lt;/li&gt;
&lt;li&gt;상태와 동작을 모두 가질 수 있으며, 속성이 변하더라도 동일한 ID라면 같은 엔티티로 간주된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;값 객체(Value Object)&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;어그리거트(Aggregate)&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;루트 엔티티(Aggregate Root)를 통해서만 접근할 수 있고, 어그리거트 내부 상태는 루트 엔티티를 통해 관리된다.&lt;/li&gt;
&lt;li&gt;어그리거트 내에 엔티티 혹은 값 객체는 동일한 생명주기(생성, 삭제)를 가져야하며 모든 작업 (등록, 수정, 삭제, 조회)은 루트 어그리거트를 통해서 수행되어야 한다. (작업의 일관성)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;레포지토리 (Repository)&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;도메인 서비스(Domain Service)&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;팩토리(Factory)&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;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;명세 (Specification)&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;b&gt;복잡한 조건&lt;/b&gt;을 재사용 가능하고 테스트하기 쉬운 방식으로 표현한다.&lt;/li&gt;
&lt;li&gt;조건을 객체로 추상화하여 코드 중복을 줄이고, 가독성을 높일 수 있다.&lt;/li&gt;
&lt;li&gt;엔티티가 특정 조건을 만족하는지 확인할 때 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;도메인 모델 (Domain Model)&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;b&gt;도메인 모델링&lt;/b&gt;은 특정 도메인의 개념, 규칙, 관계, 그리고 동작을 추상화하여 &lt;b&gt;소프트웨어 설계에 반영하는 작업이다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>IT/DDD - 도메인 주도 개발</category>
      <category>DDD</category>
      <category>DOMAIN</category>
      <category>도메인</category>
      <category>도메인주도개발</category>
      <category>도메인주도설계</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/284</guid>
      <comments>https://bamdule.tistory.com/284#entry284comment</comments>
      <pubDate>Wed, 27 Nov 2024 15:54:26 +0900</pubDate>
    </item>
    <item>
      <title>AWS EC2 젠킨스 CI/CD를 구축하고 ECR 배포 파이프라인 생성하기</title>
      <link>https://bamdule.tistory.com/283</link>
      <description>&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;이번 게시글에서는 AWS EC2에 젠킨스를 설치하는 방법에 대해서 소개해보겠다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;1. Amazon EC2 생성&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Amazon Machine Image(AMI)&amp;nbsp;: Amazon Linux 2023 AMI&lt;br /&gt;(리눅스 버전에 따라서 젠킨스 설치가 달라지니 정확한 설치를 위해서 해당 AMI를 선택해야한다)&lt;br /&gt;&lt;br /&gt;2. 인스턴스 유형 : t3.medium (선택)&lt;br /&gt;3. 키페어 생성&lt;br /&gt;4. 보안 그룹 생성 : 8080 포트와 22포트를 열어두어야함&lt;br /&gt;5. 스토리지 8GB&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. EC2 SSH 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키페어 생성 위치로 이동한 다음 SSH를 통해서 EC2로 이동한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;cd {key-pair.pem 위치} &lt;br /&gt;예시 : ssh -i &quot;jenkins-key.pem&quot; ec2-user@54.180.202.88&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. EC2 jenkins 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1725947873453&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. Jenkins 설치 전에 시스템 패키지를 최신 상태로 갱신
$ sudo yum update -y

# 2. Jenkins 패키지 저장소를 시스템의 YUM 레포지토리 목록에 추가
$ sudo wget -O /etc/yum.repos.d/jenkins.repo \
    https://pkg.jenkins.io/redhat-stable/jenkins.repo

# 3. Jenkins-CI의 GPG 키 가져오기
$ sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
# rpm --import 명령은 패키지를 설치할 때 해당 패키지가 신뢰할 수 있는 소스에서 온 것임을 확인하기 위해 사용되는 GPG 키를 가져온다

# 4. YUM 패키지 업그레이드
$ sudo yum upgrade

# 5. Java 설치 (Amazon Linux 2023)
$ sudo dnf install java-17-amazon-corretto -y

# 6. Jenkins 설치
$ sudo yum install jenkins -y

# 7. Jenkins 서비스 자동 시작 설정
$ sudo systemctl enable jenkins

# 8. Jenkins 서비스 시작
$ sudo systemctl start jenkins

9. Jenkins 상태 확인
$ sudo systemctl status jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. EC2에 Git 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1725948182046&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 패키지 목록 업데이트
$ sudo dnf update -y
# Git 설치
$ sudo dnf install git -y
# 설치 확인
$ git --version&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. EC2에 Docker 설치&lt;/h3&gt;
&lt;pre id=&quot;code_1725948202674&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Docker 설치
$ sudo dnf install docker -y

# Docker 서비스 시작 및 자동 시작 설정
$ sudo systemctl start docker
$ sudo systemctl enable docker

# Docker 버전 확인
$ docker --version

# Jenkins 사용자를 docker 그룹에 추가
$ sudo usermod -aG docker jenkins

# Jenkins 재시작
$ sudo systemctl restart jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 젠킨스 잠금 해제 및 Get Started &lt;span data-token-index=&quot;1&quot;&gt;Customize Jenkins&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에 ec2 public ip, 포트 8080를 입력해서 젠킨스 서버에 접근한다&lt;br /&gt;ex) &lt;a href=&quot;http://xxx.xxxx.xxxx.xxxx:8080&quot;&gt;http://xxx.xxxx.xxxx.xxxx:8080&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1239&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRGvdD/btsJxXLwtam/2J8rx6sqVQv3nrx5h6PCKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRGvdD/btsJxXLwtam/2J8rx6sqVQv3nrx5h6PCKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRGvdD/btsJxXLwtam/2J8rx6sqVQv3nrx5h6PCKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRGvdD%2FbtsJxXLwtam%2F2J8rx6sqVQv3nrx5h6PCKK%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;611&quot; height=&quot;386&quot; data-origin-width=&quot;1239&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 경로의 initialAdminPassword 파일에 접근해서 비밀번호를 확인한 후 입력한다&lt;/p&gt;
&lt;pre id=&quot;code_1725948324275&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;1480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O9Oqv/btsJxmrvSLN/GBPaV4kb5dSiWGB7NCUnJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O9Oqv/btsJxmrvSLN/GBPaV4kb5dSiWGB7NCUnJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O9Oqv/btsJxmrvSLN/GBPaV4kb5dSiWGB7NCUnJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO9Oqv%2FbtsJxmrvSLN%2FGBPaV4kb5dSiWGB7NCUnJK%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;589&quot; height=&quot;586&quot; data-origin-width=&quot;1488&quot; data-origin-height=&quot;1480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Install suggeseted plugins를 선택한다&lt;/li&gt;
&lt;li&gt;유용한 젠킨스 플러그인을 설치해준다.&lt;/li&gt;
&lt;li&gt;어드민 계정 정보를 입력하고 젠킨스 접근 URL을 입력한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 젠킨스 플러그인 설치&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Jenkins 관리 &amp;gt; Plugins &amp;gt; Available plugins
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Git Parameter (선택) : 배포 전 git branch 또는 Tag를 선택할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;AWS&amp;nbsp;Credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. Credentials 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;github과 aws ecr credentials를 생성해야한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) github credentials 생성&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Github Personal access tokens 생성&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;Github 사이트로 이동해서 토큰을 생성한다&lt;/li&gt;
&lt;li&gt;Settings / Developer Settings / Personal access tokens (classic) / token 생성&lt;/li&gt;
&lt;li&gt;github에서 repository 관련 권한을 허용한 상태로 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Github Credentials 등록&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;jenkins 관리 &amp;gt; Credentials &amp;gt; System &amp;gt; Glabal credentials &amp;gt; Add Credentials 으로 이동&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;2713&quot; data-origin-height=&quot;1307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWdG1/btsJysdp99C/uaBbsnkjXaluHy6ID2s2a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWdG1/btsJysdp99C/uaBbsnkjXaluHy6ID2s2a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWdG1/btsJysdp99C/uaBbsnkjXaluHy6ID2s2a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWdG1%2FbtsJysdp99C%2FuaBbsnkjXaluHy6ID2s2a0%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;708&quot; height=&quot;341&quot; data-origin-width=&quot;2713&quot; data-origin-height=&quot;1307&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Username with password Kind 선택&lt;/li&gt;
&lt;li&gt;Scope는 Global로 선택&lt;/li&gt;
&lt;li&gt;username에 github username을 입력한다&lt;/li&gt;
&lt;li&gt;password에 github에서 생성한 토큰을 입력한다&lt;/li&gt;
&lt;li&gt;ID는 파이프라인에서 참조할 수 있는 값이므로 의미 있는 이름으로 입력한다 (ex : github-credentials)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) AWS ECR 배포 credentials 등록&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;IAM 정책&lt;/b&gt;&lt;/span&gt;, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;IAM 사용자&lt;/b&gt;&lt;/span&gt;를 미리 만들어야한다&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;1515&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cWGMoe/btsJx7tRn7o/4sWpbonYJmfEKtZnISnlMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cWGMoe/btsJx7tRn7o/4sWpbonYJmfEKtZnISnlMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cWGMoe/btsJx7tRn7o/4sWpbonYJmfEKtZnISnlMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWGMoe%2FbtsJx7tRn7o%2F4sWpbonYJmfEKtZnISnlMk%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;811&quot; height=&quot;167&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IAM 정책 생성&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/image-push-iam.html&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 링크에서 모든 리포지토리로 푸시하는 데 필요한 권한 JSON을 복사한다&lt;/li&gt;
&lt;li&gt;정책 생성으로 이동 후 JSON 값을 입력한다&amp;nbsp;&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;2688&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMkW3Z/btsJwZciEjB/fMEXgozbTcO9w4kIPZUt40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMkW3Z/btsJwZciEjB/fMEXgozbTcO9w4kIPZUt40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMkW3Z/btsJwZciEjB/fMEXgozbTcO9w4kIPZUt40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMkW3Z%2FbtsJwZciEjB%2FfMEXgozbTcO9w4kIPZUt40%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;2688&quot; height=&quot;1310&quot; data-origin-width=&quot;2688&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;아래 정책 JSON 참조&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1725949348977&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;ecr:CompleteLayerUpload&quot;,
                &quot;ecr:GetAuthorizationToken&quot;,
                &quot;ecr:UploadLayerPart&quot;,
                &quot;ecr:InitiateLayerUpload&quot;,
                &quot;ecr:BatchCheckLayerAvailability&quot;,
                &quot;ecr:PutImage&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bio5zH/btsJyioqfXf/5kAqXtH1w74KnGGg36kHUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bio5zH/btsJyioqfXf/5kAqXtH1w74KnGGg36kHUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bio5zH/btsJyioqfXf/5kAqXtH1w74KnGGg36kHUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbio5zH%2FbtsJyioqfXf%2F5kAqXtH1w74KnGGg36kHUk%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;2716&quot; height=&quot;1399&quot; data-origin-width=&quot;2716&quot; data-origin-height=&quot;1399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IAM 사용자 생성&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;2329&quot; data-origin-height=&quot;603&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/df98Lv/btsJy1NOCyR/q2e6cDTV51vAyNyzzv2jT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/df98Lv/btsJy1NOCyR/q2e6cDTV51vAyNyzzv2jT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/df98Lv/btsJy1NOCyR/q2e6cDTV51vAyNyzzv2jT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdf98Lv%2FbtsJy1NOCyR%2Fq2e6cDTV51vAyNyzzv2jT1%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;2329&quot; height=&quot;603&quot; data-origin-width=&quot;2329&quot; data-origin-height=&quot;603&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 정책 연결을 선택하고 미리 만들어둔 정책을 연결한다&amp;nbsp;&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;2355&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5bD1H/btsJzhv7A4J/J0X4yPHKQWjJ9cCyNWLtw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5bD1H/btsJzhv7A4J/J0X4yPHKQWjJ9cCyNWLtw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5bD1H/btsJzhv7A4J/J0X4yPHKQWjJ9cCyNWLtw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5bD1H%2FbtsJzhv7A4J%2FJ0X4yPHKQWjJ9cCyNWLtw1%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;2355&quot; height=&quot;925&quot; data-origin-width=&quot;2355&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;/figure&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS ECR&amp;nbsp; Deploy Credentials 등록&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;jenkins 관리 &amp;gt; Credentials &amp;gt; System &amp;gt; Glabal credentials &amp;gt; Add Credentials 으로 이동&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;2763&quot; data-origin-height=&quot;1375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rVXCD/btsJx2Ungtf/ER8wlS386zj6nvgVMn1klK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rVXCD/btsJx2Ungtf/ER8wlS386zj6nvgVMn1klK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rVXCD/btsJx2Ungtf/ER8wlS386zj6nvgVMn1klK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrVXCD%2FbtsJx2Ungtf%2FER8wlS386zj6nvgVMn1klK%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;2763&quot; height=&quot;1375&quot; data-origin-width=&quot;2763&quot; data-origin-height=&quot;1375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kind를 AWS Credentials로 선택한다&lt;/li&gt;
&lt;li&gt;Scope는 Glabal&lt;/li&gt;
&lt;li&gt;ID는 파이프라인에서 참조하기 위해 명확하게 지어준다.&lt;/li&gt;
&lt;li&gt;IAM 사용자의 액세스키인 Access Key ID와 Secret Access Key를 입력한다.&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;2438&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7h41l/btsJx5pZHCS/zD5n5MvSqqjVlcFxYrHsiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7h41l/btsJx5pZHCS/zD5n5MvSqqjVlcFxYrHsiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7h41l/btsJx5pZHCS/zD5n5MvSqqjVlcFxYrHsiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7h41l%2FbtsJx5pZHCS%2FzD5n5MvSqqjVlcFxYrHsiK%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;2438&quot; height=&quot;393&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이 두개의 Credentials가 생성되었는지 확인한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. AWS ECR 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon ECR &amp;gt; 프라이빗 레지스트리 &amp;gt; 리포지토리 &amp;gt; 리포지토리 생성으로 이동한다&lt;/li&gt;
&lt;li&gt;ECR Repository를 생성했으면 URI를 복사해둔다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파이프라인 작성 시 필요하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;10. 젠킨스 파이프라인 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 아이템 생성을 클릭하고 아이템 이름을 입력하고 파이프라인을 선택한 다음 OK를 누른다&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;2394&quot; data-origin-height=&quot;958&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqEFqp/btsJwUJKK3x/LqETfOnCmKtdYsULtGSn2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqEFqp/btsJwUJKK3x/LqETfOnCmKtdYsULtGSn2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqEFqp/btsJwUJKK3x/LqETfOnCmKtdYsULtGSn2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqEFqp%2FbtsJwUJKK3x%2FLqETfOnCmKtdYsULtGSn2K%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;2394&quot; height=&quot;958&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;958&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VKVFz/btsJyV1fYbT/BbPu7Bj3tyuxkbnAnaR4k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VKVFz/btsJyV1fYbT/BbPu7Bj3tyuxkbnAnaR4k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VKVFz/btsJyV1fYbT/BbPu7Bj3tyuxkbnAnaR4k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVKVFz%2FbtsJyV1fYbT%2FBbPu7Bj3tyuxkbnAnaR4k1%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;1510&quot; height=&quot;1207&quot; data-origin-width=&quot;1510&quot; data-origin-height=&quot;1207&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;파이프 라인을 입력한다&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;1974&quot; data-origin-height=&quot;961&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qwTBf/btsJxyM1PPL/s6ukd8SEdCbsZX2LMiEdok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qwTBf/btsJxyM1PPL/s6ukd8SEdCbsZX2LMiEdok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qwTBf/btsJxyM1PPL/s6ukd8SEdCbsZX2LMiEdok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqwTBf%2FbtsJxyM1PPL%2Fs6ukd8SEdCbsZX2LMiEdok%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;1974&quot; height=&quot;961&quot; data-origin-width=&quot;1974&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;파이프라인&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1726029651822&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pipeline {
    agent any
    options {
        timeout(time: 1, unit: 'HOURS')
    }

   environment {
        TIME_ZONE = 'Asia/Seoul'
        
        // GitHub
        GIT_TARGET_BRANCH = '배포할 브랜치 명'
        GIT_REPOSITORY_URL = 'Github repository URL'
        GIT_CREDENTIONALS_ID = 'Jenkins Github Credentials Id'


        // AWS ECR
        AWS_ECR_CREDENTIAL_ID = 'Jenkins AWS ECR Credentials Id'
        AWS_ECR_URI = 'ECR URI'
        AWS_ECR_IMAGE_NAME = 'ECR Image Name'
        AWS_REGION = 'ECR Region'
        
    }

    stages {
        stage('init') {
            steps {
                echo 'init stage'
                deleteDir()
            }
        }
        
        stage('Cloning Repository') {
            steps {
                echo 'Cloning Repository'
                git branch: &quot;${GIT_TARGET_BRANCH}&quot;,
                    credentialsId: &quot;${GIT_CREDENTIONALS_ID}&quot;,
                    url: &quot;${GIT_REPOSITORY_URL}&quot;
            }
        }
        
        stage('Build Gradle') {
            steps {
                echo 'Build Gradle'
                dir('.') {
                    sh '''
                        pwd
                        chmod +x ./gradlew
                        ./gradlew build --exclude-task test
                    '''
                }
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    sh '''
                        docker build -t ${AWS_ECR_IMAGE_NAME} .
                        docker tag ${AWS_ECR_IMAGE_NAME} ${AWS_ECR_URI}/${AWS_ECR_IMAGE_NAME}:${BUILD_NUMBER}
                    '''
                }
            }
        }
        
        stage('Push to ECR') {
            steps {
              withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: &quot;${AWS_ECR_CREDENTIAL_ID}&quot;]]) {
                    script {
                        sh '''
                        aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ECR_URI}
                        docker push ${AWS_ECR_URI}/${AWS_ECR_IMAGE_NAME}:${BUILD_NUMBER}
                        
                        '''
                    }
                }
            }
        }
        
        stage('Clean Up Docker Images on Jenkins Server') {
            steps {
                echo 'Cleaning up unused Docker images on Jenkins server'
                sh &quot;docker image prune -f --all&quot;
            }
        }
    }

    post {
        success {
            echo 'Pipeline succeeded'
        }
        failure {
            echo 'Pipeline failed'
        }
    }
}&lt;/code&gt;&lt;/pre&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;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;init&amp;nbsp;&lt;/b&gt;&lt;br /&gt;이전에 작업했던 내용들을 모두 초기화 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cloning Repository&lt;/b&gt;&lt;br /&gt;Github에서 Repository를 Clone 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Build Gradle&lt;br /&gt;&lt;/b&gt;소스 코드를 Gradle Build 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Build Docker Image&lt;br /&gt;&lt;/b&gt;Docker Image Build 한다 (Dockerfile이 존재해야함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Push to ECR&lt;br /&gt;&lt;/b&gt;Docker Image를 AWS ECR로 Push한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Clean Up Docker Images on Jenkins Server&lt;br /&gt;&lt;/b&gt;Build한 Docker Image를 지운다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 environment에 정의된 변수 값을 정확하게 입력해주어야 한다.&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;1817&quot; data-origin-height=&quot;952&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clZ4m8/btsJzsRTqoi/0wDOqKsk4hHWVDYyPFtkk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clZ4m8/btsJzsRTqoi/0wDOqKsk4hHWVDYyPFtkk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clZ4m8/btsJzsRTqoi/0wDOqKsk4hHWVDYyPFtkk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclZ4m8%2FbtsJzsRTqoi%2F0wDOqKsk4hHWVDYyPFtkk1%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;1817&quot; height=&quot;952&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;952&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추천하는 방법은 파이프라인의 Stage를 하나씩 입력해서 한번씩 빌드해보면서 성공여부를 확인하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;11. 젠킨스 시간 설정 (선택)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;젠킨스 시간은 기본적으로 UTC로 설정되어 있다. 한국 시간으로 설정하는 방법을 알아보자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726030644975&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 현재 타임존 확인
$ timedatectl

# Asia/Seoul로 타임존을 변경한다
$ sudo timedatectl set-timezone Asia/Seoul

# 변경되었는지 확인
$ timedatectl

# 젠킨스 재시작
$ sudo systemctl restart jenkins&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;젠킨스 관리 &amp;gt; System Information으로 이동하여 타임존이 Asia/Seoul으로 변경되었는지 확인한다&amp;nbsp;&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;980&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cousYI/btsJyKFMmSa/i9rnrZWZv9Yjpk4VZKEv60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cousYI/btsJyKFMmSa/i9rnrZWZv9Yjpk4VZKEv60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cousYI/btsJyKFMmSa/i9rnrZWZv9Yjpk4VZKEv60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcousYI%2FbtsJyKFMmSa%2Fi9rnrZWZv9Yjpk4VZKEv60%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;595&quot; height=&quot;236&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;12. 파이프라인 Gradle Build 시 전처리 작업 수행하기 (선택)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Build 하기 전 application.yml 이나 다른 설정 파일을 소스코드에 복사하는 경우가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726030971596&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    ....
        stage('Gradle Build before process') {
            steps {
                echo 'Gradle Build before process'
                script {
                    sh '''
                    mkdir -p ${WORKSPACE}/src/main/resources
                    sudo cp ${APPLICATION_YML} ${WORKSPACE}/src/main/resources/application.yml
                    '''
                }
            }
        }      
    ....&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위와 같이 Gradle Build 전 Stage를 추가한다.&lt;/li&gt;
&lt;li&gt;${APPLICATION_YML}는 파일이 위치한 전체 경로이고, ${WORKSPACE}는 소스코드 경로이다.&lt;/li&gt;
&lt;li&gt;${APPLICATION_YML}는 직접 environment에 추가해주어야한다&amp;nbsp; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;젠킨스가 sudo 명령어를 실행하기 위해서 권한을 추가한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726031113956&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# sudo vi /etc/sudoers

적당한 곳에 아래 텍스트 추가
jenkins ALL=(ALL) NOPASSWD: ALL
읽기 전용 파일이므로 wq! 로 강제저장하고 종료&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;13. 원하는 Git Branch를 선택해서 배포하기 (선택)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Git Parameter 플러그인을 설치하고 파이프라인으로 이동한다&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;1633&quot; data-origin-height=&quot;1411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3mhzG/btsJxysOMcM/d19V39hUOwXKJfiKE0nWpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3mhzG/btsJxysOMcM/d19V39hUOwXKJfiKE0nWpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3mhzG/btsJxysOMcM/d19V39hUOwXKJfiKE0nWpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3mhzG%2FbtsJxysOMcM%2Fd19V39hUOwXKJfiKE0nWpK%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;1633&quot; height=&quot;1411&quot; data-origin-width=&quot;1633&quot; data-origin-height=&quot;1411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;빌드 매개변수를 활성화하고 Git Parameter를 선택한다.&lt;/li&gt;
&lt;li&gt;Name에 브랜치 명에 접근할 수 있는 변수명을 입력한다 (중요)&lt;/li&gt;
&lt;li&gt;Branch Filter는 정확히 브랜치 명만 가져올 수 있도록 필터링을 설정한다 &lt;b&gt;origin/(.*)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;밑줄 친 속성 이외에도 원하는 값을 입력하면 된다.&lt;/li&gt;
&lt;li&gt;설정이 완료되었다면 파이프라인을 수정한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1726032121904&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   environment {
        ...
        GIT_TARGET_BRANCH = &quot;${GIT_BRANCH_PARAMETER}&quot;
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;GIT_TARGET_BRANCH에 직접 브랜치 명을 입력했던 것을 GIT_Parameter name에 입력했던 값으로 변경한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;브랜치 목록은 한번 배포한 다음에 적용되는 것으로 보인다. default 브랜치로 한번 배포한 다음 브랜치 목록이 잘 노출되는지 확인하자&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;1119&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LcuiI/btsJyLSmCPW/6OnupL3T2ZvM42Eb9tqBN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LcuiI/btsJyLSmCPW/6OnupL3T2ZvM42Eb9tqBN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LcuiI/btsJyLSmCPW/6OnupL3T2ZvM42Eb9tqBN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLcuiI%2FbtsJyLSmCPW%2F6OnupL3T2ZvM42Eb9tqBN1%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;1119&quot; height=&quot;607&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위와 같이 &lt;b&gt;지금 빌드&lt;/b&gt; 버튼이 &lt;b&gt;파라미터와 함께 빌드&lt;/b&gt;로 변경된 것을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;브랜치를 선택한 다음 빌드하면 원하는 브랜치로 빌드가 되는 것을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;마지막으로 AWS ECR에 Push 되었는지 확인해보자&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;1621&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG8rN4/btsJxgzfvRy/5kK6IXZJGvftJqvrKGBnwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG8rN4/btsJxgzfvRy/5kK6IXZJGvftJqvrKGBnwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG8rN4/btsJxgzfvRy/5kK6IXZJGvftJqvrKGBnwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG8rN4%2FbtsJxgzfvRy%2F5kK6IXZJGvftJqvrKGBnwk%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;1621&quot; height=&quot;456&quot; data-origin-width=&quot;1621&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다음 글은 ECR Push 후 ECS에 배포하는 작업을 포스팅해 볼 예정입니다.&lt;/li&gt;
&lt;li&gt;도움이 되었다면 공감 한번씩 눌러주세요!&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>aws ecr</category>
      <category>ci/cd</category>
      <category>jenkins</category>
      <category>젠킨스</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/283</guid>
      <comments>https://bamdule.tistory.com/283#entry283comment</comments>
      <pubDate>Wed, 11 Sep 2024 14:28:17 +0900</pubDate>
    </item>
    <item>
      <title>[JDK] 가비지 컬렉션(garbage collection)에 대한 기초적인 지식</title>
      <link>https://bamdule.tistory.com/282</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GC(Garbage Collection, 가비지 컬렉션)는 JVM(Java Virtual Machine)에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;메모리 관리를 자동화하는 메커니즘&lt;/b&gt;&lt;/span&gt;으로, 더 이상 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;참조되지 않는 객체를 자동으로 식별하고 메모리에서 제거하여 메모리 누수를 방지&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&lt;br /&gt;JDK의 버전이 올라가면서 GC(Garbage Collection) 방식은 지속적으로 개선되어 왔다. 새로운 GC 알고리즘이 도입되고 기존의 GC 알고리즘이 최적화되었지만, 기본적인 Major GC와 Minor GC의 매커니즘 자체는 근본적으로 변경되지는 않은 듯 하다.&lt;br /&gt;&lt;br /&gt;GC에 대한 기초적인 내용과 Major GC와 Minor GC 매커니즘에 대해서 공부해보자&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC를 알아보기 전에 JVM 메모리 구조에 대해서 간단하게 알아보자&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;1. JVM (Java Virtual Machine) 메모리 구조&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;JVM 메모리는 크게 &lt;b&gt;메소드 영역&lt;/b&gt;, &lt;b&gt;힙&lt;/b&gt;, &lt;b&gt;스택&lt;/b&gt;,&lt;b&gt; PC Registers&lt;/b&gt;, &lt;b&gt;네이티브 메소드 스택 영역&lt;/b&gt;으로 나누어진다&amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;메소드 영역 (Method Area)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메소드 영역은 클래스 구조(메타데이터), 메소드, 필드 정보, 메소드의 바이트코드, 그리고 &lt;b&gt;상수 풀(Constant Pool)&lt;/b&gt; 등을 저장하는 영역이다. 이 영역은 클래스 로딩 시 필요한 메모리를 할당하며, JVM 시작 시 생성되어 프로그램이 종료될 때까지 유지된다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;힙 (Heap)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 영역은 자바 애플리케이션에서 런타임 시 동적으로 생성된 객체들이 주로 저장되는 메모리 영역이다. new 키워드로 생성된 인스턴스나 배열의 객체가 저장된다. 대부분의 GC는 해당 영역에서 발생한다&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;스택 (Stack)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택 영역은 각 스레드마다 생성되는 메모리 공간으로, 메소드 호출 시 생성되는 프레임(프레임에는 메소드의 매개변수, 로컬 변수, 중간 계산 결과 등이 저장됨)을 저장한다. 스택은 메소드 호출이 끝나면 프레임이 제거되며, LIFO(Last In, First Out) 방식으로 작동한다. 스택 영역은 스레드별로 독립적이며, 주로 메소드 호출과 관련된 임시 데이터를 저장한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt; PC&amp;nbsp; 레지스터 &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PC 레지스터는 JVM이 현재 실행 중인 명령어의 주소를 저장하는 작은 메모리 공간이다. 각 스레드는 자신만의 PC 레지스터를 가지고 있으며, 스레드가 어느 명령어를 실행 중인지 추적하는 데 사용된다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt; 네이티브&amp;nbsp;메소드&amp;nbsp;스택&amp;nbsp;(Native&amp;nbsp;Method&amp;nbsp;Stacks) &lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바&amp;nbsp;외&amp;nbsp;언어로&amp;nbsp;작성된&amp;nbsp;네이티브&amp;nbsp;코드를&amp;nbsp;위한&amp;nbsp;메모리&amp;nbsp;영역이다&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;클래스 로더 (Class Loader)&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클래스 로더&lt;/b&gt;는 자바 클래스 파일을 JVM으로 로딩하고, JVM이 실행할 수 있도록 준비하는 역할을 담당한다. 자바는 런타임 시에 필요한 클래스를 동적으로 로딩하며, 클래스 로더는 이 과정을 처리하는 컴포넌트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 로더의 주요 기능은 다음과 같다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;클래스 로딩&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스 파일(.class)을 JVM 메모리 구조 중 메소드 영역(Method Area)으로 로드한다. 여기서 클래스의 구조, 메소드 정보, 필드 정보, 상수 풀 등이 JVM의 메모리 공간에 저장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt; 클래스 링크&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로드된 클래스 파일을 JVM의 메모리 구조에 맞게 링크하는 과정이다. 링크 단계는 세 가지 하위 단계로 나뉜다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검증&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스 파일이 JVM의 명세에 맞는지 검증한다. 이 과정에서 클래스 파일의 포맷, 바이트코드의 유효성 등이 확인된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;준비&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스의 정적 필드를 메모리에 할당하고 기본값으로 초기화한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분석&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상수 풀(Constant Pool) 내의 심볼릭 레퍼런스를 실제 메모리 주소로 변경합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클래스 초기화&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스 로딩의 마지막 단계로, 정적 초기화 블록(static block)과 정적 필드를 초기화하는 단계이다.&lt;/li&gt;
&lt;li&gt;이 단계에서 클래스가 실제로 실행될 준비가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;Execution Engine&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Execution Engine&lt;/b&gt;은 JVM에서 로드된 클래스 파일의 바이트코드를 실제로 실행하는 컴포넌트이다. &lt;b&gt;Execution Engine&lt;/b&gt; 은 자바 바이트코드를 기계어로 변환하여 CPU에서 실행할 수 있도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;바이트 코드 해석 시 &lt;b&gt;인터프리터,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;JIT 컴파일러&lt;/b&gt;를 사용하며 각 해석기에 대한 내용은 다음과 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인터프리터 (Interpreter)&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;JIT 컴파일러 (Just-In-Time Compiler)&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;애플리케이션이 시작될 때, 인터프리터가 바이트코드를 즉시 실행하여 초기 실행을 처리하고, JIT 컴파일러는 애플리케이션이 실행되는 동안, 자주 실행되는 코드(핫스팟 코드)를 감지하고 이를 기계어로 컴파일하여 성능을 향상시킨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 로더는 자바 클래스 파일을 메모리에 로드하고, 이를 Execution Engine이 이해할 수 있는 형태로 준비한다. Execution Engine은 Class Loader에 의해 메모리에 로드된 클래스의 바이트코드를 실제로 실행하여 프로그램의 동작을 구현한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dqUzJP/btsJeNXcwip/BwokFiMUj02SfCkFxqyttK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dqUzJP/btsJeNXcwip/BwokFiMUj02SfCkFxqyttK/img.png&quot; data-alt=&quot;JDK 구조 (위키백과 참고)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dqUzJP/btsJeNXcwip/BwokFiMUj02SfCkFxqyttK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdqUzJP%2FbtsJeNXcwip%2FBwokFiMUj02SfCkFxqyttK%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;1152&quot; height=&quot;698&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JDK 구조 (위키백과 참고)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;2. Heap 영역의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Young Generation과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Old Generation&lt;/b&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Heap 영역은 객체가 할당되는 메모리 영역으로, GC의 주요 대상이다&lt;br /&gt;힙은 크게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Young Generation&lt;/b&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Old Generation&lt;/b&gt;으로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Young Generation&lt;/b&gt;: 새로 생성된 객체가 할당되는 영역이다. 이 영역은 다시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Eden&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Survivor1&lt;/b&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Survivor2&lt;/b&gt;로 나뉜다&lt;br /&gt;대부분의 객체가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Young Generation&lt;/b&gt;에서 생성되고 참조되지 않으면 제거된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Young Generation&lt;/b&gt;에 대한 가비지 컬랙션을 Minor GC라고 부른다&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Eden Space&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 새롭게 생성된 객체가 최초 할당되는 영역이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Survivor Spaces&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: 두 개의 서바이버 영역(S1, S2)이 있으며, 에덴 영역에서 살아남은 객체가 이 영역으로 이동한다. 이 영역은 번갈아 가며 사용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Old Generation&lt;/b&gt;: Young Generation에서 살아남은 객체가 옮겨지는 영역으로, 오래된 객체가 저장된다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Young Generation에서 오래 살아남은 객체가 이동하는 영역이다. 이 영역에 있는 객체들은 수명이 길거나 프로그램이 종료될 때까지 남아 있는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Old Generation&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 대한 가비지 컬랙션을 Major GC라고 부른다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Permanent Generation&lt;/b&gt;: 클래스 메타데이터와 같은 JVM의 메타데이터를 저장하는 영역이다. JDK 8부터 Metaspace로 대체되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;3. GC의 기본 동작 방식&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;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;객체가 생성될 때 JVM은 힙 메모리의 Young Generation 내의 Eden 영역에 객체를 할당한다&lt;/li&gt;
&lt;li&gt;Eden 영역이 가득 차면 Minor GC가 트리거된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 마킹 (Marking)&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;GC는 힙 메모리에서 여전히 사용 중인 객체와 그렇지 않은 객체를 식별한다.&lt;/li&gt;
&lt;li&gt;이 과정을 &quot;마킹&quot;이라고 하며, 살아있는 객체는 마킹된다.&lt;/li&gt;
&lt;li&gt;참조되지 않는 객체는 마킹되지 않으며, 삭제 대상이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 객체 이동 (Minor GC)&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;살아남은 객체는 Eden 영역에서 Survivor 영역(S1 또는 S2)으로 이동한다.&lt;/li&gt;
&lt;li&gt;여러 번의 GC를 통해 Survivor 영역에서 살아남은 객체는 Old Generation으로 승격된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 객체 삭제 및 메모리 해제&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;Minor GC는 주로 Young Generation에서 동작하며, Major GC는 Old Generation에서 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5) 메모리 압축 (Compaction)&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;객체 삭제 후 남은 메모리에 단편화가 발생할 수 있다. 이 경우 GC는 메모리를 압축하여 단편화를 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;메모리 압축은 주로 Major GC나 Full GC의 일부로 수행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메모리 단편화란?&lt;br /&gt;메모리 할당과 해제가 반복되면서, 연속된 메모리 블록 사이에 작은 빈 공간이 생겨나는 경우 총 메모리 공간은 충분하지만 연속적인 큰 메모리 블록을 할당할 수 없는 상황을 의미한다.&lt;br /&gt;&lt;br /&gt;이를 해결하기 위해서 사용 중인 메모리를 힙의 한쪽으로 몰아 빈 메모리 블록을 연속된 공간으로 만드는 작업을 수행하는데 이 작업을 메모리 압축이라고 한다&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;4. GC의 주요 유형&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) Minor GC&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;Young Generation&lt;/b&gt;에서 발생한다&lt;/li&gt;
&lt;li&gt;Eden 영역에서 생성된 대부분의 객체는 수명이 짧기 때문에 빠르게 제거된다.&lt;/li&gt;
&lt;li&gt;살아남은 객체는 Survivor 영역으로 이동하며, 일정 횟수의 GC를 거친 객체는 Old Generation으로 승격된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) Major GC&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;Old Generation&lt;/b&gt;에서 발생합니다.&lt;/li&gt;
&lt;li&gt;수명이 긴 객체들을 관리하며, Minor GC보다 더 많은 시간이 소요된다.&lt;/li&gt;
&lt;li&gt;Major GC는 힙 메모리의 많은 부분을 정리하며, 성능에 큰 영향을 미칠 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) Full GC&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;전체 힙 메모리(Young + Old Generation)를 대상으로 수행된다.&lt;/li&gt;
&lt;li&gt;모든 영역을 동시에 청소하므로, 일시 중지 시간이 길어진다.&lt;/li&gt;
&lt;li&gt;Full GC가 자주 발생하면 애플리케이션 성능이 크게 저하될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cS0Nzp/btsJfOnnrNN/lXVQkY6sQcZxXCkrBfa8k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cS0Nzp/btsJfOnnrNN/lXVQkY6sQcZxXCkrBfa8k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cS0Nzp/btsJfOnnrNN/lXVQkY6sQcZxXCkrBfa8k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcS0Nzp%2FbtsJfOnnrNN%2FlXVQkY6sQcZxXCkrBfa8k1%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;650&quot; height=&quot;342&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기존의 Major GC와 Minor GC 개념은 새로운 GC 알고리즘에서도 유지되고 있다. 다만, 새로운 GC 알고리즘들은 이들 GC 과정의 효율성을 높이기 위해 다양한 최적화를 적용했다.&lt;br /&gt;&lt;br /&gt;Minor GC는 여전히 Young Generation에서 주로 발생하며, 새로운 객체가 메모리에 할당될 때 주기적으로 발생한다. G1 GC, ZGC, Shenandoah GC 모두 Minor GC 과정을 최적화하여 더 짧은 지연 시간과 효율적인 메모리 관리를 제공한다.&lt;br /&gt;&lt;br /&gt;Major GC도 Old Generation의 메모리를 정리하는 과정이지만, 새로운 GC 알고리즘은 이 과정을 병행으로 수행하거나, 더 효율적인 메모리 회수 전략을 사용하여 중단 시간을 최소화하고 있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;JDK 버전이 올라가면서 GC 알고리즘은 크게 개선되었으나 기본적인 Major GC와 Minor GC 매커니즘은 유지되고 있으며, 이를 더 효율적으로 수행하기 위해 다양한 기술이 적용되고 있다. 개발자는 애플리케이션의 요구사항에 따라 적절한 GC 알고리즘을 선택하고, JVM 튜닝을 통해 성능을 최적화할 수 있다.&lt;/blockquote&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>garbage collection</category>
      <category>gc</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/282</guid>
      <comments>https://bamdule.tistory.com/282#entry282comment</comments>
      <pubDate>Sat, 24 Aug 2024 18:49:33 +0900</pubDate>
    </item>
    <item>
      <title>[JPA] 엔티티 상속 전략 활용 예제</title>
      <link>https://bamdule.tistory.com/281</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;서비스의 데이터 구조 설계 시 JPA 엔티티 상속 전략을 활용해서 확장성과 다형성 등 객체지향의 이점을 활용할 수 있다.&lt;br /&gt;예를 들어서 상위 타입인 쿠폰 엔티티가 있고, 하위 타입인 퍼센트 할인 엔티티, 고정 할인 엔티티가 있다고 가정했을 때, 쿠폰 적용 로직에 대해서 다형성 전략을 활용해 구현할 수 있다.&lt;br /&gt;&lt;br /&gt;이 글에 정리된 예시는 JPA 엔티티 상속 전략 활용 방법 및 추상 클래스를 통한 다형성 활용 방법에 대한 내용이다.&lt;br /&gt;우선 JPA 상속 전략의 종류에 대해서 알아보자&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;1. JPA 상속 전략 종류&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;JPA는 &lt;b&gt;JOINED&lt;/b&gt;, &lt;b&gt;SINGLE_TABLE&lt;/b&gt;, &lt;b&gt;TABLE_PER_CLASS&lt;/b&gt;의 세 가지 상속 전략을 지원하며, 각각의 전략은 데이터베이스에 서로 다른 방식으로 테이블을 구성한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; JOINED&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상속 구조를 구성하기 위해 공통 속성을 저장하는 상위 테이블과 개별 속성을 저장하는 하위 테이블로 구성된다.&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 정규화, 필요한 데이터만 조회 가능, 확장 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 저장 시, 상위 테이블과 하위 테이블에 데이터를 저장하며, 데이터 조회 시 하위 테이블의 개수 만큼 left join을 해서 쿼리 성능이 저하될 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SINGLE_TABLE&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 데이터가 하나의 테이블에 저장된다.&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿼리가 단순하고 성능이 좋다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;테이블에 많은 열이 생기고, 불필요한 컬럼이 다수 존재할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;데이터 응집도가 떨어짐&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TABLE_PER_CLASS&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 엔티티 클래스가 독립적인 테이블로 생성되며, 상속 구조를 반영하지 않는다.&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;간단한 쿼리, 테이블 간의 의존성 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;동일한 컬럼을 여러 테이블에 중복 저장, 다형성 쿼리에서 성능 저하.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속 전략에 따라서 테이블의 구조가 변경되며, 실제 비즈니스 로직에 영향은 없다.&amp;nbsp;&lt;br /&gt;상황에 맞는 전략을 활용해보면 좋을 것 같다. 예제는 JOINED 전략을 활용해보도록 하자&lt;br /&gt;개인적으로 실무에서는&amp;nbsp; &lt;b&gt;JOINED&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;SINGLE_TABLE &lt;/b&gt;두 전략을 활용할 수 있을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOINED를 활용하면 정규화 측면에서 필요한 데이터만 테이블에 저장되기 때문에 테이블 가독성이 및 데이터 응집도가 올라가지만 테이블 종류가 많아지면 조회 성능이 낮아진다는 단점이 있다. SINGLE_TABLE&lt;span&gt;은 한개 테이블만 활용하기 때문에 조회 속도 및 테이블 관리에 용이하지만 서로 관련 없는 데이터들이 다수 존재하기 때문에 가독성과 응집도가 낮아지며, 컬럼이 많아질수록 데이터간 관련성을 확인하기 여러울 수 있다.&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;2. JPA 상속 전략 예제&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;1) 테이블 구조&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci3BSS/btsJdbxpqKI/oEgK2AbVO2aTeXtnCLdJz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci3BSS/btsJdbxpqKI/oEgK2AbVO2aTeXtnCLdJz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci3BSS/btsJdbxpqKI/oEgK2AbVO2aTeXtnCLdJz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci3BSS%2FbtsJdbxpqKI%2FoEgK2AbVO2aTeXtnCLdJz0%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;919&quot; height=&quot;512&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;coupon
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 쿠폰의 상위 테이블이며 쿠폰 공통 정보를 저장한다&lt;/li&gt;
&lt;li&gt;모든 하위 테이블은 쿠폰 테이블의 기본키를 PK로 가지는 식별관계이다.&lt;/li&gt;
&lt;li&gt;coupon_type으로 쿠폰 정보를 구별한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fixed_discount_coupon
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;고정 할인 쿠폰이며, 할인 금액 컬럼을 갖고 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;percent_discount_coupon
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;퍼센트 할인 쿠폰이며 할인 퍼센트 컬럼을 갖고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;2) JPA 쿠폰 엔티티&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;Money&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724393593968&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record Money(double amount) {
    public static final Money ZERO = Money.of(0);

    public static Money of(double amount) {
        return new Money(amount);
    }

    public Money calculatePercent(double percent) {
        return Money.of(this.amount * percent);
    }

    public Money minus(Money money) {
        return Money.of(this.amount - money.amount);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;금액 관련 Value Object이다. 불변 객체이며, 계산 관련 메소드 실행 시 계산 결과가 반영된 새로운 Money 인스턴스를 반환한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;Coupon&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724393531258&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = &quot;coupon_type&quot;)
public abstract class Coupon {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = &quot;name&quot;, nullable = false)
    private String name;

    @Column(name = &quot;coupon_type&quot;, insertable = false, updatable = false)
    @Enumerated(EnumType.STRING)
    private CouponType type;

    protected Coupon(String name, CouponType type) {
        this.name = name;
        this.type = type;
    }

    public abstract Money use(Money price);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠폰의 공통 정보를 갖는 클래스이다. 추상클래스이며, 쿠폰의 공통 기능을 추상메소드로 정의했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Inheritance(strategy = InheritanceType.JOINED)&lt;/b&gt;는 부모 엔티티로 선언한다는 것을 의미하며 상속 전략으로 JOINED 속성을 적용했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@DiscriminatorColumn(name = &quot;coupon_type&quot;)&lt;/b&gt;은 하위 타입을 구분하는데 필요한 컬럼의 이름을 명시하는 어노테이션이다.&lt;/li&gt;
&lt;li&gt;쿠폰 사용 메소드를 추상메소드로 정의했다. Money 타입의 price 변수를 매개변수로 받아서 쿠폰이 적용되었을 때 결과 값을 반환한다&lt;/li&gt;
&lt;li&gt;CouponType 변수 선언은 선택사항이다. 별도로 선언하지 않아도 &lt;b&gt;&amp;nbsp;@DiscriminatorColumn(name = &quot;coupon_type&quot;)&amp;nbsp;&lt;/b&gt;와 같이 어노테이션을 선언했기 때문에 자동적으로 생성된다.&lt;/li&gt;
&lt;li&gt;couponType 변수는 단순 조회용 변수이므로 insertable,updatable 속성을 false로 설정했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FixedDiscountCoupon&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724395086041&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@DiscriminatorValue(&quot;FIXED_DISCOUNT&quot;)
public class FixedDiscountCoupon extends Coupon {

    @AttributeOverride(name = &quot;amount&quot;, column = @Column(name = &quot;discount_amount&quot;))
    @Embedded
    private Money discountAmount;

    private FixedDiscountCoupon(String name, Money discountAmount) {
        super(name, CouponType.FIXED_DISCOUNT);
        this.discountAmount = discountAmount;
    }

    public static FixedDiscountCoupon create(String name, Money discountAmount) {
        return new FixedDiscountCoupon(name, discountAmount);
    }

    @Override
    public Money use(Money price) {
        return price.minus(discountAmount);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;고정 할인 쿠폰 엔티티이다. Coupon 엔티티를 상속받으며 @DiscriminatorValue(&quot;FIXED_DISCOUNT&quot;) 어노테이션의 값을 통해서 couponType이 저장된다.&lt;/li&gt;
&lt;li&gt;여기서 핵심은 추상메소드를 오버라이딩 한 use 메소드이다.&lt;/li&gt;
&lt;li&gt;추상 클래스에 대한 다형성을 활용하기 위해 use 메소드를 재정의했다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;각 쿠폰마다 할인 방식이 다르기 때문에 각 쿠폰마다 추상메소드를 재정의한다.&lt;/li&gt;
&lt;li&gt;고정할인 쿠폰은 정해진 값 만큼 할인되기 때문에 매개변수로 전달된 price에 discountAmount 값을 차감한 결과를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PercentDiscountCoupon&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724395361043&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@DiscriminatorValue(&quot;PERCENT_DISCOUNT&quot;)
public class PercentDiscountCoupon extends Coupon {


    @Column(name = &quot;discount_percent&quot;)
    private double discountPercent;

    private PercentDiscountCoupon(String name, double discountPercent) {
        super(name, CouponType.PERCENT_DISCOUNT);
        this.discountPercent = discountPercent;
    }

    public static PercentDiscountCoupon create(String name, double discountPercent) {
        return new PercentDiscountCoupon(name, discountPercent);
    }

    @Override
    public Money use(Money price) {
        return price.minus(price.calculatePercent(discountPercent));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;퍼센트 할인 쿠폰이며, 저장된 퍼센트 만큼 금액을 할인한다.&lt;/li&gt;
&lt;li&gt;use 추상 메소드를 재정의해서 다형성 패턴을 활용할 수 있게 했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CouponRepository&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724395592015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface CouponRepository extends JpaRepository&amp;lt;Coupon, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠폰을 저장하거나 조회할 때 사용할 수 있는 CouponRepository를 정의했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CouponTest&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724396470754&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class CouponTest {
    @Test
    public void 고정할인_쿠폰_사용_테스트() {
        //given
        Coupon 고정_할인_쿠폰 = FixedDiscountCoupon.create(&quot;고정 할인 쿠폰&quot;, Money.of(1000));

        //when
        Money discountPrice = 고정_할인_쿠폰.use(Money.of(2000));

        //then
        Assertions.assertThat(discountPrice).isEqualTo(Money.of(1000));
    }

    @Test
    public void 퍼센트할인_쿠폰_사용_테스트() {
        //given
        Coupon 퍼센트_할인_쿠폰 = PercentDiscountCoupon.create(&quot;퍼센트_할인_쿠폰&quot;, 0.1);

        //when
        Money discountPrice = 퍼센트_할인_쿠폰.use(Money.of(2000));

        //then
        Assertions.assertThat(discountPrice).isEqualTo(Money.of(1800));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;고정 할인 쿠폰, 퍼센트 할인 쿠폰을 생성 후 Coupon 클래스에 저장했다.&lt;/li&gt;
&lt;li&gt;추상 클래스는 하위 클래스의 인스턴스를 저장할 수 있고 추상메소드 호출 시 각 하위클래스에서 재정의한 메소드의 구현이 동작하기 때문에 캡슐화 및 다형성을 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CouponRepositoryTest&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724396739900&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DataJpaTest
class CouponRepositoryTest {

    @Autowired
    public CouponRepository couponRepository;

    @Autowired
    public TestEntityManager testEntityManager;


    @Test
    public void 고정할인_쿠폰_저장() {
        Coupon coupon = couponRepository.save(FixedDiscountCoupon.create(&quot;고정 할인 쿠폰&quot;, Money.of(1000)));

        Assertions.assertThat(coupon.getId()).isNotNull();
        Assertions.assertThat(coupon.getType()).isEqualTo(CouponType.FIXED_DISCOUNT);
        Assertions.assertThat(((FixedDiscountCoupon) coupon).getDiscountAmount()).isEqualTo(Money.of(1000));
    }

    @Test
    public void 퍼센트할인_쿠폰_저장() {
        Coupon coupon = couponRepository.save(PercentDiscountCoupon.create(&quot;퍼센트 할인 쿠폰&quot;, 0.5));

        Assertions.assertThat(coupon.getId()).isNotNull();
        Assertions.assertThat(coupon.getType()).isEqualTo(CouponType.PERCENT_DISCOUNT);
        Assertions.assertThat(((PercentDiscountCoupon) coupon).getDiscountPercent()).isEqualTo(0.5);
    }

    @Test
    public void 고정할인_쿠폰_조회() {
        Coupon coupon = couponRepository.save(FixedDiscountCoupon.create(&quot;고정 할인 쿠폰&quot;, Money.of(1000)));
        testEntityManager.flush();
        testEntityManager.clear();

        Coupon findCoupon = couponRepository.findById(coupon.getId()).get();

        Assertions.assertThat(findCoupon.getId()).isNotNull();
        Assertions.assertThat(findCoupon.getType()).isEqualTo(CouponType.FIXED_DISCOUNT);
        Assertions.assertThat(((FixedDiscountCoupon) findCoupon).getDiscountAmount()).isEqualTo(Money.of(1000));
    }

    @Test
    public void 퍼센트할인_쿠폰_조회() {
        Coupon coupon = couponRepository.save(PercentDiscountCoupon.create(&quot;퍼센트 할인 쿠폰&quot;, 0.1));
        testEntityManager.flush();
        testEntityManager.clear();

        Coupon findCoupon = couponRepository.findById(coupon.getId()).get();

        Assertions.assertThat(findCoupon.getId()).isNotNull();
        Assertions.assertThat(findCoupon.getType()).isEqualTo(CouponType.PERCENT_DISCOUNT);
        Assertions.assertThat(((PercentDiscountCoupon) findCoupon).getDiscountPercent()).isEqualTo(0.1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠폰을 저장하거나 조회에 대한 테스트이다. 단건 쿠폰 조회 시 Coupon 클래스를 반환하기 때문에 하위 클래스의 상세 정보를 알기 위해서 다운 캐스팅을 해야한다. 실제 로직에서는 CouponType값으로 if문 혹은 switch문을 활용해서 쿠폰 상세 정보를 알 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;추상화하는 이유는 코드의 복잡성을 낮추고, 다형성을 활용해서 객체지향 관점에서 OCP(개방 폐쇄 원칙)를 준수한 코드를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;3. 쿠폰 생성을 위한 추상 팩토리 메소드 패턴&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쿠폰 생성 시 여러가지 방법이 있지만 추상 팩토리 메소드를 활용해서 간편하게 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1724399462154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record CouponCreateCommand(CouponType couponType, String couponName, Double fixedDiscountAmount,
                                  Double percentDiscountAmount) {
}

public interface CouponFactory {
    Coupon create(CouponCreateCommand command);

    CouponType getType();
}

@Component
public class FixedDiscountCouponFactory implements CouponFactory {

    @Override
    public Coupon create(CouponCreateCommand command) {
        return FixedDiscountCoupon.create(command.couponName(), Money.of(command.fixedDiscountAmount()));
    }

    @Override
    public CouponType getType() {
        return CouponType.FIXED_DISCOUNT;
    }
}

@Component
public class PercentDiscountCouponFactory implements CouponFactory {

    @Override
    public Coupon create(CouponCreateCommand command) {
        return PercentDiscountCoupon.create(command.couponName(), command.percentDiscountAmount());
    }

    @Override
    public CouponType getType() {
        return CouponType.PERCENT_DISCOUNT;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;CouponFactory 인터페이스에 create 메소드를 선언하고, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;CouponFactory&lt;span&gt;를 구현한 &lt;/span&gt;&lt;/span&gt;PercentDiscountCouponFactory, FixedDiscountCouponFactory를 선언한다.&lt;/li&gt;
&lt;li&gt;여기서 트레이드 오프는 CouponCreateCommand에 모든 쿠폰에 대한 정보가 선언되어 있다는 점이다.&lt;/li&gt;
&lt;li&gt;쿠폰 종류가 늘어남에 따라 변수의 종류가 많아진다. 이 부분은 각 쿠폰 생성에 대한 API 엔드포인트를 추가하고 쿠폰 타입 별로 쿠폰 생성 서비스를 추가하면 해결할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1724399800331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CouponCreateUseCase {
    private final Map&amp;lt;CouponType, CouponFactory&amp;gt; couponFactoryMap;
    private final CouponRepository couponRepository;

    public CouponCreateUseCase(List&amp;lt;CouponFactory&amp;gt; couponFactories, CouponRepository couponRepository) {
        this.couponFactoryMap = couponFactories.stream()
            .collect(Collectors.toMap(CouponFactory::getType, Function.identity()));
        this.couponRepository = couponRepository;
    }


    @Transactional
    public Long create(CouponCreateCommand command) {
        Coupon coupon = Optional.ofNullable(couponFactoryMap.get(command.couponType()))
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 쿠폰 생성 팩토리&quot;))
            .create(command);

        return couponRepository.save(coupon).getId();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp;CouponFactory를 구현한 Bean을 List로 주입받고 CouponFactory.getType() 의 Enum 값을 Key로 Bean 자체를 Value로 정의한 Map을 생성한다.&lt;/li&gt;
&lt;li&gt;그리고 command의 CouponType으로 CouponFactory를 검색하고 Coupon 엔티티를 생성한 다음 Coupon을 저장하는 유스케이스이다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JPA 상속 전략을 활용한 예제를 구현해보았다.&amp;nbsp;&lt;br /&gt;중요한 점은 JPA 상속 전략을 사용 방법을 아는 것 보다 어떻게 엔티티를 설계하고 활용할 것인지 고민하는 것이 더 중요하다.&lt;br /&gt;&lt;br /&gt;JPA 상속 전략에 매몰되어서 잘못 사용할 경우 오히려 복잡성이 증가하는 결과를 초례할 수 있다.&amp;nbsp;&lt;br /&gt;해당 전략이 활용되면 좋은 케이스 인지 잘 판단해서 사용하자&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>@Inheritance</category>
      <category>jpa</category>
      <category>jpa 엔티티 상속 전략</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/281</guid>
      <comments>https://bamdule.tistory.com/281#entry281comment</comments>
      <pubDate>Thu, 22 Aug 2024 19:33:02 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] SQS (Simple Queue Service)와 Spring 연동하기</title>
      <link>https://bamdule.tistory.com/280</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SQS에 대해서 공부한 내용을 블로그에 정리하는 목적으로 해당 게시글을 작성하게 되었다.&lt;br /&gt;SQS를 적절하게 사용하면 어려운 문제에 대해서 아주 효과적으로 처리할 수 있는데 예를 들어서 선착순 티켓팅 시스템이나 알림 발송 시스템 등 순간적으로 많은 트래픽이 발생하는 작업에 대해서 유연하게 대처할 수 있다.&lt;br /&gt;&lt;br /&gt;이번 게시글에서 SQS에 대한 간단한 설명과 설정 그리고 Spring boot와 연동하는 방법에 대해서 설명하겠다.&lt;/blockquote&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;1.&lt;span&gt; SQS&amp;nbsp;란&amp;nbsp;(Amazon&amp;nbsp;Simple&amp;nbsp;Queue&amp;nbsp;Service)&amp;nbsp;?&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;2.&lt;span&gt;&lt;span&gt; SQS를 사용하는 이유&lt;br /&gt;3. SQS 유형&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;4.&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;SQS 생성 및 설정 방법&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;5. SQS와 Spring Boot 연동&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;6. 결론&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;1. SQS 란 (Amazon Simple Queue Service) ?&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS SQS(Amazon Simple Queue Service)는 메시지를 송수신할 수 있는 완전 관리형 메시지 큐 서비스이다.&lt;br /&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이를 통해 분산 시스템 간의 비동기 통신을 가능하게 하며, 메시지의 유실 없이 안정적인 데이터 전달을 보장한다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;2. &lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;SQS를 사용하는 이유&lt;/span&gt;&lt;/b&gt; &lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;1) &lt;b&gt;비동기 처리 및 작업 분리&lt;/b&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 비동기 처리를 가능하게 해준다. 시스템에서 시간이 오래 걸리는 작업이나 비동기적으로 처리해야 하는 작업이 있을 때, SQS를 사용하면 작업을 큐에 넣고 리시버에서 작업 수행하도록 설계할 수 있다.&amp;nbsp;&lt;br /&gt;이런식으로 작업을 큐로 전송하는 서버와 작업을 처리하는 서버로 분리하면 처리량과 안정성을 높일 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;2)&lt;span&gt; 시스템&amp;nbsp;간의&amp;nbsp;결합도&amp;nbsp;감소&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 아키텍처에서는 서비스 간의 결합도를 낮추는 것이 매우 중요하다. SQS는 서비스 간의 직접적인 통신을 피하고, 메시지를 큐를 통해 전달함으로써 서비스 간의 결합도를 낮출 수 있다. 이를 통해 각 서비스는 독립적으로 개발, 배포 및 확장할 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;3)&lt;span&gt;&lt;span&gt; 확장성과&amp;nbsp;유연성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 AWS의 관리형 서비스로, 메시지 큐의 크기와 처리량을 자동으로 확장할 수 있다. 대규모 메시지를 처리할 수 있으며 트래픽이 급증해도 시스템이 이를 처리할 수 있도록 자동으로 확장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 수동으로 인프라를 관리할 필요 없고, 사용한 만큼만 비용을 소모할 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;4)&lt;span&gt;&lt;span&gt;&lt;span&gt; 내결함성과&amp;nbsp;안정성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 메시지의 안정적이고 지속적인 전달을 보장한다. 메시지가 처리되지 못했을 경우 Dead-Letter Queue를 사용해 해당 메시지를 별도로 관리할 수 있다. 이는 시스템 오류나 장애가 발생하더라도 메시지를 안전하게 처리할 수 있도록 도와준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dead-Letter Queue를 사용하면 별도의 큐를 관리해야 하므로 추가적인 비용이 발생할 수 있다. 하지만 중요한 작업이고 실패할 경우 안정적으로 재시도 작업을 원한다면 SQS는 아주 쉽게 해당 기능을 제공한다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;5) 보안&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 AWS IAM을 사용해 액세스 제어를 제공하며, 전송 중인 데이터와 대기열에 저장된 데이터를 AWS KMS(Key Management Service)를 통해 암호화할 수 있다. 이를 통해 민감한 데이터를 안전하게 처리할 수 있다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;6) 저비용&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS는 메시지의 수에 따라 비용이 청구되므로, 사용한 만큼만 비용을 지불하게 된다. 이는 예산을 효율적으로 관리할 수 있게 해주며, 특히 트래픽이 변동적인 애플리케이션에 유리하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt; &lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;3. SQS 유형&lt;/span&gt;&lt;/b&gt; &lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;&lt;b&gt;SQS는 크게 표준 대기열, FIFO 대기열로 나뉘어진다. 목적에 따라서 적절히 선택해야한다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;1) 표준 대기열&lt;/b&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;무제한 처리량&lt;/b&gt;: 표준 대기열은 API 작업당 거의 무제한의 초당 트랜잭션(TPS)을 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소한 한 번은 전달&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 메시지가 최소한 한 번 전달되고, 가끔 2개 이상의 메시지 복사본이 전달될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시지 전송 순서 보장은 안됌&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 가끔 메시지가 전송된 순서와 다르게 전달될 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;2) FIFO 대기열&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;높은 처리량&lt;/b&gt;: 기본적으로 FIFO 대기열은 초당 최대 300개의 메시지(초당 300개의 전송, 수신 또는 삭제 작업)를 지원합한다. 작업당 최대 10개 메시지를 일괄 처리할 경우, FIFO 대기열은 초당 3000개의 메시지까지 지원할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정확히 한 번 처리&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 메시지가 한 번 전달되고 소비자가 이를 처리 및 삭제할 때까지 유지된다. 중복 메시지는 대기열에 올라가지 않는다..&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선입선출 전달&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;: 메시지가 전송되고 수신되는 순서가 엄격하게 지켜진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt; &lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;4.&amp;nbsp;SQS 생성 및 설정 방법&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt; &lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;1) SQS IAM 사용자 생성&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;사용자 세부 정보 지정&lt;/span&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;2709&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oRcIF/btsI25ZiLrw/kTKzG0VwL4dKWuE7AkZvZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oRcIF/btsI25ZiLrw/kTKzG0VwL4dKWuE7AkZvZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oRcIF/btsI25ZiLrw/kTKzG0VwL4dKWuE7AkZvZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoRcIF%2FbtsI25ZiLrw%2FkTKzG0VwL4dKWuE7AkZvZk%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;2709&quot; height=&quot;755&quot; data-origin-width=&quot;2709&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;권한 설정 및 사용자 생성&lt;/span&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;2693&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw2ZgB/btsI5CncPOX/BwiIJBoTYEb0F1o8v7kbCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw2ZgB/btsI5CncPOX/BwiIJBoTYEb0F1o8v7kbCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw2ZgB/btsI5CncPOX/BwiIJBoTYEb0F1o8v7kbCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw2ZgB%2FbtsI5CncPOX%2FBwiIJBoTYEb0F1o8v7kbCk%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;2693&quot; height=&quot;1190&quot; data-origin-width=&quot;2693&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2281&quot; data-origin-height=&quot;969&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IwPbu/btsI5zD5Mmp/qPGxmxB8YdyRB3Prg92QT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IwPbu/btsI5zD5Mmp/qPGxmxB8YdyRB3Prg92QT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IwPbu/btsI5zD5Mmp/qPGxmxB8YdyRB3Prg92QT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIwPbu%2FbtsI5zD5Mmp%2FqPGxmxB8YdyRB3Prg92QT0%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;2281&quot; height=&quot;969&quot; data-origin-width=&quot;2281&quot; data-origin-height=&quot;969&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQS 전용 IAM 사용자를 생성했다면 액세스 키를 만들고 ARN을 복사해 두자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;2) Amazon SQS 대기열 생성&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;대기열 생성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I7gAc/btsI3SkRYxC/3PXbBDZ41DOBkT20KvdkO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I7gAc/btsI3SkRYxC/3PXbBDZ41DOBkT20KvdkO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I7gAc/btsI3SkRYxC/3PXbBDZ41DOBkT20KvdkO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI7gAc%2FbtsI3SkRYxC%2F3PXbBDZ41DOBkT20KvdkO1%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;1996&quot; height=&quot;805&quot; data-origin-width=&quot;1996&quot; data-origin-height=&quot;805&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;표준 대기열을 선택하고 큐 이름을 입력한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &lt;b&gt;대기열 구성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bb3SxF/btsI4YRWNNV/g2CDbQDm3OxM35xRlEwdDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bb3SxF/btsI4YRWNNV/g2CDbQDm3OxM35xRlEwdDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bb3SxF/btsI4YRWNNV/g2CDbQDm3OxM35xRlEwdDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbb3SxF%2FbtsI4YRWNNV%2Fg2CDbQDm3OxM35xRlEwdDK%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;1982&quot; height=&quot;621&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;표시 제한 시간 (Visibility Timeout)&lt;/b&gt;&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이 설정은 메시지가 중복 처리되는 것을 방지하기 위해 사용된다. 메시지가 소비자에게 전달되면 해당 메시지는 표시 제한 시간 동안 큐에서 &quot;잠금(lock)&quot; 상태가 된다. 이 기간 내에 소비자는 메시지를 처리하고 SQS에 처리가 완료되었음을 알려야 한다. 만약 표시 제한 시간이 지나도 처리가 완료되지 않으면, 해당 메시지의 잠금이 풀리며 다른 소비자가 메시지를 소비할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;전송&amp;nbsp;지연&amp;nbsp;(Delay&amp;nbsp;Seconds)&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;전송 지연은 메시지가 큐에 추가되자마자 소비자에게 전달되지 않고, 지정된 시간 동안 대기 상태가 되는 설정이다. 이 설정은 메시지의 즉각적인 처리가 필요하지 않거나, 특정 작업이 지연되어 실행되기를 원할 때 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지&amp;nbsp;수신&amp;nbsp;대기&amp;nbsp;시간&amp;nbsp;(Receive&amp;nbsp;Message&amp;nbsp;Wait&amp;nbsp;Time)&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지 수신 대기 시간&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;은 SQS가 폴링 방식으로 메시지를 검색할 때, 메시지가 큐에 없을 경우 대기하는 시간을 지정하는 설정이다. 이 설정을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;짧은 폴링&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;긴 폴링&lt;/b&gt;을 제어할 수 있다.&amp;nbsp; 짧은 폴링은 메시지가 없을 때 즉시 응답하지만, 긴 폴링은 설정된 시간 동안 큐에 메시지가 들어올 때까지 대기한다. 긴 폴링을 사용하면 네트워크 트래픽과 비용을 줄일 수 있다.&lt;/li&gt;
&lt;li&gt;대기하는 주체는 폴링 요청을 하는 클라이언트(리시버)이며 메시지 수신 대기시간을 길게 설정하면 폴링 할 때 메시지가 없을 경우 바로 연결을 끊지 않고 설정한 시간동안 대기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지&amp;nbsp;보존&amp;nbsp;기간&amp;nbsp;(Message&amp;nbsp;Retention&amp;nbsp;Period)&lt;/b&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지 보존 기간은 큐에 들어온 메시지가 큐에서 삭제되지 않고 보관되는 최대 시간을 설정한다. 메시지가 이 기간 동안 처리되지 않으면, 자동으로 삭제된다.&lt;/li&gt;
&lt;li&gt;메시지 보존 기간을 길게 설정하면 메시지를 오랜 기간 동안 보관할 수 있지만, 이 경우 큐에 메시지가 쌓이면서 스토리지 비용이 증가할 수 있다. 반면, 보존 기간을 짧게 설정하면 메시지가 빠르게 삭제되므로 큐의 크기를 줄일 수 있지만, 메시지 손실 위험이 증가할 수 있다.&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;암호화&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1651&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxWjKN/btsI4zkzGP9/y69jhQi9gMBEDiryHETJ41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxWjKN/btsI4zkzGP9/y69jhQi9gMBEDiryHETJ41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxWjKN/btsI4zkzGP9/y69jhQi9gMBEDiryHETJ41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxWjKN%2FbtsI4zkzGP9%2Fy69jhQi9gMBEDiryHETJ41%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;1651&quot; height=&quot;316&quot; data-origin-width=&quot;1651&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지에 대한 암호화 기능을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;SQS 큐 전송자와 큐 수신자에 대한 액세스 정책&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxKZkD/btsI4TQPFEb/GkFMdjXew4KH78ejKIkPQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxKZkD/btsI4TQPFEb/GkFMdjXew4KH78ejKIkPQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxKZkD/btsI4TQPFEb/GkFMdjXew4KH78ejKIkPQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxKZkD%2FbtsI4TQPFEb%2FGkFMdjXew4KH78ejKIkPQk%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;403&quot; height=&quot;473&quot; data-origin-width=&quot;804&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;이전에 생성해둔 SQS IAM에 대한 ARN 링크를 복사해서 큐 전송자와 큐 수신자에 입력한다.&lt;/li&gt;
&lt;li&gt;큐 전송자와 수신자를 별도의 사용자로 나누고 싶다면 IAM 사용자를 추가로 생성해주면 된다.&lt;/li&gt;
&lt;li&gt;IAM 사용자 외에도 역할에 SQS 관련 권한을 넣어주고 해당 역할에 대한 ARN 정보를 입력해도 된다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt; Dead-Letter Queue 정책 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OxKls/btsI4ZQToMO/KNNcIC7vT9RYEfUydmq2R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OxKls/btsI4ZQToMO/KNNcIC7vT9RYEfUydmq2R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OxKls/btsI4ZQToMO/KNNcIC7vT9RYEfUydmq2R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOxKls%2FbtsI4ZQToMO%2FKNNcIC7vT9RYEfUydmq2R0%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;611&quot; height=&quot;555&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;1387&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;수신자에서 작업이 실패될 경우 &lt;b&gt;Dead-Letter Queue로&amp;nbsp;&lt;/b&gt;실패 작업에 대한 메시지를 전송할 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;해당 설정을 해주려면 위에서 대기열을 생성했던 것과 같이 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Dead-Letter Queue &lt;/b&gt;전용으로 생성해주어야 한다.&lt;/li&gt;
&lt;li&gt;그 다음 해당 대기열의 ARN을 복사해서 대기열 ARN에 입력해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 설정이 완료되었으면 대기열을 생성해주자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;5. &lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;SQS와 Spring Boot 연동&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt; &lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 버전에 따라서 연동하는 방법이 다를 수 있다. 이번 게시글에서는 jdk 17과 spring 3.x 으로 연동 테스트를 진행하겠다. Spring project를 생성하고 아래 코드를 참고해서 테스트해보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723619902112&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.6'
}

...

dependencies {
	...
    implementation platform(&quot;io.awspring.cloud:spring-cloud-aws-dependencies:3.0.1&quot;)
    implementation 'io.awspring.cloud:spring-cloud-aws-starter-sqs'
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1723620106866&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloud:
  aws:
    credentials:
      access-key: access-key
      secret-key: secret-key
    region:
      static: ap-northeast-2
    sqs:
      queue:
        name: my-test-queue&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SQS IAM의 access-key와 secret-key를 입력하고 queue의 region과 name을 입력하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;AwsSqsConfiguration&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;AWS SQS 관련 설정 클래스이다. 편의상 메시지 전송 설정과 메시지 수신 설정을 하나의 클래스에 정의하고 동일한 비동기 클라이언트를 사용하도록 설정한다&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723620642789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory;
import io.awspring.cloud.sqs.operations.SqsTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sqs.SqsAsyncClient;

import java.time.Duration;

@Slf4j
@Configuration
public class AwsSqsConfiguration {

    private final String accessKey;

    private final String secretKey;

    private final String region;

    public AwsSqsConfiguration(
        @Value(&quot;${cloud.aws.credentials.access-key}&quot;) String accessKey,
        @Value(&quot;${cloud.aws.credentials.secret-key}&quot;) String secretKey,
        @Value(&quot;${cloud.aws.region.static}&quot;) String region
    ) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.region = region;
    }

    /**
     * SQS 비동기 클라이언트
     *
     * @return
     */
    @Bean
    public SqsAsyncClient sqsAsyncClient() {
        return SqsAsyncClient.builder()
            .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
            .region(Region.of(region))
            .build();
    }
    
    /**
     * SQS 메시지 생성 템플릿
     *
     * @return
     */
    @Bean
    public SqsTemplate sqsTemplate() {
        return SqsTemplate.newTemplate(sqsAsyncClient());
    }    

    /**
     * SQS Listener 설정
     */
    @Bean
    public SqsMessageListenerContainerFactory&amp;lt;Object&amp;gt; defaultSqsListenerContainerFactory() {
        return SqsMessageListenerContainerFactory
            .builder()
            .configure(sqsContainerOptionsBuilder -&amp;gt;
                sqsContainerOptionsBuilder
                    .maxConcurrentMessages(10) // 컨테이너의 스레드 풀 크기
                    .maxMessagesPerPoll(10) // 한 번의 폴링 요청으로 수신할 수 있는 최대 메시지 수를 지정
                    .acknowledgementInterval(Duration.ofSeconds(5)) // AWS SQS 응답 간격
                    .acknowledgementThreshold(10) // AWS SQS 응답 최소 개수
            )
            .sqsAsyncClient(sqsAsyncClient())
            .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;크게 SqsAsyncClient, SqsTemplete, SqsMessageListenerContainerFactory으로 구성된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; SqsAsyncClient&lt;/b&gt; : AWS SQS 비동기 통신을 제공하며, access-key와 secret-key, region을 설정한 후 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; SqsTemplete&lt;/b&gt; : SQS로 메시지 생성 및 전송 템플릿 클래스이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; SqsMessageListenerContainerFactory&amp;nbsp;&lt;/b&gt;: SQS 메시지 수신 컨테이너 팩토리 클래스이다. 비동기로 수신하며 설정에 따라서 효율적으로 작업을 수행할 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;maxConcurrentMessages&lt;/b&gt; : 컨테이너의 스레드 풀을 설정한다. 기본값은 10이며 해당 설정에 따라서 병렬로 처리할 수 있는 스레드 수를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;maxMessagesPerPoll&lt;/b&gt; : 한 번의 폴링(polling)으로 SQS 큐에서 가져올 수 있는 최대 메시지 수를 지정한다. 일종의 버퍼 역할을 한다고 보면된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;acknowledgementInterval&lt;/b&gt; : 메시지를 처리한 후 즉시 SQS로 응답을 보내지 않고, 설정한 시간 만큼 기다렸다가 시간 내에 처리된 모든 메시지에 대해 한 번에 SQS로 응답을 보낸다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; acknowledgementThreshold&lt;/b&gt; : 설정한 개수의 메시지가 처리될 때마다 SQS로 응답을 보낸다. 설정한 개수 이하의 메시지가 처리된 경우에는 확인을 보내지 않고, 개수 충족 시 한 번에 SQS로 응답을 보낸다.&lt;/li&gt;
&lt;li&gt;일반적으로 acknowledgementInterval과 acknowledgementThreshold는 함께 사용되어, 시간과 메시지 수 기준 중 어느 한 가지 조건이 충족될 때 SQS로 응답을 보낼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자세한 설정 정보를 확인하려면 아래 링크를 참조하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://docs.awspring.io/spring-cloud-aws/docs/3.0.0/reference/html/index.html#sqscontaineroptions-descriptions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.awspring.io/spring-cloud-aws/docs/3.0.0/reference/html/index.html#sqscontaineroptions-descriptions&lt;/a&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;NotificationSender&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;알림 전송 관련 인터페이스 및 구현체를 정의한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723622039858&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 알림 메시지
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class Notification {
    private String message;
    private LocalDateTime createAt;

    public static Notification create(String message) {
        return new Notification(
            message,
            LocalDateTime.now()
        );
    }
}

// 알림 전송 결과
public record NotificationSendResult(
    String messageId,
    boolean success
) {

    public static NotificationSendResult success(String messageId) {
        return new NotificationSendResult(messageId, true);
    }

    public static NotificationSendResult failure() {
        return new NotificationSendResult(null, false);
    }
}

// 알림 전송 인터페이스
public interface NotificationSender {
    NotificationSendResult sendNotification(Notification notification);
}


// AWS SQS 알림 전송 구현체
@Slf4j
@RequiredArgsConstructor
@Component
public class AwsSqsNotificationSender implements NotificationSender {

    @Value(&quot;${cloud.aws.sqs.queue.name}&quot;)
    private String queueName;

    private final ObjectMapper objectMapper;

    private final SqsTemplate template;

    @Override
    public NotificationSendResult sendNotification(Notification notification) {
        try {
            String message = objectMapper.writeValueAsString(notification);

            SendResult&amp;lt;String&amp;gt; result = template.send(to -&amp;gt; to
                .queue(queueName)
                .payload(message));

            return NotificationSendResult.success(result.messageId().toString());
        } catch (Exception e) {
            log.error(&quot;send notification error : &quot;, e);
            return NotificationSendResult.failure();
        }
    }
}

@RequiredArgsConstructor
@RequestMapping(&quot;/api/v1/send-notification&quot;)
@RestController
public class NotificationSendController {

    private final NotificationSender notificationSender;

    @PostMapping
    public ResponseEntity&amp;lt;NotificationSendResult&amp;gt; send(@RequestBody String message) {
        return ResponseEntity.ok(notificationSender.sendNotification(Notification.create(message)));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;AWS SQS 요청 및 응답 DTO를 생성하고 AWS SQS 구현체를 작성한 알림 전송 컨트롤러로 메시지를 전송해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;알림 메시지 전송 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;알림 메시지 전송 엔트포인트로 요청을 보내고 SQS에 메시지가 수신되었는지 확인해보자&amp;nbsp;&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;1310&quot; data-origin-height=&quot;780&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pXPqs/btsI5hqi7oi/TmLGaLy2fF5SggFekpiOYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pXPqs/btsI5hqi7oi/TmLGaLy2fF5SggFekpiOYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pXPqs/btsI5hqi7oi/TmLGaLy2fF5SggFekpiOYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpXPqs%2FbtsI5hqi7oi%2FTmLGaLy2fF5SggFekpiOYK%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;662&quot; height=&quot;394&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;780&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;AWS SQS 메시지 전송 및 수신 페이지로 이동한다&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;2649&quot; data-origin-height=&quot;1366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oIii3/btsI4msmKo6/Fx6lRZBigIfNkYcJCnmQW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oIii3/btsI4msmKo6/Fx6lRZBigIfNkYcJCnmQW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oIii3/btsI4msmKo6/Fx6lRZBigIfNkYcJCnmQW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoIii3%2FbtsI4msmKo6%2FFx6lRZBigIfNkYcJCnmQW1%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;2649&quot; height=&quot;1366&quot; data-origin-width=&quot;2649&quot; data-origin-height=&quot;1366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지 수신을 확인해보면 사용가능한 메시지 2개를 확인할 수 있고, 메시지 폴링 버튼을 누르면 확인가능한 메시지 두건을 확인할 수 있다. 메시지의 ID를 보면 postman의 응답에 있는 messageId 와 동일한 것을 알 수 있다.&lt;/li&gt;
&lt;li&gt;폴링해서 메시지를 확인한다고 해도 메시지가 소모된 것은 아니며 SQS를 통해 메시지를 사용했고 삭제해달라는 요청을 보내야 비로서 메시지를 큐에서 제거한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;SQS 메시지 수신 Listener&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1723623504621&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class AwsSqsListener {

    @SqsListener(value = &quot;${cloud.aws.sqs.queue.name}&quot;)
    public void listen(String message) {
        log.info(&quot;notification : {}&quot;, message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;@SqsListener(value = &quot;${cloud.aws.sqs.queue.name}&quot;)을 어노테이션을 선언한 Bean에 대해서 SQS 메시지 수신을 수행한다.&lt;/li&gt;
&lt;li&gt;SqsMessageListenerContainerFactory 설정에 따라서 동시에 처리할 수 있는 리시버(워커 또는 노드 라고도 한다)개수를 지정할 수 있다.&lt;/li&gt;
&lt;li&gt;서버 인스턴스 사양에 따라서 적절하게 설정 해주면 효율적인 SQS 메시지 처리가 가능하다&lt;/li&gt;
&lt;li&gt;@SqsListener 어노테이션이 작동하지 않는 경우가 있는데 spring boot 3.0으로 변경되면서 연동하는 방식이 달라졌다고 한다. 라이브러리 버전을 잘 체크해보자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1723623804034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2024-08-14T17:22:59.582+09:00  INFO 38884 --- [ntContainer#0-1] i.s.s.i.sqs.receiver.AwsSqsListener      : notification : {&quot;message&quot;:&quot;{\r\n    \&quot;message\&quot; : \&quot;반갑습니다\&quot;\r\n}&quot;,&quot;createAt&quot;:&quot;2024-08-14T17:09:33.4354856&quot;}
2024-08-14T17:22:59.641+09:00  INFO 38884 --- [ntContainer#0-2] i.s.s.i.sqs.receiver.AwsSqsListener      : notification : {&quot;message&quot;:&quot;{\r\n    \&quot;message\&quot; : \&quot;안녕하세요\&quot;\r\n}&quot;,&quot;createAt&quot;:&quot;2024-08-14T17:12:50.1727803&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위와 같이 메시지가 잘 수신된 것을 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;별도의 설정을 하지 않으면 작업 종료 시 SQS에 응답하면서 메시지는 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;6.&lt;span&gt;&amp;nbsp;결론&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;AWS SQS가 아니어도 Apache Kafka, Redis message broker, rebbitMQ 등을 활용해서 메시징 시스템을 구축할 수 있다. 전략적으로 현재 상황에 맞는 메시징 시스템을 선택하면 된다.&lt;br /&gt;&lt;br /&gt;메시지 전송자와 메시지 수신자 (워커, 노드)를 별도의 서버로 구현하는 것이 좋으며 이러한 메시징 시스템을 적절하게 활용하는 것이 매우 중요하다&lt;/blockquote&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>AWS</category>
      <category>simple queue service</category>
      <category>spring boot</category>
      <category>SQS</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/280</guid>
      <comments>https://bamdule.tistory.com/280#entry280comment</comments>
      <pubDate>Wed, 14 Aug 2024 17:30:37 +0900</pubDate>
    </item>
    <item>
      <title>[Java] 스프링에서 자주 사용하는 디자인 패턴 - 전략 패턴</title>
      <link>https://bamdule.tistory.com/279</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;디자인 패턴 관련해서 공부를 해도 실제 실무에서 어떻게 적용하는지 의문이었던 적이 있었다.&lt;br /&gt;실무 개발을 진행하면서 여러 시행착오를 통해 유지보수 용이한 코드를 작성했던 경험을 공유 해보고자 한다&lt;br /&gt;전략 패턴을 소개하는 용도로 작성한 게시글이라 완성된 코드가 아니다.&lt;br /&gt;전략 패턴을 어떻게 활용했는지 참고하는 용도로 봤으면 좋겠다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;개발 요구사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;미리 등록해둔 결제 수단(카드, 페이)을 이용해서 도서를 구매한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;개발 예제&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;PaymentMethod, PaymentItem, PaymentCommand, PaymentResult&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722854038016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결제수단
public enum PaymentMethod {
    CARD, PAY,
}

// 결제 전략 Not Found 예외
public class PaymentStrategyNotFoundException extends RuntimeException {
    public PaymentStrategyNotFoundException() {
        super(&quot;결제 전략을 찾을 수 없습니다.&quot;);
    }
}

// 결제 상품
@RequiredArgsConstructor
@Getter
public class PaymentItem {
    private final String name;
    private final int price;
    private final int quantity;
}

// 결제 커맨드
@RequiredArgsConstructor
@Getter
public class PaymentCommand {
    private final Long userId;
    private final Long paymentMethodId;
    private final PaymentMethod paymentMethod;
    private final PaymentItem paymentItem;
}

@Getter
@RequiredArgsConstructor
public class PaymentResult {
    private final Long paymentId;
    private final Long paymentMethodId;
    private final PaymentMethod paymentMethod;
    private final PaymentItem paymentItem;
    private final String message;
    private final boolean success;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각각 결제 수단, 결제 상품, 결제 커맨드, 결제결과를 저장하는 클래스이다.&lt;/li&gt;
&lt;li&gt;결제 커맨드에는 회원 아이디, 결제 수단 아이디, 결제수단 타입, 결제 상품 정보가 저장되어 있다.&lt;/li&gt;
&lt;li&gt;결제 수단은 카드, 페이가 존재하며 중복되지 않는 아이디로 관리되고 있다고 가정한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;PaymentStrategy&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722853999261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 결제 전략 인터페이스
public interface PaymentStrategy {
    PaymentResult pay(PaymentCommand command);
}

// 카드 결제 전략 구현체
@RequiredArgsConstructor
@Service
public class CardPaymentStrategy implements PaymentStrategy {

    private final CardPaymentApi cardPaymentApi; 
    private final CardPaymentMethodRepository cardPaymentMethodRepository;  

    @Override
    public PaymentResult pay(PaymentCommand command) {
        ... 결제 로직
    }
}

// 페이 결제 전략 구현체
@RequiredArgsConstructor
@Service
public class PayPaymentStrategy implements PaymentStrategy {

    private final PayPaymentApi PayPaymentApi;  
    private final PayPaymentMethodRepository payPaymentMethodRepository;  

    @Override
    public PaymentResult pay(PaymentCommand command) {
        ... 결제 로직
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&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;span style=&quot;color: #666666;&quot;&gt;BookPaymentUseCase&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class BookPaymentUseCase {

    private final CardPaymentStrategy cardPaymentStrategy;
    private final PayPaymentStrategy payPaymentStrategy;

    public PaymentResult pay(PaymentCommand command) {
        PaymentResult paymentResult = getPaymentStrategy(command.getPaymentMethod())
            .pay(command);

        // 결제 이후에 작업들

        return paymentResult;
    }

    private PaymentStrategy getPaymentStrategy(PaymentMethod paymentMethod) {
        switch (paymentMethod) {
            case PAY:
                return payPaymentStrategy;
            case CARD:
                return cardPaymentStrategy;
            default:
                throw new PaymentStrategyNotFoundException();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;도서를 구매하는 역할을 가진 &lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;BookPaymentUseCase &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;클래스에서 결제 전략에 따라 도서를 구매하고 결과를 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;중요하게 보아야할 부분은 getPaymentStrategy(...)메소드 인데 paymentMethod 값에 따라서 결제 전략을 반환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;결제 전략이 존재하지 않으면 PaymentStrategyNotFoundException 예외가 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;하지만 &lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;BookPaymentUseCase&lt;/span&gt;&lt;/b&gt;는 OCP(개방 폐쇄 법칙, 확장에는 열려있고 변경에는 닫혀있다.)를 지키지 않는다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;새로운 전략이 추가될 때마다 &lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;BookPaymentUseCase&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;를 수정해주어야 한다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666;&quot;&gt;아래와 같이 OCP를 지키도록 개선해보자&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;PaymentStrategyProvider&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface PaymentStrategy {
    PaymentResult pay(PaymentCommand command);

    PaymentMethod getPaymentMethod(); // 메소드 추가
}


@Service
public class PaymentStrategyProvider {
    private final Map&amp;lt;PaymentMethod, PaymentStrategy&amp;gt; provider;

    // PaymentStrategyProvider는 Bean이기 때문에 의존성 주입 시 PaymentStrategy로 구현한 클래스를 List로 주입받을 수 있다.
    public PaymentStrategyProvider(List&amp;lt;PaymentStrategy&amp;gt; paymentStrategies) {
        this.provider = paymentStrategies
            .stream()
            .collect(Collectors.toMap(PaymentStrategy::getPaymentMethod, Function.identity()));
    }
    
    // paymentMethod를 매개변수로 받아서 PaymentStrategy를 반환한다. 
    // PaymentStrategy 구현체가 존재하지 않으면PaymentStrategyNotFoundException 예외를 발생시킨다
    public PaymentStrategy provide(PaymentMethod paymentMethod) {
        return Optional.ofNullable(provider.get(paymentMethod))
            .orElseThrow(PaymentStrategyNotFoundException::new);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;PaymentStratgy 인터페이스에&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt; PaymentMethod getPaymentMethod(&lt;/b&gt;);&lt;/span&gt; 메소드를 추가하고 각 구현체에 반영한다.&lt;/li&gt;
&lt;li&gt;PaymentStrategyProvider는 paymentMethod 값에 따라서 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;PaymentStratgy 구현체를 반환하는 역할을 가진 클래스이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&amp;nbsp;PaymentStrategyProvider는 Bean이기 의존성 주입 시 PaymentStrategy로 구현한 클래스를 List로 주입받을 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;해당 리스트를 Map으로 변환하는데 key 값은 각 구현체의 getPaymentMethod() 값이고, 값으로 결제 전략 구현체가 저장되도록 한다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;provide(...) 메소드를 호출해서 기존에 swtich문을 대체한다.&lt;/li&gt;
&lt;li&gt;이로서 새로운 결제 전략이 추가되더라도 코드 수정을 하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;이는 스프링의 의존성 주입 특성을 활용하는 것인데, 의존성 주입 시 생성자에 특정 인터페이스를 받도록 선언해두면 그에 따른 구현체를 주입시켜준다. 그리고 리스트 형태로도 받을 수 있다.&lt;/li&gt;
&lt;li&gt;스프링이 아니더라도 PaymentStrategyConfig를 만들어 PaymentStrategyProvider를 직접 생성해주는 방식으로 사용해도 된다.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;BookPaymentUseCase 개선&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class BookPaymentUseCase {

    private final PaymentStrategyProvider paymentStrategyProvider;

    public PaymentResult pay(PaymentCommand command) {
        PaymentResult paymentResult = getPaymentStrategy(command.getPaymentMethod())
            .pay(command);

        // 결제 이후에 작업들

        return paymentResult;
    }

    private PaymentStrategy getPaymentStrategy(PaymentMethod paymentMethod) {
        return paymentStrategyProvider.provide(paymentMethod);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;PaymentStrategyProvider를 활용하도록 수정하자&lt;/li&gt;
&lt;li&gt;고수준 모듈 (BookPaymentUseCase)은 저수준 모듈 (결제 모듈들..)을 직접적으로 의존하지 않고, 새로운 전략이 추가되더라도 고수준 모듈 자체를 수정하지 않아도 된다. (OCP)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;결제 전략 패턴을 적용한 클래스 다이어그램&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHfw5H/btsITTdks3B/qEhi4TFQKA0zuZK0Vvt3a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHfw5H/btsITTdks3B/qEhi4TFQKA0zuZK0Vvt3a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHfw5H/btsITTdks3B/qEhi4TFQKA0zuZK0Vvt3a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHfw5H%2FbtsITTdks3B%2FqEhi4TFQKA0zuZK0Vvt3a1%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;1480&quot; height=&quot;667&quot; data-origin-width=&quot;1480&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt; PaymentStrategyProvider&amp;nbsp; 테스트 코드&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;

@DisplayName(&quot;PaymentStrategyProvider 테스트 &quot;)
class PaymentStrategyProviderTest {

    @Test
    public void 카드결제_전략조회_성공() {
        PaymentStrategyProvider paymentStrategyProvider = new PaymentStrategyProvider(
            Arrays.asList(new CardPaymentStrategy(), new PayPaymentStrategy()));

        PaymentStrategy paymentStrategy = paymentStrategyProvider.provide(PaymentMethod.CARD);

        Assertions.assertThat(paymentStrategy.getClass()).isEqualTo(CardPaymentStrategy.class);
    }

    @Test
    public void 페이결제_전략조회_성공() {
        PaymentStrategyProvider paymentStrategyProvider = new PaymentStrategyProvider(
            Arrays.asList(new CardPaymentStrategy(), new PayPaymentStrategy()));

        PaymentStrategy paymentStrategy = paymentStrategyProvider.provide(PaymentMethod.PAY);

        Assertions.assertThat(paymentStrategy.getClass()).isEqualTo(PayPaymentStrategy.class);
    }

    @Test
    public void 원하는_결제_전략이_없을경우_예외발생() {
        PaymentStrategyProvider paymentStrategyProvider = new PaymentStrategyProvider(new ArrayList&amp;lt;&amp;gt;());

        Assertions.assertThatThrownBy(() -&amp;gt; paymentStrategyProvider.provide(PaymentMethod.PAY))
            .isInstanceOf(PaymentStrategyNotFoundException.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;개발에 정답이 있는 것은 아니지만 내가 생각하는 좋은 코드란 SOLID 원칙을 잘 준수한 코드라고 생각한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>Strategy pattern</category>
      <category>전략 패턴</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/279</guid>
      <comments>https://bamdule.tistory.com/279#entry279comment</comments>
      <pubDate>Mon, 5 Aug 2024 20:30:00 +0900</pubDate>
    </item>
    <item>
      <title>[DB] foreign key와 공유 락에 관하여</title>
      <link>https://bamdule.tistory.com/278</link>
      <description>&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;/p&gt;
&lt;pre id=&quot;code_1722512451500&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: java.sql.SQLTransactionRollbackException: (conn=644) Deadlock found when trying to get lock; try restarting transaction
	at org.mariadb.jdbc.export.ExceptionFactory.createException(ExceptionFactory.java:303)
	at org.mariadb.jdbc.export.ExceptionFactory.create(ExceptionFactory.java:378)
	at org.mariadb.jdbc.message.ClientMessage.readPacket(ClientMessage.java:172)
	at org.mariadb.jdbc.client.impl.StandardClient.readPacket(StandardClient.java:915)
	at org.mariadb.jdbc.client.impl.StandardClient.readResults(StandardClient.java:854)
	at org.mariadb.jdbc.client.impl.StandardClient.readResponse(StandardClient.java:773)
	at org.mariadb.jdbc.client.impl.StandardClient.execute(StandardClient.java:697)
	at org.mariadb.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:93)
	at org.mariadb.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:342)
	at org.mariadb.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:319)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:194)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;SQLTransactionRollbackException 예외가 발생했고 Lock을 얻는 과정에서 Deadlock이 발생했다&quot;&lt;/b&gt; 라는 메시지였다.&lt;/li&gt;
&lt;li&gt;테스트는 다음과 같다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;상품 응모 이벤트가 있고 정해진 수만큼만 응모가 가능하다.&lt;/li&gt;
&lt;li&gt;예를 들어, 선착순으로 100명까지 상품 응모가 가능한 이벤트가 있다고 가정하고 해당 이벤트에서 동시에 100번 응모했을 때 원하는 결과가 나오는지 테스트했다.&lt;/li&gt;
&lt;li&gt;엔티티는 &lt;b&gt;EventProduct&lt;/b&gt;, &lt;b&gt;ProductDrawEvent&lt;/b&gt;, &lt;b&gt;ProductDrawEventHistory&lt;/b&gt;가 있고, 각각 &lt;b&gt;이벤트 상품&lt;/b&gt;, &lt;b&gt;상품 응모 이벤트&lt;/b&gt;, &lt;b&gt;상품 응모 이벤트 이력&lt;/b&gt; 엔티티이다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;특정 유저가 이벤트 응모에 참가하면 &lt;b&gt;ProductDrawEvent&lt;/b&gt;의 drawQuantity가 한개 차감된다. drawQuantity가 0이 된 경우 상품 응모가 모두 소진되었다는 예외 메시지가 반환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &lt;b&gt;EventProduct&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;b&gt;ProductDrawEvent&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;,&amp;nbsp;&lt;/span&gt;&lt;b&gt;ProductDrawEventHistory 엔티티&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722513109250&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Table(name = &quot;event_product&quot;)
@Entity
public class EventProduct {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @Column(name = &quot;name&quot;, nullable = false)
    private String name;

    public static EventProduct of(String name) {
        return new EventProduct(null, name);
    }
}

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Table(name = &quot;product_draw_event&quot;)
@Entity
public class ProductDrawEvent {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;event_product_id&quot;, nullable = false)
    private EventProduct eventProduct;

    @Column(name = &quot;product_quantity&quot;, nullable = false)
    private Long productQuantity;

    @Column(name = &quot;draw_quantity&quot;, nullable = false)
    private Long drawQuantity;

    public boolean hasEventProductDrawQuantities() {
        return this.drawQuantity &amp;gt; 0;
    }

    public ProductDrawEventHistory draw(Long userId) {
        if (hasEventProductDrawQuantities()) {
            drawQuantity--;
            return ProductDrawEventHistory.of(userId, this);
        }

        throw new IllegalStateException(&quot;상품 응모 개수가 모두 소진되었습니다.&quot;);
    }

    public static ProductDrawEvent of(EventProduct eventProduct, Long productQuantity, Long drawQuantity) {
        return new ProductDrawEvent(null, eventProduct, productQuantity, drawQuantity);
    }
}

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Table(name = &quot;product_draw_event_history&quot;,
    uniqueConstraints = {
        @UniqueConstraint(
            name = &quot;uk_product_draw_event_history_product_draw_event_id_user_id&quot;,
            columnNames = {&quot;product_draw_event_id&quot;, &quot;user_id&quot;}
        )
    }, indexes = @Index(name = &quot;idx_product_draw_event_history_user_id&quot;, columnList = &quot;user_id&quot;))
@Entity
public class ProductDrawEventHistory {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;product_draw_event_id&quot;, nullable = false)
    private ProductDrawEvent productDrawEvent;

    @Column(name = &quot;user_id&quot;, nullable = false)
    private Long userId;

    @Column(name = &quot;is_winner&quot;)
    private boolean isWinner;

    @Column(name = &quot;create_at&quot;)
    private LocalDateTime createAt;

    public static ProductDrawEventHistory of(Long userId, ProductDrawEvent productDrawEvent) {
        return new ProductDrawEventHistory(null, productDrawEvent, userId, false, LocalDateTime.now());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;ProductDrawEventEnterUseCase&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상품 응모 이벤트 참가 유스케이스&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722513550151&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class ProductDrawEventEnterUseCase {
    private final ProductDrawEventRepository productDrawEventRepository;
    private final ProductDrawEventHistoryRepository productDrawEventHistoryRepository;

    @Transactional
    public Long draw(Long userId, Long productDrawEventId) {
        ProductDrawEvent productDrawEvent = productDrawEventRepository.findById(productDrawEventId)
            .orElseThrow(() -&amp;gt; new IllegalStateException(&quot;존재하지 않는 이벤트 입니다.&quot;));

        return productDrawEventHistoryRepository.save(productDrawEvent.draw(userId))
            .getId();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;draw 메소드 실행 시 상품 응모 이벤트를 조회하고 남은 응모 회수가 있으면 상품 응모 이벤트 내역을 생성한다.&lt;/li&gt;
&lt;li&gt;동시성 이슈가 발생하는지 확인하기 위해서 비관적 락을 걸지 않고 productDrawEvent를 조회하게 했다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;그리고 아래와 같은 테스트 코드를 작성했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722513829107&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class ProductDrawEventEnterUseCaseTest {

    @Autowired
    public EventProductRepository eventProductRepository;

    @Autowired
    public ProductDrawEventRepository productDrawEventRepository;

    @Autowired
    public ProductDrawEventHistoryRepository productDrawEventHistoryRepository;

    @Autowired
    public ProductDrawEventEnterUseCase productDrawEventEnterUseCase;

    @BeforeEach
    public void setup() {
        productDrawEventHistoryRepository.deleteAll();
        productDrawEventRepository.deleteAll();
        eventProductRepository.deleteAll();
    }

    @Test
    public void 동시에_1000번_이벤트상품_응모_요청_성공_테스트() throws InterruptedException {
        final String eventProductName = &quot;Apple 맥북 프로 14 M2&quot;;
        final long productQuantity = 10L;
        final long drawQuantity = 1000L;

        final int nThreads = 20;

        final EventProduct eventProduct = EventProduct.of(eventProductName);

        // 미리 이벤트 상품과 상품 응모 이벤트를 생성한다.
        eventProductRepository.save(eventProduct);
        ProductDrawEvent productDrawEvent = productDrawEventRepository.save(ProductDrawEvent.of(eventProduct, productQuantity, drawQuantity));

        List&amp;lt;Callable&amp;lt;Long&amp;gt;&amp;gt; tasks = new ArrayList&amp;lt;&amp;gt;();

        // task 목록을 생성한다
        for (long index = 0; index &amp;lt; drawQuantity; index++) {
            final long userId = index;
            tasks.add(() -&amp;gt; productDrawEventEnterUseCase.draw(userId, productDrawEvent.getId()));
        }

        // 스레드 개수롤 고정적으로 nThreads 만큼 생성
        ExecutorService executorService = Executors.newFixedThreadPool(nThreads);

        // 멀티 스레드로 동시에 상품 응모를 진행한다.
        List&amp;lt;Future&amp;lt;Long&amp;gt;&amp;gt; futures = executorService.invokeAll(tasks);

        // 상품 응모 결과 확인
        for (Future&amp;lt;Long&amp;gt; future : futures) {
            // 예외가 발생하지 않았는지 확인
            assertThatCode(future::get)
                .doesNotThrowAnyException();
        }

        // 작업 종료
        executorService.shutdown();

        ProductDrawEvent resultProductDrawEvent = productDrawEventRepository.findById(eventProduct.getId()).get();
        Assertions.assertThat(resultProductDrawEvent.getDrawQuantity()).isEqualTo(0L);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위 테스트를 실행했을 때 레이스 컨디션 증상이 발생해서 동시성 이슈가 발생할 것으로 예상했는데 데드락 예외가 발생하는 것이다.&lt;/li&gt;
&lt;li&gt;ProductDrawEventEnterUseCase를 살펴보았을 때 별도로 Lock을 거는 부분이 없었는데 데드락 예외가 발생한 것이다..... (이게 무슨일이지)&lt;/li&gt;
&lt;li&gt;그래서 찾아보았는데 충격적인 사실을 알았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;테이블에 데이터를 insert 할 때 외래키 제약조건이 걸린 외부 테이블의 id를 저장하는 경우 해당 외부 테이블 행에 공유락을 건다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;즉 자식 테이블에서 데이터 insert 시 실행 시 부모 테이블 행에 공유락이 걸리는 것이다.&amp;nbsp;&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ProductDrawEventEnterUseCase 코드를 보면 &lt;b&gt;productDrawEventHistoryRepository.save(productDrawEvent.draw(userId))&lt;/b&gt; 부분이 있는데 productDrawEventHistory를 insert하고 productDrawEvent의 drawQuantity를 한개 차감한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;단일 스레드로 한개씩 수행된다면 문제 없지만 동시에 여러 스레드가 해당 메소드를 실행하면 deadlock 문제가 발생하는 것이다.&lt;/li&gt;
&lt;li&gt;productDrawEventHistory insert 시 productDrawEvent의 id (외래키)를 저장하고 이때 해당 행에 공유락이 걸린다.&lt;/li&gt;
&lt;/ul&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: 11.4728%;&quot;&gt;순서&lt;/td&gt;
&lt;td style=&quot;width: 41.4729%;&quot;&gt;트랜잭션 1&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%;&quot;&gt;트랜잭션 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.4728%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 41.4729%;&quot;&gt;&lt;br /&gt;insert into product_draw_event_history (user_id, product_draw_event_id) values (1, 1);&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;id 1번 product_draw_event에 공유락 선점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.4728%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 41.4729%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;insert into product_draw_event_history (user_id,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;product_draw_event_id) values (2, 1);&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;id 1번 product_draw_event에 공유락 선점 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.4728%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 41.4729%;&quot;&gt;update&amp;nbsp;product_draw_event&amp;nbsp;&lt;br /&gt;set&amp;nbsp;draw_quantity&amp;nbsp;=&amp;nbsp;draw_quantity&amp;nbsp;-&amp;nbsp;1&amp;nbsp;&lt;br /&gt;where&amp;nbsp;id&amp;nbsp;=&amp;nbsp;1;&lt;br /&gt;&lt;b&gt;id 1번 product_draw_event에 쓰기락 대기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.4728%;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 41.4729%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 47.0542%;&quot;&gt;update&amp;nbsp;product_draw_event&amp;nbsp;&lt;br /&gt;set&amp;nbsp;draw_quantity&amp;nbsp;=&amp;nbsp;draw_quantity&amp;nbsp;-&amp;nbsp;1&amp;nbsp;&lt;br /&gt;where&amp;nbsp;id&amp;nbsp;=&amp;nbsp;1;&lt;br /&gt;&lt;b&gt;id 1번 product_draw_event에 쓰기락 대기 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;id 1번&amp;nbsp; &lt;b&gt;product_draw_event&lt;/b&gt; 행에 공유 락이 걸린 상태에서 트랜잭션 1, 2 모두 id 1번&amp;nbsp; &lt;b&gt;product_draw_event&lt;/b&gt; 에 쓰기 락을 걸려고 하기 때문에 데드락이 발생한다.&lt;/li&gt;
&lt;li&gt;더 정확하게 확인해보기 위해 db client에서 테스트해보았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722516089693&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tx1
start transaction;

insert into product_draw_event_history (create_at, is_winner, user_id, product_draw_event_id)
values (now(), false, 1, 1);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 생성 후 1번 상품 응모 이벤트에 대해서 응모 이력을 생성한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;커밋하지 않으면 1번 상품 응모 이벤트에 공유락이 걸린 상태로 대기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722516149222&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# tx2
start transaction;

update product_draw_event
set draw_quantity = draw_quantity - 1
where id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;새로운 트랜잭션에서 1번 상품 응모 이벤트의 남은 응모 회수를 1 차감하는 update 문을 실행한다.&lt;/li&gt;
&lt;li&gt;update 문 실행 시 쓰기락을 걸기 때문에 1번 상품 응모 이벤트에 공유락이 걸려 있으면 공유락이 해제 될 때까지 대기한다.&amp;nbsp;&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;481&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7CubJ/btsIRv4ACw0/s3FjFU7SKDzk4CaQBX31r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7CubJ/btsIRv4ACw0/s3FjFU7SKDzk4CaQBX31r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7CubJ/btsIRv4ACw0/s3FjFU7SKDzk4CaQBX31r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7CubJ%2FbtsIRv4ACw0%2Fs3FjFU7SKDzk4CaQBX31r1%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;481&quot; height=&quot;181&quot; data-origin-width=&quot;481&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&amp;nbsp;1번 트랜잭션을 커밋하기 전까지 2번 트랜잭션의 update 작업은 대기하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;결론&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;insert 시 foreign key가 걸린 id가 있다면 해당 부모 테이블에 공유락이 걸린다. (dbms 마다 다름)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;혹여나 알 수 없는 데드락 증상이 발생한다면 해당 이슈를 참고해보자.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>deadlock</category>
      <category>Foreign key</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/278</guid>
      <comments>https://bamdule.tistory.com/278#entry278comment</comments>
      <pubDate>Thu, 1 Aug 2024 21:54:45 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 비동기 요청으로 인한 동시성 테스트 하기</title>
      <link>https://bamdule.tistory.com/277</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 게시글은 내용 정리 목적으로 작성되었습니다. 틀린 내용이 있다면 언제든지 말씀해 주세요&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;1. &lt;span data-token-index=&quot;0&quot;&gt;Callable, &lt;span data-token-index=&quot;0&quot;&gt;Future&lt;span data-token-index=&quot;0&quot;&gt;, &lt;span data-token-index=&quot;0&quot;&gt;ExecutorService, &lt;span data-token-index=&quot;0&quot;&gt;Executors&amp;nbsp; 설명&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;2.&lt;span&gt; 비동기 기능 테스트 예제&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;3. 결론&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;API를 개발하다 보면 동시성 테스트를 해야하는 경우가 있다.&lt;br /&gt;예를 들어서 포인트 차감, 선착순 이벤트 응모, 재고 관리가 대표적인 예다.&lt;br /&gt;자바 서블릿은 멀티 스레드 환경을 제공하기 때문에 동시에 여러 요청을 받을 수 있다. &lt;br /&gt;이 경우 적절한 조치를 취해서 동시에 요청한 경우라도 데이터 일관성, 무결성을&amp;nbsp; 유지해야한다.&lt;br /&gt;이러한 비동기 요청에 대해서 테스트할 수 있는 방법을 알아보자&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;1.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-token-index=&quot;0&quot;&gt;Callable,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-token-index=&quot;0&quot;&gt;Future,&lt;span&gt; &lt;/span&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;ExecutorService,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span data-token-index=&quot;0&quot;&gt;Executors&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위 기능은 java.util.concurrent 에서 제공하며 JDK 1.5부터 지원되고 Java의 동시성 프로그래밍을 보다 효율적으로 관리하기 위해 도입된 클래스와 인터페이스이다.&lt;/li&gt;
&lt;li&gt;각 인터페이스 및 클래스에 대해서 간단하게 숙지하고 예제를 보면 쉽게 이해가 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;1) Callable&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Callable은 자바의 인터페이스로, 비동기 작업을 수행하고 결과를 반환하는 메서드를 정의한다.&lt;/li&gt;
&lt;li&gt;Runnable 인터페이스와 유사하지만, Runnable은 반환값이 없고 예외를 던질 수 없는 반면, Callable은 반환값이 있고 예외를 던질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722404699910&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Callable&amp;lt;V&amp;gt; {

    //비동기 작업을 수행하고 결과를 반환한다
    V call() throws Exception;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;2) &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Future&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Future는 비동기 작업의 결과를 나타내는 인터페이스이다.&lt;/li&gt;
&lt;li&gt;비동기 작업이 완료될 때까지 기다리고 작업의 결과를 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722404829655&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Future&amp;lt;V&amp;gt; {
	
    // 작업을 취소한다.
    boolean cancel(boolean mayInterruptIfRunning);

    // 작업이 취소되었는지 확인한다
    boolean isCancelled();
    
    //작업이 완료되었는지 확인한다.
    boolean isDone();

    //작업이 완료될 때까지 기다렸다가 결과를 반환한다
    V get() throws InterruptedException, ExecutionException;
    
    //지정된 시간 동안 기다렸다가 결과를 반환한다. 시간이 초과되면 TimeoutException을 던진다.
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;3) &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;ExecutorService&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ExecutorService는 Executor의 하위 인터페이스로, 더 많은 기능을 제공하여 스레드 풀을 관리하고, 비동기 작업을 제출하고, 종료하는 등의 작업을 할 수 있다.&lt;/li&gt;
&lt;li&gt;Runnable, Callable 인터페이스를 작업으로 등록이 가능하다&lt;b&gt;.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;여러 메소드가 있지만 크게 invokeAll, shutdown 메소드를 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722408279648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;   // tasks를 받아 동시에 실행하고 그 결과를 List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt;에 담아서 반환한다
   &amp;lt;T&amp;gt; List&amp;lt;Future&amp;lt;T&amp;gt;&amp;gt; invokeAll(Collection&amp;lt;? extends Callable&amp;lt;T&amp;gt;&amp;gt; tasks)
        throws InterruptedException;

    // 작업 종료
    void shutdown();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;미리 정의해둔 task를 동시에 실행하고 모두 종료될 때 까지 대기한다.&lt;/li&gt;
&lt;li&gt;설정해둔 스레드 만큼 작업을 동시에 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;3) &lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Executors&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Executors는 다양한 유형의 ExecutorService를 생성할 수 있는 유틸리티 클래스이다&lt;/li&gt;
&lt;li&gt;여러 종류의 스레드 풀을 쉽게 만들 수 있는 정적 팩토리 메서드를 제공한다.&lt;/li&gt;
&lt;li&gt;newFixedThreadPool(int nThreads): 고정된 크기의 스레드 풀을 생성한다&lt;/li&gt;
&lt;li&gt;newCachedThreadPool(): 필요에 따라 새로운 스레드를 생성하고, 이전 스레드를 재사용하는 스레드 풀을 생성한다&lt;/li&gt;
&lt;li&gt;newSingleThreadExecutor(): 단일 스레드를 사용하여 작업을 순차적으로 실행하는 ExecutorService를 생성한다&lt;/li&gt;
&lt;li&gt;newScheduledThreadPool(int corePoolSize): 지정된 수의 스레드를 가진 스케줄링 가능한 스레드 풀을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt; newScheduledThreadPool 사용 예제&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722408691951&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);

        Runnable task = () -&amp;gt; System.out.println(&quot;Executing task at &quot; + System.currentTimeMillis() + &quot; by &quot; + Thread.currentThread().getName());

        // 5초 후에 작업 실행
        scheduledThreadPool.schedule(task, 5, TimeUnit.SECONDS);

        // 초기 지연 후 10초마다 작업 실행
        scheduledThreadPool.scheduleAtFixedRate(task, 5, 10, TimeUnit.SECONDS);

        // 초기 지연 후 10초 간격으로 작업 실행 (이전 작업 종료 후 10초 지연)
        scheduledThreadPool.scheduleWithFixedDelay(task, 5, 10, TimeUnit.SECONDS);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;2.&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;비동기 기능 테스트 예제&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;회원 포인트 차감에 대한 유스케이스를 만들어서 동시에 포인트가 차감될 경우 포인트가 일관성 있게 차감되는지 테스트한다
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특정 회원이 110 포인트를 갖고 있고 1포인트 씩 100번을 동시에 차감했을 경우 10포인트가 남아있는지 테스트한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;테스트 환경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;스프링 부트 3.3.2&lt;/li&gt;
&lt;li&gt;JPA&lt;/li&gt;
&lt;li&gt;H2&lt;/li&gt;
&lt;li&gt;JDK 17&lt;/li&gt;
&lt;li&gt;Junit 5, assertj&lt;/li&gt;
&lt;li&gt;lombok&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;build.gradle dependencies&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722409452484&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722409667033&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: test
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MariaDB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
    driverClassName: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: update&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;UserPoint&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722409221499&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
public class UserPoint {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;
    private Long points;

    @Column(name = &quot;user_id&quot;)
    private Long userId;

    public void use(Long usePoints) {
        if (this.points &amp;lt; usePoints) {
            throw new IllegalArgumentException(&quot;포인트가 부족합니다&quot;);
        }
        this.points -= usePoints;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UserRepository&lt;/h4&gt;
&lt;pre id=&quot;code_1722409302857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserPointRepository extends JpaRepository&amp;lt;UserPoint, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional&amp;lt;UserPoint&amp;gt; findWithExclusiveLockByUserId(Long userId);

    Optional&amp;lt;UserPoint&amp;gt; findByUserId(Long userId);

}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;UserPointDeductUseCase&lt;/h4&gt;
&lt;pre id=&quot;code_1722409403600&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RequiredArgsConstructor
@Service
public class UserPointDeductUseCase {
    private final UserPointRepository userPointRepository;

    @Transactional
    public Long usePoints(Long userId, Long usePoints) {

//      비관적 락을 걸지 않았을 때 포인트 차감을 확인 하기 위해 잠시 주석 
//        UserPoint userPoint = userPointRepository.findWithExclusiveLockByUserId(userId)
//            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 고객입니다.&quot;));

        UserPoint userPoint = userPointRepository.findByUserId(userId)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;존재하지 않는 고객입니다.&quot;));

        userPoint.use(usePoints);
        Long remainingPoints = userPoint.getPoints();

        log.info(&quot;userId : {} usePoints  : {}, remainingPoints : {}&quot;, userId, usePoints, remainingPoints);

        return remainingPoints;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;UserPoinstDeductUseCaseTest&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1722409814763&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ActiveProfiles(&quot;test&quot;)
@SpringBootTest
class UserPointDeductUseCaseTest {

    @Autowired
    public UserPointDeductUseCase userPointDeductUseCase;

    @Autowired
    public UserPointRepository userPointRepository;

    @BeforeEach
    public void setup() {
        userPointRepository.deleteAll();
    }

    @Test
    public void 동시에_포인트_차감_테스트() throws InterruptedException {
        // given
        long userId = 1;
        long remainingPoints = 110L;
        int taskCount = 100;
        long usePoints = 1L;
        int threads = 10;

        // 스레드 실행기 생성
        ExecutorService executorService = Executors.newScheduledThreadPool(threads);

        // 미리 회원 포인트 엔티티를 생성한다.
        userPointRepository.save(new UserPoint(null, remainingPoints, userId));

        List&amp;lt;Callable&amp;lt;Long&amp;gt;&amp;gt; tasks = new ArrayList&amp;lt;&amp;gt;();

        //태스크를 미리 정의한다.
        for (int index = 0; index &amp;lt; taskCount; index++) {
            tasks.add((() -&amp;gt; userPointDeductUseCase.usePoints(userId, usePoints)));
        }

        // when
        //회원 포인트 차감 태스크 호출
        List&amp;lt;Future&amp;lt;Long&amp;gt;&amp;gt; futures = executorService.invokeAll(tasks);


        //then
        // 결과 확인
        for (Future&amp;lt;Long&amp;gt; future : futures) {
            //예외가 발생하지 않았는지 확인
            // 포인트가 부족하면 예외가 발생한다.
            assertThatCode(future::get)
                .doesNotThrowAnyException();
        }

        // 스레드 실행기 종료
        executorService.shutdown();

        UserPoint resultsUserPoint = userPointRepository.findByUserId(userId).get();
        Assertions.assertThat(resultsUserPoint.getPoints()).isEqualTo(remainingPoints - (taskCount * usePoints));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;두가지를 테스트해볼 건데 우선 UserPointDeductUseCase에 비관적락을 걸지 않은 상태에서 테스트를 실행해보자&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722409968357&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
2024-07-31T16:12:16.676+09:00  INFO 10000 --- [pool-2-thread-8] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 105
2024-07-31T16:12:16.676+09:00  INFO 10000 --- [pool-2-thread-4] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 102
2024-07-31T16:12:16.677+09:00  INFO 10000 --- [pool-2-thread-2] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.677+09:00  INFO 10000 --- [pool-2-thread-6] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.678+09:00  INFO 10000 --- [pool-2-thread-1] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.678+09:00  INFO 10000 --- [pool-2-thread-8] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.678+09:00  INFO 10000 --- [pool-2-thread-7] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.679+09:00  INFO 10000 --- [pool-2-thread-3] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 103
2024-07-31T16:12:16.679+09:00  INFO 10000 --- [pool-2-thread-1] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 101
2024-07-31T16:12:16.679+09:00  INFO 10000 --- [pool-2-thread-5] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 101
2024-07-31T16:12:16.679+09:00  INFO 10000 --- [pool-2-thread-8] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 101
2024-07-31T16:12:16.680+09:00  INFO 10000 --- [pool-2-thread-4] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 103
2024-07-31T16:12:16.680+09:00  INFO 10000 --- [pool-2-thread-6] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 100
2024-07-31T16:12:16.681+09:00  INFO 10000 --- [pool-2-thread-8] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.681+09:00  INFO 10000 --- [pool-2-thread-7] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 104
2024-07-31T16:12:16.681+09:00  INFO 10000 --- [pool-2-thread-9] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 102
2024-07-31T16:12:16.681+09:00  INFO 10000 --- [pool-2-thread-6] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 102
2024-07-31T16:12:16.682+09:00  INFO 10000 --- [pool-2-thread-2] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 103
2024-07-31T16:12:16.682+09:00  INFO 10000 --- [ool-2-thread-10] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 103
2024-07-31T16:12:16.683+09:00  INFO 10000 --- [pool-2-thread-3] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 101
2024-07-31T16:12:16.683+09:00  INFO 10000 --- [pool-2-thread-6] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 100
2024-07-31T16:12:16.683+09:00  INFO 10000 --- [pool-2-thread-7] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 100

Expected :10L
Actual   :101L&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;10개의 스레드를 생성하고 총 100번 1포인트씩 차감한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[pool-2-thread-7]&amp;nbsp;&lt;/b&gt;부분을 확인해보면 1~10 까지 스레드 풀에 있는 스레드를 돌아가면서 사용하는 것을 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;100포인트가 차감되어 10포인트가 남은 것을 예상했지만 실제로는 101 포인트가 남아서 테스트가 실패했다.&lt;/li&gt;
&lt;li&gt;테스트가 실패한 이유는 비동기로 UserPointDeductUseCase.usePoints()를 호출해서 동시성 이슈가 발생했기 때문이다.&lt;/li&gt;
&lt;li&gt;하나의 자원을 동시에 수정할 때 순차적으로 값을 수정하지 않기 때문에 마지막으로 업데이트한 값으로 갱신되는데 이 때문에 원하는 결과가 나오지 않은 것이다.&lt;/li&gt;
&lt;li&gt;이를 해결하기 위해서 각 트랜잭션이 특정 자원을 수정할 때 락을 걸어야 한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;락을 거는 동안 자원을 선점하고 트랜잭션이 종료되면 다음 트랜잭션이 해당 자원을 선점하는 방식이다.&lt;/li&gt;
&lt;li&gt;이번 테스트에서는 비관적 락을 걸어 동시성 이슈를 해결할 것이다.&lt;/li&gt;
&lt;li&gt;UserPointDeductUseCase에 주석쳤던 부분을 해제해서 비관적락이 걸린 상태로 다시 테스트를 실행해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1722410417335&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
2024-07-31T16:19:55.784+09:00  INFO 59196 --- [pool-2-thread-2] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 17
2024-07-31T16:19:55.785+09:00  INFO 59196 --- [pool-2-thread-6] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 16
2024-07-31T16:19:55.785+09:00  INFO 59196 --- [pool-2-thread-1] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 15
2024-07-31T16:19:55.786+09:00  INFO 59196 --- [pool-2-thread-7] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 14
2024-07-31T16:19:55.787+09:00  INFO 59196 --- [pool-2-thread-8] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 13
2024-07-31T16:19:55.788+09:00  INFO 59196 --- [pool-2-thread-9] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 12
2024-07-31T16:19:55.789+09:00  INFO 59196 --- [pool-2-thread-5] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 11
2024-07-31T16:19:55.790+09:00  INFO 59196 --- [pool-2-thread-3] o.e.s.service.UserPointDeductUseCase     : userId : 1 usePoints  : 1, remainingPoints : 10&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;포인트가 순차적으로 1포인트씩 총 100포인트가 차감된 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666;&quot;&gt;3. 결론&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;ExecutorService는 동시성 이슈를 테스트하기에 매우 유용하다.&lt;br /&gt;해당 라이브러리가 존재하지 않았다면 직접 API를 호출하는 방식으로 테스트를 해야했을 것이다.&lt;br /&gt;이런 수고를 덜려면 ExecutorService 사용법을 잘 숙지해보자&lt;/blockquote&gt;</description>
      <category>IT/백엔드 필수 교양</category>
      <category>동시성 테스트</category>
      <category>비동기 테스트</category>
      <author>Bamdule</author>
      <guid isPermaLink="true">https://bamdule.tistory.com/277</guid>
      <comments>https://bamdule.tistory.com/277#entry277comment</comments>
      <pubDate>Wed, 31 Jul 2024 16:24:38 +0900</pubDate>
    </item>
  </channel>
</rss>