<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyeseong.log</title>
        <link>https://velog.io/</link>
        <description>Dev_Hyeseong</description>
        <lastBuildDate>Wed, 26 Mar 2025 17:47:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyeseong.log</title>
            <url>https://velog.velcdn.com/images/hyeseong-int/profile/c6f277ea-e843-4b11-9465-6faace60dba3/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyeseong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyeseong-int" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[동시성 문제 해결하기 - 2(비관적 락, 낙관적 락)]]></title>
            <link>https://velog.io/@hyeseong-int/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-2%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD</link>
            <guid>https://velog.io/@hyeseong-int/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-2%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD</guid>
            <pubDate>Wed, 26 Mar 2025 17:47:40 GMT</pubDate>
            <description><![CDATA[<p>진행하는 프로젝트의 쿠폰 발급 서비스에 동시성 문제가 있음을 파악했다.
이를 테스트하고 해결하는 과정을 기록해보고자 한다.</p>
<hr>
<h1 id="목차">목차</h1>
<ol>
<li>문제상황 정리</li>
<li>비관적 락과 낙관적 락</li>
<li>비관적 락 적용</li>
<li>낙관적 락 적용</li>
<li>결과 비교</li>
<li>결론</li>
</ol>
<hr>
<h1 id="1-문제상황-정리">1. 문제상황 정리</h1>
<blockquote>
<p><a href="https://velog.io/@hyeseong-int/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-1%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%8C%8C%EC%95%85">지난 글</a>에서 쿠폰 발급시 동시성 문제가 발생하는 것을 확인했다.</p>
</blockquote>
<p>1번 테스트는 100개 제한의 쿠폰을 동시에 120회 발급 시도하였고,
2번 테스트는 인당 1개 발급 가능한 쿠폰을 동시에 100회 발급 시도하였다.</p>
<p>결과는 두 테스트 모두 100개, 1개를 초과 발급하여 동시성 문제가 발생하는 것을 확인 하였다.</p>
<p>이 동시성 문제를 해결하기 위해 락을 걸고자 한다.</p>
<h1 id="2-비관적-락과-낙관적-락">2. 비관적 락과 낙관적 락</h1>
<h2 id="21-락이란">2.1. 락이란?</h2>
<p>DB에 여러 사용자, 서비스 혹은 스레드 등이 동시에 접근하는 상황에서 데이터의 무결성, 일관성을 지키기 위한 자원에 대한 잠금
동시성 문제를 해결하기 위해 사용한다.</p>
<h3 id="락의-종류">락의 종류</h3>
<ul>
<li><strong>S-Lock(공유 락,Shared Lock) :</strong> 테이블을 조회 할 경우 사용됨. 여러 사용자가 동시에 데이터를 읽어도 일관성 문제 발생하지 않기 때문에, S-Lock 끼리는 동시 접근 가능. 락이 걸려있는 동안 데이터 변경 불가.</li>
<li><strong>X-Lock(베타 락, Exclusive Lock) :</strong> 데이터의 변경이 발생할 경우 사용됨. 다른 세션, 스레드 등이 자원에 접근하는 것을 막고 해제될 때 까지 읽기 쓰기 접근이 불가능 함. 트랜잭션 끝까지 유지됨.</li>
<li><strong>Update-Lock(업데이트 락) :</strong> 데이터 수정 전 X 락을 걸기 전에 데드락 방지를 위해 거는 락. 일반적으로 Update 쿼리의 필터(WHERE)가 실행되는 과정에서 적용.</li>
<li><strong>Intent-Lock(내재 락) :</strong> 사용자가 요청한 범위에 락을 걸 수 있는지 여부를 빠르게 확인하기 위해 사용 됨.</li>
</ul>
<h2 id="22-비관적-락pessimistic-lock">2.2. 비관적 락(Pessimistic Lock)</h2>
<blockquote>
<p>데이터에 동시성 문제가 발생할 것이라고 가정하고, 수정전에 미리 접근을 제한하는 방식</p>
</blockquote>
<p>비관적 락은 데이터 충돌이 많이 발생할 것 같은 상황에서 주로 사용 됨.
한번에 같은 데이터에 접근을 못하게 막는 방식으로 여러 데이터의 충돌을 방지함.</p>
<p>데이터 접근이 엄격하게 통제되므로 데이터의 일관성이 중요한 경우 사용.</p>
<ul>
<li><strong>장점</strong><ul>
<li>데이터의 일관성과 동시성을 보장할 수 있음</li>
<li>데이터에 대한 잠금을 설정하여 충돌을 방지할 수 있음</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>동시성이 낮아짐. 트랜잭션동안 다른 트랜잭션이 접근할 수 없음</li>
<li>때문에 시스템 성능이 저하될 수  있음</li>
</ul>
</li>
</ul>
<h2 id="23-낙관적-락optimisitc-lock">2.3. 낙관적 락(Optimisitc Lock)</h2>
<blockquote>
<p>데이터를 읽을 때 락을 걸지 않고, 변경 시점에 충돌을 검사하는 방식</p>
</blockquote>
<p>낙관적 락은 데이터 충돌이 비교적 적게 발생할 것 같은 상황에서 주로 사용 됨.
충돌이 발생한 경우, 롤백을 수행하거나, 충돌을 해결하는 로직을 사용.</p>
<p>JPA에서는 Version 필드를 만들어서 해당 필드를 관리함.</p>
<ul>
<li><strong>장점</strong><ul>
<li>동시성이 높아짐. 동시에 여러 트랜잭션이 데이터에 접근하고 변경 할 수 있음</li>
<li>충돌 발생 시 롤백을 피하고 충돌을 해결 할 수 있는 기회를 제공</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>충돌 발생시 롤백이 발생할 수 있음</li>
<li>충돌 업데이트 로직에 따라 뒷 요청이 무시될 수 있다.</li>
</ul>
</li>
</ul>
<h1 id="3-비관적-락-적용">3. 비관적 락 적용</h1>
<p>비관적 락 적용은 쿼리를 진행할 때 락을 거는 키워드를 붙여서 적용할 수 있다.</p>
<p>일반 쿼리를 적용하는 레파지토리 일 경우 <code>@Lock(LockModeType.PESSIMISTIC_WRITE)</code> 키워드를 붙이면 된다.</p>
<p>필자가 진행하는 프로젝트는 QueryDSL을 통해 쿼리를 빌드하기 때문에 아래와 같이 작성하면 된다.</p>
<pre><code class="language-java">//UserCouponCustomRepositoryImpl.java
    @Override
    public Coupon findByIdWithPessimisticLock(Long couponId) {
        Coupon result = queryFactory
                .selectFrom(coupon)
                .where(coupon.couponId.eq(couponId))
                .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                .fetchFirst();

        if(result == null) {
            throw new CouponNotFoundException(&quot;존재하지 않는 쿠폰입니다&quot;);
        }

        return result;
    }



//CouponService.java  
    //issue coupon (create UserCoupon)
    //Only Admin can issue &quot;ALL&quot; type coupon
    //every user can issue &quot;LIMIT&quot; type coupon
    //&quot;LIMIT&quot; type coupon can issue amount less than &quot;couponAmount&quot;
    @Transactional
    public UserCouponResponseDto issueCoupon(Long couponId, String userRole, Long userId){

        Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId);

        //ALL 쿠폰 발급시 관리자 권한 체크
        if(!checkIsAdmin(userRole) &amp;&amp; coupon.getCouponType().equals(CouponType.ALL)){
            throw new PermissionNotfoundException(&quot;쿠폰발급 권한이 없는 사용자입니다.&quot;);
        }

        //check amount
        if (coupon.getCouponAmountRemaining() &lt;= 0) {
            throw new CouponAmountErrorException(&quot;쿠폰이 소진되었습니다.&quot;);
        }

        //check already have
        if(userCouponRepository.findUserCouponByUserIdAndCouponId(userId, couponId).isPresent()){
            throw new CouponAmountErrorException(&quot;이미 보유한 쿠폰입니다&quot;);
        }

        coupon.decreaseCoupon();

        UserCoupon userCoupon = UserCoupon.builder()
                .userId(userId)
                .coupon(coupon)
                .isUsed(false)
                .build();

        userCouponRepository.save(userCoupon);
        return UserCouponResponseDto.from(userCoupon);
    }</code></pre>
<p>위와 같이 쿼리를 작성하면 조회 쿼리에서 <code>FOR UPDATE</code> 가 추가되며 락을 획득한 상태에서 로직이 실행되는 것을 볼 수 있다.
해당 코드가 실행될 때의 Hibernate 로그는 아래와 같다</p>
<pre><code class="language-sql">select c1_0.coupon_id,c1_0.available,c1_0.coupon_amount_remaining,c1_0.coupon_amount_total,c1_0.coupon_code,c1_0.coupon_maximum,c1_0.coupon_minimum,c1_0.coupon_name,c1_0.coupon_type,c1_0.discount_percentage,c1_0.discount_price,c1_0.discount_type,c1_0.expiry_date,c1_0.issue_date from coupon c1_0 where c1_0.coupon_id=? limit ? for update</code></pre>
<h1 id="4-낙관적-락-적용">4. 낙관적 락 적용</h1>
<p>낙관적 락은 실질적으로 락을 걸지 않고, 버전 체크를 통해서 충돌을 확인한다.
따라서 Coupon Entity에 Version을 저장하는 필드가 필요하다.</p>
<p>트랜잭션을 진행하며 버전간 충돌이 발생하면 충돌이 발생한 버전에 대해서 롤백을 진행하고 다음 트랜잭션을 진행한다.</p>
<pre><code class="language-java">//Coupon.java
    @Getter
    @NoArgsConstructor
    @EntityListeners(AuditingEntityListener.class)
    @Entity
    public class Coupon {

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

        //중략

        @Version
        private Long version;
    }



//CouponCustomRepositoryImpl.java
    @Override
    public Coupon findByIdWithPessimisticLock(Long couponId) {
        Coupon result = queryFactory
                .selectFrom(coupon)
                .where(coupon.couponId.eq(couponId))
                .setLockMode(LockModeType.OPTIMISTIC)
                .fetchFirst();

        if(result == null) {
            throw new CouponNotFoundException(&quot;존재하지 않는 쿠폰입니다&quot;);
        }

        return result;
    }

    @Override
    public Coupon findByIdOrElseThrow(Long couponId) {
        Coupon result = queryFactory
                .selectFrom(coupon)
                .where(coupon.couponId.eq(couponId))
                .fetchFirst();

        if(result == null) {
            throw new CouponNotFoundException(&quot;존재하지 않는 쿠폰입니다.&quot;);
        }

        return result;
    }



//CouponService.java
    @Transactional
    public UserCouponResponseDto issueCoupon(Long couponId, String userRole, Long userId){

        Coupon coupon = getCoupon(couponId);

        //ALL 쿠폰 발급시 관리자 권한 체크
        if(!checkIsAdmin(userRole) &amp;&amp; coupon.getCouponType().equals(CouponType.ALL)){
            throw new PermissionNotfoundException(&quot;쿠폰발급 권한이 없는 사용자입니다.&quot;);
        }

        //TODO: 추후 락 구현 필요
        //check amount
        if (coupon.getCouponAmountRemaining() &lt;= 0) {
            throw new CouponAmountErrorException(&quot;쿠폰이 소진되었습니다.&quot;);
        }

        //check already have
        if(userCouponRepository.findUserCouponByUserIdAndCouponId(userId, couponId).isPresent()){
            throw new CouponAmountErrorException(&quot;이미 보유한 쿠폰입니다&quot;);
        }

        coupon.decreaseCoupon();

        UserCoupon userCoupon = UserCoupon.builder()
                .userId(userId)
                .coupon(coupon)
                .isUsed(false)
                .build();

        userCouponRepository.save(userCoupon);
        return UserCouponResponseDto.from(userCoupon);
    }

    private Coupon getCoupon(Long couponId){
        return couponRepository.findByIdOrElseThrow(couponId);
    }</code></pre>
<h1 id="5-결과-비교">5. 결과 비교</h1>
<h2 id="51-비관적-락-결과">5.1. 비관적 락 결과</h2>
<table>
<thead>
<tr>
<th>회차</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
</tr>
</thead>
<tbody><tr>
<td>발급 개수</td>
<td>100</td>
<td>100</td>
<td>100</td>
<td>100</td>
<td>100</td>
</tr>
</tbody></table>
<p>발급 개수가 일정하게 제한 개수를 초과하지 않는 결과가 나오는 것을 확인하였다.
즉 동시성 문제가 발생하지 않는다는 것이다.</p>
<p>다만, 락을 걸고 트랜잭션 처리 후 락을 해제 하는 과정을 순차적으로 진행하다 보니 처리율이 낮아지는, 즉 동시성이 떨어지는 모습이 보였다.</p>
<p>특히 DB의 성능이 떨어지는 상황에서 요청이 지연되자, 아래와 같이 HikariPool에서 타임아웃이 발생하는 모습도 보였다.</p>
<pre><code>Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms (total=10, active=10, idle=0, waiting=33)</code></pre><h2 id="52-낙관적-락-결과">5.2. 낙관적 락 결과</h2>
<table>
<thead>
<tr>
<th>회차</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
</tr>
</thead>
<tbody><tr>
<td>발급 개수</td>
<td>100</td>
<td>100</td>
<td>100</td>
<td>100</td>
<td>100</td>
</tr>
</tbody></table>
<p>비관적락과 마찬가지로 제한개수를 초과하지 않으며 동시성 문제가 발생하지 않는 문제를 보였다.
또한 락을 거는 비관적 락 보다 우월한 속도 성능을 보였다.</p>
<p>다만 충돌로 인해 많은 예외가 발생하였다.
또한 <strong>충돌로 인한 롤백이 다수 발생하여 여러 트랜잭션이 롤백으로 무시되는 현상</strong>이 발견되었다.</p>
<p>아래 사진을 보면 <code>UserCouponId</code>가 <strong>5, 16, 27...</strong>으로 띄엄띄엄 증가하는 모습을 보인다.
이는 여러 트랜잭션이 동시에 쿠폰 발급을 시도하고, 낙관적 락 충돌이 발생하여 일부 트랜잭션이 롤백되었음을 시사한다.
<strong>즉 1<del>4, 6</del>15 등의 트랜잭션은 충돌로 인해 무효화되었다는 것이다.</strong>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/51bcdb33-d839-4ce1-ac0a-9c43b799228b/image.png" alt=""></p>
<p><del>게다가 낙관적 락의 방식에서는 서버 단에서 롤백된 트랜잭션을 다시 시도하는 방법이 없다.
어플리케이션(프론트) 단에서 에러를 처리해 다시 시도하던지, 사용자가 재 요청하는 방법밖에 없다는 것이다.</del></p>
<p>이렇게 무시된 트랜잭션은 Retryable을 통해 재시도 처리를 따로 해주어야 한다.</p>
<h1 id="6-결론">6. 결론</h1>
<blockquote>
<p>결과적으로 두 방식 모두 동시성 문제를 예방할 수 있는 해결책이나, 각자의 문제점을 안고 있으며 문제 상황에 맞는 해결 방법을 적용할 필요가 있다.</p>
</blockquote>
<p><strong>비관적 락</strong>의 경우 느린 성능을 보유하였지만, 락과 해제를 통해 대기중인 트랜잭션이 누락 없이 해결되도록 동작하는 모습을 보인다.
<strong>낙관적 락</strong>의 일부 트랜잭션이 무효화 되고, 잦은 충돌에 예외가 많이 발생하지만, 상대적으로 빠른 처리속도를 보여준다.</p>
<blockquote>
<p>따라서 <strong>낮은 DB의 처리 속도</strong>와 <strong>동시성 문제가 낮은 빈도로 발생할 것</strong>으로 예상하여 낙관적 락으로 우선 적용하고자 한다.</p>
</blockquote>
<p>다음 글에서는 동시성 문제를 해결 할 수 있는 다른 방법들을 더 알아보고자 한다.</p>
<hr>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://hstory0208.tistory.com/entry/%EB%9D%BDLock%EC%9D%B4%EB%9E%80-Lock%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9CDeadLock">https://hstory0208.tistory.com/entry/%EB%9D%BDLock%EC%9D%B4%EB%9E%80-Lock%EC%9D%98-%EC%A2%85%EB%A5%98%EC%99%80-%EA%B5%90%EC%B0%A9%EC%83%81%ED%83%9CDeadLock</a></li>
<li><a href="https://velog.io/@soongjamm/Select-%EC%BF%BC%EB%A6%AC%EB%8A%94-S%EB%9D%BD%EC%9D%B4-%EC%95%84%EB%8B%88%EB%8B%A4.-X%EB%9D%BD%EA%B3%BC-S%EB%9D%BD%EC%9D%98-%EC%B0%A8%EC%9D%B4">https://velog.io/@soongjamm/Select-%EC%BF%BC%EB%A6%AC%EB%8A%94-S%EB%9D%BD%EC%9D%B4-%EC%95%84%EB%8B%88%EB%8B%A4.-X%EB%9D%BD%EA%B3%BC-S%EB%9D%BD%EC%9D%98-%EC%B0%A8%EC%9D%B4</a></li>
<li><a href="https://velog.io/@yellowsunn/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95">https://velog.io/@yellowsunn/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%98%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B0%A9%EB%B2%95</a></li>
<li><a href="https://velog.io/@lsb156/JPA-Optimistic-Lock-Pessimistic-Lock">https://velog.io/@lsb156/JPA-Optimistic-Lock-Pessimistic-Lock</a></li>
<li><a href="https://ksh-coding.tistory.com/125">https://ksh-coding.tistory.com/125</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 문제 해결하기 - 1(동시성 문제 파악)]]></title>
            <link>https://velog.io/@hyeseong-int/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-1%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%8C%8C%EC%95%85</link>
            <guid>https://velog.io/@hyeseong-int/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-1%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%8C%8C%EC%95%85</guid>
            <pubDate>Sun, 23 Mar 2025 15:12:07 GMT</pubDate>
            <description><![CDATA[<p>진행하는 프로젝트의 쿠폰 발급 서비스에 동시성 문제가 있음을 파악했다.
이를 테스트하고 해결하는 과정을 기록해보고자 한다.</p>
<hr>
<h1 id="목차">목차</h1>
<ol>
<li>동시성 문제란?</li>
<li>배경지식</li>
<li>테스트 - 동시성 문제 확인</li>
<li>테스트 결과 - 동시성 문제 발생</li>
<li>동시성 문제 해결 방법</li>
</ol>
<hr>
<h1 id="1-동시성-문제란">1. 동시성 문제란?</h1>
<p>동시성 문제란 하나의 자원에 두개 이상의 스레드, 세션에서 가변 데이터를 동시에 제어할 때 나타나는 문제이다.
하나의 세션이 데이터에 접근중일 때, 다른 세션에서 동시에 접근할 경우 동시성 문제로 인해 데이터 정합성에 문제가 생긴다.</p>
<p>현재 프로젝트의 쿠폰 발급 서비스에서 한정된 쿠폰을 여러 사람이 동시에 발급하고자 할 때 동시성 문제가 발생할 가능성이 있어 이를 파악하고 해결해보고자 한다.</p>
<h1 id="2-배경-지식">2. 배경 지식</h1>
<ul>
<li>*<em>Race condition(경쟁상태) : *</em> 두 개 이상의 프로세스가 공통 자원을 병행적으로 접근할 때, 접근 순서에 따라 실행 결과가 달라지는 상황. 두개의 스레드 혹은 세션 등이 자원을 놓고 경쟁하는 상황</li>
<li>*<em>Dead Lock(교착상태) : *</em> 프로세스가 두 자원 모두에 접근해야 하는 상황에서, 두 개의 프로세스가 각자 한 자원을 점유하였을 때, 각자 소유한 리소스를 해제하지 않고 무한정 대기하게 되는 상황</li>
<li>*<em>Starvation(기아상태) : *</em> 특정 프로세스의 우선순위가 낮아 원하는 자원을 할당받지 못하는 상황</li>
</ul>
<h1 id="3-테스트---동시성-문제-확인">3. 테스트 - 동시성 문제 확인</h1>
<p>동시성 문제가 발생하는지 확인하기 위해 Lock 등 동시성문제 처리가 되지 않은 상태에서 테스트를 진행한다.
Locust를 통해 서버에 요청을 보내 결과를 확인하는 방식으로 진행한다.</p>
<h2 id="31-테스트-시나리오">3.1. 테스트 시나리오</h2>
<p><strong>1번 테스트:</strong> 100매 발급 가능한 쿠폰을 120명이 발급 시도한다. -&gt; 정상적인 경우 100매 성공, 20매 실패
<strong>2번 테스트:</strong> 1일당 1매 발급 가능한 쿠폰을 1인이 동시에 여러번 발급 시도한다. -&gt; 정상적인 경우 1매만 발급 성공</p>
<h2 id="32-테스트-코드">3.2. 테스트 코드</h2>
<p><strong>1번 테스트</strong></p>
<pre><code class="language-python">from locust import HttpUser, task, between, SequentialTaskSet
import requests
import random
import string

#시나리오:
#다수의 사용자가 한번에 동시에 쿠폰 발급을 시도하는 시나리오

HOST = &quot;http://localhost:8080&quot;

class BoardServiceUser(SequentialTaskSet):

    def on_start(self):
        # 1. 회원가입
        randEmail = self.generate_random_email()

        response = requests.post(HOST + &quot;/api/v1/users&quot;, json={
            &quot;email&quot; : randEmail,
            &quot;password&quot;: &quot;test1234&quot;,
            &quot;nickname&quot;: &quot;testUser&quot;,
            &quot;userRole&quot; : &quot;USER&quot;,
            &quot;phoneNumber&quot; : &quot;01012340000&quot;
        })
        data = response.json()
        if data[&quot;status&quot;] == &quot;success&quot;:
            print(&quot;회원가입 성공&quot;)
        else:
            print(&quot;회원가입 실패:&quot;, data[&quot;message&quot;])
            self.interrupt()


        # 2.로그인 요청
        response = requests.post(HOST + &quot;/api/v1/users/login&quot;, json={
            &quot;email&quot;: randEmail,
            &quot;password&quot;: &quot;test1234&quot;
        })

        data = response.json()
        if data[&quot;status&quot;] == &quot;success&quot;:
            token = data[&quot;data&quot;]
            self.client.headers.update({&#39;Authorization&#39;: f&#39;Bearer {token}&#39;})
        else:
            print(&quot;로그인 실패:&quot;, data[&quot;message&quot;])
            self.interrupt() 


    @staticmethod
    def generate_random_email():
        random_string = &#39;&#39;.join(random.choices(string.ascii_lowercase + string.digits, k=8))
        return f&quot;test_{random_string}@example.com&quot;

    #쿠폰 발급
    @task
    def issueCoupone(self):
        response = self.user.client.post(HOST + &quot;/api/v1/coupons/user-coupons/1&quot;)
        print(response)

class WebsiteUser(HttpUser):
    host = HOST
    tasks = [BoardServiceUser]
    wait_time = between(1, 1)</code></pre>
<p><strong>2번 테스트</strong> </p>
<pre><code class="language-python">from locust import HttpUser, task, between, SequentialTaskSet
import requests
import random
import string

#시나리오:
#단일 사용자가 하나의 쿠폰에 대해 여러번 쿠폰 발급을 시도하는 시나리오

HOST = &quot;http://localhost:8080&quot;

class BoardServiceUser(SequentialTaskSet):

    def on_start(self):

        # 2.로그인 요청
        response = requests.post(HOST + &quot;/api/v1/users/login&quot;, json={
            &quot;email&quot;: &quot;test@test.com&quot;,
            &quot;password&quot;: &quot;test1234&quot;
        })

        data = response.json()
        if data[&quot;status&quot;] == &quot;success&quot;:
            token = data[&quot;data&quot;]
            self.client.headers.update({&#39;Authorization&#39;: f&#39;Bearer {token}&#39;})
        else:
            print(&quot;로그인 실패:&quot;, data[&quot;message&quot;])
            self.interrupt() 

    #쿠폰 발급
    @task
    def issueCoupone(self):
        response = self.user.client.post(HOST + &quot;/api/v1/coupons/user-coupons/1&quot;)
        print(response)

class WebsiteUser(HttpUser):
    host = HOST
    tasks = [BoardServiceUser]
    wait_time = between(1, 1)</code></pre>
<h2 id="33-테스트-진행">3.3. 테스트 진행</h2>
<p><strong>1번 테스트</strong>
*<em>시나리오: *</em>120명의 사용자가 랜덤 회원가입 후 로그인하여 인증 키를 얻고, 해당 키를 첨부해 쿠폰 발급을 시도한다.
*<em>기대 결과: *</em> 120명의 사용자가 생성된 후, 100매의 쿠폰이 발급된다. 20개의 요청은 에러가 발생한다.
*<em>참고 사항: *</em> 환경상 발생할 수 있는 차이를 고려해 10회 실시한다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/347dd073-fbf2-439e-931b-bdacc2f69bfd/image.png" alt=""></p>
<p><strong>2번 테스트</strong>
*<em>시나리오: *</em> 한명의 사용자가 100번의 쿠폰 발급 요청을 동시에 시도하는것을 모사하여 쿠폰발급을 시도한다.
*<em>기대 결과: *</em> 1명의 사용자에 1매의 발급이 가능한만큼, 매 회 1매의 쿠폰만이 발급되고. 99개의 에러가 발생한다.
*<em>참고사항: *</em> 환경 상 발생할 수 있는 차이를 고려해 10회 실시한다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/31046b52-75ef-487d-b332-cc77eb68ccf4/image.png" alt=""></p>
<h1 id="4-테스트-결과---동시성-문제-발생">4. 테스트 결과 - 동시성 문제 발생</h1>
<h2 id="41-1번-테스트-결과">4.1. 1번 테스트 결과</h2>
<table>
<thead>
<tr>
<th>회차</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>평균</th>
</tr>
</thead>
<tbody><tr>
<td>생성된 쿠폰 수</td>
<td>105</td>
<td>101</td>
<td>101</td>
<td>101</td>
<td>101</td>
<td>101</td>
<td>100</td>
<td>103</td>
<td>101</td>
<td>101</td>
<td>101.5</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/hyeseong-int/post/20633de3-c4ae-4578-b854-869ee6d1c569/image.png" alt=""></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>Locust를 통해 10회의 테스트를 진행하였으며, 매 회 100개의 쿠폰만 생성될 것을 기대하였으나, 평균 1.5개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.</p>
<h2 id="42-2번-테스트-결과">4.2. 2번 테스트 결과</h2>
<table>
<thead>
<tr>
<th>회차</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>평균</th>
</tr>
</thead>
<tbody><tr>
<td>생성된 쿠폰 수</td>
<td>9</td>
<td>7</td>
<td>8</td>
<td>4</td>
<td>8</td>
<td>4</td>
<td>10</td>
<td>5</td>
<td>6</td>
<td>2</td>
<td>6.3</td>
</tr>
<tr>
<td><img src="https://velog.velcdn.com/images/hyeseong-int/post/ee8594e4-1147-4485-91ab-b618787bc779/image.png" alt=""></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>Locust를 통해 10회의 테스트를 진행하였으며, 매 회 1개의 쿠폰만 발급될 것을 기대하였으나. 평균 6.3개의 쿠폰이 더 발급되며, 동시성 문제가 발생하는 것을 확인했다.</p>
<h1 id="5-동시성-문제-해결-방법">5. 동시성 문제 해결 방법</h1>
<p>동시성 문제에는 비관적 락(Pessimistic Lock), 낙관적 락(Optimistic Lock), 분산락 등의 해결방법이 있는데, 이를 알아보고 알맞은 방법을 이용해 해결해보고자 한다.</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://iredays.tistory.com/125">https://iredays.tistory.com/125</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Locust로 서버 성능 테스트하기]]></title>
            <link>https://velog.io/@hyeseong-int/Locust%EB%A1%9C-%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyeseong-int/Locust%EB%A1%9C-%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 20 Mar 2025 11:38:12 GMT</pubDate>
            <description><![CDATA[<p>개발 한 서버의 성능이 어느정도인지 객관적 지표로 파악 후 개선단계를 진행하고자 한다.
그 첫번째 단계인 성능 파악을 진행한 과정을 기록한다.</p>
<hr>
<h1 id="목차">목차</h1>
<ol>
<li>성능 테스트</li>
<li>Locust란?</li>
<li>Locust 설치</li>
<li>스크립트 작성</li>
<li>부하테스트 실행</li>
<li>결과</li>
<li>성능 개선</li>
</ol>
<hr>
<h1 id="1-성능테스트">1. 성능테스트</h1>
<blockquote>
<p>성능 테스트는 왜 필요한가?</p>
</blockquote>
<p>서버 기능 작성을 마쳤다. 서버는 정상적으로 작동한다. 하지만 그렇지 않을수도 있다.</p>
<p>사람들이 몰리면 부하가 발생한 서버는 예상한 것과 다르게 작동할 수 있기 때문이다. (락 발생, 쿼리 지연, 병목 현상 등)
때문에 위와 같은 문제를 해결하기 위해 성능 테스트, 부하테스트를 진행하려 한다.</p>
<h2 id="11-성능테스트-종류">1.1 성능테스트 종류</h2>
<ul>
<li>*<em>부하테스트(Load Test): *</em> 특정 부하를 제한시간동안 부여. 일반적으로 1시간 기준</li>
<li>*<em>스트레스 테스트 : *</em> peak Load 보다 높은 부하(일반적으로 2배)에 어떻게 대응하는지, 정상 복구되는지 테스트</li>
<li>*<em>내구 테스트(Endurance Test): *</em> 장기간에 걸쳐 나타나는 문제(메모리 누수)를 확인하기 위해 장시간에 걸쳐 시스템의 안정성을 검사</li>
<li>*<em>스파이크 테스트(Spike Test): *</em> 급작스러운 사용량 급증 시나리오에 대해 시뮬레이션 </li>
<li>*<em>중단점 테스트(Breaking Point Test): *</em> 동시단말 사용자 수를 점진적 증가시켜, 시스템 장애 시점을 결정하는 테스트</li>
</ul>
<h2 id="12-성능테스트-목적">1.2. 성능테스트 목적</h2>
<p>성능 테스트를 통해 시스템의 성능상 한계와 리소스의 한계를 파악할 수 있다.
최대 성능이 어느정도인지 파악해 서버를 최적화 할 수 있다.
서버를 최적화하는 과정을 아래의 단계를 따른다</p>
<blockquote>
<p><strong>성능 측정 &lt;-&gt; 결함 검출 &lt;-&gt; 병목 제거 &lt;-&gt; 용량 검증</strong>  =&gt; 반복</p>
</blockquote>
<h2 id="13-배경지식">1.3. 배경지식</h2>
<ul>
<li><p>Response Time, 응답시간: 사용자의  Request 시점에서 시스템이 Response 하기까지의 종단시간.</p>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/3fb65862-24bc-450a-8682-e544e4e3990c/image.png" width="50%">
</li>
<li><p>User: 서버를 사용중인 사람</p>
<ul>
<li>Current User: 언제든 부하를 줄 수 있는 사용자</li>
<li>Active User: 실제로 부하를 주고 있는 사용자</li>
</ul>
</li>
<li><p>TPS(Transaction Per Test) : 초당 트랜잭션 수</p>
</li>
<li><p>Saturation Point, 임계점: 최대 TPS가 나타나는 최초의 지점. </p>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/da168601-2f5c-4918-aacc-86a3698b5a20/image.png" width="50%">


</li>
</ul>
<h1 id="2-locust란">2. Locust란?</h1>
<blockquote>
<p>다양한 부하테스트 툴(JMeter, Locust, K6)중 가볍고(로컬에서 요청 발생시킬 예정), 러닝커브가 적은(Python으로 스크립트 작성 가능) Locust를 선택하게 되었다.</p>
</blockquote>
<p>Locust는 오픈소스 부하테스트 도구이다.
Python으로 스크립트를 작성할 수 있으며, 웹 애플리케이션의 성능을 측정할 수 있도록 도와준다.
웹 인터페이스를 제공하여 실시간으로 테스트를 모니터링하고 결과를 분석하기 쉽도록 한다.</p>
<h2 id="21-locust의-장점">2.1. Locust의 장점</h2>
<ul>
<li>쉽게 설치가 가능</li>
<li>Python 스크립트를 통해 쉽게 작성 가능</li>
<li>빠르게 테스트 진행 가능</li>
<li>웹 인터페이스 지원으로 결과 시각화</li>
</ul>
<h1 id="3-locust-설치">3. Locust 설치</h1>
<p>pip를 통해 로컬에 Locust를 설치해준다.
pip가 설치되어있지 않다면 다음 <a href="https://edblab.tistory.com/12">링크를</a> 참고하여 Python, pip 등을 설치하여 환경을 준비해준다</p>
<pre><code class="language-shell">#설치
$ pip install locust
# 설치확인
$ locust -V</code></pre>
<h1 id="4-스크립트-작성">4. 스크립트 작성</h1>
<p><code>locustfile.py</code> 파일에 작성해도 되며, <code>locust -f {경로}</code> 명령어를 통해 경로를 첨부해 실행해도 된다.</p>
<h2 id="41-스크립트">4.1. 스크립트</h2>
<ul>
<li><code>wait_time</code> : 가상 사용자의 대기시간 정의</li>
<li><code>host</code> : 테스트 할 대상 호스트 정의.(주소, IP 등)</li>
<li><code>@task</code> : 테스트 할 작업을 정의하는 데코레이터. TaskSet이 실행될 때 TaskSet 안의 task를 랜덤 하게 선택해서 실행한다. 가중치를 줄 수 있다.</li>
<li><code>seq_task</code>: 테스트 할 작업을 순서까지 정의하는 데코레이터. TaskSequence가 실행될 때 안의 seq_task 순서에 따라 실행한다.</li>
<li><code>setup &amp; teardown</code> : setup과, teardown은 TaskSet의  생성시, 종료시 한번씩 실행된다.</li>
<li><code>on_start,on_stop</code>: 매 클라이언트마다 실행된다.&#39;</li>
</ul>
<blockquote>
<p>실행 순서
Locust setup
TaskSet setup
TaskSet on_start
TaskSet tasks…
TaskSet on_stop
TaskSet teardown
Locust teardown</p>
</blockquote>
<h2 id="42-필요한-모듈-임포트">4.2. 필요한 모듈 임포트</h2>
<p>기본적인 HTTP 요청을 테스트하려면 HttpUser와 task 데코레이터를 임포트해야 한다.</p>
<pre><code class="language-python">from locust import HttpUser, task, TaskSet</code></pre>
<h2 id="43-httpuser-클래스-정의">4.3. HttpUser 클래스 정의</h2>
<p><code>HttpUser</code> class를 상속받아 가상의 사용자를 정의한다.</p>
<pre><code class="language-python">class QuickstartUser(HttpUser):
    # 사용자 간 대기 시간 설정
    wait_time = between(1, 2)  
    # 테스트 대상 호스트 설정
    host = &quot;http://example.com&quot;  

    @task
    def index_page(self):
        self.client.get(&quot;/&quot;)

    @task
    def about_page(self):
        self.client.get(&quot;/about/&quot;)</code></pre>
<p>아래는 스크립트 예제이다.</p>
<pre><code class="language-python">from locust import HttpUser, task, between

class QuickstartUser(HttpUser):
    # 시뮬레이션 된 유저는 작업을 실행하기 전 5~9초 기다린다.
    wait_time = between(5, 9)

    def on_start(self):
        # 토큰으로 인증 헤더를 초기화
        self.client.headers.update({&#39;Authorization&#39;: &#39;Bearer TOKEN_HERE&#39;})

    @task(1)
    def index_page(self):
        # 대기 시간동안 아래 네번의 Get 요청을 진행
        self.client.get(&quot;/api/hosts&quot;)
        self.client.get(&quot;/api/tests&quot;)
        self.client.get(&quot;/api/result/1488&quot;)
        self.client.get(&quot;/api/result/1463&quot;)</code></pre>
<p>아래는 로그인 방식을 처리하는 예제이다.</p>
<pre><code class="language-python">from locust import HttpUser, TaskSet, task
import requests

class UserBehavior(TaskSet):
  def on_start(self):

        #로그인 요청 작성
      response = requests.post(&quot;http://mysite.com/login&quot;, {
        &quot;username&quot;: &quot;user&quot;,
        &quot;password&quot;: &quot;pass&quot;
      })

      # 1. response header에서 토큰 설정
      self.client.headers.update({&#39;Authorization&#39;: response.headers.get(&#39;token&#39;)})

      # 2. cookies에서 토큰 설정
      self.client.cookies.set(&#39;Authorization&#39;, response.cookies.get(&#39;token&#39;))

      # 3. response body에서 토큰 설정
      token = str(response.content.decode().find(&#39;token&#39;)
      self.client.headers.update({&#39;Authorization&#39;: token)})

  # 나머지 테스트 코드 작성
</code></pre>
<h2 id="44-스크립트-작성">4.4. 스크립트 작성</h2>
<p>위 코드 작성 방법을 바탕으로 로그인 이후 단순 게시글 조회를 진행하는 테스트 코드를 작성해보았다.</p>
<pre><code class="language-python">from locust import HttpUser, task, TaskSet, between
import requests
import re

HOST = &quot;[호스트 경로]:8900&quot;

class UserBehavior(TaskSet):
    def on_start(self):
        #로그인 요청
        response = requests.post(HOST + &quot;/api/auth/login&quot;, json={
                &quot;userEmail&quot;: &quot;testEmail@test.com&quot;,
                &quot;userPassword&quot; :&quot;Test&quot; 
        })

        data = response.json()
        if data[&quot;code&quot;] == 200:
            set_cookie = response.headers.get(&#39;Set-Cookie&#39;)
            if set_cookie:
                token_match = re.search(r&#39;accessToken=([^;]+)&#39;, set_cookie)
                if token_match:
                    token = token_match.group(1)
                    print(token)  # 순수 토큰 값 출력
                    self.client.cookies.set(&#39;accessToken&#39;, token)

                else:
                    print(&quot;토큰을 찾을 수 없습니다.&quot;)
            else:
                print(&quot;Set-Cookie 헤더가 없습니다.&quot;)
        else:
            print(&quot;로그인실패:&quot;, data[&quot;message&quot;])

    @task
    def get_board(self):
        with self.client.get(&quot;/api/board/1&quot;, catch_response=True) as response:
            if response.status_code == 200:
                print(&quot;게시글 조회 성공&quot;)
            else:
                print(&quot;게시글 조회 실패: {response.status_code}&quot;)
                response.faiure(f&quot;게시글 조회 실패: {response.status_code}&quot;)


class WebsiteUser(HttpUser):
    host = HOST
    tasks = [UserBehavior]
    wait_time = between(1, 1)</code></pre>
<h1 id="5-부하-테스트-실행">5. 부하 테스트 실행</h1>
<h2 id="51-부하-테스트-환경">5.1. 부하 테스트 환경</h2>
<p>기기: 시놀로지 DS220+
CPU: INTEL Celeron J4025(2코어, 2 GHz)
RAM: 18GB</p>
<h2 id="52-locust-실행">5.2. Locust 실행</h2>
<p>아래 명령어를 실행하여 로커스트를 실행한다.</p>
<pre><code class="language-shell">locust -f [파일경로]</code></pre>
<p>이후 아래 명령어에 나오는 URL로 접속해주면 웹 인터페이스를 볼 수 있다.
별도의 설정이 없었다면 <a href="http://localhost:8089/%EB%A1%9C">http://localhost:8089/로</a> 접속하면 된다.</p>
<h2 id="53-테스트-설정">5.3. 테스트 설정</h2>
<p>위 주소로 접속하면 아래와 같은 화면이 나온다
<img src="https://velog.velcdn.com/images/hyeseong-int/post/efa78384-8274-41bf-91b1-fdde9c736e27/image.png" alt="">
각 값의 의미는 아래와 같다</p>
<ul>
<li><code>Number of User</code> : 최대(피크) 동시접속 유저 수</li>
<li><code>Ramp Up</code> : 초마다 증가될 유저 수(사용자 점진적 증가)</li>
<li><code>Host</code> : 테스트를 진행할 서버 주소. 코드에서 지정해 줬다면 미리 입력되어있다.</li>
</ul>
<p>테스트를 원하는 값을 입력해주고 start 버튼을 누른다.</p>
<p>아래 조건을 입력 후 진행해보았다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/76b24dcf-e42f-437b-a611-b1cdfb7cd6f3/image.png" alt=""></p>
<h1 id="6-결과">6. 결과</h1>
<h2 id="61-결과-지표">6.1. 결과 지표</h2>
<ul>
<li><code>RPS</code>: 초당 요청 수. 값이 높을 수록 초당 처리할 수 있는 요청수가 많음을 의미. 성능 테스트의 부하를 알 수 있음.</li>
<li><code>Failures</code>: 실패 비율. 요청이 실패한 비율과 횟수를 표기</li>
<li><code>50th Percentile</code>: 요청 응답시간의 중앙값. 평균보다 왜곡이 적은 응답 값(평균은 극단적 값을 포함)</li>
<li><code>95th Percentile</code>:요청 응답시간의 95번째 백분위의 수. 성능의 일관성을 평가할 수 있음</li>
<li><code>Number Of User</code>: 활성 사용자 수</li>
</ul>
<h2 id="62-결과-분석">6.2. 결과 분석</h2>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/80fd26e6-31d0-4257-8cfc-4e921f7209fa/image.png" alt=""></p>
<ul>
<li>테스트 시작 후 RPS가 점차 증가하여 0.2 ~ 0.4 범위를 변동</li>
<li>응답 시간이 테스트 시작부터 꾸준히 증가하여 60,000ms(60초) 이상까지 도달</li>
<li>50번째와 95번째 백분위수 응답 시간이 거의 동일하게 증가하는 것으로 보아, 대부분의 요청이 매우 느리게 처리되고 있음을 알 수 있음</li>
</ul>
<h2 id="63-종합-평가">6.3. 종합 평가</h2>
<ul>
<li>테스트 대상 시스템은 100명의 사용자를 처리하지만, 초당 요청 수가 매우 낮고 응답 시간이 매우 높음</li>
<li>시스템이 요청을 제대로 처리하지 못하고 있거나, 매우 느리게 처리하고 있음을 나타냄</li>
<li>낮은 RPS와 높은 응답 시간은 시스템의 심각한 성능 문제를 시사</li>
</ul>
<h1 id="7-성능-개선">7. 성능 개선</h1>
<blockquote>
<p>... 심각하다</p>
</blockquote>
<p>성능 개선을 위해 아래 목록을 점검하고자 한다.</p>
<ul>
<li>시스템 자원 점검: 서버의 CPU, 메모리, 디스크 I/O 등을 점검하여 병목 지점 확인</li>
<li>데이터베이스 성능 개선: 데이터베이스 쿼리 최적화, 인덱싱 등을 통해 데이터베이스 성능을 개선</li>
<li>애플리케이션 코드 분석: 애플리케이션 코드의 성능 저하 요인을 분석하고 개선</li>
<li>네트워크 점검: 네트워크 지연이나 대역폭 문제를 점검</li>
</ul>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://loadforge.com/directory">https://loadforge.com/directory</a></li>
<li><a href="https://engineering-skcc.github.io/performancetest/Performance-Testing-Terminologies/">https://engineering-skcc.github.io/performancetest/Performance-Testing-Terminologies/</a></li>
<li><a href="https://dewble.tistory.com/entry/concept-of-performance-test">https://dewble.tistory.com/entry/concept-of-performance-test</a></li>
<li><a href="https://bcho.tistory.com/1369">https://bcho.tistory.com/1369</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Jenkins 파이프라인으로 배포하기]]></title>
            <link>https://velog.io/@hyeseong-int/Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9C%BC%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyeseong-int/Jenkins-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9C%BC%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 14 Mar 2025 07:41:58 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 마무리하면서 젠킨스로 Synology NAS DS220+에 백엔드 서버를 배포하는 과정에서 젠킨스를 사용해보았다.
해당 과정에 대해 정리해보려 한다.</p>
<blockquote>
<p>해당 과정을 진행하기에 DS220+의 기본 2GB 메모리는 너무 작다. 필자는 추가 메모리 16GB를 증량하여 총 18GB의 메모리로 운용중이다.
시놀로지 NAS의 메모리 증량은 아래 글 참고
&#39;<a href="https://me.tistory.com/672">참고 글</a>&#39;
(필자는 다양한 램을 시도하여 보았으나 2R 로 시작하는 램만 성공하였다)</p>
</blockquote>
<h1 id="목차">목차</h1>
<ol>
<li>젠킨스란?</li>
<li>왜 젠킨스를 사용하는가</li>
<li>젠킨스 설치</li>
<li>젠킨스 설정</li>
<li>젠킨스 작업 생성 및 설정</li>
<li>젠킨스 파이프라인 작성</li>
<li>배포 자동화</li>
</ol>
<hr>
<h1 id="1-젠킨스-란">1. 젠킨스 란?</h1>
<p>Jenkins란 소프트웨어 개발 시 CI/CD를 제공하는 툴이다.
CI/CD(Continuous Integration/Continuous Deployment)는 소프트웨어의 지속적 통합 / 지속적 배포를 말하는데, 이 과정을 자동적으로 진행해준다.</p>
<p>젠킨스의 특징은 아래와 같다.</p>
<ul>
<li>JRE(Java Runtime Environment)에서 동작</li>
<li>각종 플러그인을 통해 자동화 처리 가능</li>
<li>웹 UI를 지원하여 사용 편리</li>
<li>파이프라인(선언적 / 스크립트)를 이용하여 복잡한 빌드 및 배포 단계를 정의 및 자동화 할 수 있음</li>
</ul>
<h1 id="2-왜-젠킨스를-사용하는가">2. 왜 젠킨스를 사용하는가?</h1>
<p>GitHub Actions 를 비롯한 많은 CI/CD 툴이 존재하지만, 젠킨스를 선택한 이유는 아래와 같다.</p>
<p><strong>1. 다양한 플러그인 생태계</strong></p>
<ul>
<li>젠킨스는 수많은 플러그인을 지원하여 다양한 기술, 다양한 플랫폼과의 연결을 지원한다.</li>
<li>따라서 넓은 확장성을 갖는다.</li>
<li><em>2. 높은 유연성, 사용자 정의 가능성*</em></li>
<li>다양한 파이프라인 기능을 통해 복잡한 빌드 단계를 자유롭게 정의하고 자동화 가능하다.</li>
<li>Groovy 스크립트를 사용하여 다양한 빌드, 배포 단계의 사용자 정의가 가능하다.</li>
<li><em>3. 수많은 사용자*</em></li>
<li>많은 사용자를 보유하고 있어, 레퍼런스가 많고 활성화된 커뮤니티의 도움을 받을 수 있다.</li>
</ul>
<p>위와같은 사유로 CI/CD 툴로 Jenkins를 선택하게 되었다.</p>
<h1 id="3-젠킨스-설치">3. 젠킨스 설치</h1>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/3150c1dc-8246-4bf4-a712-f813e300604c/image.png" alt=""></p>
<p>시놀로지 나스의 Docker 컨테이너를 관리하는 프로그램인 Container Manager가 설정되어있다는 가정 하에 진행한다.</p>
<p>UI를 통한 Jenkins 설치는 아래 글을 참고하자.
<a href="https://gdpark.tistory.com/304">UI를 통한 Jenkins 설치</a></p>
<h2 id="31-터미널-연결">3.1. 터미널 연결</h2>
<p>시놀로지에서 지원하는 UI툴을 통해 설치해도 되지만, 편리한 명령어 사용을 위해 Putty를 통해 NAS에 접속한다.</p>
<blockquote>
<p>Synology NAS의 제어판 &gt; 터미널 및 SNMP &gt; 터미널의 SSH 서비스 활성화를 통해 접속을 활성화할 수 있다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/fda5301c-bcbb-4b45-a609-4944eeb7bca2/image.png" alt=""></p>
</blockquote>
<h2 id="32-docker-명령어-통해-jenkins-설치">3.2. Docker 명령어 통해 Jenkins 설치</h2>
<pre><code class="language-bash">docker run -itd --name jenkins -p 9090:8080 jenkins/jenkins:lts</code></pre>
<p>위 명령어를 통해 도커에 Jenkins를 최신 버전으로 설치 한다.
포트 설정은 개인에 맞게 하면 된다.</p>
<pre><code class="language-bash">docker ps</code></pre>
<p>설치 후 위 명령어를 통해 젠킨스가 작동하는 지 확인해준다. 아래처럼 Jenkins 컨테이너가 뜬다면 성공이다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/597c02be-1283-4f86-8de8-b1020408e847/image.png" alt=""></p>
<h2 id="33-네트워크-설정">3.3. 네트워크 설정</h2>
<p>NAS 내부에서는 Jenkins에 접근이 가능하지만, 외부 환경에서 젠킨스에 접속하려면 포트 설정이 필요하다.</p>
<blockquote>
<p>제어판 &gt; 외부 엑세스 &gt; 라우터 구성 &gt; 생성 에서
내장 응용 프로그램 선택 후 &#39;Docker Jenkins&#39; 를 찾아서 활성화 해준다.
(보이지 않으면 사용자 지정 포트에서 설정해줘도 괜찮다)</p>
</blockquote>
<p>포트 개방시 아래와 같이 젠킨스 포트가 활성화 된것이 보인다. (보안 사유로 가린 것이 많은 것을 양해 바란다)</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/cb63a1ad-1369-473b-b35b-4c25fa595c01/image.png" alt=""></p>
<p>추가로 외부 접속까지 허용할 필요가 있다면 공유기 등의 포트 포워딩 설정을 진행한다.</p>
<h1 id="4-젠킨스-설정">4. 젠킨스 설정</h1>
<p>NAS의 외부 접속 IP 혹은 내부 IP를 통해 젠킨스에 접속한다.
나의 경우 내부 접속 IP가 192.168.0.3 인 관계로
192.168.0.3:9090 으로 접속해주면 됐다.</p>
<p>접속하면 젠킨스 초기 비밀번호를 입력하여 Unlock 하라는 페이지가 뜬다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/2a424a4a-a60f-45b0-83b9-d7b2bd88cec9/image.png" alt=""></p>
<p>위에서 접속했던 대로 NAS에 SSH 접속을 통해 접속한 후, 설치한 젠킨스 컨테이너 내부에 접속해야 한다.</p>
<p>아래 명령어를 통해 젠킨스 컨테이너에 접속 후</p>
<pre><code class="language-bash">docker exec -it jenkins /bin/bash</code></pre>
<p>아래 명령어로 initialPassword를 얻는다 </p>
<pre><code class="language-bash">cat /var/jenkins_home/secrets/initialAdminPassword</code></pre>
<p>패스워드를 입력하면 플러그인 설치 화면이 뜬다.
좌측의 추천 플러그인 설치를 선택했다.</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/2cec801d-e445-4cd8-83ed-3b4066a8e740/image.png" alt=""></p>
<p>캡쳐하지 못했으나,
각종 플러그인을 설치하는 화면이 뜨고 (DS220+ 기준 20분 가량 소요되었다), 어드민 계정을 생성하는 화면이 뜬다.</p>
<p>해당 단계를 완료하면 젠킨스 접속이 완료된다.</p>
<p>위 단계로 초기 설정을 마쳤다</p>
<h1 id="5-젠킨스-작업-생성-및-설정">5. 젠킨스 작업 생성 및 설정</h1>
<h2 id="51-작업-생성">5.1. 작업 생성</h2>
<p>젠킨스 메인 화면에서 <code>새로운 작업</code>을 선택 한다.</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/fe5fd85a-1e83-47f5-a4f6-79757dfcf9ac/image.png" alt=""></p>
<p>위 화면에서 아이템 명을 입력한 후 아래에서 Pipeline을 선택한다.</p>
<h2 id="52-작업-설정">5.2. 작업 설정</h2>
<ul>
<li>General 설정
GitHub Project를 선택해 깃허브 레포지토리의 URL을 넣어주고,
아래 GitHub hook trigger for GITScm Polling을 활성화 해 웹훅을 통해 푸시 된 신호를 받도록 한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/b2cecb3f-e7ab-46b3-9ffd-eb49d9b12b56/image.png" alt=""></p>
<ul>
<li><p>Advanced Project Option
필요 시 작성하도록 한다.</p>
</li>
<li><p>Pipeline
Definition에서 Pipeline script from SCM을 선택한 후 SCM 에 Git을 선택한다.</p>
</li>
</ul>
<p>Repository URL을 마찬가지로 깃허브 레포지토리 URL을 채워넣고 Credentials는 해당되는 경우 작성한다.</p>
<p>Branches to build는 푸시를 감지하고 빌드할 브랜치를 선택한다. 기본은 <code>*/master</code>로 되어있는데, 깃허브 정책 변경에 따라 기본 브랜치가 main으로 바뀐지 꽤 지났으므로 <code>*/main</code>으로 수정해준다.</p>
<p>마지막으로 Script Path는 JenkinsFile이 위치할 경로를 작성해준다.
필자의 프로젝트는 최상위 경로에 위치할 예정이라 파일명인 <code>Jenkinsfile</code>로만 작성해둔다</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/1849313a-d3d1-4579-940f-17aba4ccd99a/image.png" alt=""></p>
<p>위와같이 실행하면 파이프라인 아이템 생성이 완료된다.</p>
<h2 id="53-환경변수-설정">5.3. 환경변수 설정</h2>
<p>각 아이템에서 빌드 및 실행시에 사용할 환경변수를 설정해 줄 필요가 있다.
GitHub에 올린 코드를 가져오기 때문에, Secret 값들은 환경변수 처리후 Jenkins에서 빌드 시에 삽입해주는 것이다.</p>
<blockquote>
<p>젠킨스 관리(설정) &gt; Security &gt; Credential 메뉴 진입</p>
</blockquote>
<p>아래와 같이 저장소가 보인다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/4d578d69-614d-45d2-9176-90330065ad8e/image.png" alt=""></p>
<p>(global) 부분을 눌러서 진입 후 우 상단의 
+Add Credentials 선택</p>
<p>kind에서 Secret text 선택
ID에는 환경변수 명을, 
Secret에 값을 넣는다.</p>
<p>필요시 Description을 작성한다.</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/66d08050-e7fa-4c46-b8d9-d2637b676512/image.png" alt=""></p>
<p>Create를 누르면 저장 완료된다.</p>
<p>위 과정을 반복해 필요한 환경변수를 모두 작성해준다.</p>
<h2 id="54-깃허브-웹훅-설정">5.4. 깃허브 웹훅 설정</h2>
<p>깃허브에서 푸시 될 때마다 젠킨스에 알려주도록 웹 훅 설정이 필요하다.</p>
<p>깃허브 &gt; 해당 레포지토리 &gt; Setting &gt; Webhooks에 접속 후 </p>
<p>Payload URL은 외부에서 접속 가능한 Jenkins URL + /github-webhook/</p>
<p>Contents type 은 application/json</p>
<p>나머지는 아래와 같이 선택 후 생성한다</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/fd0f7ce2-e8cf-423d-8548-0c0fc0b4cea8/image.png" alt=""></p>
<p>기본적인 설정은 모두 완료되었다.</p>
<h1 id="6-젠킨스-파이프라인-작성">6. 젠킨스 파이프라인 작성</h1>
<p>젠킨스 파이프라인 코드를 작성하는데에는 </p>
<p>1) Web UI 를 통해서 직접 스크립트 작성
2) SCM 통해서 Jenkinsfile에 스크립트 작성
등의 방식이 있다고 한다.</p>
<p>우리는 2번 방식을 선택하였기에, 레포지토리에 Jenkinsfile을 작성해줘야한다.</p>
<h2 id="61-선언적-스크립트-파이프라인">6.1. 선언적, 스크립트 파이프라인</h2>
<p>Jenkinsfile의 파이프라인을 작성하는데에는 역시 두가지 방식이 있는데 </p>
<p>1) Declarative Pipline
2) Scripted Pipeline
이렇게 두가지 방식이 있다고 한다.</p>
<p>각 방식의 장단점을 간단히 살펴보자면
<strong>1. 선언적 파이프라인</strong>
<strong>장점</strong></p>
<ul>
<li>구문이 단순하고 직관적이라 낮은 러닝커브</li>
<li>높은 코드 가독성</li>
<li><em>단점*</em></li>
<li>구문과 구조의 제약으로 복잡한 워크플로우 작성 어려움</li>
</ul>
<p><strong>2. 스크립트 파이프라이</strong>
<strong>장점</strong></p>
<ul>
<li>높은 유연성과 복잡한 워크플로우 구현 가능</li>
<li>Groovy 언어의 강력한 기능 사용 가능</li>
<li><em>단점*</em></li>
<li>Groovy 언어에 대한 높은 러닝커브</li>
</ul>
<p>따라서 필자는 복잡한 워크플로우를 구현할 필요는 없기때문에 선언적 파이프라인으로 구현하였다.</p>
<h2 id="62-선언적-파이프라인-구조">6.2. 선언적 파이프라인 구조</h2>
<pre><code>Jenkinsfile (Declarative Pipeline)
pipeline {
    agent any 
    stages {

          #빌드단계를 정의
        stage(&#39;Build&#39;) { 
            steps {
                // 
            }
        }

        #테스트 단계를 정의
        stage(&#39;Test&#39;) { 
            steps {
                // 
            }
        }

        #배포 단계를 정의
        stage(&#39;Deploy&#39;) { 
            steps {
                // 
            }
        }
    }
}</code></pre><p><strong>pipeline</strong></p>
<ul>
<li>파이프라인은 CD(지속적 배포) 파이프라인의 사용자 정의 모델</li>
<li>파이프라인 코드는 애플리케이션 빌드, 테스트, 배포 단계를 포함하는 전체 빌드 프로세스를 정의</li>
</ul>
<p><strong>Stage</strong></p>
<ul>
<li><code>stage</code>블록은 전체 파이프라인에서 개념적으로 구별되는 작업의 하위집합 (&#39;빌드&#39;, &#39;테스트&#39;, &#39;배포&#39;)을 정의하는 블록</li>
</ul>
<p><strong>Step</strong></p>
<ul>
<li>단일 작업</li>
<li>특정 시점에 Jenkins에 무엇을 해야할지 알려주는 역할</li>
</ul>
<h2 id="63-pipeline-코드">6.3. pipeline 코드</h2>
<h3 id="631-environment">6.3.1 environment</h3>
<p>환경변수 값을 설정해주는 부분의 블럭이다.
서비스 명, 이미지 명, 각종 포트 및 환경변수를 설정해준다.
단순 선언부이다.</p>
<pre><code class="language-groovy">environment {
        SERVICE_NAME = &#39;cranebackend_v2&#39;
        IMAGE_TAG = &quot;cranebackend_v2:latest&quot;
        LOCAL_PORT = &quot;8900&quot;
        // ✅ 환경 변수 설정
        JWT_SECRET = credentials(&#39;JWT_SECRET&#39;)
        REDIS_HOST = credentials(&#39;REDIS_HOST&#39;)
        REDIS_PORT = credentials(&#39;REDIS_PORT&#39;)
        MYSQL_URL_V2 = credentials(&#39;MYSQL_URL_V2&#39;)
        MYSQL_USER = credentials(&#39;MYSQL_USER&#39;)
        MYSQL_PASSWORD = credentials(&#39;MYSQL_PASSWORD&#39;)

        TZ = &quot;Asia/Seoul&quot;
        SLACK_CHANNEL = &quot;build-deploy&quot;
        SLACK_SUCCESS_COLOR = &quot;#2C953C&quot;
        SLACK_FAIL_COLOR = &quot;#FF3232&quot;
        SLACK_MESSAGE_UNIT = &quot;==================================================&quot;
        SLACK_DURATION_TIME_MESSAGE = &quot;&quot;
        SLACK_MESSAGE_BUILDER = &quot;&quot;
    }
    //슬랙 알림 부분은 무시하자</code></pre>
<h3 id="632-check-changes-stage">6.3.2. Check Changes stage</h3>
<p>github 코드를 가져와 변경사항이 있는지 감지하는 부분이다.
변경사항이 없는 경우 error를 발생시켜 단계를 중단시킨다.</p>
<pre><code class="language-groovy">    stage(&#39;Check Changes&#39;) {
            steps {
                script {
                    def changes = sh(script: &quot;git diff --name-only HEAD^&quot;, returnStdout: true).trim()

                    if (!changes) {
                        currentBuild.result = &#39;NOT_BUILT&#39;
                        error(&#39;No changes in cranebackend_v2 directory, skipping build&#39;)
                    }
                    echo &quot;✅ cranebackend_v2 변경 사항 감지됨. 빌드를 진행합니다.&quot;
                }
            }
        }</code></pre>
<h3 id="633-checkout-stage">6.3.3. Checkout stage</h3>
<p>레포지토리의 브랜치를 checkout 해오는 스테이지이다.
가장 최근 커밋을 가져오게 되어있다.</p>
<pre><code class="language-groovy">stage(&#39;Checkout&#39;) {
            steps {
                git branch: &#39;main&#39;,
                    url: &#39;https://github.com/CraneWebProject/Crane_web_backend_v2&#39;

                script {
                    def gitCommit = sh(script: &#39;git rev-parse HEAD&#39;, returnStdout: true).trim()
                    echo &quot;✅ 현재 빌드하는 커밋: ${gitCommit}&quot;
                }
            }
            post {
                failure {
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: &quot;${SLACK_MESSAGE_BUILDER}&quot; + stageFailSlackMessage(&quot;Git Checkout&quot;) + &quot;${SLACK_MESSAGE_UNIT}&quot;
                    )
                }
            }
        }</code></pre>
<h3 id="634-build-stage">6.3.4. Build stage</h3>
<p>shell script를 실행시켜 빌드를 진행하는 단계.
빌드 실패 시 슬랙 메시지를 보내는 부분이 있다.
슬랙 세팅을 해주지 않았다면 제외해도 된다.</p>
<pre><code class="language-groovy">stage(&#39;Build&#39;) {
            steps {
                script{
                    long startTime = new Date().getTime()
                    sh &#39;&#39;&#39;
                        chmod +x gradlew
                        ./gradlew clean build -x test
                    &#39;&#39;&#39;
                    long endTime = new Date().getTime()
                    SLACK_DURATION_TIME_MESSAGE = getStageDurationMessage(startTime, endTime)
                    SLACK_MESSAGE_BUILDER += &quot;:white_check_mark: Build 성공! (${SLACK_DURATION_TIME_MESSAGE}) \n&quot;
                }

            }
            post {
                success {
                    echo &quot;✅ Build 단계 완료&quot;
                }
                failure {
                    error &quot;❌ Build 단계 실패&quot;
                    slackSend (
                        channel: SLACK_CHANNEL,
                        color: SLACK_FAIL_COLOR,
                        message: &quot;${SLACK_MESSAGE_BUILDER}&quot; + stageFailSlackMessage(&quot;Build&quot;) + &quot;${SLACK_MESSAGE_UNIT}&quot;
                    )
                }
            }
        }</code></pre>
<h3 id="635-docker-build-and-run-stage">6.3.5. Docker build and Run Stage</h3>
<p>도커 컨테이너를 빌드하고 기존 컨테이너 삭제 후, 새로운 컨테이너를 실행하는 단계이다.
위 단계를 실행하기 위해서는 프로젝트에 알맞는 Dockerfile이 필요하다.</p>
<p>환경변수는 빌드시, 실행시 모두 필요하기때문에 모두 작성해서 넣어준다.</p>
<pre><code class="language-groovy">stage(&#39;Docker Build &amp; Run&#39;) {
            steps {
                script {
                    long startTime = new Date().getTime()

                    sh &#39;&#39;&#39;
                    echo &quot;🔍 현재 REDIS_HOST 값: $REDIS_HOST&quot;
                    echo &quot;🔍 현재 REDIS_PORT 값: $REDIS_PORT&quot;

                    docker build -t $IMAGE_TAG \
                        --build-arg TZ=$TZ \
                        --build-arg JWT_SECRET=$JWT_SECRET \
                        --build-arg REDIS_HOST=$REDIS_HOST \
                        --build-arg REDIS_PORT=$REDIS_PORT \
                        --build-arg MYSQL_URL_V2=$MYSQL_URL_V2 \
                        --build-arg MYSQL_USER=$MYSQL_USER \
                        --build-arg MYSQL_PASSWORD=$MYSQL_PASSWORD \
                        .

                    # 기존 컨테이너 종료 후 삭제
                    docker stop $SERVICE_NAME || true
                    docker rm $SERVICE_NAME || true

                    # 새 컨테이너 실행
                    docker run -d --name $SERVICE_NAME \
                        -e TZ=$TZ \
                        -e JWT_SECRET=$JWT_SECRET \
                        -e REDIS_HOST=$REDIS_HOST \
                        -e REDIS_PORT=$REDIS_PORT \
                        -e MYSQL_URL_V2=$MYSQL_URL_V2 \
                        -e MYSQL_USER=$MYSQL_USER \
                        -e MYSQL_PASSWORD=$MYSQL_PASSWORD \
                        -p $LOCAL_PORT:8900 $IMAGE_TAG
                    &#39;&#39;&#39;

                    long endTime = new Date().getTime()
                    SLACK_DURATION_TIME_MESSAGE = getStageDurationMessage(startTime, endTime)
                    SLACK_MESSAGE_BUILDER += &quot;:white_check_mark: Deploy 성공 (${SLACK_DURATION_TIME_MESSAGE}) \n&quot;
                    SLACK_MESSAGE_BUILDER += &quot;:tada: `${env.JOB_NAME}` 배포 파이프라인이 성공적으로 완료되었습니다. :beer:\n&quot; + &quot;${env.SLACK_MESSAGE_UNIT}&quot;
                }
            }
        }</code></pre>
<h3 id="636-post-단계">6.3.6. post 단계</h3>
<p>성공 및 실패 결과를 슬랙 메시지로 전송하는 단계이다.
상술하였듯, 슬랙 세팅을 원하지 않는경우 제외하면 된다.</p>
<pre><code class="language-groovy">post {
        success {

            echo &quot;&quot;&quot;
            ===========================================
            ✅ Pipeline Successfully Completed
            Service: ${SERVICE_NAME}
            Image: ${IMAGE_TAG}
            Port: ${LOCAL_PORT}
            ===========================================
            &quot;&quot;&quot;

            slackSend (
                channel: SLACK_CHANNEL,
                color: SLACK_SUCCESS_COLOR,
                message: SLACK_MESSAGE_BUILDER
            )
        }
        failure {
            echo &quot;&quot;&quot;
            ===========================================
            ❌ Pipeline Failed
            Service: ${SERVICE_NAME}
            Stage: ${currentBuild.result}
            ===========================================
            &quot;&quot;&quot;

             slackSend (
                channel: SLACK_CHANNEL,
                color: SLACK_FAIL_COLOR,
                message: &quot;${SLACK_MESSAGE_BUILDER}&quot; + stageFailSlackMessage(&quot;Deploy&quot;) + &quot;${SLACK_MESSAGE_UNIT}&quot;
            )
        }
        always {
            cleanWs()
        }
    }</code></pre>
<h3 id="637-각종-메서드">6.3.7. 각종 메서드</h3>
<pre><code class="language-groovy">
// Stage 경과 시간을 계산하여 메시지를 생성하는 함수
def getStageDurationMessage(long startTime, long endTime) {
    long durationMillis = endTime - startTime

    long durationSeconds = (long) (durationMillis / 1000) % 60
    long durationMinutes = (long) (durationMillis / (1000 * 60)) % 60

    def durationMessage = &quot;&quot;
    if (durationMinutes &gt; 0) {
        durationMessage += &quot;${durationMinutes}분 &quot;
    }
    durationMessage += &quot;${durationSeconds}초&quot;

    return durationMessage
}


// 슬랙 실패 메세지를 생성하는 함수
def stageFailSlackMessage(stageName) {
    return &quot;:alert: ${stageName} 단계에서 배포가 실패하였습니다. \n&quot;
}</code></pre>
<h2 id="64-dockerfile">6.4. Dockerfile</h2>
<p>참고를 위해 덧붙인다. 각 프로젝트에 알맞은 Dockerfile을 작성한다.</p>
<pre><code>#dockerfile
FROM openjdk:17
WORKDIR /app

#환경변수 정의
ARG JWT_SECRET
ARG MYSQL_PASSWORD
ARG MYSQL_URL_V2
ARG MYSQL_USER
ARG REDIS_HOST
ARG REDIS_PORT

# 환경변수 설정
ENV JWT_SECRET=${JWT_SECRET}
ENV MYSQL_PASSWORD=${MYSQL_PASSWORD}
ENV MYSQL_URL_V2=${MYSQL_URL_V2}
ENV MYSQL_USER=${MYSQL_USER}
ENV REDIS_HOST=${REDIS_HOST}
ENV REDIS_PORT=${REDIS_PORT}

COPY build/libs/CraneWebBackend_v2-0.0.1-SNAPSHOT.jar app.jar

EXPOSE 8900

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre><h1 id="7-배포-자동화">7. 배포 자동화</h1>
<p>위 과정을 모두 마치면, 설정해둔 레포지토리의 해당 브랜치에 커밋이 될때마다 자동으로 감지하여 빌드가 실행된다.</p>
<p>생성된 아이템들의 빌드 상태는 아래와 같이 젠킨스 메인에서 한눈에 볼 수 있다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/dbb81eac-1715-4c21-b838-ab95024f5d3b/image.png" alt=""></p>
<p>생성된 도커 컨테이너는 SSH 접속을 통해 
<code>docker ps</code> 명령어나, 아래와 같이 시놀로지 웹의 Container Manager를 통해 조회할 수 있다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/9ced2972-b4a1-4b25-b118-eaf3d0905b08/image.png" alt=""></p>
<h1 id="8-참고-자료">8. 참고 자료</h1>
<ul>
<li><a href="https://www.jenkins.io/doc/book/pipeline/#pipeline-syntax-overview">젠킨스 공식문서</a></li>
<li><a href="https://velog.io/@bbkyoo/Jenkins">https://velog.io/@bbkyoo/Jenkins</a></li>
<li><a href="https://code00.tistory.com/123">https://code00.tistory.com/123</a></li>
<li><a href="https://velog.io/@revelation/Jenkins-pipeline-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0">https://velog.io/@revelation/Jenkins-pipeline-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0</a></li>
<li><a href="https://seongwon.dev/DevOps/20220715-CICD%EA%B5%AC%EC%B6%95%EA%B8%B01/">https://seongwon.dev/DevOps/20220715-CICD%EA%B5%AC%EC%B6%95%EA%B8%B01/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[코프링 엔티티 생성 시 ID 기본 값은 어떤게 좋을까?]]></title>
            <link>https://velog.io/@hyeseong-int/%EC%BD%94%ED%94%84%EB%A7%81-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%83%9D%EC%84%B1-%EC%8B%9C-ID-%EA%B8%B0%EB%B3%B8-%EA%B0%92%EC%9D%80-%EC%96%B4%EB%96%A4%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@hyeseong-int/%EC%BD%94%ED%94%84%EB%A7%81-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%83%9D%EC%84%B1-%EC%8B%9C-ID-%EA%B8%B0%EB%B3%B8-%EA%B0%92%EC%9D%80-%EC%96%B4%EB%96%A4%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Mon, 03 Mar 2025 15:14:04 GMT</pubDate>
            <description><![CDATA[<p>현재 개발중인 프로젝트는 MSA 구조로 개발중인 Spring boot 프로젝트이며, 특정 서비스는 JAVA 대신 Kotlin으로 작성되고 있다.</p>
<p>Entity 코드를 작성하던 도중 엔티티 생성 시 기본 값에 대한 고민이 발생 해 공부 해 본 부분을 정리해 본다.</p>
<hr>
<h1 id="목차">목차</h1>
<ol start="0">
<li>들어가며</li>
<li>개념 및 배경 지식</li>
<li>각 방식의 비교</li>
<li>두 방식 모두 작동하는 이유</li>
<li>각 방식의 장단점</li>
<li>추천하는 방식</li>
<li>결론</li>
</ol>
<hr>
<h1 id="0-🤔들어가며">0. 🤔들어가며</h1>
<p>스프링 부트에서 Java를 사용하여 Entity를 생성할 때에는 아래와 같은 코드를 사용한다.</p>
<pre><code class="language-java">@Getter
@NoArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;user_id&quot;)
    private Long userId;

    @Column(nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    //각종 필드 및 메서드 생략
}</code></pre>
<p>위 코드에서 특히 ID 부분에 집중해보자.</p>
<p>JAVA Entity를 생성할 때에는 위와 같은 방식으로 <code>NULL</code> 값 혹은 <code>0L</code> 값을 ID의 초기 값으로 넣어 생성해 왔다.</p>
<blockquote>
<p>허나, 코틀린은 Null이 가능한 Type을 완전히 다르게 취급하여 Null 안정성을 강조한다.</p>
</blockquote>
<p>코틀린은 Null 처리를 명확하게 하기 위해 Null이 가능한 값과 불가능한 값의 타입을 다르게 정의한다.
즉 위 코드를 코틀린으로 작성했을 때 기본 값이 Null인지, 0L인지에 따라 아래와 같이 타입이 달라지는 것이다.</p>
<pre><code class="language-kotlin">//1번 방식 (초기 값이 Null)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long? = null

//2번 방식 (초기 값이 0L)
1. @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long = 0L,</code></pre>
<h3 id="🤔그럼-어떤-방식이-맞는가">🤔그럼 어떤 방식이 맞는가</h3>
<blockquote>
<p>0L을 기본값으로 두어 Null을 피하는게 안정적인 방식일까?
Null을 기본 값으로 두어 현실의 객체 모델링 방식을 반영하는 것이 맞을까?</p>
</blockquote>
<p>코드를 작성하다 위 두 방식 중 어떤 방식이 좋은지 고민이 되어 공부해 보려 한다.</p>
<hr>
<h1 id="1-개념-및-배경지식">1. 개념 및 배경지식</h1>
<h2 id="jpa-관련-배경지식">JPA 관련 배경지식</h2>
<h3 id="db-자동증가-기능-활용"><strong>DB 자동증가 기능 활용</strong></h3>
<p>ID 부분에는 <code>@GeneratedValue(strategy = GenerationType.IDENTITY)</code> 전략을 사용하여 ID 생성을 자동증가 하도록 DB에 위임하는 방식이다. </p>
<h3 id="jpa는-id-생성-시-관여x"><strong>JPA는 ID 생성 시 관여X</strong></h3>
<p>JPA는 엔티티를 영속화 할 때 별도의 값을 생성하거나, 전달하지 않는다. DB에 새로운 레코드 삽입을 요청할 뿐이다.</p>
<h3 id="db는-id-생성-후-jpa에-반환"><strong>DB는 ID 생성 후 JPA에 반환</strong></h3>
<p>위 코드에 따르면 자동 증가된 ID를 생성하여 레코드에 삽입하고 해당 값을 JPA에 알려준다.</p>
<h3 id="jpa는-반환받은-id로-엔티티-업데이트"><strong>JPA는 반환받은 ID로 엔티티 업데이트</strong></h3>
<p>JPA는 DB로 부터 받은 ID 값으로 엔티티의 ID 필드를 업데이트 한다. 이 시점에서 엔티티는 영속 상태가 되는 것이다.</p>
<h2 id="kotlin-관련-배경지식">Kotlin 관련 배경지식</h2>
<h3 id="null이-될수-있는-type">null이 될수 있는 type</h3>
<p>코틀린은 type에 ?를 붙임으로서 null이 가능한 변수임을 명시적으로 표현한다.</p>
<hr>
<h1 id="2-각-방식의-비교">2. 각 방식의 비교</h1>
<h2 id="21-nullable-type-사용">2.1. Nullable Type 사용</h2>
<pre><code class="language-kotlin">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long? = null</code></pre>
<ul>
<li><strong><code>boardId</code></strong>를 <strong>nullable</strong> 타입(<strong><code>Long?</code></strong>)으로 선언</li>
<li>엔티티가 처음 생성될 때 <strong><code>Null</code></strong> 상태로 두다가 DB에 저장되면 자동으로 ID가 할당됨</li>
</ul>
<h2 id="22-0l-방식-사용">2.2. 0L 방식 사용</h2>
<pre><code class="language-kotlin">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var boardId: Long = 0L</code></pre>
<ul>
<li><strong><code>boardId</code></strong>를 <strong>non-nullable</strong> 타입(<strong><code>Long</code></strong>)으로 선언하고 기본값으로 <strong><code>0L</code></strong>을 할당</li>
<li>DB에 저장되면 자동으로 ID가 할당되지만, 엔티티 생성 시 기본값 <strong><code>0L</code></strong>로 초기화됨</li>
</ul>
<hr>
<h1 id="3-두-방식-모두-작동하는-이유">3. 두 방식 모두 작동하는 이유</h1>
<h2 id="31-개념적-설명">3.1. 개념적 설명</h2>
<p>배경 지식에서 설명하였듯, <code>@GeneratedValue(strategy = GenerationType.IDENTITY)</code> 전략은 ID 생성을 전적으로 DB에 위임하는 방식이다.
어떤 방식으로 초기 값이 설정 되던간에, JPA는 해당 값을 쿼리에 포함하지 않고, DB에도 전달하지 않는다.</p>
<p>아래 표는 Chat GPT에게 도움을 받아본 Hibernate 내부 처리 및 DB 별 처리 방식이다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/6b0b3181-d74d-48e1-9e66-a371d818d4a3/image.png"></p>
<h3 id="nullablelong--null-인-경우">nullable(<code>Long? = null</code>) 인 경우</h3>
<ul>
<li>자동으로 null을 감지하여 ID를 할당.</li>
<li>JPA는 <strong><code>null</code></strong> 을 기본키가 없는 상태로 판단.</li>
<li>Hibernate에서 새 엔티티로 판단하여 DB에 Insert(AutoIncreament) 사용 (표 참고)</li>
</ul>
<h3 id="non-nullablelong--0-인-경우">non-nullable(<code>Long = 0</code>) 인 경우</h3>
<ul>
<li>자동으로 0L을 더미 값으로 감지하여 ID를 할당한다.</li>
<li>0L은 Jpa 구현체 (Hibernate)에서 내부적으로 특별 처리 되어 <code>&#39;ID가 없음을 의미&#39;</code>하도록 동작한다.</li>
<li>Hibernate에서 새 엔티티로 판단하지만 0L이라는 ID 필드의 값을 삽입하려 시도.
(즉, 신규 엔티티이지만, 0L이라는 유효한 ID 값을 갖고있다고 판단 함)</li>
</ul>
<h2 id="32-코드-분석">3.2. 코드 분석</h2>
<p>위 내용을 코드를 기반으로 분석해보자</p>
<p>spring Data JPA의 <code>save()</code> 메서드</p>
<pre><code class="language-java">@Transactional
    public &lt;S extends T&gt; S save(S entity) {
        Assert.notNull(entity, &quot;Entity must not be null&quot;);
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return (S)this.entityManager.merge(entity);
        }
    }</code></pre>
<ul>
<li>isNew() 메서드를 통해서 새로운 엔티티이면 persist()를 호출하여 저장하고, 그렇지 않으면 merge()를 호출하여 기존 값을 업데이트 한다.</li>
</ul>
<p><code>isNew()</code> 메서드</p>
<pre><code class="language-java"> public boolean isNew(T entity) {
        ID id = (ID)this.getId(entity);
        Class&lt;ID&gt; idType = this.getIdType();
        if (!idType.isPrimitive()) {
            return id == null;
        } else if (id instanceof Number) {
            return ((Number)id).longValue() == 0L;
        } else {
            throw new IllegalArgumentException(String.format(&quot;Unsupported primitive id type %s&quot;, idType));
        }
    }</code></pre>
<ul>
<li>entity의 ID가 Null이면 true를 반환한다</li>
<li>entity의 ID가 숫자형 0L이면 true를 반환한다.</li>
</ul>
<p>따라서 초기 값이 <code>0L</code>이든 <code>Null</code>이든 잘 작동하는 것을 알 수 있다.</p>
<hr>
<h1 id="4-각-방식의-장단점">4. 각 방식의 장단점</h1>
<h2 id="41-nullable-방식">4.1. Nullable 방식</h2>
<h3 id="장점">장점</h3>
<p><strong>엔티티 생명주기 명확화</strong></p>
<ul>
<li>null 값을 통해 엔티티의 생명주기 (영속화 이전 / 이후) 구분이 코드 레벨에서도 명확하다.</li>
<li>영속화 된 후에야 비로소 ID가 부여되는 방식</li>
<li>현실의 객체 모델링 방식을 그대로 반영한다.</li>
<li>ID 값을 DB에서 생성한다는 IDENTITY 전략의 의도를 명확히 표현할 수 있다.</li>
</ul>
<p><strong>NullSafety 극대화 및 안정성 확보</strong></p>
<ul>
<li>코틀린의 강력한 Null safety 기능을 활용하여 <code>NPE(Null Pointer Exception)</code>을 원천 차단할 수 있다.</li>
<li>ID가 Null일 수 있음을 항상 고려하고 코딩하며 런타임 오류를 예방한다.</li>
<li>다양한 Null safety 연산자<code>(?., !!. ?:)</code>를 사용해 null 처리를 간결하게 할 수 있다.</li>
</ul>
<h3 id="단점">단점</h3>
<p><strong>번거로운 Nullable 필드 처리</strong></p>
<ul>
<li>코틀린의 Nullable 필드는 null 가능성을 항상 염두에 두고 코드를 작성해야하기에 번거로움이 발생할 수 있음
(코틀린의 Nullsafety 기능을 사용하여 안전한 코드를 작성할 수 있도록 하는 실질적 장점이 될 수 있음.)</li>
</ul>
<h2 id="42-non-nullable-방식">4.2. Non-Nullable 방식</h2>
<h3 id="장점-1">장점</h3>
<p><strong>Null check 회피 가능</strong></p>
<ul>
<li>ID에 Null 값이 들어가지 않기 때문에 Nullcheck 필요성이 없음.
(다만 영속화 되지 않은 엔티티 Id 필드에 더미 값을 넣은 것과 같아 의미 왜곡의 여지가 있어 실질적 단점일 수 있음)</li>
</ul>
<h3 id="단점-1">단점</h3>
<p><strong>의미 없는 0L 값으로 오류 및 혼란 야기</strong></p>
<ul>
<li>할당되지 않은 ID에 0L이라는 더미 값을 넣어 의미를 왜곡 할 수 있음.</li>
<li>영속화 이전 / 이후를 혼동하게 만들 수 있음</li>
<li>DB별 0L에 대한 처리 차이로 혼동 및 오류 발생 가능 (e.g. PostgreSQLDB는 ID값이 0 부터 A.I.)</li>
</ul>
<hr>
<h1 id="5-추천하는-방식">5. 추천하는 방식</h1>
<blockquote>
<p>Nullable 방식을 선택하기로 결정</p>
</blockquote>
<ol>
<li>Kotlin의 null safety기능 활용 가능</li>
<li>객체 지향 설계 원칙에 부합</li>
<li>의미론적, 상태적 명확성 확보 (영속, 비영속 상태 비교 가능)</li>
</ol>
<p>위 근거를 바탕으로 Nullable 한 아래 방식을 선택하기로 하였다.</p>
<pre><code class="language-kotlin">@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val boardId: Long? = nul</code></pre>
<hr>
<h1 id="6-결론">6. 결론</h1>
<p>코틀린은 NullSafe를 중시 하기때문에, Null 값을 회피하는 것이 더 안전할 것이라 생각하였으나, Null값이 있을 수 있는 부분은 Nullable하게 두고, Null 처리를 명확하게 하는 것이 오히려 안전한 방식임을 알게 되었다.</p>
<p>내부 동작에서 JPA는 <code>Null</code> 과 <code>0L</code> 모두 새 객체로 인식하지만, 0L의 경우 DB에 값을 insert 할 위험이 있으며 이후 DB에 따라 오류가 발생할 여지도 있다.</p>
<p>Nullable 한 방식은 <strong>안정적</strong>이고 <strong>잘 설계되</strong>었고, <strong>유지보수가 용이한</strong> 코드임을 확인할 수 있었다.</p>
<p>따라서 해당 방식을 선택하고자 한다</p>
<blockquote>
<p>위 글은 제가 공부하면서 얻은 정보들을 정리한 글입니다.
오타, 잘못된 개념 등에 대해 지적해 주시면 제게 많은 배움이 있을 것 같습니다.
감사합니다</p>
</blockquote>
<hr>
<h1 id="7-참고-자료">7. 참고 자료</h1>
<p><a href="https://ittrue.tistory.com/482">https://ittrue.tistory.com/482</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL 사용법]]></title>
            <link>https://velog.io/@hyeseong-int/QueryDSL-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@hyeseong-int/QueryDSL-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 11 Feb 2025 14:26:43 GMT</pubDate>
            <description><![CDATA[<p>현재 개발중인 프로젝트에 QueryDSL을 적용하면서 학습했던 내용을 정리해본다.</p>
<hr>
<h1 id="목차">목차</h1>
<ol start="0">
<li>들어가며</li>
<li>QueryDSL이란?</li>
<li>개념 및 배경지식</li>
<li>필요성 및 장단점</li>
<li>적용 방법</li>
<li>코드 작성 방법</li>
<li>최적화 방법</li>
<li>결론</li>
</ol>
<hr>
<h1 id="0-들어가며">0. 들어가며</h1>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/291466d7-1d48-4916-b971-9bfd5eb56b2e/image.png" width="50%">

<p>현재 진행중인 프로젝트에서는 JPQL을 사용하여 레포지토리 단 코드를 작성하고 있었다.
다만 쿼리가 길어지고 많아지며 불편함을 느끼고 개발에 어려움이 많아지게 된다.
이때, QueryDSL을 사용해보라는 피드백을 받아 학습하게 되었다.</p>
<h1 id="1-querydsl이란">1. QueryDSL이란?</h1>
<blockquote>
<p>Querydsl is a framework which enables the construction of type-safe SQL-like queries for multiple backends including JPA, MongoDB and SQL in Java. (<a href="https://github.com/querydsl/querydsl">QueryDSL Github</a>)
Querydsl은 Java에서 JPA, MongoDB, SQL을 포함한 여러 백엔드에 대해 타입 안전한 SQL 유사 쿼리를 구성할 수 있게 해주는 프레임워크입니다.</p>
</blockquote>
<p>QueryDSL은 JPQL(Java Persistence Query Language)를 사용해 쿼리를 작성하는데에 도움을 주는 프레임워크이다.
JPQL은 SQL과 유사하지만, DB 테이블이 아닌 엔티티 객체를 대상으로 쿼리를 작성한다.
QueryDSL은 이 JPQL 쿼리를 타입세이프 할 뿐만 아니라, 다양한 편의성과 이점을 갖고 사용할 수 있도록 해준다.</p>
<h1 id="2-개념-및-배경지식">2. 개념 및 배경지식</h1>
<h2 id="jpql의-한계">JPQL의 한계</h2>
<p>JPQL은 SQL과 유사한 문법의 쿼리를 작성하게 된다.
하지만 다음과같이 문자열 형태로 쿼리를 작성하게 된다.</p>
<pre><code>@Query(&quot;SELECT r FROM Reservation r WHERE r.reservationStartAt &gt;= :startOfDay AND r.reservationStartAt &lt; :startOfNextDay AND r.contentsId = :contentsId&quot;)
List&lt;Reservation&gt; findAllByResDateAndContentsId(
    @Param(&quot;startOfDay&quot;) LocalDateTime startOfDay,
    @Param(&quot;startOfNextDay&quot;) LocalDateTime startOfNextDay,
    @Param(&quot;contentsId&quot;) Long contentsId
);</code></pre><p>위와 같이 String으로 쿼리를 작성하게 되면 다음과같은 단점이 있다</p>
<ul>
<li><strong>런타임 시점에 오류 확인 가능:</strong> 컴파일 시점에 문법 오류를 검출할 수 없다.</li>
<li><strong>쿼리 가독성 저하:</strong> 복잡한 쿼리를 문자열로 작성하여 가독성이 떨어진다.</li>
<li><strong>코드 자동완성 사용 불가:</strong> IDE 에서 문자열 내에 쿼리문을 작성하기 때문에, 자동완성 기능의 도움을 받는 데에 한계가 있다.</li>
</ul>
<h2 id="querydsl의-등장">QueryDSL의 등장</h2>
<p>위와 같은 JPQL 코드의 단점을 해결하기 위해 QueryDSL이 등장하게 된다.
QueryDSL은 쿼리를 메서드 호출 형식으로 작성할 수 있게된다.</p>
<h1 id="3-필요성-및-장단점">3. 필요성 및 장단점</h1>
<p>아래와 같은 필요성으로 프로젝트에 QueryDSL을 도입하기로 하였다.</p>
<h2 id="필요성">필요성</h2>
<ul>
<li><strong>타입 세이프:</strong> 쿼리 작성 시 타입 오류를 방지하여 애플리케이션 런타임 안정성을 높인다.</li>
<li><strong>생산성 향상:</strong> 쿼리 작성시간을 단축하고 생산성을 높인다.</li>
<li><strong>유지보수 용이성:</strong> 복잡한 쿼리를 모듈화하여 코드 유지보수를 용이하게 한다.
뿐만 아니라 현재는 MySQL만 사용중인 프로젝트이지만, 서비스 확장 시 추가 DB 사용 계획이 있는만큼, 다양한 DB를 지원하는 점도 사용 근거중 하나이다.</li>
</ul>
<h2 id="장점">장점</h2>
<ul>
<li><strong>컴파일 시점 오류 검출:</strong> 쿼리 문법 오류를 컴파일 시점에 검출 가능하다.</li>
<li><strong>코드 재사용성:</strong> 자주 사용되는 쿼리를 메서드로 만들어 재사용 가능하다.</li>
<li><strong>쿼리 가독성 향상:</strong> 복잡한 쿼리를 메서드 호출 형태로 표현하여 가독성을 높일 수 있다.</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li><strong>학습 곡선:</strong> QueryDSL API를 익히는데는 추가적인 학습이 약간 필요하다.</li>
<li><strong>쿼리 성능:</strong> JPQL쿼리와 비교시, QueryDSL의 성능이 항상 더 좋지는 않다. (추후 직접적인 성능 비교를 해보고자 한다)</li>
</ul>
<h1 id="4-적용방법">4. 적용방법</h1>
<p>프로젝트 환경은 아래와 같다.</p>
<ul>
<li>JAVA 17</li>
<li>Spring boot 3.3.4</li>
<li>QueryDSL 5.0.0</li>
</ul>
<h2 id="41-의존성-추가">4.1. 의존성 추가</h2>
<p><code>build.gradle</code> 파일 내에 QueryDSL 의존성을 추가한다.</p>
<pre><code>// Query DSL 관련 스크립트 및 설정
buildscript {
    ext {
        queryDslVersion = &quot;5.0.0&quot;
    }
}

plugins {
    id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot;
}

dependencies {
    //QueryDSL 관련 라이브러리
    implementation &quot;com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties[&#39;querydsl.version&#39;]}:jakarta&quot;
    implementation &quot;com.querydsl:querydsl-core&quot;
    implementation &quot;com.querydsl:querydsl-collections&quot;
    annotationProcessor &quot;com.querydsl:querydsl-apt:${dependencyManagement.importedProperties[&#39;querydsl.version&#39;]}:jakarta&quot;
    // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
    // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;
    // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

def querydslDir = &#39;build/generated/querydslDir&#39;

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir}

sourceSets {
    main.java.srcDir querydslDir
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}</code></pre><p>만약 스프링 부트 버전이 <code>2.X.</code> 버전이라면 빌드시, 아래와 같은 에러가 발생하게 된다.</p>
<blockquote>
<p>Unable to load class &#39;javax.persistence.Entity&#39;.</p>
</blockquote>
<p>다음 링크를 참고하여 버전에 맞는 <code>build.gradle</code> 파일을 작성하도록 하자.
<a href="https://ttmcr.tistory.com/entry/QueryDSL-Unable-to-load-class-javaxpersistenceEntity-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95">[QueryDSL][Spring] Unable to load class &#39;javax.persistence.Entity&#39; 에러 해결 방법</a></p>
<h2 id="42-gradle-reload-실행">4.2. gradle reload 실행</h2>
<p>gradle reload를 실행한다.</p>
<h2 id="43-q-type-클래스-생성">4.3. Q-Type 클래스 생성</h2>
<p>QueryDSL은 엔티티 클래스를 기반으로 Q-Type 클래스를 생성한다.
이는 쿼리 작성시 엔티티 속성을 참조하는데 사용되기 때문에, QueryDSL 사용에 필수이다.
아래 두 방법 중 하나를 선택하여 Q-Type 클래스를 생성하도록 한다.</p>
<ol>
<li><p>Bash Shell 사용
프로젝트 경로에서 다음 명령어를 실행한다.</p>
<pre><code>./gradlew compileQuerydsl</code></pre></li>
<li><p>IDE 등 내부 기능 사용
Gradle Tasks &gt; other &gt; compileQuerydsl를 실행</p>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/2606a837-1e28-401e-b93e-2711dcb2f1bb/image.png" width="50%">


</li>
</ol>
<h2 id="44-클래스-생성-확인">4.4. 클래스 생성 확인</h2>
<p>위에서 <code>build.gradle</code> 파일에 정의한 경로( 위 파일에서는 build/generated/querydsl)내부에 Q 로 시작하는 클래스가 생성되었는지 확인한다.</p>
<h2 id="45-설정-시-발생-할-수-있는-에러">4.5. 설정 시 발생 할 수 있는 에러</h2>
<blockquote>
<ol>
<li>querydsl cannot find symbol</li>
</ol>
</blockquote>
<p>annotation Processor 설정이 잘못되었을 경우 발생 할 수 있는 문제이다.
 원인 1) Q 클래스가 제대로 생성되지 않음.
 경로를 다시 확인하고, Q 클래스를 다시 생성 해준다.</p>
<p> 원인 2) Annotation Processor 설정을 확인
 설정 &gt; Build,Execution,Deployment &gt; Compiler &gt; Annotation Processor 설정에서
 Enable annotation processing 설정이 활성화되어있는지 확인하고,
 왼쪽에 gradle Imported로 되어있는 그룹을 화살표를 클릭해 Default 그룹으로 이동해준다.</p>
<p> 설정 적용 후 재실행 시 문제없이 빌드 된다.
 <img src="https://velog.velcdn.com/images/hyeseong-int/post/f60e90bb-b6f7-42ec-9d67-bd7372c8fd2a/image.png" width="100%"></p>
<blockquote>
<ol start="2">
<li>attempt to recreate a file for type querydsl</li>
</ol>
</blockquote>
<p>Q 파일을 재 생성하며 생기는 문제이다.
build clean을 통해 Q 파일을 모두 제거하고 새로 build를 하였다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/7ecd179a-0e1b-45bd-9d5f-f795c50b27ac/image.png" width="50%"></p>
<p>이렇게 한 후에도 빌드가 제대로 되지 않는다면 빌드 환경을 Gradle에서 InteliJ로 변경 해 보는 것도 해결에 도움이 된다고 한다.
(설정 &gt; build tools -&gt; gradle : gradle -&gt; intellj)
참고: <a href="https://lahezy.tistory.com/94">query dsl 파일 중복생성, 파일을 찾지 못하는오류</a></p>
<blockquote>
<ol start="3">
<li>Could not find class file for 에러</li>
</ol>
</blockquote>
<p>위 두 에러를 해결 한 후에도 이러한 에러가 발생하는 경우가 있다.
이런 경우 build clean을 통해 생성된 Q-class를 모두 삭제하고 직접 빌드 하지 않고 바로 애플리케이션을 실행하면 실행 된다.
참고: <a href="https://velog.io/@suhsein/QueryDSL-build.gradle-%EC%84%A4%EC%A0%95-QClass-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0">QueryDSL - build.gradle 설정, QClass 생성하기</a></p>
<h1 id="5-코드-작성-방법">5. 코드 작성 방법</h1>
<h2 id="51-querydsl-기본-사용법">5.1. QueryDSL 기본 사용법</h2>
<p>다음은 가장 기본적인 QueryDSL을 사용한 JPQL 생성 및 실행 방법이다.</p>
<pre><code>QEntity entity = QEntity.entity;

jpaQueryFactory
            .select(entity) 
            .from(entity)
            .where(entity.id.eq(id))
            .fetch();</code></pre><p>@Query 에서 String으로 사용하던 Select문, From절, Where절을 모두 메서드 호출하듯 사용한다.</p>
<p><strong>1. select()</strong></p>
<ul>
<li>SQL을 통해 가져올 데이터를 지정</li>
<li>클래스 접근자를 통해 특정 데이터만 가져올 수 있음(e.g. entity.name)</li>
</ul>
<p><strong>2. from()</strong></p>
<ul>
<li>조회 할 테이블을 지정</li>
</ul>
<p><strong>3. where()</strong></p>
<ul>
<li>어떤 조건으로 데이터를 검색할지 지정</li>
<li>제공 값과 동일, 범위연산, 포함 등 다양한 조건 검색</li>
<li>여러 조건을 걸 수 있으며, AND, OR연산 모두 가능<pre><code>member.username.eq(&quot;member1&quot;) // username = &#39;member1&#39;   
member.username.ne(&quot;member1&quot;) //username != &#39;member1&#39;  
member.username.eq(&quot;member1&quot;).not() // username != &#39;member1&#39;
member.username.isNotNull() //이름이 is not null   
member.age.in(10, 20) // age in (10,20)  
member.age.notIn(10, 20) // age not in (10, 20)   
member.age.between(10,30) //between 10, 30
member.age.goe(30) // age &gt;= 30 
member.age.gt(30) // age &gt; 30   
member.age.loe(30) // age &lt;= 30 
member.age.lt(30) // age &lt; 30
member.username.like(&quot;member%&quot;) //like 검색 
member.username.contains(&quot;member&quot;) // like ‘%member%’ 검색 
member.username.startsWith(&quot;member&quot;) //like ‘member%’ 검색</code></pre>위 코드 출처 : <a href="https://akku-dev.tistory.com/117">스프링부트에 QueryDSL 적용기 - 2 (설치 및 사용법, Spring Data JPA와 함께 사용하기)</a></li>
</ul>
<p><strong>4. fetch()</strong></p>
<ul>
<li>쿼리를 생성하고 조회된 List를 반환하는 역할</li>
<li>조회 갯수에 따라 fetchOne(), fetch() 등 사용</li>
<li>조회 쿼리가 아닌 실행 쿼리의 경우 execute() 사용해 실행</li>
</ul>
<h2 id="52-querydsl-객체-생성">5.2. QueryDSL 객체 생성</h2>
<p>QueryDSL이 쿼리 생성을 할 수 있도록 EntityManager를 주입한다.</p>
<pre><code>@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory queryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}
</code></pre><h2 id="53-레포지토리-클래스-생성">5.3. 레포지토리 클래스 생성</h2>
<h3 id="기존-reposistory-코드">기존 Reposistory 코드</h3>
<p>기존 Repository 코드는 다음과 같다.</p>
<pre><code>public interface ReservationRepository extends JpaRepository&lt;Reservation, Long&gt;, ReservationCustomRepository {

    @Query(&quot;SELECT r FROM Reservation r WHERE r.contentsId = :contentsId&quot;)
    List&lt;Reservation&gt; getReservationListByContentsId(@Param(&quot;contentsId&quot;) Long contentsId);

    @Query(&quot;SELECT r FROM Reservation r WHERE r.userId = :userId&quot;)
    List&lt;Reservation&gt; getReservationListByUserId(@Param(&quot;userId&quot;) Long userId);

    @Query(&quot;SELECT r FROM Reservation r WHERE r.reservationStartAt &gt;= :startOfDay AND r.reservationStartAt &lt; :startOfNextDay AND r.contentsId = :contentsId&quot;)
    List&lt;Reservation&gt; findAllByResDateAndContentsId(
            @Param(&quot;startOfDay&quot;) LocalDateTime startOfDay,
            @Param(&quot;startOfNextDay&quot;) LocalDateTime startOfNextDay,
            @Param(&quot;contentsId&quot;) Long contentsId
    );

    default Reservation findByIdOrElseThrow(Long id) {
        return findById(id).orElseThrow(() -&gt; new ReservationNotFoundException(&quot;존재하지 않는 예약입니다.&quot;));
    }
}</code></pre><p>JPQL의 단점대로 String으로 쿼리를 작성하면서 가독성이 낮고, 빌드 시점에 오류를 알 수 없다.
따라서 아래 방법대로 클래스를 생성하도록 하였다.</p>
<hr>
<blockquote>
<img src="https://velog.velcdn.com/images/hyeseong-int/post/9ffbf3d6-7800-4231-a6b3-ffa2bc40714e/image.png" width="30%">
위 다이어그램 구조대로 객체를 생성하였다.
</blockquote>
<h3 id="customrepository-클래스">CustomRepository 클래스</h3>
<p>인터페이스에 사용할 레포지토리 메서드를 작성한다.</p>
<pre><code>public interface ReservationCustomRepository {
    List&lt;Reservation&gt; getReservationListByContentsId(Long contentsId);

    List&lt;Reservation&gt; getReservationListByUserId(Long userId);

    List&lt;Reservation&gt; findAllByReservationDateAndContentsId(LocalDateTime startOfDay, LocalDateTime startOfNextDay, Long contentsId);

    Reservation findByIdOrElseThrow(Long id);
}</code></pre><h3 id="customrepositoryimpl-클래스">CustomRepositoryImpl 클래스</h3>
<p>그 후 위 인터페이스 클래스를 구현한다.
설명은 주석 및 아래에서 덧붙이겠다.</p>
<pre><code>@RequiredArgsConstructor
//Spring bean으로 등록하기 위해 Repository 어노테이션 추가
@Repository 
public class ReservationCustomRepositoryImpl implements ReservationCustomRepository{

    //QueryDSL 쿼리를 생성하고 실행하는 핵심 클래스.
    private final JPAQueryFactory queryFactory;
    //Reservation 엔티티의 속성을 쿼리에서 사용하기 위해 인스턴스를 생성
    QReservation reservation = QReservation.reservation;

    @Override
    public List&lt;Reservation&gt; getReservationListByContentsId(Long contentsId) {

        return queryFactory
                .selectFrom(reservation)
                .where(reservation.contentsId.eq(contentsId))
                .fetch();
    }

    @Override
    public List&lt;Reservation&gt; getReservationListByUserId(Long userId) {

        return queryFactory
                .selectFrom(reservation)
                .where(reservation.userId.eq(userId))
                .fetch();
    }

    @Override
    public List&lt;Reservation&gt; findAllByReservationDateAndContentsId(
            LocalDateTime startOfDay,
            LocalDateTime startOfNextDay,
            Long contentsId
    ) {
        return queryFactory
                .selectFrom(reservation)
                .where(reservation.reservationStartAt.goe(startOfDay)
                        .and(reservation.reservationEndAt.lt(startOfNextDay))
                        .and(reservation.contentsId.eq(contentsId)))
                .fetch();

    }

    @Override
    public Reservation findByIdOrElseThrow(Long id) {
        Reservation result = queryFactory
                .select(reservation)
                .where(reservation.reservationId.eq(id))
                .fetchOne();

        if(result == null) {
            throw new ReservationNotFoundException(&quot;존재하지 않는 예약입니다.&quot;);
        }
        return result;
    }
}</code></pre><h1 id="6-최적화-방법">6. 최적화 방법</h1>
<p><strong>1. 벌크 연산</strong>
<code>update()</code> 또는 <code>delete()</code>같은 벌크 연산은 JPQL을 사용하는 것이 효율적이다.
QueryDSL의 벌크연산은 내부적으로 결국 JPQL로 변환하기 때문에 직접 사용하는것이 성능상 유리하다.</p>
<p><strong>2. 인덱스 활용</strong>
인덱싱을 활용해 쿼리 성능을 향상시킬 수 있다</p>
<p><strong>3. 쿼리 튜닝</strong>
QueryDSL의 쿼리 실행계획을 분석하고 튜닝하여 성능을 최적화 할 수 있다.</p>
<p><strong>4. @Query 어노테이션과 혼용</strong>
QueryDSL은 동적 쿼리 생성에 유용하다.
다만, 복잡한 정적 쿼리는 JPQL쿼리를 직접 작성하는  것이 성능상 더 좋을 수 있다.</p>
<h1 id="7-결론">7. 결론</h1>
<p>QueryDSL은 쿼리 작성을 편리하게 해주고, 가독성, 생산성 등을 높여주는 유용한 프레임워크이다.
다만 QueryDSL에 대해 자세히 알고, 상황에 따라 필요에 맞게 잘 사용하는 것이 성능적, 효율적으로 더 이득이 될 수 있다.
필요에 따라 적절한 사용을 통해 좋은 애플리케이션을 개발하도록 하자.</p>
<h1 id="참고자료">참고자료</h1>
<ul>
<li><a href="https://blog.naver.com/innogrid/222725730056">Spring Data JPA + Query DSL 사용기</a></li>
<li><a href="https://velog.io/@jkijki12/Spring-QueryDSL-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">[Spring] QueryDSL 완벽 이해하기</a></li>
<li><a href="https://sjh9708.tistory.com/174">[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security란?]]></title>
            <link>https://velog.io/@hyeseong-int/Spring-Security</link>
            <guid>https://velog.io/@hyeseong-int/Spring-Security</guid>
            <pubDate>Fri, 16 Feb 2024 06:25:47 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/fa665d58-952c-4eb6-b776-c1a97cddc015/image.png" alt=""></p>
<p>지금까지의 주먹구구식 스프링 시큐리티 사용에서 벗어나고자, 스프링 시큐리티에 대해 자세하게 공부를 해보고자 합니다.</p>
<p>최신 버전인 스프링 시큐리티 6 버전을 기준으로 공부합니다.</p>
<h1 id="spring-security란">Spring Security란?</h1>
<p>스프링 프레임워크에서 인증(Authentication) 및 인가(Authorization), 권한 관리 등의 기능을 제공하는 프레임워크입니다.</p>
<p>스프링 시큐리티에서는 필터(Filter)와, 필터로 이루어진 필터 체인의 흐름에 따라 인증과 인가를 처리합니다.</p>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li>인증(Authnetication): 보호된 리소스에 접근한 대상에 대해 누구인지 확인하는 과정</li>
<li>인가(Authorization): 해당 리소스에 대해 접근할 권한을 갖고있는지 확인하는 과정</li>
<li>권한: 특정 리소스에 대한 접근 제한. 모든 리소스는 접근에 대한 제어 권한이 걸려있음. 인가 과정에서 해당 리소스에 제한된 권한을 가졌는지 확인함.</li>
<li>접근 주체(Principal): 보호된 리소스에 접근하는 대상</li>
</ul>
<hr>
<h1 id="spring-security-특징">Spring Security 특징</h1>
<ul>
<li>보안과 관련된 다양한 로직과 커스텀 옵션을 사용할 수 있어, 보안 관련 로직을 직접 작성하지 않아도 됨.</li>
<li>Filter 기반 동작으로, MVC와 분리하여 관리 및 동작함.</li>
<li>어노테이션을 통한 간단한 설정</li>
<li>인증 관리자(Authentication Manager)와, 접근 결정 관리자(Access Decision Mananger)를 통해 사용자의 리소스 접근을 관리함.</li>
<li>인증 관리자는 UsernamePasswordAuthenticationFilter, 접근 결정 관리자는 FilterSecurityInterceptor가 수행함.</li>
</ul>
<h1 id="spring-security-구조">Spring Security 구조</h1>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/0de6b868-564f-4c6b-afb3-9f357b35873b/image.png" alt=""></p>
<ul>
<li><code>SecurityContextPersistenceFilter</code>:
<code>SecurityContextRepository</code>에서 <code>SecurityContext</code>를 로드하고 저장하는 일을 담당.</li>
<li><code>LogoutFilter</code>: 로그아웃 URL로 지정된 가상 URL에 대한 요청을 감시하고 매칭되는 요청이 있으면 로그아웃 시킴.</li>
<li><code>UsernamePasswordAuthenticationFilter</code>: Username, PW로 이루어진 폼 기반 인증에 사용하는 가상 URL 요청을 감시하고, 요청이 있으면 사용자의 인증을 진행</li>
<li><code>DefaultLoginPageGenerating Filter</code>: 폼 기반, Open ID 기반 인증에 사용하는 가상 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성함.</li>
<li><code>BasicAuthenticationFilter</code>: HTTP 기본 인증 헤더를 감시하고 이를 처리함.</li>
<li><code>RequestCacheAwareFilter</code>: 로그인 성공 이후 인증 요청에 의해 가로채어진 사용자의 원래 요청을 재 구성하는데 사용됨.</li>
<li><code>AnonymousAuthenticationFilter</code>: 이 필터 호출 시점가지 인증을 받지 못했다면, 인증토큰에서 사용자가 익명으로 나타남</li>
<li><code>SessionManagementFilter</code>: 인증된 주체를 바탕으로 세션 트래킹을 처리해 단일 주체와 관련한 모든 세션들이 트래킹 되도록 도움을 줌.</li>
<li><code>ExceptionTranslationFilter</code>: 보호된 요청을 처리하는동안 발생할 수 있는 예외의 기본 라우팅과 위임을 처리.</li>
<li><code>FilterSecurityIntercptor</code>: 권한부여와 관련한  결정을 <code>AccessDecisionManager</code>에게 위임해 권한부여 결정 및 접근 제어 결정을 쉽게 만들어 줌.</li>
</ul>
<h1 id="인증정보를-담는-객체">인증정보를 담는 객체</h1>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/5c4e60ed-52cc-4900-a9f7-b5b6bfd9d571/image.png" alt=""></p>
<h3 id="principal">Principal</h3>
<ul>
<li>인증 주체에 대한 정보를 담고있는 객체</li>
<li>Spring Security가 제공하는 인증정보의 한 부분이다.<h3 id="authentication">Authentication</h3>
</li>
<li>실질적으로 인증정보를 담고있는 객체</li>
<li><code>Principal</code>(내가 누구인지 / 로그인 ID에 해당하는 값), <code>Credential</code>(인증자격 / 비밀번호에 해당하는 값),
<code>Authorities</code>(권한정보 / ROLE에 해당하는 값)의 세 객체로 구성되어있다.</li>
</ul>
<h3 id="securitycontextholder">SecurityContextHolder</h3>
<ul>
<li>시큐리티가 최종적으로 제공하는 객체</li>
<li><code>SecurityContextHolder</code>는 <code>SecurityContext</code>객체를 Thread-local로 제공하여, 같은 Thread에서는 매개로 주고받지 않아도 인증정보에 접근이 가능하다.</li>
</ul>
<h1 id="spring-security동작-과정">Spring Security동작 과정</h1>
<p>Spring Security는 Dispatcher Servlet으로 가기 전에 요청을 가로 채 Filter에서 인증과 인가를 처리합니다.
<img src="https://velog.velcdn.com/images/hyeseong-int/post/5861a180-0391-4807-a7a0-dbe71ef45e3e/image.png" alt=""></p>
<ol>
<li>사용자가 로그인 정보로 로그인 요청</li>
<li><code>AuthenticationFilter</code>가 정보를 인터셉트하여 <code>UsernamePasswordAuthentication Token(Authentication 객체)</code> 생성하여 <code>AuthenticationManager</code>에게 <code>Authentication</code> 객체를 전달.</li>
<li><code>AuthenticationManager</code> 인터페이스를 거쳐 <code>AutheticationProvider</code>에게 (2)의 정보 전달(Authentication 형태), 등록된 <code>AuthenticationProvider</code>를 조회하여 인증 요구</li>
<li><code>AuthenticationProvider</code>는 <code>UserDetailsService</code>를 통해 입력받은 (3)의 사용자 정보를 DB에서 조회<ul>
<li><code>supports()</code> 메소드를 통해 실행 가능한지 체크</li>
<li><code>authenticate()</code> 메소드를 통해 DB에 저장된 이용자의 정보와 입력한 로그인 정보 비교<ul>
<li>DB 이용자 정보: <code>UserDetailsService</code> 의 <code>loadUserByUsername()</code> 메소드를 통해 불러옴</li>
<li>입력 로그인 정보: (3)에서 받았던 <code>Authentication</code> 객체(<code>UsernameAuthentication Token</code>)</li>
</ul>
</li>
<li>일치하는 경우 <code>Authentication</code> 반환</li>
</ul>
</li>
<li><code>AuthenticationManger</code>는 <code>Authentication</code> 객체를 <code>AuthenticationFilter</code>로 전달</li>
<li><code>AuthenticationFilter</code>는 전달받은 <code>Authentication</code> 객체를 <code>LoginSuccessHandler</code>로 전송하고 <code>SecurityContextHolder</code>에 담음</li>
<li>성공시 <code>AuthenticationSuccessHandle</code>, 실패기 <code>AuthenticationFailureHandle</code> 실행</li>
</ol>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://docs.spring.io/spring-security/reference/index.html">https://docs.spring.io/spring-security/reference/index.html</a></li>
<li><a href="https://devuna.tistory.com/55">https://devuna.tistory.com/55</a></li>
<li><a href="https://thalals.tistory.com/436">https://thalals.tistory.com/436</a></li>
<li><a href="https://velog.io/@franc/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%ED%9D%90%EB%A6%84%EA%B3%BC-%EA%B5%AC%EC%A1%B0">https://velog.io/@franc/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%ED%9D%90%EB%A6%84%EA%B3%BC-%EA%B5%AC%EC%A1%B0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 스터디 - 그리디 알고리즘]]></title>
            <link>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EA%B7%B8%EB%A6%AC%EB%94%94-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EA%B7%B8%EB%A6%AC%EB%94%94-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Tue, 06 Feb 2024 07:04:50 GMT</pubDate>
            <description><![CDATA[<h1 id="그리디-알고리즘">그리디 알고리즘</h1>
<blockquote>
<p>눈앞의 이익만 추구하는 알고리즘</p>
</blockquote>
<pre><code>do{
    가장 좋아보이는 선택을 한다;
} until(해 구성 완료)</code></pre><p>그리디 알고리즘은 최종 해답에 도달하기까지, <strong>각 단계에서 최적이라고 생각되는, 가장 좋아보이는 선택</strong>을 반복하는 알고리즘이다.</p>
<h1 id="그리디-알고리즘-특징">그리디 알고리즘 특징</h1>
<ul>
<li>지역적으로 최적인 해가, 전역적으로 최적인 해라는 보장은 없다.</li>
<li>각 단계의 선택이 다음 단계의 선택에 영향을 끼치지 않는다.</li>
</ul>
<h1 id="근사-알고리즘">근사 알고리즘</h1>
<p>그리디 알고리즘은 최적해를 보장하지 않는다. 하지만 지역적으로는 최적해를 매번 선택한다는 점에서 최적해에 근접한 값을 구할 수 있다.</p>
<h1 id="ps에서의-그리디">PS에서의 그리디</h1>
<h2 id="최적해가-보장되지-않는-예">최적해가 보장되지 않는 예</h2>
<ul>
<li><p>이진트리의 최적합 경로 찾기</p>
<blockquote>
<p>모든 노드를 다 탐색하지 않으면 최적해를 찾을 수 없음.</p>
</blockquote>
</li>
<li><p>보따리 문제 (부피가 M인 보따리에 부피가 W<sub>i</sub> , 가치가 P<sub>i</sub>인물건 최대한 많이 넣기)</p>
</li>
<li><p>거스름돈 문제</p>
<blockquote>
<p>동전 액면이 작은 단위의 배수가 아닌 경우 최적해를 찾을 수 없음.</p>
</blockquote>
</li>
</ul>
<h2 id="최적해가-보장되는-예">최적해가 보장되는 예</h2>
<ul>
<li>최소 신장 트리(MST)</li>
<li>회의실 시간 분배 문제
등</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[BFS, 너비 우선탐색을 알아보자!]]></title>
            <link>https://velog.io/@hyeseong-int/BFS-%EB%84%88%EB%B9%84-%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeseong-int/BFS-%EB%84%88%EB%B9%84-%EC%9A%B0%EC%84%A0%ED%83%90%EC%83%89%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 01 Feb 2024 07:31:05 GMT</pubDate>
            <description><![CDATA[<p>DFS(Depth-First Search)와 BFS(Breadth-First Search) 모두 대표적인 그래프에서 모든 정점을 방문하기 위한 탐색법이다.</p>
<p>그래프란?
그래프는 정점과 간선을 통해 자료를 표현하는 방식이다.
정점(Vertex)은 대상 및 개체를 나타낸다.
간선(Edge)은 정점간의 관계를 나타낸다.
간선은 방향성을 가질 수 있으며, 가중치를 가질 수도 있다.</p>
<h1 id="bfs-너비우선-탐색">BFS, 너비우선 탐색</h1>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/c900a349-a62c-4ec6-8e14-b38fb7ccf65c/image.png" alt=""></p>
<p>직관적인 이해를 위해 트리에서의 너비 우선 탐색 예시를 가져왔다.
루트를 기준으로 먼저 깊이가 1인 루트의 자식을 차례로 방문한다.
그 다음 깊이가 2인 루트의 자식의 자식을 방문한다.
위 방식으로 깊이를 늘려가며 모든 노드를 탐색하는 방식이다.</p>
<p>즉 루트에서 시작하여 인접한 노트를 먼저 탐색하는 방식이다.</p>
<p>위와같은 이유로 너비 우선 탐색(Breadth-First Search)라고 불린다.</p>
<h3 id="bfs의-특징">BFS의 특징</h3>
<ul>
<li>재귀적으로 동작하지 않는다.</li>
<li>방문한 노드를 기록해야한다. 그렇지 않으면 무한루프에 빠질 수 있다.</li>
<li>큐(Queue)를 사용하여 탐색한다.(선입선출)</li>
<li>깊이 우선 탐색보다 구현이 복잡하다.</li>
</ul>
<h3 id="bfs의-탐색-과정">BFS의 탐색 과정</h3>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/8d5cc68b-36bd-4228-b839-090419296d70/image.png" alt="">
출처 : <a href="https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html">https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html</a></p>
<h3 id="bfs의-구현">BFS의 구현</h3>
<ul>
<li>자료구조 큐(Queue)를 이용하여 구현</li>
</ul>
<h4 id="ps에서의-bfs">PS에서의 BFS</h4>
<ul>
<li>최단경로 탐색</li>
<li>거리 탐색 등</li>
</ul>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html">https://gmlwjd9405.github.io/2018/08/15/algorithm-bfs.html</a>
<a href="https://takeitoutamber.medium.com/binary-tree-right-side-view-bfs-87a215b6237c">https://takeitoutamber.medium.com/binary-tree-right-side-view-bfs-87a215b6237c</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DFS, 깊이 우선 탐색을 알아보자!]]></title>
            <link>https://velog.io/@hyeseong-int/DFS-%EA%B9%8A%EC%9D%B4-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeseong-int/DFS-%EA%B9%8A%EC%9D%B4-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 01 Feb 2024 07:18:38 GMT</pubDate>
            <description><![CDATA[<p>DFS(Depth-First Search)와 BFS(Breadth-First Search) 모두 대표적인 그래프에서 모든 정점을 방문하기 위한 탐색법이다.</p>
<h4 id="그래프란">그래프란?</h4>
<p>그래프는 정점과 간선을 통해 자료를 표현하는 방식이다.
<strong>정점(Vertex)</strong>은 대상 및 개체를 나타낸다.
<strong>간선(Edge)</strong>은 정점간의 관계를 나타낸다.
간선은 방향성을 가질 수 있으며, 가중치를 가질 수도 있다.</p>
<h1 id="dfs-깊이-우선-탐색">DFS, 깊이 우선 탐색</h1>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/a9cfd9c5-f804-467e-bcb6-5b7f054becdc/image.png" alt="">
직관적인 이해를 위해 트리를 기준으로 DFS가 설명된 이미지를 가져왔다.</p>
<p>DFS는 루트를 기준으로 루트의 자식 정점을 하나 방문한 다음 아래로 내려갈 수 있는 곳까지 내려간다. 더 이상 내려갈 수 없는 경우, 올라오다가 내려갈 수 있는 곳이 다시 보인다면 내려가는식으로 탐색한다.</p>
<p>즉 루트 노드에서 시작해 다음 분기로 넘어가기 전까지, 해당 분기를 완벽하게 탐색하는 방법 이라고 설명할 수 있다.</p>
<p>위와 같은 이유로 BFS(깊이 우선 탐색)이라고 불린다.</p>
<h3 id="dfs의-특징">DFS의 특징</h3>
<ul>
<li>자기 자신을 호출하는 재귀 순한의 형태를 띔.</li>
<li>단순 검색 속도는 BFS에 비해 느림.</li>
<li>노드 방문 여부를 반드시 검사해야한다.<ul>
<li>그렇지 않으면 무한하게 노드를 순환하게 된다.</li>
</ul>
</li>
</ul>
<h3 id="dfs의-탐색-과정">DFS의 탐색 과정</h3>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/61b078e4-c22d-4f50-a61f-ff03ecd1870c/image.png" alt="">
출처: <a href="https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html">https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html</a></p>
<h3 id="dfs의-구현">DFS의 구현</h3>
<ol>
<li>재귀 호출 구현방법</li>
<li>명시적 스택 사용: 방문 정점을 스택에 저장 후 꺼내서 작업</li>
</ol>
<h4 id="ps에서의-dfs">PS에서의 DFS</h4>
<ul>
<li>미로, 지도 등의 탐색</li>
<li>전체를 방문하는데 걸리는 횟수 등 유형의 문제</li>
</ul>
<p>참고자료
<a href="https://takeitoutamber.medium.com/binary-tree-right-side-view-bfs-87a215b6237c">https://takeitoutamber.medium.com/binary-tree-right-side-view-bfs-87a215b6237c</a>
<a href="https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html">https://gmlwjd9405.github.io/2018/08/14/algorithm-dfs.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 스터디 - 재귀]]></title>
            <link>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%9E%AC%EA%B7%80</link>
            <guid>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%9E%AC%EA%B7%80</guid>
            <pubDate>Wed, 24 Jan 2024 06:26:43 GMT</pubDate>
            <description><![CDATA[<h1 id="재귀recursion">재귀(Recursion)</h1>
<blockquote>
<p>하나의 함수에서 자기 자신을 다시 호출해 작업을 수행하는 알고리즘</p>
</blockquote>
<p>재귀는 어떤 문제를 해결하는 과정에서 자신과 똑같지만 크기가 다른 문제를 발견하고, 이들의 관계를 파악해 문제 해결에 접근하는 방식이다.</p>
<p>재귀 알고리즘을 사용하면, 크고 복잡한 문제가 주어졌을 때, 문제의 크기를 결과를 얻을 수 있을정도로 점진적으로 줄인 다음, 그 결과를 이용하여 더 큰 문제를 풀어내, 처음 주어진 문제를 해결할 수 있다.</p>
<h1 id="귀납적-사고">귀납적 사고</h1>
<p>재귀 문제풀이를 위해서는 절차지향적 사고가 아닌, 귀납적 사고를 통해 문제에 접근하는 것이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/hyeseong-int/post/0b33eae8-d984-4047-beeb-a0a9c2e1f077/image.png" alt=""> <em>(출처 위키백과)</em></p>
<p>위 사진을 보면 N번째까지 늘어진 도미노의 모습을 볼 수 있다. 
1번 도미노가 쓰러지고, N번 도미노도 쓰러지면, N+1번째 도미노도 쓰러진다 =&gt; 모든 도미노는 쓰러진다는 결론에 도달</p>
<p>위와 같은 접근법이 귀납적 접근법이다.</p>
<h1 id="재귀함수-주의할-점">재귀함수 주의할 점</h1>
<h3 id="base-condition">Base Condition</h3>
<p>재귀함수는 특정 입력에 대해 자기 자신을 호출하지 않고 종료해야한다. 
또한 모든 입력은 Base Condition으로 수렴해야한다.</p>
<p>그렇지 않을 경우 스스로를 무한히 호출하다가 런타임 에러, 스택 오버플로우가 발생하게 될 것이다.</p>
<h3 id="함수를-명확하게-정의하기">함수를 명확하게 정의하기</h3>
<p>재귀함수를 만들때에는 함수를 명확하게 정의해야한다.</p>
<ol>
<li>함수의 인자로 어떤 값을 받을지</li>
<li>어디까지 계산 후 넘겨줄 것인지
위 두가지를 명확히 해야한다.</li>
</ol>
<h1 id="ps에서의-재귀함수">PS에서의 재귀함수</h1>
<h3 id="재귀함수와-반복문">재귀함수와 반복문</h3>
<p>모든 재귀함수는 재귀 구조 없이, 단순 반복문으로만 동일 동작을 수행하는 함수를 만들 수 있다.</p>
<ul>
<li>반복문을 사용하면, 메모리와 시간적 측면에서 이득이 있다. (재귀 함수를 호출하는데에 대한 비용 감소)</li>
<li>재귀함수를 사용하면 코드가 간결해진다.</li>
</ul>
<p>따라서 반복문으로 간단히 구현이 가능하면, 반복문으로 구현하는 것이 좋고, 코드가 너무 복잡해질 것 같은 경우, 재귀함수를 사용하여 풀이하는 것이 좋다.</p>
<h3 id="문제-유형">문제 유형</h3>
<p>Linked List, 이진 트리, 그래프 등의 유형을 재귀 알고리즘을 통해 풀이할 수 있다.</p>
<p>또한 순열, 조함 같은 경우의 수를 따지는 문제도 재귀 알고리즘이 많이 활용된다.</p>
<h3 id="복잡도-분석">복잡도 분석</h3>
<p>재귀 함수가 어떻게 호출될지 알면 재귀 알고리즘의 복잡도를 구할 수 있다.</p>
<p>함수 내에서 재귀호출이 일어나는 횟수를 통해 시간복잡도를 구할 수 있고, 호출 스택의 최대 깊이를 통해 공간 복잡도를 구할 수 있다.</p>
<p><a href="https://www.algodale.com/algorithms/recursion/#%EB%B3%B5%EC%9E%A1%EB%8F%84-%EB%B6%84%EC%84%9D">예시</a></p>
<hr>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://www.algodale.com/algorithms/recursion/">https://www.algodale.com/algorithms/recursion/</a>
<a href="https://blog.encrypted.gg/943?category=773649">https://blog.encrypted.gg/943?category=773649</a>
<a href="https://novlog.tistory.com/entry/Algorithm-%EC%9E%AC%EA%B7%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Recursive-Algorithm">https://novlog.tistory.com/entry/Algorithm-%EC%9E%AC%EA%B7%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Recursive-Algorithm</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[알고리즘 스터디-진행방식]]></title>
            <link>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%A7%84%ED%96%89%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@hyeseong-int/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%8A%A4%ED%84%B0%EB%94%94-%EC%A7%84%ED%96%89%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Tue, 23 Jan 2024 01:40:14 GMT</pubDate>
            <description><![CDATA[<h1 id="스터디-개요">스터디 개요</h1>
<p>취준 코딩테스트 준비 겸 알고리즘 스터디를 진행하기로 했다.
혼자 진행하는 알고리즘 문제풀이는 강제성이 없어서인지 동기부여가 덜하고, 지속하기가 어려워 스터디를 모집하였다.</p>
<h3 id="🙄사실은">🙄사실은...</h3>
<p>나는 복학 전부터 심심하면 알고리즘 문제를 풀어보아서, 잘하지는 않지만 solved.ac 기준 골드5 랭크를 기록하고 있다.
그래도 나보다 많은 알고리즘을 경험해본 사람이 스터디에 들어와서 가르쳐주면 좋겠다는 생각이 은연중에 있었다.
하지만 어쩌다보니, 알고리즘 문제풀이를 처음 해보는 후배 두명과, 조금 해본 동기 한명 총 4명이 스터디를 진행하게 되었고, 내가 리드를 맡게 되었다.
하지만, 함께 공부하는 스터디이고, 서로 공부하고 서로 알려주며 발전할 수 있는 내용이 많은 분야이기 때문에, 기초를 다시 잡는다는 생각으로 진행해보려고 한다!</p>
<h1 id="진행방식">진행방식</h1>
<h3 id="✅이-스터디에서-다룰-것">✅이 스터디에서 다룰 것</h3>
<ul>
<li>각 유형별 알고리즘 이론 스터디</li>
<li>각 유형별 알고리즘 풀이 (그리디, 그래프, 최단경로, 이분탐색, 분할정복, 완전탐색, 구현, 문자열 등)</li>
<li>PS를 위한 입출력 등</li>
<li>다양한 플랫폼 경험</li>
<li>가능하다면 SQL 공부</li>
</ul>
<h3 id="❌이-스터디에서-다루지-않을-것">❌이 스터디에서 다루지 않을 것</h3>
<ul>
<li>수학 (부족한 부분은 개별 공부)</li>
<li>각 언어별 문법</li>
<li>자료구조</li>
</ul>
<h2 id="스터디-진행">스터디 진행</h2>
<ul>
<li>주 N회 스터디 진행(온라인 / 오프라인)</li>
<li>스터디 진행 시각:</li>
<li>문제풀이 사이트<ul>
<li>백준 및 프로그래머스의 문제를, 문제 유형별로 나누어 풀이한다.</li>
<li><a href="https://www.acmicpc.net/">백준 온라인 저지</a>  / <a href="https://solved.ac/">solved.ac</a>(백준 문제 태그 및 랭킹)</li>
<li><a href="https://programmers.co.kr/">프로그래머스</a></li>
</ul>
</li>
</ul>
<h2 id="문제풀이-진행">문제풀이 진행</h2>
<ul>
<li>언어는 자유 선택</li>
<li>1시간 이상 문제 풀리지 않을 경우, 타인 코드 참고(복붙 금지!)</li>
<li>문제 풀이 코드는 깃허브 레포에 푸시 / 풀리퀘 하여 깃허브 사용에 익숙해지기</li>
<li>문제 풀이 후 상호 코드리뷰</li>
</ul>
<blockquote>
<p>많은 분들이 이야기하고 나도 동의하는 부분이지만, 1시간 이상 풀리지 않는 문제를 무작정 잡고 있거나, 마구잡이 구현으로 풀이하는 것 보다, 모범 사례를 참고하여 구현해보고, 이를 학습하는 것이 더 낫다고 생각했다.</p>
</blockquote>
<h4 id="스터디-전-준비">스터디 전 준비</h4>
<ul>
<li>할당 된 개념 공부 및 블로그에 정리</li>
<li>할당 된 문제 풀이(문제 수는 조율 예정)</li>
<li>풀이한 문제 코드 공유(깃허브)</li>
<li>공유된 스터디원 코드 리뷰<ul>
<li>깃허브 레포 &gt; Pull request &gt; file Chnaged에서 리뷰 작성</li>
</ul>
</li>
</ul>
<h4 id="스터디">스터디</h4>
<ul>
<li>할당된 주제 발표 (발표자 랜덤 선정)</li>
<li>할당된 문제풀이 발표(전체 발표)</li>
<li>코드리뷰 및 좋은 코드 공유</li>
</ul>
<blockquote>
<p>본인의 문제풀이에 대해 설명할 수 있어야 면접등에도 대비할 수 있다고 생각해 문제풀이 발표는 전체 진행하기로 한다.</p>
</blockquote>
<h4 id="스터디-종료-후">스터디 종료 후</h4>
<ul>
<li>다음 개념 할당</li>
<li>다음 문제 할당</li>
<li>스터디에서 다룬 주제 복습, 코드리뷰 반영 등</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>위와같은 방식으로 스터디를 진행해보고, 상시 피드백을 받으며 운영방식을 조금씩 수정해 갈 예정이다.
스터디가 끝날 즈음에는 모두가 많이 발전해있기를 바란다...!!</p>
]]></description>
        </item>
    </channel>
</rss>