<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>HS의 (개발)블로그</title>
    <link>https://xerar.tistory.com/</link>
    <description>짹짹</description>
    <language>ko</language>
    <pubDate>Thu, 9 Jul 2026 22:22:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>_sparrow</managingEditor>
    <image>
      <title>HS의 (개발)블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3502460/attach/420dbc1c807547a8a459f587c2cb0ecd</url>
      <link>https://xerar.tistory.com</link>
    </image>
    <item>
      <title>Springboot 레이어드 아키텍처에서 테스트 코드 작성하기</title>
      <link>https://xerar.tistory.com/120</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Layered Architecture에 대해 간략하게 내용을 정리한 후 테스트하기 좋은 상태로 변환해 가는 과정에 대해서 보여주고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Layered Architecture 란?&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;843&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNT1Se/btsnqTXXm4l/8RB4xFr5AQ4VXeLpEHu7uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNT1Se/btsnqTXXm4l/8RB4xFr5AQ4VXeLpEHu7uK/img.png&quot; data-alt=&quot;https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNT1Se/btsnqTXXm4l/8RB4xFr5AQ4VXeLpEHu7uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNT1Se%2FbtsnqTXXm4l%2F8RB4xFr5AQ4VXeLpEHu7uK%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;609&quot; height=&quot;456&quot; data-origin-width=&quot;1127&quot; data-origin-height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 일반적인 아키텍처 패턴으로, n-tier 아키텍처 패턴이라고도 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층화 아키텍처 패턴은 각각의 수평적인 계층으로 구성된 컴포넌트들을 가지고 있으며, 각 계층은 애플리케이션 내에서 특정한 역할을 수행한다.&lt;br /&gt;계층화 아키텍처 패턴은 몇 개의 표준 계층으로 구성되어 있는데, 주로 프레젠테이션, 비즈니스, 영속성, 데이터베이스 등의 계층으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레젠테이션 계층, 비즈니스 계층, 영속성 계층, 데이터베이스 계층이라는 네 가지 계층이 일반적으로 존재한다.&lt;br /&gt;&lt;b&gt;- 프레젠테이션 계층&lt;/b&gt;은 사용자 인터페이스와 브라우저 간의 통신 및 화면 표시 로직을 처리하는데 &lt;span style=&quot;background-color: #ffffff; color: #191919; text-align: start;&quot;&gt;대표적인 구성요소는 View와 Controller가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 비즈니스 계층&lt;/b&gt;은 요청과 관련된 특정 비즈니스 로직을 실행하는 역할을 수행한다. &lt;b&gt;Persistence Layer&lt;/b&gt;에서 불러온 데이터에 대해 로직을 실행 후 &lt;b&gt;Presentation Layer&lt;/b&gt; 로 전달한다. 대표적으로 Service가 있다.&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;b&gt;- 영속성 계층&lt;/b&gt;은 데이터 액세스 및 데이터의 영구 저장을 처리한다. 대표적으로 DB와 상호작용 및 트랜잭션 관리하기 위한 JPA / JDBC 가 이 영역에 해당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 데이터베이스 계층&lt;/b&gt;은 실제 데이터베이스와의 상호작용을 처리한다. MySQL, MariaDB, PostgreSQL, MongoDB 등 데이터베이스가 위치한 계층을 의미한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;Layered Architecture의 문제점&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;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AEXw6/btsnohyQppS/iOwENI990aTgTqxFwIfoo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AEXw6/btsnohyQppS/iOwENI990aTgTqxFwIfoo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AEXw6/btsnohyQppS/iOwENI990aTgTqxFwIfoo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAEXw6%2FbtsnohyQppS%2FiOwENI990aTgTqxFwIfoo0%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;653&quot; height=&quot;445&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지와 같이 클라이언트 요청은 순차적으로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Layers에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;접근하고 마지막 Database Layer에서 필요한 데이터를 조회한 후 다시 역행해서 클라이언트에게 응답한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 요청을 처리하기 위해서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;DB에 접근해 데이터를 조회한 후&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이를 바탕으로 비즈니스 로직이 수행된다는 점이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이로인해 문제점이 여러 개 생긴다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 테스트 코드 작성시에 항상 DB가 필요하며 Spring Test를 기반으로 한 무거운 테스트를 수행해야 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring을 기반으로 Test 하는 것은 스프링 컨테이너를 만들고 빈주입 등등 많은 과정이 필요할 뿐만 아니라 H2를 연결한다고 하면 더 많은 시간이 소요된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이는 테스트 코드가 많다면 매번 테스트를 수행에 몇 초를 대기해야 하는 문제가 발생한다. 매번 테스트를 수행할 때마다 긴 시간이 소요된다고 하면 개발자에게도 테스트수행하기에 부담이 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. DB의 상태는 언제든지 변경될 수 있다. 그렇기 때문에 테스트 성공이 보장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 내부 row에 의존적이기 때문에 특정 결괏값이 항상 보장되지 않을 수 있으며 동일한 결과를 얻기 위해 동일한 DB 상태를 구성해놓아야 한다. 즉 DB에 매우 강결합된 모습이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class GetUserCouponsTest {
    @Autowired
    private UserCouponRepository userCouponRepository;
    private ArrayList&amp;lt;String&amp;gt; userEmails;


    @BeforeEach
    void getUserEmails() {
        this.userEmails = new UserEmailDummy().getUserEmail();
    }

    @Test
    @Order(1)
    @DisplayName(&quot;특정 이메일(유저)가 소유한 쿠폰 갯수 구하기&quot;)
    void getUserCoupons() {
        final int USER_COUPON = 10;
        String userEmail = userEmails.get(0);
		
        // userCouponRepository에서 데이터가 없다면 테스트에 성공할 수 있을까?
        List&amp;lt;UserCoupon&amp;gt; userCoupons = userCouponRepository.findUserCouponByEmail(userEmail);
        int userCouponSize = userCoupons.size();

        assertEquals(USER_COUPON, userCouponSize);
    }
}&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;소규모 프로젝트이거나 단순 CRUD 프로젝트가 아닌 복잡한 프로젝트의 경우 더더 테스트가 힘들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결책 : &amp;nbsp;의존관계 역전 활용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 지향 프로그래밍에서 유지보수 가능하고 확장 가능하도록 하는 원칙인 SOLID 원칙에 대해 알고 있어야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #374151; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;단일 책임 원칙 (Single Responsibility Principle, SRP): 클래스나 모듈은 하나의 책임만 가져야 한다. 즉, 클래스는 단 하나의 변경 이유만을 가져야 하며, 기능이나 역할이 여러 개인 클래스는 분리되어야 한다.&lt;/li&gt;
&lt;li&gt;개방-폐쇄 원칙 (Open-Closed Principle, OCP): 소프트웨어의 엔티티는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 즉, 기존의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;리스코프 치환 원칙 (Liskov Substitution Principle, LSP): 상위 타입의 객체는 하위 타입의 객체로 대체 가능해야 한다. 즉, 하위 클래스는 상위 클래스의 기능을 변경하지 않고 사용할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;인터페이스 분리 원칙 (Interface Segregation Principle, ISP): 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하도록 강요받지 않아야 한다. 즉, 인터페이스는 클라이언트에 필요한 기능에만 집중하도록 분리되어야 한다.&lt;/li&gt;
&lt;li&gt;의존관계 역전 원칙 (Dependency Inversion Principle, DIP): 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 추상화된 인터페이스에 의존해야 한다. 즉, 의존성은 추상화에 의존해야 하며 구체화에 의존해서는 안된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중 &lt;b&gt;&lt;span style=&quot;color: #374151; text-align: left;&quot;&gt;의존관계&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #374151; text-align: left;&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-filename=&quot;스크린샷 2023-07-12 오후 11.24.09.png&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1kmiU/btsnrr1nY9t/HdQegUCykgK7ALPIpOK5V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1kmiU/btsnrr1nY9t/HdQegUCykgK7ALPIpOK5V1/img.png&quot; data-alt=&quot;개선 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1kmiU/btsnrr1nY9t/HdQegUCykgK7ALPIpOK5V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1kmiU%2Fbtsnrr1nY9t%2FHdQegUCykgK7ALPIpOK5V1%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;596&quot; height=&quot;387&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.24.09.png&quot; data-origin-width=&quot;1712&quot; data-origin-height=&quot;1112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.18.57.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgNFtj/btsnnVwxJ5l/89oMakM6lElhzXy8b6fvnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgNFtj/btsnnVwxJ5l/89oMakM6lElhzXy8b6fvnK/img.png&quot; data-alt=&quot;개선 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgNFtj/btsnnVwxJ5l/89oMakM6lElhzXy8b6fvnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgNFtj%2FbtsnnVwxJ5l%2F89oMakM6lElhzXy8b6fvnK%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;624&quot; height=&quot;517&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.18.57.png&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개선 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 전 객체 간 강결합 일때의 문제점은 여러가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;1. 유연성의 저하: 강한 결합은 한 객체의 변경이 다른 객체에 영향을 미칠 수 있다. 하나의 객체를 수정하면 해당 객체와 직접적으로 상호 작용하는 다른 객체도 수정해야 할 수 있다.&lt;br /&gt;2. 재사용의 어려움: 재사용하려는 객체를 사용하기 위해서는 해당 객체와 강하게 결합된 모든 객체도 함께 가져와야 할 수 있다.&lt;br /&gt;3. 테스트의 어려움: 강한 결합은 객체를 독립적으로 테스트하기 어렵게 만든다. 객체를 단독으로 테스트하기 위해서는 해당 객체와 강하게 결합된 다른 객체들을 더미 객체 또는 목 객체로 대체해야 할 수 있다.&lt;br /&gt;4. 확장의 어려움: 강한 결합은 시스템에 새로운 기능을 추가하거나 변경할 때 어려움을 줄 수 있다. 기존의 객체들에 대한 변경이 필요하고, 새로운 객체를 추가하기 위해 기존 객체들과 함께 수정해야 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 후 객체간 결합되어 있던 부분을 &lt;b&gt;인터페이스를 통해 추상화&lt;/b&gt;하면서 느슨한 결합이 되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserService 객체는 UserRepositoryImpl에 무엇이 구현되어 있는지 전혀 알 필요가 없고 의존관계 주입된 객체를 사용하기만 하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 실제 Production 환경에서는 UserRepositoryImpl를 사용하고 테스트 코드 작성 시엔 FakeUserRepository를 객체로 사용할 수 있으며 Business Layer 이후의 Persistence Layer, Database Layer의 강결합을 약하게 할 수 있다.&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-filename=&quot;스크린샷 2023-07-12 오후 11.37.02.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GPDN2/btsnpUXfaam/EF4xZoQePsXGp9tNkhbfdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GPDN2/btsnpUXfaam/EF4xZoQePsXGp9tNkhbfdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GPDN2/btsnpUXfaam/EF4xZoQePsXGp9tNkhbfdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGPDN2%2FbtsnpUXfaam%2FEF4xZoQePsXGp9tNkhbfdK%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;629&quot; height=&quot;478&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.37.02.png&quot; data-origin-width=&quot;1942&quot; data-origin-height=&quot;1476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 각 레이어마다 의존관계 역전을 통해 느슨한 결합을 통해 레이어별로 테스트하기 쉬운 환경을 만들 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.55.47.png&quot; data-origin-width=&quot;2046&quot; data-origin-height=&quot;1428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A9vKl/btsno5LEhBz/YKHOkYfDB0VsX6ldDPwk0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A9vKl/btsno5LEhBz/YKHOkYfDB0VsX6ldDPwk0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A9vKl/btsno5LEhBz/YKHOkYfDB0VsX6ldDPwk0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA9vKl%2Fbtsno5LEhBz%2FYKHOkYfDB0VsX6ldDPwk0K%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;2046&quot; height=&quot;1428&quot; data-filename=&quot;스크린샷 2023-07-12 오후 11.55.47.png&quot; data-origin-width=&quot;2046&quot; data-origin-height=&quot;1428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느슨한 결합으로 특정 레이어를 테스트할 때 진짜 객체가 아닌 테스트 대역을 사용해서 순수 로직만 테스트할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 대역&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dummy : 아무 동작하지 않는 객체&lt;/li&gt;
&lt;li&gt;fake : Local에서 사용허거나 테스트하기 위해 만들어진 가짜 객체로 자체 로직 보유&lt;/li&gt;
&lt;li&gt;stub : 미리 준비된 값을 세팅 후 특정 메서드 호출 시 특정 결괏값 도출&lt;/li&gt;
&lt;li&gt;mock : Stub 역할에 메서드 호출 확인 객체 (검증 로직 보유)&lt;/li&gt;
&lt;li&gt;spy : 메서드 호출 전부 기록 후 확인용 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stub, mock, spy 같은 경우 각 특징이 다르지만 stub의 경우 특정 응답값을 전달해 주는 외부 컴포넌트에, Mock은 Stub의 기능에 &amp;nbsp;특정 결괏값 검증 로직에, spy는 메서드가 실행됐는지 확인해야 하는 로직에 사용되면 좋을듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fake 객체는 &amp;nbsp;다른 테스트 대역과 달리 상호작용할 수 있는 로직이 있어 최대한 본래의 콘크리트 객체를 모방할 수 있어서 다른 테스트 대역보다는 Fake의 테스트 대역이 persistence Layer에 적합하다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot; data-token-index=&quot;0&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;// Fake 객체 활용 예시&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&lt;code&gt;@Test
void 메일_발송시_파라미터가_잘_주입되어_발송되는지_확인한다() {
    // given
    FakeMailSender fakeMailSender = new FakeMailSender();
    CertificationService certificationService = new CertificationService(fakeMailSender);

    // when
    certificationService.send(&quot;test@gmail.com&quot;, &quot;인증코드 발송 : &quot; + 1, &quot;abcde&quot;);

    // then
    assertThat(fakeMailSender.getTo()).isEqualTo(&quot;test@gmail.com&quot;);
    assertThat(fakeMailSender.getSubject()).isEqualTo(&quot;인증코드 발송 : 1&quot;);
    assertThat(fakeMailSender.getText()).isEqualTo(&quot;abcde&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() {
    FakeUserRepository fakeUserRepository = new FakeUserRepository();
    FakePostRepository fakePostRepository = new FakePostRepository();
    TestClockHolder testClockHolder = new TestClockHolder(1670000000000L);

    postService = new PostServiceImpl(fakeUserRepository, fakePostRepository, testClockHolder);

    User user1 = User.builder()
            .id(1L)
            .email(&quot;user1@gmail.com&quot;)
            .nickname(&quot;user1&quot;)
            .verificationCode(&quot;abcx&quot;)
            .status(UserStatus.UNCERTIFIED)
            .lastLoginAt(0L)
            .build();
    fakeUserRepository.save(user1);

    fakePostRepository.save(Post.builder()
            .id(1L)
            .title(&quot;제목&quot;)
            .text(&quot;내용&quot;)
            .createdAt(1665000000000L)
            .updatedAt(null)
            .user(user2)
            .likeCount(0L)
            .build());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 코드는 &lt;a href=&quot;https://github.com/kuninaa19/testable-architecture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/kuninaa19/testable-architecture&lt;/a&gt; 에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드 작성에 대해서는 상세하게 적지 않았는데, 가장 중요한 건 &lt;b&gt;의존관계 역전 &lt;/b&gt;필요성을 이해하는 것이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 외부 컴포넌트와 실제 DB를 사용하지 않게 되면서 &amp;nbsp;통합 테스트 대신 유닛 테스트를 수행할 수 있도록 하며&amp;nbsp;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;순수하게 &amp;nbsp;각 Layer 별로 테스트를 수행할 수 있기 때문이다.&lt;/span&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;개인적으로 테스트 영역은 Business Layer만 테스트해도 되지 않을까라는 생각을 하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presentation Layer는 Business Layer를 DTO로 converting 해주는 역할만 수행하기 때문에 개인적으로는 안 해도 되지 않을까라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 개발자가 DTO와 Domain 객체를 잘 mapping 못했을 경우도 있겠지만 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Persistence Layer는 JPA를 사용하고 도메인을 중심으로 개발된 코드라면 JPA는 단순히 Input/Output 용으로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Entity를 불러오는 역할일 텐데 Presentation Layer와 마찬가지로 안 해도 되지 않을까라는 생각이 들었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단지 저의 생각일 뿐이고 다른 의견이 있으시다면 댓글로 작성해 주신다면 정말 감사하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크로 작성한 github 레포지토리에는 Domain Entity와 DB Entity가 분리되어 있는데 다음 기회에 관련 글을 작성해볼까 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐랄까 Domain Entity와 DB Entity가 결합됐을 때와 분리됐을 때 트레이드오프가 존재하는데 현재 Domain Entity와 DB Entity가 결합된 형태의 개발에 익숙하기 때문에 좀 더 학습한 후 공유하도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 궁금하시다면 &lt;a href=&quot;https://youtu.be/g6Tg6_qpIVc?t=2103&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;를 눌러주세요&lt;/p&gt;</description>
      <category>Project</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/120</guid>
      <comments>https://xerar.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 12 Jul 2023 23:56:01 +0900</pubDate>
    </item>
    <item>
      <title>엔티티의 컬럼은 어떤 타입을 써야할까?</title>
      <link>https://xerar.tistory.com/119</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;u&gt;개요&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1685456783722&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;USER&quot;)
public class User {
    @Id
    @Column(name = &quot;user_id&quot;)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = &quot;user_name&quot;)
    private String name;
    
    @Column(name = &quot;phone_number&quot;)
    private Integer phoneNumber;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;엔티티를 작성하다가 문득 컬럼 타입을 원시형으로 쓰지않고 참조형으로 사용하는지 궁금증이 생겼다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;단순한 생각으로는 참조형으로 사용할때 Null이 허용된다는 점인데 각각의 특징을 간략하게 알아보고 타입 선택 전략에 대해 생각해보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;u&gt;요약&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;불리언, 숫자형 자료형&amp;nbsp;대해서 어떤 타입을 쓸 것인지 고려하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- Not null 이어야한다면 원시형 타입을 사용하자. 그게 아니라면 @Column 어노테이션에서 nullable 옵션을 활용하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- nullable이라면 참조형 타입을 사용하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그 외의 자료형에 대해서는 참조형 타입을 쓸 수 밖에 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;추가적으로 hibernate 문서에서는 참조형 타입을 쓰는 것을 권장한다.&lt;/span&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;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;원시형 타입 [&amp;nbsp;Primitive Type ]&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 원시형은 null 을 저장할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; color: #333333;&quot;&gt;만약 null을 저장하려고 하면 컴파일 오류가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 원시형은 값을 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;쓰레드 별로 할당받는 Stack 메모리에 직접 값을 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 이는 참조형(객체)보다 메모리를 조금 아낄 수 있고 메모리 소비를 최소화 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 원시형은 불변성을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한 번 생성된 값은 변경할 수 없다. 변수에 다른 값을 할당하면 새로운 값이 할당되고 이전의 값은 사리진다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: #333333;&quot;&gt;참조형 타입 [ Reference Type ]&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 참조형은 값으로 null을 가질 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;null은 아무런 객체를 참조하지 않는 상태를 나타내며, 참조형 변수가 null 값을 가지면 해당 변수는 어떠한 객체도 참조하지 않는 것을 의미한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;- 참조형은 객체를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;공통 Heap 메모리를 사용한다. &lt;span style=&quot;text-align: start;&quot;&gt;참조형은 데이터를 직접 저장하는 것이 아니라, 객체의 메모리 주소를 저장하여 해당 객체에 접근한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: #333333;&quot;&gt;&lt;u&gt;부가 정보&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;각 타입별 기본값&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;참조형 타입인 Long, Integer, Boolean의 기본값은 null이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 원시형 타입인 long, Int의 기본값은 0이고, boolean은 false이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이를 바탕으로 원치않는 기본 값 초기화가 될 수 있으니, 값 자체를 진짜로 초기화 하지않았다라는 용도로 null을 사용할 수 있겠고 Not null 컬럼에 대해서는 원시형 타입을 사용하는 것이 좋겠다. 혹은 @Column 어노테이션의 nullable 옵션을 통해 null 허용 여부를 제어할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1685624098113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;USER&quot;)
public class User {
   ...
	// null 금지 설정
    @Column(name = &quot;phone_number&quot;, nullable = false)
    private Integer phoneNumber;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;주의 해야할 점은 원시형 null 값에 대한 에러가 발생하니 null을 저장하지않도록 조심해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;hibernate 문서에서는 nullable 타입(Reference Type)을 사용하라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #000000; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;We recommend that you declare consistently-named identifier attributes on persistent classes and that you use a nullable (i.e., non-primitive) type.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: #333333;&quot;&gt;&lt;u&gt;결론&lt;/u&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;hibernate 문서대로 컬럼 타입을 참조형으로 해도 되고, null 허용되지않는 요소에 대해서는 원시형을 쓰거나 @Column nullable 옵션으로 제어를 하는게 좋다고 생각이 든다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결론적으로 숫자형 자료형과 불리언 자료형에 대해서 &amp;nbsp;어떤 타입을 사용할지 회사 규칙에 따라서 혹은 비즈니스 로직에 적합한 타입을 선택하면 될듯하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;출처&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;[ hibernate 문서 ] :&amp;nbsp;&lt;a style=&quot;color: #333333;&quot; href=&quot;https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#entity-pojo-identifier&quot;&gt;https://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#entity-pojo-identifier&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Web/JPA</category>
      <category>Hibernate</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/119</guid>
      <comments>https://xerar.tistory.com/119#entry119comment</comments>
      <pubDate>Tue, 30 May 2023 23:26:53 +0900</pubDate>
    </item>
    <item>
      <title>다양한(?) 동시성 제어 방법 맛보기</title>
      <link>https://xerar.tistory.com/118</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;u&gt;&lt;/u&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5월쯤이었을까 특정 항공사에서 10000명 정도에 한해 선착순으로 홍콩 무료 항공권 이벤트를 했다. &lt;a href=&quot;https://flights.cathaypacific.com/ko_KR/offers/world-of-winners.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무료 항공권에 혹해서 이벤트를 참여했는데 뉴스기사에서 10만 명 넘는 사람의 신청자가 몰렸다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꽤 많은 사람들이 몰린 이벤트였고 항공사에서 이벤트를 처리하는 방식이 신기했다. 해당 항공사에서 처리한 방식은 클라이언트 요청에 대해 요청비율(RPS)로 제한하는 방식으로 Nginx 같은 웹서버를 통해 요청비율을 넘는 IP주소들은 Queue에 대기시키고 조금씩 그리고 순차적으로 이벤트 페이지에 접속하도록 하는 트래픽 제어하는 방식을 사용한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 물론 동시성 제어하는 방식과는 분명히 다른 케이스이지만 많은 사람들이 몰릴 때 어떻게 동시성 제어를 하는 게 좋고 각 특징 그리고 최적의 방법을 나름대로 생각해 보기 위해 테스트를 수행하게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JPA를 활용한 어플리케이션 레벨에서의 Optimistic Locking&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. MySQL을 활용한 RDB에서의 Pessimistic Locking&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Redis를 활용한 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Distributed Locking&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;3가지 방법을 수행할 것이고 Lock 특징, 장단점 그리고 TPS 수치를 기반으로 설명할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;데이터 구조&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;정말 단순하다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 3.23.38.png&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEBDoQ/btsluepBkj0/lnz9rhM4O4el55NpwwyfI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEBDoQ/btsluepBkj0/lnz9rhM4O4el55NpwwyfI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEBDoQ/btsluepBkj0/lnz9rhM4O4el55NpwwyfI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEBDoQ%2FbtsluepBkj0%2Flnz9rhM4O4el55NpwwyfI0%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;744&quot; height=&quot;332&quot; data-filename=&quot;스크린샷 2023-06-26 오후 3.23.38.png&quot; data-origin-width=&quot;1780&quot; data-origin-height=&quot;794&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. JPA를 활용한 Optimistic Locking&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Version 어노테이션을 사용한 트랜잭션 충돌 체크하는 방식이고 &lt;b&gt;최초 커밋만 인정&lt;/b&gt;하는 방식이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션 커밋&lt;/b&gt;시 @Version 필드값을 데이터베이스의 컬럼값과 비교하고 값을 +1을 수행한다. 동일하지 않을 경우 예외발생한다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 트랜잭션을 커밋하기 전까지 트랜잭션이 충돌하는지 알 수 없다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 구현 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;낙관적 락을 사용하기 위해 version 컬럼 만들기&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Version 어노테이션만 있다면 낙관적 락을 따로 사용하지 않아도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1687760514632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;EVENT&quot;)
public class Event {
    @Id
    @Column(name = &quot;event_id&quot;)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = &quot;event_name&quot;)
    private String name;

    @Column(name = &quot;quantity&quot;)
    private int quantity;

    @Column(name = &quot;rest_quantity&quot;)
    private int restQuantity;

    @Version
    private int version;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;Service Layer&lt;/p&gt;
&lt;pre id=&quot;code_1687763261143&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TicketService {

	...

// 이벤트 신청에 대한 티켓 발급 로직
    @Transactional
    public CreateTicketResponseDto save(CreateTicketRequestDto createTicketRequestDto) {
    // 고객 정보 조회
        Customer customer = customerRepository.findById(createTicketRequestDto.getCustomerId()).orElseThrow(IllegalArgumentException::new);
	// 이벤트 정보 조회
        Event event = eventRepository.findById(createTicketRequestDto.getEventId()).orElseThrow(IllegalArgumentException::new);

	// 티켓 생성
        Ticket aTicket = Ticket.createTicket(customer, event);        
	// 티켓 저장
        Ticket ticket = this.ticketRepository.save(aTicket);

        return CreateTicketResponseDto.of(ticket);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;티켓이 정말로 발급되어도 되는지 검증 로직이 필요하다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이벤트 티켓 수량이 10개이고 발급 가능한 수량이 0인 경우 발급되어선 안되기 때문이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;Ticket Entity&lt;/p&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = &quot;TICKET&quot;)
public class Ticket {

// 영속화 전 수행하는 로직
    @PrePersist
    public void beforeSave() {
      	...
        decreaseRestQuantity();
    }

    private void decreaseRestQuantity() {
        if (event != null) {
        // 이벤트 객체에서 발급 수량이 남았는지 검증하고 수량을 제거할 것이다.
            event.decreaseRestQuantity(1);
        }
    }

   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Service Layer가 아닌 엔티티에 넣어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JPA를 활용한 코드를 작성했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@PrePersist 어노테이션으로&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;영속화(persist) 하기 전&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;발급 가능한 수량이 있는지 확인하고 감소하도록 했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 발급 수량은 Ticket 객체에서 메시지를 전달해 Event 객체에서 검증 및 처리되도록 했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉 Service Layer에서 보이지 않는 로직은 이렇다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;Ticket 생성&amp;gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 영속화하기 전 티켓 잔여량이 있는지 체크하고 수량 감소&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;Ticket 저장&amp;gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 낙관적 락 작동방식 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;낙관적 락은 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;조회시점부터 수정 시점까지 보장하기 위한 용도다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@Transactional annotation에 의해 Service Layer의 비즈니스 로직이 끝나는 순간 영속성 컨텍스트가 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;flush()를 수행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;flush()를 수행하면 쓰기 지연으로 수행되지 않았던 UPDATE SQL문을 수행하게 되는데 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;데이터베이스 버전값이 현재 버전이 아니면&lt;/span&gt; OptimisticLockException이 발생하게 되고 문제없다면&amp;nbsp;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;트랜잭션 커밋을 수행해 데이터를 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;테스트&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;event 수량 10개에 대한 테스트를 JMeter로 수행했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 조건은 1초에 50 Thread로 1회 수행하는 테스트를 진행했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Event 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 80.697675%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_name&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;quantity&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 54px;&quot;&gt;rest_quantity&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 54px;&quot;&gt;version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;항공권 행사&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 18px;&quot;&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;발급 가능한 수량이 10개뿐인데 &amp;nbsp;50 Threads를 사용했는데도 다 발급받지 못했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JMeter 클라이언트 요청 성공이 초록색이다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N6Xxa/btslud5SZf1/YNBu7UtPAQXTZ6tBwypEV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N6Xxa/btslud5SZf1/YNBu7UtPAQXTZ6tBwypEV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N6Xxa/btslud5SZf1/YNBu7UtPAQXTZ6tBwypEV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN6Xxa%2Fbtslud5SZf1%2FYNBu7UtPAQXTZ6tBwypEV1%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;168&quot; height=&quot;245&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 발생했을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Spring에 2가지 종류의 에러가 발생한다.&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;1. Lock을 얻기 위해 시도하다가 데드락 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start;&quot;&gt;co&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://m.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:&quot;&gt;m.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:&lt;/a&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Deadlock found when trying to get lock; try restarting transaction ...&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;2. version과 rest_quantity값이 맞지 않아 에러 발생&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start;&quot;&gt;org.hibernate.StaleStateException:&amp;nbsp;Batch&amp;nbsp;update&amp;nbsp;returned&amp;nbsp;unexpected&amp;nbsp;row&amp;nbsp;count&amp;nbsp;from&amp;nbsp;update&amp;nbsp;[0];&amp;nbsp;actual&amp;nbsp;row&amp;nbsp;count:&amp;nbsp;0;&amp;nbsp;expected:&amp;nbsp;1;&amp;nbsp;statement&amp;nbsp;executed:&amp;nbsp;/*&amp;nbsp;update&amp;nbsp;co&lt;/span&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;http://m.event.fcfssystem.model.Event&quot;&gt;m.event.fcfssystem.model.Event&lt;/a&gt;&lt;span style=&quot;background-color: #fafafa; color: #000000; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;*/ update EVENT set event_name=?, quantity=?, rest_quantity=?, version=? where event_id=? and version=? ..&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 에러가 발생한 일어난 이유 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 요청 클라이언트가 커넥션을 통해 DB에 접근했지만 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Lock을 &lt;/span&gt;얻을 수 없어 대기하다가 데드락으로 rollback 되어버리는 경우&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션 커밋하는 순간 조건 컬럼값이 불일치하는 경우&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ &lt;b&gt;결론 &lt;/b&gt;]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 낙관적 락은 트랜잭션이 대부분 충돌되지 않을 것이라는 판단하에 실행되는데 다수의 트랜잭션이 진행될 때 version값이 이미 변경된 경우 실패하게 된다. 그렇기 때문에 순차적인 선착순 발급이 불가능할 수 있다. 많은 요청에 대해서는 불가능이라고 보는 게 맞다.&lt;br /&gt;이러한 문제를 해결하는 방법으로 다시 락을 얻기 위해 시도하는 방법이 있다. 하지만 다시 다른 트랜잭션과 경합해야 하는 상황에 직면하기 때문에 락을 얻는다는 보장이 없다는 한계점이 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;끝으로 수량 100개에 대해 1000Threads로 테스트했을 때의 결과다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;평균 50정도의 TPS와 실패를 볼 수 있다. 하지만 조건에 맞게 수량이 딱 100건만 발급된다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 5.30.17.png&quot; data-origin-width=&quot;2210&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/besUl5/btsluiFGS8l/GeXA7bymekcSyxJrkkp9S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/besUl5/btsluiFGS8l/GeXA7bymekcSyxJrkkp9S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/besUl5/btsluiFGS8l/GeXA7bymekcSyxJrkkp9S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbesUl5%2FbtsluiFGS8l%2FGeXA7bymekcSyxJrkkp9S1%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;666&quot; height=&quot;394&quot; data-filename=&quot;스크린샷 2023-06-26 오후 5.30.17.png&quot; data-origin-width=&quot;2210&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. MySQL을 활용한 RDB에서의 Pessimistic Locking&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터베이스 트랜잭션 락 매커니즘에 의존하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 쿼리에 SELECT FOR UPDATE 구문을 사용하면서 &lt;b&gt;해당 row&lt;/b&gt;에&lt;b&gt; Lock&lt;/b&gt;을 걸어 다른 트랜잭션이 조회하지 못하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락과 다르게 version 컬럼이 필요 없으며 낙관적 락과 다르게 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 구현 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Service Layer&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1687781159502&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TicketService {

	...

// 이벤트 신청에 대한 티켓 발급 로직
    @Transactional(timeout = 5)
    public CreateTicketResponseDto save(CreateTicketRequestDto createTicketRequestDto) {

        Customer customer = customerRepository.findById(createTicketRequestDto.getCustomerId()).orElseThrow(IllegalArgumentException::new);
	// 쓰기락을 사용한 이벤트 정보 조회
        Event event  = eventRepository.findByIdWithLock(createTicketRequestDto.getEventId()).orElseThrow(IllegalArgumentException::new);

        Ticket aTicket = Ticket.createTicket(customer, event);        
        Ticket ticket = this.ticketRepository.save(aTicket);

        return CreateTicketResponseDto.of(ticket);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;EventRepository&lt;/p&gt;
&lt;pre id=&quot;code_1687782720402&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface EventRepository extends JpaRepository&amp;lt;Event, Long&amp;gt; {

// SELECT FOR UPDATE SQL문과 동일한 역할을 한다.
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select e FROM Event e WHERE e.id = :eventId&quot;)
    Optional&amp;lt;Event&amp;gt; findByIdWithLock(@Param(&quot;eventId&quot;) Long eventId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Lock 어노테이션으로 비관적 락을 사용하겠다고 명시해 주면 DB에서의 SELECT FOR UPDATE를 수행한다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 사용하고 싶지 않다면 &amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;Querydsl 쿼리빌더의 setLockMode() 메서드를 활용하는 방법도 있다.&lt;/span&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;낙관적 낙관적 락 테스트와 동일하게 event 티켓 수량 10개에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;1초에 50 Thread로 1회 수행하는 테스트를 진행했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 80.697675%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_name&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;quantity&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 54px;&quot;&gt;rest_quantity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;항공권 행사&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 18px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ticket 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 80.813954%; height: 190px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;ticket_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;created_at&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;customer_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;...(그외 컬럼)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;..&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;21:03:02.269000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;..&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;21:03:02.326000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.340000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.355000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.367000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.381000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.395000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;11&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.407000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.417000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 21:03:02.425000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JMeter 클라이언트 요청 성공이 초록색이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 10.02.30.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpv9Yr/btslhdlYugC/GPiSwUwZ6ddx2KsQPrXNOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpv9Yr/btslhdlYugC/GPiSwUwZ6ddx2KsQPrXNOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpv9Yr/btslhdlYugC/GPiSwUwZ6ddx2KsQPrXNOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpv9Yr%2FbtslhdlYugC%2FGPiSwUwZ6ddx2KsQPrXNOk%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;167&quot; height=&quot;362&quot; data-filename=&quot;스크린샷 2023-06-26 오후 10.02.30.png&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 결론과 비관적 락 추가 정보 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 낙관적 락에 비해 비교적(?) 순차처리가 가능해졌다. 쿼리를 실행하는 순서대로 해당 트랜잭션이 락을 부여받고 다른 트랜잭션은 락을 얻기 위해 대기상태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 비관적 락은 TIME_OUT을 설정해야 한다. 그렇지 않으면 락을 얻기 위해 트랜잭션이 대기하게 되는데 이는 데드락을 유발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 객체지향 코딩에 부합하지는 않겠지만 엔티티가 아닌 스칼라 타입을 조회할 때도 사용가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝으로 낙관적 락 테스트와 동일한 수량 100개에 대해 1000Threads로 테스트했을 때의 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 맞게 수량이 딱 100건만 발급될 뿐만 아니라 낙관적 락에 비해 높은 TPS가 나왔는데 아무래도 락을 먼저 부여받은 트랜잭션에 대해서 로직이 진행되다 보니 충돌이 일어나지 않아 더 좋은 성능을 내는 것 같다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-26 오후 9.53.34.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opjMm/btsluUkEvxo/QFNf25A6UQc0BIF3HZruyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opjMm/btsluUkEvxo/QFNf25A6UQc0BIF3HZruyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opjMm/btsluUkEvxo/QFNf25A6UQc0BIF3HZruyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopjMm%2FbtsluUkEvxo%2FQFNf25A6UQc0BIF3HZruyK%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;506&quot; height=&quot;1306&quot; data-filename=&quot;스크린샷 2023-06-26 오후 9.53.34.png&quot; data-origin-width=&quot;1382&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Redis를 활용한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;Distributed Locking&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 &lt;b&gt;&lt;span style=&quot;background-color: #f7f7f8; color: #374151; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;여러 서버에서 동시에 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;접근하는 자원에 대한 액세스를 제어하는 방법&lt;/b&gt;으로 분산 락을 사용하면 락을 부여받기 전까지 대기하거나 포기해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락은 여러 가지 구현 방법이 있는데 Redis의 라이브러리 Redisson을 활용한 분산락을 구현할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 활용한 분산락 사용 시 클라이언트는 Redis로부터 락을 부여받아야 로직을 실행할 수 있도록해 다른 클라이언트의 접근을 방지한다. 그리고 이는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;DB락을 사용하지 않으면서 데이터베이스의&lt;/span&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 구현 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 Aspect를 만들어서 분산 락을 구현하고자 한다.&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;Aspect를 만든 이유는 2가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. AOP가 나오게 된 이유와 일치하는데 &lt;b&gt;비즈니스 로직 안에 부가기능을 수행하는 Lock 로직이 존재하면 이해하기 어려워지고 테스트하기 어려워진다&lt;/b&gt;. 또한 매번 Lock이 필요한 비즈니스 로직마다 작성해야 해서 중복이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Service Layer에 Lock에 대한 코드를 넣게 되면 @Transactional에 의해 트랜잭션이 먼저 시작되고 Lock을 부여받게 되면서 &lt;b&gt;데이터 무결성을 보장받을 수 없기 때문이다.&lt;/b&gt; JPA 작동원리와 lock/unlock의 순서 차이로 인해 발생하게 되는데 아래의 코드에서 설명하도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;분산 락 annotation&lt;/p&gt;
&lt;pre id=&quot;code_1687940885331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DistributedLock {

    /* The name of the resource to lock */
    String value();

    /* wait for lock aquisition */
    long waitTime() default 4L;

    /* automatically unlock */
    long leaseTime() default 3L;

    TimeUnit timeUnit() default 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;size18&quot;&gt;분산 락 Aspect&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무런 설정 없이 annotation만 작성한다면 Transcational Aspect -&amp;gt; DistributedLock Aspect가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@DistributedLock이 @Transactional보다 우선적으로 실행되어야 하기 때문에 @Order를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 케이스는 해당안되지만 다른 Aspect가 최우선적으로 실행되어야 할 경우가 있을 수 있으니 @Order를 사용하지 않고 다음 pointcut으로 @Transactional이 진행되도록 할 방법을 찾으려고 했으나 아직 못 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1687941796208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
@Order(value = 1)
public class DistributedLockAspect {

    private static final String PREFIX = &quot;lock&quot;;
    private static final String SEPARATOR = &quot;_&quot;;
    private final RedissonClient redissonClient;

    public DistributedLockAspect(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Pointcut(&quot;@annotation(com.event.fcfssystem.aop.DistributedLock)&quot;)
    private void distributedLock() {
    }

    @Around(value = &quot;distributedLock()&quot;)
    public Object lock(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);

		// Redis Base의 특정 이름의 Lock 인스턴스를 부여 받는다. (Lock 부여 요청x)
        RLock lock = redissonClient.getLock(createLockName(signature.getParameterNames(), pjp.getArgs(), distributedLock));

        try {
        	// Lock을 얻기위한 시도를 한다. 락 부여 대기시간과 자동적으로 released되는 시간도 설정했다.
            boolean available = lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());
            if (!available) {
                return false;
            }

            return pjp.proceed();
        } catch (InterruptedException e) {
            throw new InterruptedException();
        } finally {
            lock.unlock();
        }
    }

    private String createLockName(String[] parameterNames, Object[] args, DistributedLock distributedLock) {
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext();

        for (int i = 0; i &amp;lt; parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }

        Object value = parser.parseExpression(distributedLock.value()).getValue(context, Object.class);

        return PREFIX + SEPARATOR + distributedLock.value() + SEPARATOR + value;
    }
}&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;size18&quot;&gt;Service Layer&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 어플리케이션 레벨의 Optimistic Lock, DB를 사용한 Pessimistic Lock 이 사용되는 코드를 전부 제거했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 락을 사용하지 않으면 무조건 동시성에 문제가 발생하는 코드다.&lt;/p&gt;
&lt;pre id=&quot;code_1687941852424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class TicketService {

	...

	// 분산 락 어노테이션을 사용했다.
    @DistributedLock(value = &quot;#createTicketRequestDto.getEventId()&quot;)
    @Transactional(timeout = 5, rollbackFor = InterruptedException.class)
    public CreateTicketResponseDto save(CreateTicketRequestDto createTicketRequestDto) {
        Customer customer = customerRepository.findById(createTicketRequestDto.getCustomerId()).orElseThrow(IllegalArgumentException::new);
        Event event = eventRepository.findById(createTicketRequestDto.getEventId()).orElseThrow(IllegalArgumentException::new);

        Ticket aTicket = Ticket.createTicket(customer, event);
        Ticket ticket = this.ticketRepository.save(aTicket);

        return CreateTicketResponseDto.of(ticket);
    }
}&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;구현 2번에 Service Layer에 Lock 관련 코드를 넣으면 무결성 보장이 안된다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA는 flush()가 수행되면서 임시 SQL 저장소에 넣어둔 SQL을 수행하게 되는데 insert를 수행하고 트랜잭션을 커밋하기 전 unlock 되어버리면서 데이터 정합성에 문제가 발생한다.&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;Case 1) &lt;b&gt;Lock이 먼저 해제&lt;/b&gt;된 후 트랜잭션 커밋이 수행되는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transcation start -&amp;gt; lock -&amp;gt; 비즈니스 로직 -&amp;gt; unlock -&amp;gt; flush() -&amp;gt; commit()&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unlock 하고 메서드가 종료된다. 그 후 데이터베이스 관련 작업이 수행되는데 unlock 되는 순간 다른 클라이언트는 lock을 부여받을 수 있게 된다. 그리고 메서드 내부 로직이 실행되고 동일하게 데이터 베이스 처리까지 수행하게 되면서 수량이 안 맞게 된다.&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;Case 2) &lt;b&gt;Lock을 먼저 발급&lt;/b&gt;받고 트랜잭션이 시작되는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock -&amp;gt; transcation start -&amp;gt; 비즈니스 로직 -&amp;gt; flush() -&amp;gt; commit() -&amp;gt; unlock&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock을 부여받은 클라이언트는 내부 로직 실행뿐만 아니라 데이터 베이스 처리가 완료되기 전까지 lock을 해제하지 않으면서 데이터 정합성을 보장한다.&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;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 락 테스트와 동일하게 event 티켓 수량 10개에 대해&lt;span&gt;&amp;nbsp;&lt;/span&gt;1초에 50 Thread로 1회 수행하는 테스트를 진행했다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Event 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 80.697675%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;event_name&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 54px;&quot;&gt;quantity&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 54px;&quot;&gt;rest_quantity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;항공권 행사&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 10%; height: 18px;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ticket 테이블&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 80.813954%; height: 190px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;ticket_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;created_at&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;customer_id&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 10px;&quot;&gt;...(그외 컬럼)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;..&lt;span&gt; 18:43:05.679000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;..&lt;span&gt; 18:43:05.738000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:05.831000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:05.931000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.031000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.137000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.237000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.332000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.444000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;.. 18:43:06.538000&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 20%; height: 18px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;JMeter 클라이언트 요청 성공이 초록색이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-28 오후 6.46.14.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n9jvP/btslKj50lJn/Z0KtcXdKkKu1crVQpZC9jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n9jvP/btslKj50lJn/Z0KtcXdKkKu1crVQpZC9jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n9jvP/btslKj50lJn/Z0KtcXdKkKu1crVQpZC9jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn9jvP%2FbtslKj50lJn%2FZ0KtcXdKkKu1crVQpZC9jk%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;200&quot; height=&quot;285&quot; data-filename=&quot;스크린샷 2023-06-28 오후 6.46.14.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;536&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 에러 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 lease time을 짧게 설정해서 자동적으로 unlock 되도록 설정해 에러를 발생시켰다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;production의 경우 lock Advice의 finally에 lock 관련 분기처리를 넣고 에러가 난 부분에 대해 로그를 수집해야 할 듯하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;락이 없는 스레드가 unlock 시도하여 에러 발생&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Exception&amp;nbsp;in&amp;nbsp;thread&amp;nbsp;&quot;Thread-2&quot;&amp;nbsp;java.lang.IllegalMonitorStateException:&amp;nbsp;attempt&amp;nbsp;to&amp;nbsp;unlock&amp;nbsp;lock,&amp;nbsp;not&amp;nbsp;locked&amp;nbsp;by&amp;nbsp;current&amp;nbsp;thread&amp;nbsp;by&amp;nbsp;node&amp;nbsp;id:&amp;nbsp;71cfd14a-b18a-4f17-a6e8-2556353f6c25&amp;nbsp;thread-id:&amp;nbsp;56&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;[ 결론과 분산 락 추가 정보 ]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 요청한 순서대로 DB에 저장하게 되면서 순차처리가 가능해졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 로직 따라 waitTime과 leaseTime 옵션을 설정해야 할 듯하다. waitTime이 너무 짧다면 대기하다가 포기해 버리면서 유저 관점에서는 선착순에 들었는데 대기시간을 너무 짧게 줘서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기회를&lt;span&gt; &lt;/span&gt;&lt;/span&gt;박탈당하게 되는 것이고 leaseTime이 너무 짧다면 로직이 진행되다가 락이 해제되고 정상적으로 진행되지 않을 것이다. 하지만 보통의 API라면 길어야 몇 초 내에 응답결과를 만들어내야 하는 게 정상이니 크게 고려를 안 해도 될지도 모르겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;끝으로 다른 락 테스트와 동일한 수량 100개에 대해 1000Threads로 테스트했을 때의 결과다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;lock을 먼저 부여받고 비즈니스 로직이 수행된 후 데이터가 저장되는 방식이다 보니 TPS가 거의 고정되어 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;잔여 수량이 고갈되어 에러가 발생하게될 로직도 비즈니스 로직이 먼저 진행되지 않아 TPS 수치가 일관된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL을 활용한 pessimistic Lock보다 TPS가 생각보다 안 나오는데 더 빠른 처리가 가능하도록 할 방법을 생각해 봐야겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그게 아니라면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;분산된 환경에서 클라이언트의 요청 순서가 중요한 로직에 대해 사용해야 할 듯하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-06-28 오후 7.08.43.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bupEg0/btslJ9WTzR0/7aaCwNczHs9MR2tRl4baJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bupEg0/btslJ9WTzR0/7aaCwNczHs9MR2tRl4baJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bupEg0/btslJ9WTzR0/7aaCwNczHs9MR2tRl4baJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbupEg0%2FbtslJ9WTzR0%2F7aaCwNczHs9MR2tRl4baJ1%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;628&quot; height=&quot;1124&quot; data-filename=&quot;스크린샷 2023-06-28 오후 7.08.43.png&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치면서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어플리케이션 기반 락, RDB의 락, Redis를 활용한 락으로 3가지 락에 대해서 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일상의 궁금증에서 시작되어 각 환경에서의 대표적인 락을 테스트를 진행해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 Name Lock, Spin Lock 등등 수많은 락들이 있는데 다음에 또 알아볼 기회가 있지 않을까 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개요에 작성한 항공사이벤트는 웹서버로 트래픽 제어하고 내부 로직은 분산 락을 사용해서 선착순을 구현했을지도 모르겠다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또 생각해 보니 클라이언트로부터 패킷이 도달하는 것까지는 고려가 되어있지 않았는데 이런 부분은 어떻게 해결했을지&amp;nbsp;궁금하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;작성한 코드는 &lt;a href=&quot;https://github.com/kuninaa19/concurrency-control&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;에 있다.&lt;/p&gt;</description>
      <category>Project</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/118</guid>
      <comments>https://xerar.tistory.com/118#entry118comment</comments>
      <pubDate>Tue, 30 May 2023 13:55:27 +0900</pubDate>
    </item>
    <item>
      <title>쿠버네티스 - 미니쿠베와 큐브 컨트롤</title>
      <link>https://xerar.tistory.com/116</link>
      <description>&lt;h1&gt;요약&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;minkube는 실 운영용 k8s은 너무나도 설정할 것들이 많고 여러개의 서버가 존재하기때문에 개발 환경 및 테스트용으로는 적합하지않다. 그렇기때문에 나온게 minkube(+ k3s) 단 하나의 서버에서 master와 node를 관리할 수 있도록 해줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubectl은 쿠버네티스에 명령을 전달하기위한 프로그램&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;k8s를 활용하기위해서는 마스터 서버 3개 그리고 노드가 다중으로 있어야하기때문에 초기설정이 나무나도 할게 많아서 연습하기에 어렵다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 하나의 서버안에서 마스터서버와 노드를 함께 활용하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 마스터서버 (api server, etcd , controller, scheduler) + 노드(proxy, kubelet)를 함께 운영&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2 CPUs or more 2GB of free memory 20GB of free disk space&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1635092111419&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;minikube 활성화

windows 일시 hyper-V(가상화) 설정이 켜져있어야함 혹은 virtual box , k3s ..

hyper-v 활성화 DISM /Online /Enable-Feature /All /FeatureName:Microsoft-Hyper-V

# 끄기 
bcdedit /set hypervisorlaunchtype off

# 켜기 
bcdedit /set hypervisorlaunchtype auto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미니쿠베 기본 명령어&lt;/h3&gt;
&lt;pre id=&quot;code_1635092064978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 버전확인 
minikube version 

# 가상머신 시작 (반드시 관리자권한으로 실행)
minikube start --driver=hyperv

# driver 에러가 발생한다면 virtual box를 사용
minikube start --driver=virtualbox

# 특정 k8s 버전 실행
minikube start --kubernetes-version=v1.20.0 

# 상태확인 
minikube status 

# 정지
minikube stop 

# 삭제 
minikube delete 

# ssh 접속
minikube ssh 

# ip 확인
minikube ip

Docker desktop 존재시
Docker Dekstop이 설치되어 있으면 minikube가 기본으로 docker driver를 사용합니다. 
docker 드라이브를 사용할 경우 서비스 노출 방법입니다.

# 쿠버네티스 서비스 이름이 wordpress라면.. 
minikube service wordpress&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;kubectl&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스에 명령을 전달하기 위한 kubectl 프로그램을 설치&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;windows&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;curl -LO &amp;lt;&lt;a href=&quot;https://storage.googleapis.com/kubernetes-release/release/v1.20.0/bin/windows/amd64/kubectl.exe&quot;&gt;https://storage.googleapis.com/kubernetes-release/release/v1.20.0/bin/windows/amd64/kubectl.exe&lt;/a&gt;&amp;gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TEST&lt;/h3&gt;
&lt;pre id=&quot;code_1635092078959&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl version&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Kubernetes</category>
      <category>k8s</category>
      <category>kubectl</category>
      <category>kubernetes</category>
      <category>minikube</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/116</guid>
      <comments>https://xerar.tistory.com/116#entry116comment</comments>
      <pubDate>Mon, 25 Oct 2021 01:16:36 +0900</pubDate>
    </item>
    <item>
      <title>nvm windows에 설치하기</title>
      <link>https://xerar.tistory.com/115</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;nvm은?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;nodejs를 커맨드로 빠른 설치와 다양한 버전을 사용할 수 있게 도와주는 버전 관리 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nvm 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nvm 설치전 정석적인 nodejs 프로그램은 삭제해주고 진행해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nvm-setup.zip을 다운로드&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/coreybutler/nvm-windows/releases&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1635064117132&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Releases &amp;middot; coreybutler/nvm-windows&quot; data-og-description=&quot;A node.js version management utility for Windows. Ironically written in Go. - coreybutler/nvm-windows&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; data-og-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cvwBAo/hyL5Gfdiuy/WsMqGHee8UMYi1cV3AjGh0/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469&quot;&gt;&lt;a href=&quot;https://github.com/coreybutler/nvm-windows/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/coreybutler/nvm-windows/releases&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cvwBAo/hyL5Gfdiuy/WsMqGHee8UMYi1cV3AjGh0/img.jpg?width=841&amp;amp;height=469&amp;amp;face=0_0_841_469');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Releases &amp;middot; coreybutler/nvm-windows&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A node.js version management utility for Windows. Ironically written in Go. - coreybutler/nvm-windows&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;473&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KWPDn/btriFjIiymv/fqbzAH4l1548Hlvq6nSb2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KWPDn/btriFjIiymv/fqbzAH4l1548Hlvq6nSb2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KWPDn/btriFjIiymv/fqbzAH4l1548Hlvq6nSb2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKWPDn%2FbtriFjIiymv%2FfqbzAH4l1548Hlvq6nSb2K%2Fimg.png&quot; data-origin-width=&quot;572&quot; data-origin-height=&quot;473&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설치 루트는 반드시 C:\특정경로\nvm으로 설정할 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 제공경로로 설치시 node버전을 변경못하는 오류 있음&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;nvm Example :&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 설치&lt;/p&gt;
&lt;pre id=&quot;code_1635064422021&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$nvm install lts

$nvm install latest

$nvm install 특정노드버전&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치된 노드버전 출력&lt;/p&gt;
&lt;pre id=&quot;code_1635064437426&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$nvm list&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 버전 사용&lt;/p&gt;
&lt;pre id=&quot;code_1635062839055&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ nvm use 16
Now using node v16.9.1 (npm v7.21.1)
$ node -v
v16.9.1
$ nvm use 14
Now using node v14.18.0 (npm v6.14.15)
$ node -v
v14.18.0
$ nvm install 12
Now using node v12.22.6 (npm v6.14.5)
$ node -v
v12.22.6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/Node.js</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/115</guid>
      <comments>https://xerar.tistory.com/115#entry115comment</comments>
      <pubDate>Sun, 24 Oct 2021 17:35:36 +0900</pubDate>
    </item>
    <item>
      <title>프로세스 조회 및 종료</title>
      <link>https://xerar.tistory.com/114</link>
      <description>&lt;pre id=&quot;code_1635044461140&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pgrep -f app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;67&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FE3ZQ/btriHCUJlcg/MW8xLKWwsyoKtw74DQfKGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FE3ZQ/btriHCUJlcg/MW8xLKWwsyoKtw74DQfKGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FE3ZQ/btriHCUJlcg/MW8xLKWwsyoKtw74DQfKGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFE3ZQ%2FbtriHCUJlcg%2FMW8xLKWwsyoKtw74DQfKGK%2Fimg.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;67&quot; data-ke-mobilestyle=&quot;widthOrigin&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1635044709504&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

PID=$(pgrep -f app)
if [ -z &quot;$PID&quot; ]
then
  echo &quot;&amp;gt; 종료할 것 없음.&quot; &amp;gt;&amp;gt; /home/ubuntu/deploy.log
else
  kill -15 $PID

  sleep 5
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방식으로 프로세스를 조회하고 종료시킬 수 있다.&lt;/p&gt;</description>
      <category>DevOps/linux</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/114</guid>
      <comments>https://xerar.tistory.com/114#entry114comment</comments>
      <pubDate>Sun, 24 Oct 2021 12:07:02 +0900</pubDate>
    </item>
    <item>
      <title>몽고DB 초기 설정</title>
      <link>https://xerar.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;몽고DB 설치&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몽고DB 포트번호 변경&lt;/p&gt;
&lt;pre id=&quot;code_1622030226700&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo vi /etc/mongod.conf

# network interface
net:
    port: 27027 -&amp;gt; 임의의 포트번호로 변경
    bindIp: 127.0.0.1 -&amp;gt; 0.0.0.0

----------------------------

 sudo service mongod restart&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;몽고DB 접속 계정만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mongodb.com/features/mongodb-authentication&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;몽고DB 인증&lt;/a&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1622032210202&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mongo --port portNumber

계정 추가
&amp;gt; use admin
switched to db admin
&amp;gt; db.createUser(
...   {
...     user: &quot;admin&quot;,
...     pwd: &quot;adminPW&quot;, // or cleartext password
...     roles: [ { role: &quot;root&quot;, db: &quot;admin&quot; } ] (슈퍼바이저)
## 		roles: [ { role: &quot;userAdminAnyDatabase&quot;, db: &quot;admin&quot; }] (일반 어드민)
...   }
... )

로그인
use admin
db.auth(&quot;userID&quot;,&quot;userPW&quot;)

-------------------
/etc/mongod.conf

security: 
	authorization: enabled&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1622032886082&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;How to Authenticate Users in MongoDB

mongo &quot;mongodb://Admin:${DBPASSWORD}@&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt;/admin?authSource=admin&quot;

어드민 계정
MONGO_URL=mongodb://example:example!@localhost:47172/exampleDB?authSource=admin
DB admin - ID : example PW : example!

일반 DB 계정
MONGO_URL=mongodb://example:example!@localhost:47172/exampleDB
DB exampleDB - ID : example PW : example!&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DB/Mongo</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/101</guid>
      <comments>https://xerar.tistory.com/101#entry101comment</comments>
      <pubDate>Wed, 26 May 2021 21:48:55 +0900</pubDate>
    </item>
    <item>
      <title>node, npm 버전 관리</title>
      <link>https://xerar.tistory.com/93</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;버전 확인 및 캐시 제거&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1614857315920&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//버전확인
$ node -v
v6.0.0
// 기존에 사용 했을수도 있는 npm cache값을 제거해줍니다.
$ sudo npm cache clean -f&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;a3fa&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;n 모듈 설치 [ &lt;a href=&quot;https://www.npmjs.com/package/n&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;npm n&lt;/a&gt; ]&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;9a43&quot; data-selectable-paragraph=&quot;&quot;&gt;n 은 Node.js 버전을 관리해주는 플러그인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1614857335715&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//n모듈 설치
$ sudo npm install -g n

// node 안정버전 설치
$ sudo n stable

//lts버전 설치
$ sudo n lts

//특정버전 설치 ex&amp;gt; $ n &amp;lt;version&amp;gt; 
$ sudo n 10.0.0

//버전삭제 
$ sudo n rm 10.0.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;6a74&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;npm 업데이트&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1614857384103&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//npm 버전 확인
$ npm -v
3.1.0
$ sudo npm i -g npm
/usr/local/bin/npm -&amp;gt; /usr/local/lib/node_modules/npm/bin/npm-cli.js
/usr/local/bin/npx -&amp;gt; /usr/local/lib/node_modules/npm/bin/npx-cli.js
+ npm@6.8.0
added 2 packages from 2 contributors, removed 2 packages and updated 7 packages in 20.345s
$npm -v
3.10.0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;NPM 버전 업데이트 안될때&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1614857645803&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type 'hash -r' on your server bash.

Then check your 'npm -v' again&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;binary 링크 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1614863324664&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$sudo ln -sf /usr/local/bin/node /usr/bin/node&lt;/code&gt;&lt;/pre&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 311px; top: 1569.72px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Web/Node.js</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/93</guid>
      <comments>https://xerar.tistory.com/93#entry93comment</comments>
      <pubDate>Thu, 4 Mar 2021 17:10:52 +0900</pubDate>
    </item>
    <item>
      <title>Docker timezone 적용하기</title>
      <link>https://xerar.tistory.com/87</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7JEQT/btqRTQZJzYN/YPMuDgfqA6B8mUGkke6Gt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7JEQT/btqRTQZJzYN/YPMuDgfqA6B8mUGkke6Gt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7JEQT/btqRTQZJzYN/YPMuDgfqA6B8mUGkke6Gt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7JEQT%2FbtqRTQZJzYN%2FYPMuDgfqA6B8mUGkke6Gt0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;nodejs Dockerfile에만 ENV로 TZ을 작성해준다&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ENV TZ=Asia/Seoul&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 적용 완료&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/87</guid>
      <comments>https://xerar.tistory.com/87#entry87comment</comments>
      <pubDate>Wed, 30 Dec 2020 18:03:10 +0900</pubDate>
    </item>
    <item>
      <title>Redis local 설치하기</title>
      <link>https://xerar.tistory.com/80</link>
      <description>&lt;p&gt;&lt;a href=&quot;https://redis.io/topics/quickstart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;redis.io/topics/quickstart&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1608821661275&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Redis Quick Start &amp;ndash; Redis&quot; data-og-description=&quot;*Redis Quick Start This is a quick start document that targets people without prior experience with Redis. Reading this document will help you: Download and compile Redis to start hacking. Use redis-cli to access the server. Use Redis from your application&quot; data-og-host=&quot;redis.io&quot; data-og-source-url=&quot;https://redis.io/topics/quickstart&quot; data-og-url=&quot;https://redis.io/topics/quickstart&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://redis.io/topics/quickstart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://redis.io/topics/quickstart&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;Redis Quick Start &amp;ndash; Redis&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;*Redis Quick Start This is a quick start document that targets people without prior experience with Redis. Reading this document will help you: Download and compile Redis to start hacking. Use redis-cli to access the server. Use Redis from your application&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;redis.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;b&gt;wget &lt;a href=&quot;http://download.redis.io/redis-stable.tar.gz&quot;&gt;http://download.redis.io/redis-stable.tar.gz&lt;/a&gt; &lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;tar xvzf redis-stable.tar.gz&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt; cd make&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;make&lt;/b&gt; 작동안되면 오류에 뜨는 라이브러리 설치&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행테스트&lt;/p&gt;
&lt;p&gt;$ redis-server 구동시 다른 터미널에서 $redis-cli ping 입력시 pong 반납확인&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;레디스 지속운영을 위한 환경설정&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;압축해제시 생성된 redis.conf 를 /etc/redis 에 옮긴 후, 환경을 구성&lt;br /&gt;환경 구성시 supervised 를 no에서 systemd으로 으로 변경하여 서비스환경으로 구동될 수 있도록 변경&lt;br /&gt;환경 구성시 dir 을 /var/lib/redis 설정하여 덤프파일및 저장공간 위치설정&lt;/p&gt;
&lt;p&gt;$sudo mkdir /etc/redis&lt;/p&gt;
&lt;p&gt;$sudo cp /redis-stable/redis.conf /etc/redis&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;$sudo vi /etc/redis/redis.conf&lt;/p&gt;
&lt;pre id=&quot;code_1608823103625&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# If you run Redis from upstart or systemd, Redis can interact with your
# supervision tree. Options:
#   supervised no      - no supervision interaction
#   supervised upstart - signal upstart by putting Redis into SIGSTOP mode
#   supervised systemd - signal systemd by writing READY=1 to $NOTIFY_SOCKET
#   supervised auto    - detect upstart or systemd method based on
#                        UPSTART_JOB or NOTIFY_SOCKET environment variables
# Note: these supervision methods only signal &quot;process is ready.&quot;
#       They do not enable continuous liveness pings back to your supervisor.
supervised systemd &amp;lt; 수정


# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir /var/lib/redis &amp;lt; 수정&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;sudo vi /etc/systemd/system/redis.service 입력&lt;/p&gt;
&lt;pre id=&quot;code_1608823322727&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Unit]
Description=Redis In-Memory Data Store
After=network.target

[Service]
User=redisuser
Group=redisuser
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf
ExecStop=/usr/local/bin/redis-cli shutdown
Restart=always

[Install]
WantedBy=multi-user.target&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;6. redis를 구동할 사용자 생성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;redis를 구동할 사용자를 생성한다. 사용자 생성시 사용자 디렉토리는 제외시키고&lt;br /&gt;redis 환경설정에서 저장공간을 이용할 /var/lib/redis 디렉토리 생성및 redis사용자&lt;br /&gt;접근권한을 준다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1608823343945&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$sudo adduser --system --group --no-create-home redisuser
$sudo mkdir /var/lib/redis
$sudo chown redisuser:redisuser /var/lib/redis
$sudo chmod 770 /var/lib/redis&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;7. 시스템 부팅시 실행되도록 구성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;시스템 부팅시 redis가 자동실행 되도록 서비스에 등록 한다.&lt;/p&gt;
&lt;p&gt;$sudo systemctl enable redis&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실행결과&lt;/p&gt;
&lt;pre id=&quot;code_1608823377929&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Created symlink from /etc/systemd/system/multi-user.target.wants/redis.service to /etc/systemd/system/redis.service.
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;8. redis구동 및 상태확인&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;redis를 서비스명령으로 구동및 상태를 확인한다.&lt;/p&gt;
&lt;p&gt;서비스 시작및 상태확인&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1608823434570&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$sudo systemctl start redis
$sudo systemctl status redis&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1608823447416&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 결과

● redis.service - Redis In-Memory Data Store
   Loaded: loaded (/etc/systemd/system/redis.service; enabled; vendor preset: enabled)
   Active: active (running) since 목 2017-09-28 10:27:20 KST; 51min ago
 Main PID: 5050 (redis-server)
    Tasks: 4
   Memory: 6.3M
      CPU: 1.576s
   CGroup: /system.slice/redis.service
           └─5050 /usr/local/bin/redis-server 127.0.0.1:6379   &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;서비스 재시작&lt;/p&gt;
&lt;pre id=&quot;code_1608823462994&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$sudo systemctl restart redis&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;원격접속 허가 &lt;span style=&quot;color: #666666;&quot;&gt;/etc/redis/redis.conf&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #666666;&quot;&gt;기본적으로 서비스는 로컬에서만 접속할 수가 있다 이를 원격으로 접속할 수 있도록 변경한다.&lt;br /&gt;/etc/redis로 이동후 redis.conf 를 변경한다.&lt;br /&gt;환경 구성시 bind 항목을 기존 127.0.0.1 을 0.0.0.0 으로 변경후&lt;br /&gt;서비스를 재시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;환경설정 bind /etc/redis/redis.conf&lt;/p&gt;
&lt;pre id=&quot;code_1608823517072&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;################################## NETWORK #####################################
# By default, if no &quot;bind&quot; configuration directive is specified, Redis listens
# for connections from all the network interfaces available on the server.
# It is possible to listen to just one or multiple selected interfaces using
# the &quot;bind&quot; configuration directive, followed by one or more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 lookback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 0.0.0.0 &amp;lt; 127.0.0.1 에서 수정&lt;/code&gt;&lt;/pre&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: -11px; top: 444.8px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>DB/Redis</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/80</guid>
      <comments>https://xerar.tistory.com/80#entry80comment</comments>
      <pubDate>Fri, 25 Dec 2020 00:25:59 +0900</pubDate>
    </item>
    <item>
      <title>let's encrypt 인증서 갱신 안될때</title>
      <link>https://xerar.tistory.com/78</link>
      <description>&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Error: couldn't get currently installed version for /opt/eff.org/certbot/venv/bin/letsencrypt:&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;600&quot; height=&quot;133&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UF2T2/btqNqdSGd3r/sYDHScR7hjk6fiqWjdkhU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UF2T2/btqNqdSGd3r/sYDHScR7hjk6fiqWjdkhU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UF2T2/btqNqdSGd3r/sYDHScR7hjk6fiqWjdkhU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUF2T2%2FbtqNqdSGd3r%2FsYDHScR7hjk6fiqWjdkhU1%2Fimg.png&quot; width=&quot;600&quot; height=&quot;133&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;- 해결 :&amp;nbsp;&lt;a href=&quot;https://community.letsencrypt.org/t/solved-usr-local-sbin-certbot-auto-851-usr-local-sbin-certbot-auto-opt-eff-org-certbot-venv-bin-letsencrypt-not-found/42545/3&quot;&gt;https://community.letsencrypt.org/t/solved-usr-local-sbin-certbot-auto-851-usr-local-sbin-certbot-auto-opt-eff-org-certbot-venv-bin-letsencrypt-not-found/42545/3&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;$&amp;nbsp;&lt;/span&gt;&lt;span&gt;rm -rf ~/.local/share/letsencrypt&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;$ sudo&amp;nbsp;&lt;/span&gt;&lt;span&gt;rm -rf ~/.local/share/letsencrypt&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;$ sudo&amp;nbsp;&lt;/span&gt;&lt;span&gt;rm -rf /opt/eff.org/certbot/&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;$ certbot-auto renew --debug&lt;/p&gt;</description>
      <category>DevOps/AWS</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/78</guid>
      <comments>https://xerar.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 14 Nov 2020 18:23:56 +0900</pubDate>
    </item>
    <item>
      <title>Docker - Mysql DB 정보 옮기기</title>
      <link>https://xerar.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Mysql DB정보를 다른 서버로 옮기려고 할 때는 저장된 데이터를 추출해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB dump를 도와주는 프로그램이 되어줄 &lt;b&gt;MySQL Workbench&lt;/b&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;직관적으로 DB를 사용하고 있었지만 이런 기능도 있는 줄은 몰랐다. 쉽고 빠르게 DB 정보를 전달할 수 있으니 같이 보자&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 정보를 가져오려는 DB에 접속하고 아래와 같이 &lt;b&gt;Data Export&lt;/b&gt;를 클릭한다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Data Import는 데이터를 가져올 수 있다. 일단 추후에 공부해보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chav6C/btqIasV2tg0/HiXMNkOiPFUtRWoYvCkFCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chav6C/btqIasV2tg0/HiXMNkOiPFUtRWoYvCkFCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chav6C/btqIasV2tg0/HiXMNkOiPFUtRWoYvCkFCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fchav6C%2FbtqIasV2tg0%2FHiXMNkOiPFUtRWoYvCkFCk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. 가져올 DB 스키마 선택하고 테이블 선택, 아래와 같이 옵션을 클릭해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4xxqq/btqIh0q0sUd/oQkAcN0r8Spg1KLssDu65k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4xxqq/btqIh0q0sUd/oQkAcN0r8Spg1KLssDu65k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4xxqq/btqIh0q0sUd/oQkAcN0r8Spg1KLssDu65k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4xxqq%2FbtqIh0q0sUd%2FoQkAcN0r8Spg1KLssDu65k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Export to Self-contained File 옵션을 사용해야 하나의 sql파일 안에 모든 DB정보가 담긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Export to Dump Project Folder 옵션은 하나의 디렉터리 안에 각각의 테이블 정보가 담긴다. sql파일이 가져올 테이블 개수만큼 존재하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 하면 Export는 끝났다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;필자는 DB를 세팅을 도커 &lt;span style=&quot;color: #333333;&quot;&gt;mysql&amp;nbsp;이미지 빌드할 때&lt;/span&gt; DB내용을 저장해서 사용할 생각이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;sql파일을 mysql 폴더에 저장하자&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lPPIz/btqIrtZD6dR/AFASQjjQQHsNrIjFmBJKkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lPPIz/btqIrtZD6dR/AFASQjjQQHsNrIjFmBJKkK/img.png&quot; data-alt=&quot;n3-testdb.sql이 Export한 파일이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lPPIz/btqIrtZD6dR/AFASQjjQQHsNrIjFmBJKkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlPPIz%2FbtqIrtZD6dR%2FAFASQjjQQHsNrIjFmBJKkK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;figcaption&gt;n3-testdb.sql이 Export한 파일이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Dockerfile 내부의 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1599667690857&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM mysql:5.7

# Mysql 환경설정
ENV MYSQL_DATABASE example
ENV MYSQL_USER root
ENV MYSQL_ROOT_PASSWORD sql123!
ENV MYSQL_ROOT_HOST '%'

#DB Dump
COPY n3-testdb.sql /docker-entrypoint-initdb.d/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;/docker-entrypoint-initdb.d/ 위치에 sql파일을 저장시켜주면 도커 컨테이너 내부에 DB정보가 담기게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps/Docker</category>
      <category>도커 #db dump #mysql workbench #Data Export</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/76</guid>
      <comments>https://xerar.tistory.com/76#entry76comment</comments>
      <pubDate>Thu, 10 Sep 2020 10:00:24 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트 비동기처리에 대한 promise 사용법</title>
      <link>https://xerar.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #0593d3; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Promise() 사용법&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 링크에 들어가서 보면 Promise를 통한 비동기 처리에 대한 사용법이 두 가지가 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하나는 &lt;b&gt;Promise를 객체에 담아서 사용하는 방법&lt;/b&gt;이고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하나는 &lt;b&gt;return Promise를 사용하는 방법&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처음엔 상세히 공부하지않고 예제대로 Promise를 객체선언하고 사용했었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 &lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;Promise&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;를 리턴해서나오는 값을 사용하기 위한 콜백 함수 then()을 정확하게 명시할 수 있어서였다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;처음엔 문제없이 처리했었는데 AJAX요청이 포함된 비동기 처리가 필요할 때 생겼다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;AJAX로 비동기 요청을 하고 처리된 값을 어딘가에 대입해야 하는 상황에서 Async와 Await를 사용하게 됐는데 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;구체적인 모든 코드가 작성되어있지는 않지만 그 문제의 코드다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594097822330&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const detectLng = (data) =&amp;gt; {
    const lng new ((resolve, reject) =&amp;gt; {
        const xhr = new XMLHttpRequest();

        xhr.open(&quot;POST&quot;, &quot;localhost/detectLangs&quot;, true);
        xhr.setRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);

        const chat = {content : '안녕'};

        xhr.onload = () =&amp;gt; resolve(xhr.responseText);
        xhr.onerror = () =&amp;gt; reject(xhr.statusText);

        xhr.send(JSON.stringify(chat));
    });
    lng.then((response) =&amp;gt; {
        const data = JSON.parse(response);
		console.log('XHR 응답 data값',data.langCode);
        return data.langCode;
        
    }).catch(error =&amp;gt; {
        console.log(`에러발생: ${error}`);
    });
};


console.log('detect() 시작 이전');
const detectLngCode = await detectLng();
console.log('대입된 detectLngCode값',detectLngCode); //undefined가 뜬다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;코드 작동 결과 결괏과이&lt;span style=&quot;color: #333333;&quot;&gt; undefined가 뜬다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blgetr/btqFtwrhzqF/kS6G9CssN9AkRsnELlk7ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blgetr/btqFtwrhzqF/kS6G9CssN9AkRsnELlk7ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blgetr/btqFtwrhzqF/kS6G9CssN9AkRsnELlk7ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblgetr%2FbtqFtwrhzqF%2FkS6G9CssN9AkRsnELlk7ZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로그를 보면 알 수 있듯이 XHR응답처리에 대한 반환까지 기다려주지 않아서 함수에 대한 반환 값이 변수 detectLngCode에 담기지 않는 걸 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이것에 대해서 찾아보니 &lt;b&gt;Await는 Promise객체가 반환되어야만 동기적으로 처리&lt;/b&gt;가 된다고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위의 코드에서는 Promise객체가 변수에 선언되어서 사용되었기 때문에 Promise객체 반환처리가 필요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 맨 하단에 return을 작성해주었는데&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594104695205&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const detectLng = (data) =&amp;gt; {
    const lng new Promise ((resolve, reject) =&amp;gt; {
        :
        :
        :
    });
    lng.then((response) =&amp;gt; {
        :
        :
        :
    }).catch(error =&amp;gt; {
        :
        :
    });
    
    return lng; &amp;lt;- 나머지코드 동일하고 이부분만 추가해서 반환
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;맨 위의 코드와는 다르게 동기적으로 처리되어서 변수값이 제대로 표기된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hygge/btqFraJUkLX/5W96k9l77SFxp0wE46SC00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hygge/btqFraJUkLX/5W96k9l77SFxp0wE46SC00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hygge/btqFraJUkLX/5W96k9l77SFxp0wE46SC00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHygge%2FbtqFraJUkLX%2F5W96k9l77SFxp0wE46SC00%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 내가 예상한 것과는 반대로 리턴 값은 {&quot;langCode&quot;:&quot;ko&quot;}가 아닌 ko가 필요했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또 뒤적뒤적거린결과 값이 제대로 안 뜨는 이유를 찾았다. &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/then&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;promise.then()&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위의 링크에서&lt;b&gt; 'then()&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;메서드는&lt;b&gt; Promise&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;를 리턴&lt;/b&gt;하고 두 개의 콜백 함수를 인수로 받습니다, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이어지는 &lt;/span&gt;then&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;호출들을 손쉽게 연결할 수 있습니다.'라고&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt; 작성되어있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위의 코드에선&amp;nbsp; Promise객체는&amp;nbsp; resolve 된 값을 lng에 대입 후 처리를 then()에서 하겠다고 한 것이어서 반환은 lng만 된다. 그래서 이어지는 then에 대한 값은 처리가 안된 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하단처럼 lng에 이어서 바로 then을 작성했는데 resolve 된 값을 then으로&amp;nbsp; lng변수 반환 전에 콜백으로 처리하겠다.라고 하면 되겠다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1594105654482&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const detectLng = (data) =&amp;gt; {
    const lng new Promise ((resolve, reject) =&amp;gt; {
        :
        :
        :
    }).then((response) =&amp;gt; { &amp;lt;- 나머지코드 동일하고 이부분만 변환 lng.then() =&amp;gt; .then() lng에 이어서 사용
        :
        :
        :
    }).catch(error =&amp;gt; {
        :
        :
    });
    
    return lng; 
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결괏값은 원하던 대로 ko가 나온다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OJdC9/btqFra4guUu/2kBLNoHenuA3edH5VrMqEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OJdC9/btqFra4guUu/2kBLNoHenuA3edH5VrMqEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OJdC9/btqFra4guUu/2kBLNoHenuA3edH5VrMqEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOJdC9%2FbtqFra4guUu%2F2kBLNoHenuA3edH5VrMqEK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;return new Promise 사용법&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;앞서 말했듯이 Await는 Promise객체가 반환이 되어야만 동기적으로 처리한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위의 코드에선 함수 하단에 return으로 Promise객체가 담긴 변수를 반환한다는걸 명시했지만 다른 방법이 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;return new Promise 사용법은 Promise객체 비동기 처리 이후에 자동 반환을 해준다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594097669413&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const detectLng = () =&amp;gt; {
    return new Promise((resolve, reject) =&amp;gt; { &amp;lt;-바뀐부분
        const xhr = new XMLHttpRequest();

        xhr.open(&quot;POST&quot;, &quot;localhost/detectLangs&quot;, true);
        xhr.setRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);

        const chat = {content : '안녕'};

        xhr.onload = () =&amp;gt; resolve(xhr.responseText);
        xhr.onerror = () =&amp;gt; reject(xhr.statusText);

        xhr.send(JSON.stringify(chat));
    }).then((response) =&amp;gt; { 
        console.log('XHR 응답 data값',data.langCode);
        return data.langCode;
        
    }).catch(error =&amp;gt; {
        console.log(`에러발생: ${error}`);
    });
};


console.log('detect() 시작 이전');
const detectLngCode = await detectLng();
console.log('대입된 detectLngCode값',detectLngCode);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;코드 작동 결과 결괏값이&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt; ko 뜬다&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4U5pb/btqFpAQbNgZ/9t6exz5eKlCB6kQKCdXd5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4U5pb/btqFpAQbNgZ/9t6exz5eKlCB6kQKCdXd5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4U5pb/btqFpAQbNgZ/9t6exz5eKlCB6kQKCdXd5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4U5pb%2FbtqFpAQbNgZ%2F9t6exz5eKlCB6kQKCdXd5k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 return new Promise의 단점은 객체가 어느 곳에 담기지 않기 때문에 함수안에 여러개의 Promise객체를 사용해야하는 상황과&amp;nbsp; promise.all 메소드을 사용할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;# 정리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. Async&amp;amp;Await에서는 Promise객체가 반환되어야 동기적으로 처리가 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Promise객체를 변수 선언해서 사용할지 선언하지 않고 사용할지 구분하자&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. then() 메서드는 처리가 약간 다를 때가 있다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Web/JavaScript</category>
      <category>자바스크립트 #비동기처리 #promise #promise.then()</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/71</guid>
      <comments>https://xerar.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 8 Jul 2020 09:00:51 +0900</pubDate>
    </item>
    <item>
      <title>자식 팝업창에서 부모함수 호출하기</title>
      <link>https://xerar.tistory.com/70</link>
      <description>&lt;p&gt;팝업창에서 부모창에 부모의 함수호출요청하고 팝업창닫음&lt;/p&gt;
&lt;pre id=&quot;code_1593923264901&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	opener.location.href='javascript:parent_function();';
	window.close();
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Web/JavaScript</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/70</guid>
      <comments>https://xerar.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 5 Jul 2020 13:29:19 +0900</pubDate>
    </item>
    <item>
      <title>Moment.js 시간표기와 시간 차이 구하기</title>
      <link>https://xerar.tistory.com/65</link>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1592729005023&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import moment from 'moment';
require('moment-timezone');
// import timezone from 'moment-timezone';
moment.tz.setDefault(&quot;Asia/Seoul&quot;);

//시간 비교 표기하기위해서
moment.updateLocale('ko', {
    relativeTime : {
        future: &quot;%s 후&quot;,
        past:   &quot;%s 전&quot;,
        s  : &quot;%d 초&quot;,
        ss : &quot;%d 초&quot;,
        m:  &quot;%d 분&quot;,
        mm: &quot;%d 분&quot;,
        h:  &quot;%d 시간&quot;,
        hh: &quot;%d 시간&quot;,
        d:  &quot;%d 일&quot;,
        dd: &quot;%d 일&quot;,
        M:  &quot;%d 달&quot;,
        MM: &quot;%d 달&quot;,
        y:  &quot;%d 년&quot;,
        yy: &quot;%d 년&quot;
    }
});

// result는 select query의 결과
console.log('1순위 시간',moment(result[0].time).subtract(9,'hours'));
// 시간대 Asia/Seoul로 되어있어서 한국어로 표기된다.
console.log('LLLL 변환',moment(result[0].time).subtract(9,'hours').format('LLLL'));
console.log('현재시간',moment().format());

// 현재와 고정시간과 비교
moment(result[0].time).subtract(9, 'hours').fromNow();
console.log('상대시간', moment('2020-06-19 19:57',&quot;YYYY-MM-DD HH:mm&quot;).fromNow());
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Mysql 시간 저장하는 칼럼으로 Datetime으로 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;저장할 때 한국시간으로 저장했고 위의 코드처럼 result [0]로 결괏값을 가져왔는데 서울시간으로 저장했으나 이상하게도 9시간 빠르게 표시됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 현재시간과 비교할때 현재 서울시간과 동일하게 하기 위해서 &lt;span style=&quot;color: #333333;&quot;&gt;임의적으로 &lt;/span&gt;저장됐던 시간대에서 9시간을 뺐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내가 GMT와 UTC를 잘 이해하지못하고 사용해서 그런 것 같긴 한데 임의적으로 시간 값을 수정해서 비교하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1시간을 45분되면 인식되어서 상대 시간 인식하는 것을 따로 처리해줬다&lt;/p&gt;
&lt;pre id=&quot;code_1592733185957&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;moment.relativeTimeThreshold('m', 60);
moment.relativeTimeThreshold('h', 24);
moment.relativeTimeThreshold('M', 30);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://stackoverrun.com/ko/q/11423427&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;참고&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</description>
      <category>Web/Node.js</category>
      <category>JavaScript #Moment.js #시간표기 #시간차이 구하기</category>
      <author>_sparrow</author>
      <guid isPermaLink="true">https://xerar.tistory.com/65</guid>
      <comments>https://xerar.tistory.com/65#entry65comment</comments>
      <pubDate>Sun, 5 Jul 2020 09:00:22 +0900</pubDate>
    </item>
  </channel>
</rss>