<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>shin-jisong.log</title>
        <link>https://velog.io/</link>
        <description>💻 늘 공부하고 발전하는 개발자</description>
        <lastBuildDate>Wed, 23 Jul 2025 09:20:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>shin-jisong.log</title>
            <url>https://velog.velcdn.com/images/shin-jisong/profile/9ceb1d80-1028-4957-9c6e-5a028febfdd1/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. shin-jisong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/shin-jisong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DB 병목 현상을 해결해 보자]]></title>
            <link>https://velog.io/@shin-jisong/DB-%EB%B3%91%EB%AA%A9-%ED%98%84%EC%83%81%EC%9D%84-%ED%95%B4%EA%B2%B0%ED%95%B4-%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@shin-jisong/DB-%EB%B3%91%EB%AA%A9-%ED%98%84%EC%83%81%EC%9D%84-%ED%95%B4%EA%B2%B0%ED%95%B4-%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 23 Jul 2025 09:20:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2ced827f-c0ed-4d5b-8cea-5f0824f31d8b/image.png" alt=""></p>
<p>안녕하세요! 오늘은 DB LOCK으로 인해서 서버가 먹통이 되어버린 경험을 풀어보려고 합니다</p>
<p>저희의 서비스는 위와 같이 유저들이 원하는 선수들을 선택하고 팀을 구성하면,
각각 선수의 점수를 바탕으로 팀 점수를 계산하고 있습니다
(해당 방식은 이전의 게시물에 상세히 서술해 두었으니 참고해 주세요!)</p>
<p>이를 위해 저희는 주기적으로 선수 기록지를 크롤링하여 개인의 점수를 업데이트 하는 과정을 거치고 있는데요
크롤링을 하고, 이를 저장하는 과정에서 아래와 같은 문제가 발생하게 됩니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/423bacff-c969-4f3a-aeef-5072fa567109/image.png" alt=""></p>
<p>해당 로그를 자세히 보면 <strong>락을 기다리는 시간이 초과하여 에러가 발생한 것</strong>을 확인할 수가 있어요</p>
<p>이를 해결하기 위해 검색해 보니 보통 초과 시간을 늘리는 방안을 많이 사용하더라고요
하지만 저는 이것이 근본적인 원인이 아닐 것 같아 이를 분석해 보고자 하였습니다</p>
<h2 id="문제를-파악해-보자">문제를 파악해 보자</h2>
<p>먼저 가장 초기의 코드를 보여드립니다
이때에 구현 일정이 엄청나게 타이트했으므로
해당 부분을 구현한 친구가 리팩토링을 추후 업무로 넘기고 작동할 수만 있는 코드를 작성하였어요
<del>아래 글까지 읽어보면 크롤링과 트랜잭션을 분리하니 지금은 넘겨 주세요</del></p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/f6972859-dfbf-4094-a356-8dfbac873004/image.png" alt=""></p>
<p>이렇게 스케줄에 따라 크롤링을 실행하게 되고</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/b0b4a686-86b4-4527-9d83-6b44f3d254fd/image.png" alt=""></p>
<p>아래와 같이 크롤링 매서드 내부에서 해당 포지션에 해당하는 선수를 읽은 다음
그 선수들의 점수를 업데이트 하는 로직을 거치게 됩니다</p>
<p>그리고 이 과정은 짧으면 30분 길면 2시간까지도 소요되는 작업이에요 해당 과정에서 위와 같은 문제가 발생하였습니다</p>
<h2 id="테스트를-진행해-보자">테스트를 진행해 보자</h2>
<p>대부분의 Spring Data JPA 환경에서는, 엔티티를 수정하면 해당 변경 사항이 곧바로 데이터베이스에 반영된다고 오해하기 쉽습니다 하지만 실제로는 그렇지 않습니다 JPA는 변경 감지(dirty checking) 기반으로 동작하기 때문에, 필드 값을 수정한 순간에는 아직 데이터베이스에 아무런 쿼리도 전송되지 않습니다 이처럼 눈에 보이지 않게 동작하는 내부 메커니즘을 정확히 이해하는 것이 매우 중요합니다</p>
<p>다시 돌아가, JPA 트랜잭션 안에서 어떤 플레이어 엔티티를 조회한 뒤 player.updateScore(score)처럼 필드 값을 변경한 후의 상황에서 보겠습니다 이때 우리가 save(player) 같은 메서드를 명시적으로 호출하지 않더라도, Spring Data JPA는 트랜잭션 커밋 시점에 자동으로 해당 변경 사항을 감지하고 UPDATE 쿼리를 생성하여 데이터베이스에 반영합니다</p>
<p>이것이 가능한 이유는, JPA가 트랜잭션이 시작될 때 영속성 컨텍스트(Persistence Context)에 해당 엔티티의 스냅샷을 보관해두고, 커밋 직전에 현재 상태와 비교하는 변경 감지(Dirty Checking) 를 수행하기 때문입니다 이 비교를 통해 어떤 필드가 바뀌었는지 판단하고, 그에 따라 flush() 단계에서 필요한 SQL 쿼리를 생성합니다 다시 말해, 개발자가 명시적으로 save()를 호출하지 않더라도 트랜잭션 내에서의 변경은 자동으로 반영됩니다</p>
<p>따라서 반드시 커밋 시점에 해당 사항이 반영되게 되고 120개의 쿼리가 동시에 날아간다고 해도,
<strong>해당 상황에서는 동시에 같은 데이터를 수정할 일이 없으며 DB 단에서 충분히 커버 가능한 양의 쿼리이기 때문에 락 문제가 발생할 가능성이 없습니다</strong></p>
<p>따라서 왜 이러한 문제가 발생하는지 이유를 도무지 찾지 못했습니다</p>
<p>혹시나 제가 인지하고 있는 사항이 다를까 봐 아래와 같은 테스트를 진행하였습니다
필드의 값을 바꾼 후에 트랜잭션을 유지하게 되고 그 사이에 read와 같은 호출이 오는 시나리오였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/cb5fd0a1-d2bb-4220-b65a-fe6cbca6e03d/image.png" alt=""></p>
<p>제 예상과 같이 update 이후 대기가 있음에도 불구하고 락은 걸리지 않았기 때문에 read 작업은 잘 수행되는 것을 볼 수 있습니다</p>
<h2 id="그럼에도-트랜잭션은-짧게-가져가야-한다">그럼에도 트랜잭션은 짧게 가져가야 한다</h2>
<p>사실 명확한 이유를 파악하지 못했지만 트랜잭션 내에서 크롤링의 업무까지 하는 것은 상당히 합리적이지 않습니다 왜냐하면 트랜잭션은 가능한 한 짧은 시간 내에 처리되어야 안정성과 성능을 보장할 수 있는데, 크롤링은 외부 네트워크나 페이지 로딩 시간에 따라 수 초에서 수 분까지 지연이 발생할 수 있는 작업이기 때문입니다 이러한 느린 작업이 트랜잭션 안에서 수행되면 트랜잭션 유지 시간이 과도하게 길어지고, 그에 따라 데이터베이스의 락 유지, 리소스 점유, 영속성 컨텍스트 메모리 증가 등의 문제가 발생하여 전체 시스템에 부하를 줄 수 있습니다</p>
<p><strong>크롤링이라는 일은 꼭 트랜잭션이 필요한 일이 아니기 때문에 분리해야 적절합니다</strong></p>
<pre><code>playerRepository.saveAll(pitchers);</code></pre><p>따라서 트랜잭션 범위를 분리하고, 점수 업데이트 이후에는 위와 같은 함수를 추가하여
변경된 데이터를 수동으로 저장하도록 수정했습니다
이때 saveAll을 사용하여 bulk insert 방식으로 처리함으로써, 성능적인 이점도 함께 얻을 수 있었습니다</p>
<h2 id="다시-돌아와서-문제-파악">다시 돌아와서 문제 파악</h2>
<p>저는 해당 문제가 생긴 원인을 알기 위해 다양한 가능성을 재고해 보았으나 도무지 해당되지 않는 부분만 존재하였습니다</p>
<ul>
<li><p><strong>같은 데이터에 대한 수정으로 인해 경합이 존재한 경우</strong>
위의 케이스에 대해서는 해당이 없습니다 
저희는 크롤링을 제외하고는 player에 대한 정보를 수정할 수 있는 API가 없고,
위 함수의 경우 모두 각각의 player의 점수를 업데이트 하기 때문에 같은 데이터를 수정했을 가능성이 없습니다</p>
</li>
<li><p><strong>선수 선택 횟수 업데이트 때문에?</strong>
해당 크롤링을 진행할 당시에 저희는 릴리즈를 앞두고 있어서 다양하게 이것저것 테스트 중이었습니다
따라서 선수 테이블과 연관된 낙관적 락이 걸려 있는 선수 선택 횟수 테이블도 활발하게 수정되고 있는 상황이었습니다
혹시 외래키로 인해 해당 과정에서 락 경합이 발생하였을까를 고려하여 테스트를 진행하였습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/224c86df-01d8-4ef3-b405-651fe0fd7e90/image.png" alt=""></p>
</li>
</ul>
<p>위 이미지와 같이 수정 중에도 문제가 없고 모든 수정이 끝난 후 쿼리가 한번에 플러시 되었습니다</p>
<h2 id="사실-이-모든-게-h2-환경에서만-용인되는-것이었을까">사실 이 모든 게 H2 환경에서만 용인되는 것이었을까?</h2>
<p>문득 이런 생각이 들었습니다
위 테스트는 모두 h2 환경에서 실행하였고 h2는 테스트용으로 지원되는 경량화된 데이터베이스입니다
하지만 mySQL은 복잡한 매커니즘을 가지고 있고 이것 때문에 위 테스트를 수행할 때 문제가 되지 않았을 수도 있습니다</p>
<p>따라서 테스트 환경을 mySQL로 바꿔 보겠습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/7ed0fde0-f557-421a-9f7d-f49055e61820/image.png" alt=""></p>
<p>도커로 mySQL을 실행하고 다시 테스트를 돌려보겠습니다
<del>노트북이 상당히 느려졌어요</del></p>
<pre><code>@Test
    void testFkLockBetweenPlayerAndChoiceCount_Massive() throws InterruptedException {
        List&lt;Long&gt; playerIds = LongStream.rangeClosed(1, 120)
                .boxed()
                .collect(Collectors.toList());

        int threadCount = 1 + playerIds.size(); // 트랜잭션 A 1개 + 트랜잭션 B 120개
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);

        // 트랜잭션 A: 선수 점수 업데이트 + 10초 락 유지
        executor.submit(() -&gt; {
            TransactionTemplate tx = new TransactionTemplate(transactionManager);
            tx.executeWithoutResult(status -&gt; {
                List&lt;Player&gt; players = playerRepository.findAllById(playerIds);
                players.forEach(p -&gt; p.updateScore(p.getScore() + 1));
                playerRepository.saveAll(players);

                System.out.println(&quot;[A] 120명 점수 업데이트 후 10초 대기 시작&quot;);
                try {
                    Thread.sleep(10000); // 락 유지
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(&quot;[A] 대기 종료, 트랜잭션 종료&quot;);
            });
            latch.countDown();
        });

        // 트랜잭션 B들: player_choice_count 업데이트 시도
        for (Long playerId : playerIds) {
            executor.submit(() -&gt; {
                try {
                    Thread.sleep(1000); // 트랜잭션 A가 먼저 락을 잡도록 지연
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                TransactionTemplate tx = new TransactionTemplate(transactionManager);
                try {
                    tx.executeWithoutResult(status -&gt; {
                        PlayerChoiceCount pcc = playerChoiceCountRepository.findByPlayerId(playerId)
                                .orElseThrow();
                        pcc.increase();
                        System.out.println(&quot;[B] playerId: &quot; + playerId + &quot; → count 증가 시도&quot;);
                    });
                    System.out.println(&quot;[B] playerId: &quot; + playerId + &quot; 트랜잭션 성공&quot;);
                } catch (Exception e) {
                    System.out.println(&quot;[B] playerId: &quot; + playerId + &quot; 트랜잭션 실패: &quot; + e.getMessage());
                }
                latch.countDown();
            });
        }

        latch.await();
        executor.shutdown();
    }</code></pre><p>위와 같은 테스트를 진행해 보았는데 역시나 오류는 없었습니다</p>
<h2 id="결론">결론</h2>
<p>사실 문제의 정확한 원인을 단정짓기는 어려웠습니다 테스트 코드 상으로는 락이 걸리지 않았고, 데이터 경합도 없었으며, 외래키로 인한 영향도 재현되지 않았기 때문입니다</p>
<p>하지만 이 경험을 통해 한 가지 중요한 사실을 깨달을 수 있었습니다 바로 서버 환경, 특히 실제 운영 DB의 락 메커니즘은 테스트 환경과는 다르게 동작할 수 있다는 점입니다
즉, 같은 코드라도 운영 환경의 DB 설정이나 트래픽, 연결 수 등에 따라 재현되지 않는 락 문제가 발생할 수 있다는 것이죠</p>
<p>그렇기 때문에 우리는 항상 트랜잭션 범위를 가능한 짧게 유지하고, 외부 I/O 작업(예: 크롤링)은 DB 작업과 분리해야 하며, 실제 운영 환경과 유사한 조건에서 테스트를 진행하는 습관이 필요합니다</p>
<p>서비스가 성장하고 사용자 수가 늘어날수록 이런 작은 락 하나가 전체 서버를 마비시킬 수 있다는 것을 잊지 말아야 합니다 </p>
<p>트랜잭션을 짧게 가져가는 것으로도 해당 문제가 재현되지 않았기 때문에 앞으로도 이런 방어적인 코드를 짜는 것에 집중해 보고자 합니다! </p>
<p><em><strong>하지만 혹시라도 가능성이 있는 시나리오를 아시는 분은 댓글 주세요</strong></em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[선수 선택 횟수 동시성 문제 해결기]]></title>
            <link>https://velog.io/@shin-jisong/%EC%84%A0%EC%88%98-%EC%84%A0%ED%83%9D-%ED%9A%9F%EC%88%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@shin-jisong/%EC%84%A0%EC%88%98-%EC%84%A0%ED%83%9D-%ED%9A%9F%EC%88%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Tue, 22 Jul 2025 06:21:16 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 빠르게 다음 글로 찾아왔습니다
이번에는 선수 선택 횟수에서 동시성을 고려한 경험을 풀어보고자 합니다</p>
<h2 id="문제가-생길-수-있는-배경은">문제가 생길 수 있는 배경은?</h2>
<p>저희 <strong>MY BASEBALL ✪ ALL STAR</strong>에서는 팀 점수 계산 시
선수 경기 기록뿐만 아니라 유저들의 선택 횟수로 가중치를 부여하고 있는데요
이러한 선택 횟수는 유저가 12명의 선수 선택을 완료하여 경기를 진행하는 시점에 업데이트 되고 있습니다</p>
<p>그러나 이 로직은 다수의 유저가 동시에 동일한 선수 선택을 완료하는 상황에서 동시성 이슈가 발생할 수 있는 구조입니다 예를 들어, 두 개 이상의 트랜잭션이 동일한 선수에 대한 선택 횟수를 동시에 읽고 수정하는 경우, 마지막에 커밋된 값만 반영되어 중간에 수행된 증가 연산이 손실될 수 있습니다 이는 대표적인 Race Condition의 사례로, 선수의 선택 횟수가 실제보다 적게 반영되는 결과를 초래할 수 있습니다!</p>
<h2 id="이를-해결할-수-있는-다양한-방법">이를 해결할 수 있는 다양한 방법</h2>
<p>선수 선택 횟수 업데이트 로직에 있어 동시성 이슈를 방지하기 위해 다양한 전략을 검토하였어요
제가 적절한 락을 위해 <strong>낙관적 락, 비관적 락, 원자적 증가 연산, 네임드 락, 분산 락</strong> 등의 다양한 락을 학습하였고 어떤 사고를 통해 최종적으로 선정하였는지 지금부터 설명드리겠습니다!</p>
<p>각각은 적용 환경과 목적에 따라 장단점이 뚜렷하게 나뉘기 때문에, 현재 로직의 특성에 맞춰 신중히 판단하고자 하였습니다</p>
<h3 id="비관적-락">비관적 락</h3>
<p>비관적 락은 반대로 충돌 가능성이 높다고 보고, 데이터에 접근하는 시점부터 락을 걸어 다른 트랜잭션의 접근을 차단하는 방식입니다
데이터 일관성을 강하게 보장할 수 있다는 장점이 있지만, 락 경합이 발생하면 대기 시간이 길어지고 성능 저하로 이어질 수 있으며, 경우에 따라 데드락이 발생할 위험도 있습니다
무엇보다 현재의 선택 로직은 그렇게까지 strict한 일관성을 요구하는 상황은 아니기 때문에, 굳이 무거운 락을 적용할 필요는 없다고 판단하였습니다</p>
<h3 id="원자적-증가-연산">원자적 증가 연산</h3>
<p>원자적 증가 연산은 SQL에서 <code>UPDATE ... SET count = count + 1</code> 형태로 처리되어 구현이 간단하고, 락 없이도 동시성 문제가 발생하지 않는다는 점에서 성능적으로 매우 뛰어난 방식입니다
단일 필드의 카운팅 로직만을 포함하고, 해당 값이 다른 도메인 로직과 독립적으로 존재할 경우에는 이상적인 선택이 될 수 있습니다
예를 들어 게시글의 조회 수, 특정 버튼의 클릭 수처럼 단순 통계성 데이터에서는 매우 효율적입니다.</p>
<p>하지만 현재의 선수 선택 로직은 단순한 수치 증가 이상의 의미를 담고 있습니다
선택 횟수 증가는 유저가 12명의 선수를 모두 선택하고 팀 구성을 마무리짓는 시점에 발생하며, 이 과정에서 선수 목록과 선택 횟수를 기반으로 TeamRoaster라는 도메인 객체를 생성합니다</p>
<p>다시 말해, 선택 횟수 증가는 다른 도메인 객체 조회 및 비즈니스 로직과 긴밀하게 엮여 있으며, 전체 트랜잭션 안에서 일관된 상태를 유지해야 할 필요가 있습니다</p>
<p>이처럼 수치 증가 결과가 즉시 조회되어 다른 연산에 활용되는 구조에서는 단순한 원자적 증가만으로는 정합성을 완전히 보장하기 어렵습니다 커밋 타이밍, 읽기 일관성 수준, 동시성 충돌 등의 요소로 인해 실제로 증가된 값과 그에 기반한 후속 연산 간에 오차가 생길 가능성이 존재합니다</p>
<h3 id="네임드-락">네임드 락</h3>
<p>네임드 락은 DB 내부에서 지정한 이름 기반의 명시적 락을 활용해 동시성을 제어할 수 있는 방법입니다. 트랜잭션 단위로 정교한 락 제어가 가능하다는 점은 장점이지만, 락이 제대로 해제되지 않거나 충돌이 많을 경우 시스템 병목의 원인이 될 수 있고, DB마다 구현과 제약이 상이하여 관리와 유지보수 부담이 존재합니다</p>
<h3 id="분산-락">분산 락</h3>
<p>분산 락은 Redis나 ZooKeeper 같은 외부 시스템을 통해 인스턴스 간 동기화를 처리하는 방식입니다 대규모 서비스나 마이크로서비스 아키텍처에서 유용하게 활용되며, 수평 확장 구조에서도 안정적인 락 처리를 보장할 수 있다는 점이 강점입니다 하지만 락 시스템 자체에 대한 운영 비용이 존재하고, 현재와 같은 단일 서버에는 과한 구조일 수 있습니다</p>
<h3 id="낙관적-락">낙관적 락</h3>
<p>마지막으로, 낙관적 락은 충돌 가능성이 낮다고 가정하고, 트랜잭션 커밋 시점에만 데이터의 버전을 비교하여 충돌 여부를 확인하는 방식입니다
이 방법은 별도의 DB 락을 점유하지 않기 때문에 동시 요청이 많은 환경에서도 시스템 자원에 부담을 주지 않는다는 점이 가장 큰 장점입니다
특히 JPA의 @Version 기능을 활용하면 비교적 간단하게 구현이 가능하며, 트랜잭션 내 여러 로직과 함께 처리할 수 있어 전체적인 일관성 확보에도 유리합니다
다만, 충돌이 발생할 경우 예외가 발생하고 재시도가 필요하다는 점은 고려해야 합니다</p>
<p>하지만 현재 로직은 유저가 12명의 선수를 모두 선택한 뒤 단 한 번만 카운트를 갱신하기 때문에, 충돌 가능성이 상대적으로 낮다고 판단하였고, 이러한 구조에 낙관적 락은 효율적이고 가벼운 선택지라고 생각했습니다</p>
<p>이러한 여러 고려사항을 종합했을 때, 
<strong>저는 낙관적 락이 현재 로직 구조와 가장 잘 맞고, 구현과 운영 측면 모두에서 효율적인 해결책이 될 수 있다고 판단하여 이를 적용하게 되었습니다.</strong></p>
<h2 id="낙관적-락의-도입">낙관적 락의 도입</h2>
<h3 id="테이블-분리">테이블 분리</h3>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/26b33cbc-b6fb-4835-82df-8eca80aca220/image.png" alt=""></p>
<p>저는 낙관적 락을 효과적으로 적용하기 위해, 선수의 선택 횟수 데이터를 기존 선수 테이블과 분리하여 별도의 PlayerChoiceCount 테이블로 관리하는 구조를 설계하였습니다 이렇게 분리함으로써 선수 정보와 선택 횟수라는 서로 다른 관심사를 명확히 구분할 수 있었고, 특히 낙관적 락을 적용할 때 필요한 버전 관리 필드를 이 테이블에 집중시켜 충돌 감지와 처리 로직을 단순화할 수 있었습니다</p>
<p>이 구조는 선수 선택 횟수 업데이트 작업이 선수 정보 조회와 독립적으로 수행될 수 있게 하여 트랜잭션 범위를 좁히는 데도 도움이 되었습니다 즉, PlayerChoiceCount 테이블에만 락이 걸리고 관리되므로, 불필요하게 선수 기본 정보에 락이 걸리는 것을 방지해 시스템 전체의 동시 처리 성능을 향상시킬 수 있었습니다 또한, 분리된 테이블을 통해 선택 횟수 관련 작업만 집중적으로 최적화하거나 확장하는 것도 용이해졌습니다</p>
<h3 id="재시도-로직">재시도 로직</h3>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/dd944739-3529-428d-84fb-53253b0e2745/image.png" alt=""></p>
<p>@Retryable 애노테이션을 사용하여, ObjectOptimisticLockingFailureException 예외 발생 시 최대 10회까지 재시도하도록 설정하였습니다 각 재시도 간에는 100밀리초의 지연을 두어 동시에 발생하는 충돌 상황을 완화하고, 잠시 후 다시 시도할 기회를 확보하도록 하였습니다</p>
<p>이와 같은 재시도 로직을 도입한 이유는 낙관적 락 방식에서 동시성 충돌이 발생할 가능성이 존재하기 때문입니다 낙관적 락은 트랜잭션 커밋 시점에 데이터 버전을 검사하여 충돌을 감지하는데, 다수의 유저가 동시에 같은 선수 선택 횟수를 증가시키려 할 경우, 충돌 예외가 발생할 수 있습니다 이때 재시도 로직이 없으면 해당 트랜잭션은 즉시 실패하게 되어 사용자 경험 저하나 데이터 불일치 문제가 생길 수 있습니다</p>
<p>따라서, 재시도 메커니즘을 통해 충돌이 일시적인 경쟁 상태에서 발생했음을 감안하고 자동으로 다시 시도함으로써, 동시성 문제를 자연스럽게 해결하고 안정적인 서비스 흐름을 유지할 수 있도록 하였습니다 </p>
<p>재시도 횟수를 최대 10회로 설정한 것은, 일반적으로 낙관적 락 충돌이 발생했을 때 짧은 재시도 반복 내에서 대부분의 경쟁 상황이 해소된다는 경험적 근거에 기반합니다 너무 적은 재시도는 일시적 충돌 해소 실패로 인한 예외 발생 빈도를 높이고, 너무 많은 재시도는 시스템 자원 낭비 및 지연을 초래할 수 있어 적절한 균형점으로 판단했습니다</p>
<p>또한 재시도 간 지연 시간을 100밀리초로 설정한 이유는, 재시도 직후 즉시 다시 시도할 경우 경쟁이 지속되어 충돌이 반복될 가능성이 높기 때문입니다 100밀리초 정도의 짧은 대기 시간은 다른 트랜잭션이 작업을 완료하고 잠금을 해제할 시간을 주어, 재시도 성공 확률을 높이는 동시에 전체 시스템에 과도한 부하를 주지 않는 선에서 결정된 값입니다</p>
<h3 id="테스트-코드">테스트 코드</h3>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/b1ec2f45-ae70-4d5c-a493-e18d20148157/image.png" alt=""></p>
<p>동시성 제어의 정확성을 검증하기 위해 위와 같이 10개의 스레드를 동시에 실행하는 동시성 테스트를 작성하였습니다 각 스레드는 동일한 선수 ID에 대해 선택 횟수를 증가시키는 increasePlayerChoiceCount 메서드를 호출하며, 모든 작업이 완료된 후 최종 선택 횟수가 정확히 10회로 증가했는지를 검증합니다</p>
<p>현실적으로 저희의 서비스에서 동시 접속자 수가 10명이 넘어가지 않음을 예측하고 스레드 수를 10개로 설정하였습니다 당연하게도 모든 접속자가 선수 선택 횟수를 증가시키는 일을 수행하지 않을 것이기에 실제 감당 가능한 동시 접속사 수는 더 클 것입니다</p>
<p>이 테스트를 통해 실제로 낙관적 락 충돌이 발생할 수 있는 환경에서 재시도 로직이 정상 작동하며, 최종 결과가 기대한 대로 정확하게 반영됨을 확인할 수 있었습니다</p>
<p>또한, 테스트 결과를 바탕으로 재시도 횟수와 재시도 간 지연 시간을 조정하였습니다 즉, 너무 짧거나 긴 지연 시간은 충돌 해결 효율과 시스템 응답성에 영향을 미치므로, 테스트에서 관찰된 실행 시간과 충돌 빈도를 참고하여 현재의 10회 재시도와 100밀리초 지연 시간으로 설정하였습니다</p>
<h2 id="마무리">마무리</h2>
<p>저는 같은 선수들을 고르더라도 늘 같은 결과가 나오지 않기를 원했어요!
선택 횟수의 오차로 인해 다양한 결과가 나오는 게
실제 야구 경기를 표방하는 것 같아 더 재미있게 느껴질 것 같기도 하였습니다</p>
<p>저희 시스템은 선택 횟수를 기반으로 팀 점수를 계산하거나 추천에 활용하기 때문에, 시점 간 정합성이 중요한 구조였습니다
따라서 낙관적 락을 통해 충돌을 감지하고, 최신 데이터를 기준으로 다시 시도함으로써 안정적으로 동시성을 제어할 수 있었습니다 👍</p>
<p>다들 이러한 배경을 고려하여 더 재미있게 <strong>MY BASEBALL ✪ ALL STAR</strong>를 즐겨 주셨으면 좋겠습니다 🏆</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이거 승패는 지피티 돌림? > 아닙니다 기록지를 크롤링해서 데이터를 기반으로...]]></title>
            <link>https://velog.io/@shin-jisong/%EC%84%A0%EC%88%98-%EC%A0%90%EC%88%98-%EA%B7%A0%EB%93%B1-%EB%B6%84%EB%B0%B0%EB%A5%BC-%EC%9C%84%ED%95%B4-%ED%95%9C-%EC%9D%BC</link>
            <guid>https://velog.io/@shin-jisong/%EC%84%A0%EC%88%98-%EC%A0%90%EC%88%98-%EA%B7%A0%EB%93%B1-%EB%B6%84%EB%B0%B0%EB%A5%BC-%EC%9C%84%ED%95%B4-%ED%95%9C-%EC%9D%BC</guid>
            <pubDate>Mon, 21 Jul 2025 06:32:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/shin-jisong/post/f006aaea-2e81-4057-96f1-fb024a6f1ae0/image.png" alt=""></p>
<p>안녕하세요! 저는 최근에 <strong>MY BASEBALL ✪ ALL STAR</strong> 를 성공적으로 릴리즈 했어요
주위에 야구 좋아하는 친구들이 재미있다는 말을 많이 해 줘서 아주 뿌듯했습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/b1cb927a-5ecf-49a7-b339-8b3cf5e1b7fa/image.png" alt=""></p>
<p>그런데 생각보다 최종 팀 점수를 랜덤이라고 생각하시는 분들이 많더라고요? 
지피티 기반이라는 의혹을 블로그 글을 통해 설명해 드리려고 찾아왔습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/e5ab51d5-b6df-4afa-9856-e9b00fe2c0c3/image.png" alt=""></p>
<p>왜냐하면 이렇게나 열심히 구현했거든요
<del>나는 진심이었어...</del></p>
<h2 id="기존-로직">기존 로직</h2>
<p>저희는 개발자 총 2명으로 해당 프로젝트를 진행했어요
팀 점수 구현에 관한 부분은 두 명 모두가 관여하였는데
다른 개발자 친구가 기록지를 통해 크롤링하여 선수 점수로 변환하는 로직을 담당하였고,
제가 해당 선수 점수를 바탕으로 팀 점수를 만드는 로직을 구현하였어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/298dd938-08e3-49a0-8758-d8110d4dde35/image.png" alt=""></p>
<p>구현을 끝내고 테스팅을 해 보는 과정에서 저는 이런 카톡을 받았습니다</p>
<p><strong>저는 분명 0점에서 100점 사이 선수들의 점수가 들어오면
이것을 0점에서 15점 사이의 팀 점수로 변환하는 코드를 작성하였는데 왜 이렇게 된 것일까요?</strong></p>
<h2 id="문제점-파악">문제점 파악</h2>
<p>저는 구현 당시에 선수들의 점수가 0점부터 100점까지 고르게 퍼져있다는 가정 하에 코드를 구현하였어요
따라서 선수의 점수를 합산하고 선택 횟수 가중치를 log로 적용해서 평균을 내는 과정이었어요</p>
<p>log를 취한 이유는 제가 이번 강의 시간에 정보 검색 강의를 들었는데
값 자체를 가중치에 적용하면 그 폭이 너무 커져서
휴리스틱하게 로그를 취하는 게 보편적으로 사용하는 방식이더라고요
그 방식이 강의 듣는 내내 합리적이라는 생각이 들어 저 또한 차용하였습니다</p>
<p>다시 돌아가, 제가 구현한 코드에서의 문제점은 <strong>0점부터 100점까지 고르게 퍼져있다</strong>는 가정이었어요</p>
<p>실제로 크롤링 후 변환된 데이터를 분석해 보니</p>
<pre><code>54.66, 35.16, 61.5, 45.55, 43.21, 63.71, 53.66, 75.35, 66.41, 55,
20.16, 29.08, 26.59, 38.42, 19.92, 24.09, 26.7, 26.33, 27.68, 15.94,
22.05, 34.08, 33.01, 41.14, 38.2, 29.74, 20.18, 39.44, 22.72, 30.43,
40.13, 45.29, 38.37, 34.58, 37.69, 34.36, 44.85, 37.88, 39.1, 31.38, 
54.18, 36.98, 34.14, 39.37, 38.77, 47.31, 47.72, 44.29, 46.09, 38.31,
36.61, 33.31, 37.05, 32.04, 37.26, 36.42, 37.36, 33.11, 39.85, 31.9, 
37.06, 35.41, 34.9, 38.77, 33.96, 43.44, 45.4, 43.2, 34.27, 44.03, 
39.21, 37.72, 32.21, 37.32, 37.06, 36.78, 36.23, 31.46, 36.76, 34.89, 
43.79, 37.33, 35.16, 34.1, 45.37, 36.08, 35.24, 34.73, 36.97, 36.38,
39.6, 41.67, 33.11, 35.77, 38.71, 33.88, 34.17, 39.18, 34.47, 31.5,
34.16, 35.1, 41.68, 36.81, 34.84, 33.73, 33.81, 38.73, 38.12, 38.17, 
42.17, 38.35, 50.59, 40.96, 41.37, 47.29, 42.12, 42.97, 37.29, 32.74</code></pre><p>이런 식의 점수 분포를 보였습니다 점수를 자세히 살펴 보면 주로 30-40점에서 머무는 것을 확인할 수가 있어요
이것 때문에 <strong>점수가 7점 혹은 8점으로 편향될 수밖에 없다는 사실을 발견</strong>했습니다</p>
<h2 id="팀-점수-시뮬레이터-구현">팀 점수 시뮬레이터 구현</h2>
<p>저는 같은 실수를 방지하기 위해 팀 점수 시뮬레이터를 우선 구현하고
이후 로직을 수정하기로 하였습니다</p>
<p>팀 점수가 고르게 나오는지 확인할 수 있는 시뮬레이션을 돌리는 로직인데요
선수 선택에 따라 팀 점수가 다양하게 나오는 것이 목표이기 때문에
정해진 SIMULATION_COUNT만큼 팀을 랜덤으로 만들어서 점수를 확인할 수 있는 간단한 로직입니다</p>
<p>해당 구현에서 저의 목표는 점수가 0점부터 15점까지 고르게 나오는 것이었습니다</p>
<pre><code>public class TeamScoreSimulation {

    private static final int SIMULATION_COUNT = 1000;

    private static final double[] PLAYER_SCORES = {선수들 점수 삽입};

    public static void main(String[] args) {
        Map&lt;Integer, Integer&gt; scoreDistribution = new TreeMap&lt;&gt;();
        Random random = new Random();

        for (int sim = 0; sim &lt; SIMULATION_COUNT; sim++) {
            // 무작위로 12명 선택
            List&lt;Integer&gt; indices = IntStream.range(0, PLAYER_SCORES.length)
                    .boxed()
                    .collect(Collectors.toList());
            Collections.shuffle(indices);
            List&lt;Integer&gt; selected = indices.subList(0, 12);

            // 무작위 choice count (1~10)
            List&lt;Long&gt; choiceCounts = selected.stream()
                    .map(i -&gt; (long) (random.nextInt(10) + 1))
                    .collect(Collectors.toList());

            // 점수 추출
            List&lt;Double&gt; selectedScores = selected.stream()
                    .map(i -&gt; PLAYER_SCORES[i])
                    .collect(Collectors.toList());

            // 팀 점수 계산
            int teamScore = TeamScoreCalculator.calculate(selectedScores, choiceCounts);

            // 분포 기록
            scoreDistribution.put(teamScore, scoreDistribution.getOrDefault(teamScore, 0) + 1);
        }

        // 결과 출력
        System.out.println(&quot;Team Score Distribution:&quot;);
        for (Map.Entry&lt;Integer, Integer&gt; entry : scoreDistribution.entrySet()) {
            System.out.printf(&quot;%2d : %s (%d)%n&quot;, entry.getKey(),
                    &quot;*&quot;.repeat(entry.getValue() / 10), entry.getValue());
        }
    }
}</code></pre><p>이것을 구현한 직후에 현재 점수 set으로 시뮬레이션을 돌려보니</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2f5542dc-2fa6-498e-9038-aa3b4bb8b372/image.png" alt=""></p>
<p>진짜 7점과 8점만 나올 수밖에 없는 구조였고 이를 개선해 보겠습니다</p>
<h2 id="팀-점수-로직-변경">팀 점수 로직 변경</h2>
<p>저는 팀 점수 로직을 변경해 이 문제를 해결해 보려고 하였습니다
여기서 전제가 필요했습니다</p>
<p>계산마다 DB에서 모든 Player의 점수 set을 읽는 로직은 성능적으로 합리적이지 않기에 지양하고 싶었어요
따라서 선수단의 점수들을 모른다는 가정하에 구현할 것, 그러나 0점부터 100점까지의 분포는 알고 있을 것이었습니다</p>
<h3 id="첫-번째-시도">첫 번째 시도</h3>
<p>이를 위해 간단한 방식으로는, 먼저 비선형 함수를 도입하는 방법이 있습니다 예를 들어, 선수 점수를 단순히 평균 내는 것이 아니라, 각 점수에 제곱근(sqrt)과 같은 함수를 적용하면 결과 분포가 더 넓게 퍼지게 됩니다 이렇게 하면 대부분 7~8점에 몰리던 팀 점수가 좀 더 다양해질 수 있습니다</p>
<p>또 다른 방식은 logit 함수를 이용하는 것입니다 logit은 점수를 0에서 1 사이로 정규화한 후, 이 값을 넓은 범위로 다시 펼쳐주는 역할을 하는 함수로, 특히 중간 점수대에 몰린 분포를 효과적으로 분산시킬 수 있습니다</p>
<p>마지막으로, 계산된 점수에 작은 무작위성(노이즈)을 추가하는 방법도 있습니다. 예를 들어 동일한 조건의 팀이라도 점수 계산 과정에 ±5% 정도의 흔들림을 주면, 같은 7점대에 머무르지 않고 6점 또는 8점 등으로 자연스럽게 분산되는 결과를 얻을 수 있습니다</p>
<p>저는 이 중에서 logit 변환을 각 선수 점수에 개별적으로 적용한 방식을 도입했습니다 즉, 12명의 선수 각각의 점수를 logit 함수로 변환하고, 이를 평균 내어 팀 점수를 계산하는 방식입니다 이렇게 하면 한 팀 안에 0점짜리 선수와 15점짜리 선수가 섞여 있을 경우, 팀 점수도 그 영향을 반영하여 보다 다양하게 나타날 수 있습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/0ba937e7-dbf3-460f-909a-bdbea7687cad/image.png" alt=""></p>
<p>하지만 효과는 미미했어요... 워낙 좁은 범위로 분포해 있는 로직이라 분산시키기가 쉽지 않았습니다</p>
<h3 id="두-번째-시도">두 번째 시도</h3>
<p>다음 시도 방식으로 <strong>Z-score + 시그모이드 정규화 방식</strong>을 도입하였습니다</p>
<p>Z-score 정규화는 특정 선수의 점수가 전체 분포(평균과 표준편차 기준)에서 얼마나 떨어져 있는지를 수치화하는 방식입니다 예를 들어, 어떤 선수가 평균보다 두 표준편차만큼 높다면 Z-score는 +2가 되고, 평균보다 한 표준편차만큼 낮다면 -1이 됩니다 이렇게 하면, 각 선수의 점수를 전체 분포 속에서 상대적인 위치로 바꿔 표현할 수 있습니다</p>
<p>그러나 Z-score는 정규화된 값이 -∞부터 +∞까지 나올 수 있어, 점수 시스템에 바로 사용하기에는 다소 불편합니다 이를 해결하기 위해, Z-score 결과에 시그모이드 함수를 적용하였습니다. 시그모이드는 입력값을 부드럽게 0과 1 사이로 압축해주는 함수로, 크거나 작은 극단적인 값을 자연스럽게 누르며, 중앙값(평균에 해당하는 Z=0)을 기준으로 0.5 근처에 위치시켜줍니다</p>
<p>이 과정을 통해, 전체 점수 분포의 형태는 고려하면서도 선수들의 실제 점수 배열을 몰라도 상대적인 위치에 따라 정규화된 값을 계산할 수 있게 되었습니다 또한 시그모이드의 특성 덕분에 이상치의 영향력이 완화되고, 정규화된 점수는 팀 점수 계산에 바로 사용할 수 있을 정도로 안정적인 범위를 갖게 됩니다</p>
<p>즉, 이 방식은 정규 분포를 가정한 점수 환경에서, 데이터셋 전체를 알 수 없는 상황에서도 각 선수의 점수를 안정적이고 비교 가능하게 정규화할 수 있는 합리적인 대안이었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c6301a74-20ed-4c33-86c2-5177973ac9e7/image.png" alt=""></p>
<p>하지만 이 또한 결국 극단적인 값을 눌러주는 효과만 있어 분포의 효과는 없었습니다</p>
<h2 id="선수-점수-로직-변경">선수 점수 로직 변경</h2>
<p>위 시도 외에도 여러 시도를 하였지만 모두 점수를 다양하게 펼치는 것에는 실패하였습니다</p>
<p>저는 선수 점수가 좁게 한정되어 있고, 해당 점수 set을 모르는 가정이라면 
팀 점수를 고르게 분포시키는 것은 
학부 시절 배운 데이터 분석의 지식으로는 어렵겠다는 결론을 내렸습니다</p>
<p>따라서 선수 점수가 특정 범위 내에 고르게 분포해 있다는 가정 하에 해당 로직을 작성하기 위해 선수 점수 로직을 변경하였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/dc06d613-7418-45df-b115-e8e56bfd93f8/image.png" alt=""></p>
<p>이런 로직을 구현하여 크롤링 이후 선수의 점수가 업데이트 되면
그 점수를 바탕으로 정렬하여 
최소 점수부터 최고 점수까지 일정 간격을 적용하여 점수를 부여하였습니다</p>
<p>이를 통해 저는 원하는 범위 내에서 일정한 간격의 점수를 부여했다는 전제로
팀 점수를 구현할 수 있었습니다</p>
<h2 id="변경된-선수-점수-로직을-바탕으로-팀-점수-구현">변경된 선수 점수 로직을 바탕으로 팀 점수 구현</h2>
<p><strong>팀 점수 산정은 개별 선수들의 점수를 일정한 범위 내에서 정규화한 뒤, 선택 확률에 따라 무작위로 한 명의 점수를 뽑아 대표 점수로 사용하는 방식</strong>으로 구현했습니다.</p>
<p>처음에는 모든 선수들의 점수를 평균 내거나, 단순히 선택 횟수에 가중치를 곱하는 등의 방식도 고려했지만, 이러한 방식은 특정 구간의 점수대가 지나치게 몰리며 중앙값 근처에 치우친 종 모양(산 모양)의 분포를 만들게 됩니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/42401e37-297a-4320-899d-41d74997f39e/image.png" alt=""></p>
<p>실제 정규화 이후 가중치만을 적용하니 다음과 같은 시뮬레이션 결과가 나왔습니다 🥲</p>
<p>선수들의 원 점수는 기본적으로 일정한 범위를 가지지만, 이 점수들을 가중치 없이 혹은 비율 기반 평균으로 계산할 경우, 결과값은 자연스럽게 중앙에 몰리는 경향을 띕니다. 특히 평균, 가중 평균, 중간값 기반 계산 로직은 통계적으로 정규분포(또는 유사 정규분포)를 유도하는 특성을 가지기 때문에, 극단적으로 높은 점수나 낮은 점수가 선택될 확률은 희박해지고, 대부분의 결과가 중간 점수대에 집중되는 결과가 나타납니다</p>
<p>하지만 제가 의도한 점수 시스템은 0점부터 15점까지의 구간에서 점수가 고르게 분포되는 구조였습니다 즉, 특정 점수대에 집중되는 쏠림 없이, 전체 점수 범위가 비교적 균등하게 사용되기를 원했습니다</p>
<p>이를 위해 저는 먼저 선수 개별 점수를 일정한 간격의 정수값으로 정규화하여 0~15 범위로 매핑하고, 여기에 선택 확률 기반의 선택 횟수 로직을 적용함으로써, 원시 점수의 절대값보다는 사용자 선택 횟수에 비례하여 각 점수가 대표 점수로 선택될 수 있도록 설계했습니다</p>
<p>이렇게 함으로써, 단순 평균 방식에서 발생하는 산 모양의 점수 분포를 피하고, 0~15 사이의 다양한 점수들이 고르게 분포될 수 있도록 유도할 수 있었습니다 이는 곧 제가 설계한 시스템이 보다 균형 잡히고 예측 불가능성을 유지한 점수 산정을 가능하게 했다는 점에서 의미가 있습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/bd2e0f09-ac79-47c8-963f-f8503118a2cc/image.png" alt=""></p>
<p>실제 시뮬레이션 결과를 보니 제가 의도했던 0점부터 15점까지 고르게! 를 구현할 수 있었어요</p>
<h2 id="마무리">마무리</h2>
<p>구현하는 데 정말 힘들었지만 이를 적용 후 플레이를 해 보니 </p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/efe3ab06-1b58-4d91-bd7e-b20a74b746e6/image.png" alt=""></p>
<p>이렇게 고르고 다양한 결과가 나오더라고요
이런 결과 덕에 AI 의심을 받았지만 그만큼 잘 분포돼 있는,
노력을 더한 코드였습니다</p>
<p>다들 즐겁게 플레이 해 주세요 ⚾</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MY BASEBALL ✪ ALL STAR : 야구 좋아하는 개발자가 올스타전 보면서 만든 사이트 ]]></title>
            <link>https://velog.io/@shin-jisong/MY-BASEBALL-ALL-STAR-%EC%95%BC%EA%B5%AC-%EC%A2%8B%EC%95%84%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%98%AC%EC%8A%A4%ED%83%80%EC%A0%84-%EB%B3%B4%EB%A9%B4%EC%84%9C-%EB%A7%8C%EB%93%A0-%EC%82%AC%EC%9D%B4%ED%8A%B8</link>
            <guid>https://velog.io/@shin-jisong/MY-BASEBALL-ALL-STAR-%EC%95%BC%EA%B5%AC-%EC%A2%8B%EC%95%84%ED%95%98%EB%8A%94-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%98%AC%EC%8A%A4%ED%83%80%EC%A0%84-%EB%B3%B4%EB%A9%B4%EC%84%9C-%EB%A7%8C%EB%93%A0-%EC%82%AC%EC%9D%B4%ED%8A%B8</guid>
            <pubDate>Sun, 20 Jul 2025 11:15:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/shin-jisong/post/d3e237ed-437d-4ed8-9415-271c49795ce9/image.png" alt=""></p>
<p>안녕하세요! 제가 최근에 <strong>MY BASEBALL ✪ ALL STAR</strong>라는 서비스를 출시했는데요
블로그에 회고 겸 기록을 남겨 보려고 글을 적어 봅니다</p>
<p><del>해당 서비스를 개발하며 만난 다양한 기술 문제들도 이어지는 글들로 쓸 예정이니 많관부 ..</del></p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/82e0d3ef-684b-4f19-8604-5416ee2b1a43/image.png" alt=""></p>
<p>해당 서비스는 당연하게도 올해 KBO 올스타 투표를 하고,
올스타전을 기다리다가 문득 아이디어가 떠올라 진행하게 되었어요</p>
<p>저는 주위에서 알아주는 스포츠 팬인데요 여름에는 야구, 겨울에는 농구를 꼭꼭 챙겨 보고 있어요!</p>
<p>올스타전 투표를 열심히 했지만 슬프게도 제가 투표한 모든 선수가 출전하지는 못하더라고요
그래서 <strong>내가 원하는 선수가 모두 출전할 수 있는 게임을 만들어 볼까?</strong> 생각하다가 해당 서비스를 기획하게 되었어요</p>
<p>사실 올스타전의 묘미는 경기 뿐만 아니라 다양한 퍼포먼스, 선수들간의 교류 등이 있지만
개발자인 제가 할 수 있는 것에 집중해서 *<em>선수를 선택하고 경기를 진행해 보자! *</em>를 중심으로 서비스 개발을 시작하였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/326be76f-7f47-479b-9d93-9665cafda582/image.png" alt=""></p>
<p>빠르게 개발하고 배포하기 위해서는 많은 인원보다는 가벼운 인원으로 진행하고 싶었어요
그래서 마음 맞는 <strong>백엔드 개발자 2명</strong>이 개발에 착수하였답니다</p>
<p>회의를 많이 진행하지 않았지만
3주 넘게 해당 프로젝트 개발을 진행하며 대략 하루에 6-8시간은 꾸준히 투자하였습니다
<del>사실 그렇게 어렵지 않을 줄 알았는데 쉽지 않더라고요</del></p>
<p>프론트엔드 개발자를 따로 구하고 싶었지만 다들 일정상으로 맞지 않아서</p>
<p><strong>개발자 1 🧑‍💻: 기획, 디자인, 백엔드
개발자 2 🧑‍💻: 기획, 프론트엔드, 백엔드</strong></p>
<p>로 진행하였습니다 만약 프로젝트가 성공적으로 끝난다면 추후에 적용하고 싶은 사안을 위해 프론트엔드 개발자를 모셔볼까 싶기도 합니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2c2127ca-96cf-4b69-a384-268db26967ef/image.png" alt=""></p>
<p>가장 중요한 <strong>12명의 내 팀 선수를 선택</strong>하는 부분!
야구장 구장의 디자인을 차용해서
각각의 위치에 원하는 선수를 배치할 수 있도록 설계하였어요
12명을 다 선택하면 게임을 시작할 수 있습니다
편의를 위해 랜덤 선택 기능도 도입하였어요 👍</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/38ca5210-df13-4f48-ae2c-34ebea371344/image.png" alt=""></p>
<p>저희가 가장 신경 쓴 부분은 이 부분인데요
여러분의 재미 요소를 위해서 내부적으로 비밀인
<del>레포지토리 가서 코드를 읽어보시면 다 나와있기는 합니다</del></p>
<p><strong>저희만의 점수 체계를 도입</strong>하였어요</p>
<ol>
<li>주기적으로 선수의 기록지를 크롤링하여</li>
<li>해당 기록지를 바탕으로 내부적인 점수 체계로 변환하고</li>
<li>변칙적인 요소를 삽입하기 위해 유저들의 선택횟수로 가중치를 넣어서</li>
<li>최종적인 팀 스코어를 계산합니다!</li>
</ol>
<p>실제로 가장 수정이 많이 들어갔던 부분이기도 합니다
그리고 더욱 더 재미 요소를 넣을 수 있는 상황 키워드까지 들어가있어요</p>
<p>현재 제시된 화면에서는 아래와 같은 상황이 표시되었네요</p>
<pre><code>타점 찬스에서 번번이 막혀 팬들이 아쉬워합니다.
역전 쓰리런으로 거머쥔 짜릿한 승리!</code></pre><p>실제 야구 팬인 저희의 애환을 담은... 다양한 경우를 넣었습니다....</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/6e3ed075-51da-445b-a056-cc815b62de1a/image.png" alt=""></p>
<p>디자인적으로도 여러분이 재미를 느낄 수 있게 전광판처럼 디자인해 보았는데 이런 소소한 재미를 느껴 보실 수 있었으면 좋겠네요 🎵</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/abca3492-bf4f-4917-9df0-c2de4fa799ce/image.png" alt=""></p>
<p>저희 서비스는 친구랑 즐기면 더더 재미있습니다
팀을 생성하고 링크를 공유하면 친구들은 나의 팀과 경기를 펼칠 수 있는데요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/49a7550f-0e2e-49c1-8633-1540db0d3b39/image.png" alt=""></p>
<p>친구가 나의 팀과 경기를 진행한 결과를 내 팀 전적 확인을 통해 알아볼 수 있어요
내 팀이 잘 이길 수 있도록 팀 구성을 더욱 열심히 하는 재미를 즐겨 보세요 🏆</p>
<p>저희 프로젝트는 상업적 특성이 절! 대! 없으며 서버비, 홍보비 등은 가난한... 취준생인... 저희의 용돈에서 지출되고 있어요 💸</p>
<p>야구 팬분들이 즐겁게 즐기실 수 있길 바라는 마음으로 운영 중입니다!</p>
<h2 id="my-baseball-✪-all-star"><a href="https://www.myallstar.my">MY BASEBALL ✪ ALL STAR</a></h2>
<p>위 링크로 접속해서 나만의 올스타 베스트 12를 만나 보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring에서 S3를 이용해 사진 추가 구현 + 기존 로직 리팩토링 (feat. 50+a시간의 고행)]]></title>
            <link>https://velog.io/@shin-jisong/Spring%EC%97%90%EC%84%9C-S3%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%82%AC%EC%A7%84-%EC%B6%94%EA%B0%80-%EA%B5%AC%ED%98%84-%EA%B8%B0%EC%A1%B4-%EB%A1%9C%EC%A7%81-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-feat.-50a%EC%8B%9C%EA%B0%84%EC%9D%98-%EA%B3%A0%ED%96%89</link>
            <guid>https://velog.io/@shin-jisong/Spring%EC%97%90%EC%84%9C-S3%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%82%AC%EC%A7%84-%EC%B6%94%EA%B0%80-%EA%B5%AC%ED%98%84-%EA%B8%B0%EC%A1%B4-%EB%A1%9C%EC%A7%81-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-feat.-50a%EC%8B%9C%EA%B0%84%EC%9D%98-%EA%B3%A0%ED%96%89</guid>
            <pubDate>Sun, 06 Jul 2025 07:52:47 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오랜만에 돌아왔습니다
막학기를 보내며 시험 기간이 끝나고 이제 기능 구현을 하나 해야 되는데
처음 구현해 보는 거라 기록하며 구현하면 좋을 것 같아서 남겨 보아요!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/3e333e1a-b3b1-4207-9e2c-74dce03af10a/image.png" alt=""></p>
<p>현재 이렇게 있는 체크리스트에 드디어 사진 추가를 구현하기로 결정</p>
<p>전략은 간단하게 아래와 같습니다</p>
<ol>
<li>최대 5장 첨부 가능</li>
<li>제일 첫 번째로 첨부한 사진이 썸네일</li>
<li>이미지 업로드는 기존 체크리스트 API와 합침 / 이미지 삭제 API만 따로 만듦</li>
</ol>
<p>아래는 전략 회의에서 결정된 방향이에요</p>
<pre><code>이미지 삭제 API만 만든다 

    체크리스트 작성 시에는 multipart 전송하고, 
    체크리스트 수정 시에는 newImages만 전송해서 newImages에는 multipart를 전송한다
    새로운 이미지는 multipart 업로드 후 저장한다
    체크리스트 수정 시 이미지 삭제 요청이 있다면 따로 API 요청을 보내야 한다
    체크리스트 삭제 시 일괄 삭제

    → 사용하지 않는 이미지 서버에서 주기적 삭제 처리를 하지 않아도 된다
    → 유저가 사진 삭제할 때 url일 경우에는 서버에게 삭제 요청을 보내 줘야 한다</code></pre><h2 id="계획-수립">계획 수립</h2>
<p>이를 구현하기 위해서 일이 복잡할 것 같아 할 일을 미리 정리하고 가겠습니다</p>
<ol>
<li>개발 서버 S3 설정 ✅</li>
<li>개발 서버 S3 연결 설정 및 테스트 작성 ✅</li>
<li>사진 CRUD 기능 구현 및 테스트 작성 ✅</li>
<li>사진 압축 기능 구현 및 테스트 작성 ✅</li>
<li>로컬 DB 구조 변경 ✅</li>
<li>체크리스트 작성 API 리팩토링 및 테스트 작성 ✅</li>
<li>체크리스트 수정 API 리팩토링 및 테스트 작성 ✅</li>
<li>사진 삭제 API 구현 ✅</li>
<li>체크리스트 삭제 API 리팩토링 및 테스트 작성 ✅</li>
<li>체크리스트 조회 API 리팩토링 및 테스트 작성 ✅</li>
<li>체크리스트 전체 조회 API 리팩토링 및 테스트 작성 ✅</li>
<li>운영 서버 S3 설정 ✅</li>
<li>운영 서버 S3 연결 설정 및 테스트 작성 ✅</li>
<li>운영 서버 DB 구조 변경 ✅</li>
<li>노션 API 문서 재정리 ✅</li>
<li>변경된 YML 반영하기 ✅</li>
<li>PR 올리기 ✅</li>
</ol>
<h2 id="1-개발-서버-s3-설정-설정">1. 개발 서버 S3 설정 설정</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a458bf74-c3e7-42f6-90db-6959e9b169b8/image.png" alt=""></p>
<p>기존의 프론트 쪽에서 S3를 만들어 둔 게 있어서 혹시나 버킷 두 개는 프리티어 안에 해당되지 않을까 봐 검색해 보았는데!
다행히 용량 기준으로 요금을 책정해서 버킷의 개수는 상관이 없다고 합니다 👍</p>
<p>dev 환경의 S3 설정 당시 아래의 링크를 참고했어요
궁금한 설정 부분은 따로 공부하면서 설정 이유를 파악했습니다</p>
<p>🔗 <a href="https://yel-m.tistory.com/19">https://yel-m.tistory.com/19</a></p>
<p>발급한 키는 주요 키 모음 내부 문서에 기재해 두었습니다 (듣고 있겠지 방끗 팀)</p>
<h2 id="2-개발-서버-s3-연결-설정-및-테스트-작성">2. 개발 서버 S3 연결 설정 및 테스트 작성</h2>
<p>기능 구현 전에는 테스트 실행 필수! 잊지 맙시다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a1013c05-3369-4309-ac56-3479bba06426/image.png" alt=""></p>
<p>의존성을 추가해 주었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/0505cca4-3b6d-42f0-9da2-2c9b3fa398f5/image.png" alt=""></p>
<p>yml에 설정 정보를 추가해 줬습니다
해당 S3는 개발 서버에서 사용할 예정이니 dev, local, test, read-write-test yml에 모두 같은 S3 정보를 기입해 줬습니다!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/637044ac-2d2a-4237-81df-fb9c54636104/image.png" alt=""></p>
<p>접근 가능한지 테스트 짜고 실행해 봤는데 Access Denied가 떴어요
테스트가 중요한 이유.......... 🥲
-&gt; 다행히 버킷 이름 실수라 금방 해결했습니다!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/47f09155-a7ad-4f56-a17a-9f57de18a62e/image.png" alt=""></p>
<p>테스트는 다음과 같이 작성하였습니다</p>
<h2 id="3-사진-crud-기능-구현-및-테스트-작성">3. 사진 CRUD 기능 구현 및 테스트 작성</h2>
<p>S3 자체에 사진 CRUD 기능을 구현한 후, 기존의 로직에 적용해 보겠습니다
S3에서는 R, U는 사실상 필요 없는 로직이기 때문에 업로드와 삭제를 구현해 볼게요!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2de055ba-15d7-45cc-8dbe-9fde18882451/image.png" alt=""></p>
<p>먼저 저장소 클라이언트로 추상화 인터페이스를 만들어주었습니다
저장소가 S3가 아닌 다른 것으로 바뀌어도 빠르게 부품을 바꿀 수 있게 하기 위함이에요
이때, 저장 위치를 분리해 주고 싶어서 folder로 관리하였습니다
S3의 경우, 폴더 기능이 제공되지 않지만 프리픽스로 작성할 경우 UI 상에서 분리해서 볼 수 있다고 합니다
업로드와 삭제 기능을 구현해 주었고, 코드를 작성해 보았습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/65b21306-9349-4b8e-b7b2-7012b1ed8a97/image.png" alt=""></p>
<p>아래로 테스트도 작성해 주었습니다
Mock을 사용할지, 실제 환경을 사용할지 고민하였는데 S3는 유료 자원이며 테스트를 돌릴 때마다 요청이 간다면 해당 자원에 대한 가격 또한 문제가 될 거라 판단하였어요
따라서 Mock으로 작성해 주었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/8df69aa8-d8af-4160-9a0b-76a039c595a5/image.png" alt=""></p>
<h2 id="4-사진-압축-기능-구현-및-테스트-작성">4. 사진 압축 기능 구현 및 테스트 작성</h2>
<p>전략은 앞선 블로그 글에서 세워두었는데요 
구현하기 위해서 가장 많이 사용하는 라이브러리인 <code>thumbnailater</code>를 사용했습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2b3ce139-94df-4584-bf93-6c485f71f42f/image.png" alt=""></p>
<p>사진 최적화 관련 유틸성 클래스를 만들어 주었습니다
저번 글에서는 리사이즈와 파일 타입 변환만을 진행할 예정이라고 하였는데요,
범용성을 가진 클래스임을 고려하여 최적화와 압축 정도의 많이 쓸 것 같은 유틸을 만들어두고
필요한 대로 사용하려고 합니다!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2184dae3-c1f3-4ba1-86d1-d223fa7c5ebc/image.png" alt=""></p>
<p>테스트까지 작성 완료</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/df1b6fcd-f3bd-433b-9b5f-e46cdf0f73eb/image.png" alt=""></p>
<h2 id="5-로컬-db-구조-변경">5. 로컬 DB 구조 변경</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/cdab4ad3-9124-4fba-831f-6af01d6e2fb1/image.png" alt=""></p>
<p>위와 같이 Checklist 테이블에 1:N으로 연결되는 Checklist_image 테이블을 만들어 주는 걸로 ERD를 변경하였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/788669f6-5d66-4a9e-97fb-c25f063463ff/image.png" alt=""></p>
<p>로컬 DB는 schema.sql 파일로 관리하기 때문에 해당 부분만 변경해 주면 구조 변경을 할 수 있어요
테스트 스키마 파일도 변경해 주었습니다</p>
<h2 id="6-체크리스트-작성-api-리팩토링-및-테스트-작성">6. 체크리스트 작성 API 리팩토링 및 테스트 작성</h2>
<p>이제 기존 코드에 적용해야 되는데요
많은 부분이 변경될 것 같아서 심호흡을 하고 들어가야 됩니다..........</p>
<p>위에서는 ImageOptimizationUtil이 InputStream을 반환하도록 구현하였는데
StorageClient 구현상 Multipart를 받도록 되어 있어서 통일성이 지켜지지 않더라고요</p>
<p>Multipart를 최적화한 후에는 Multipart를 return하는 것이 개발자들 인식상으로도 자연스러운 흐름일 것 같아 <code>BanggoodMultipart</code>라는 함수를 구현하고 이를 반환하도록 변경하였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/233994e7-c888-41e3-8f54-95ade10f3dd4/image.png" alt=""></p>
<p>구현하다가 테스트 돌렸는데
엔티티 삼종 세트 필드 추가 안 해서 테스트 깨지길래 빠르게 추가하기...
테스트의 중요성이랄까요
<img src="https://velog.velcdn.com/images/shin-jisong/post/a6795c26-a4ec-4c3e-ad1d-39f89cf65640/image.png" alt=""></p>
<p>구현하고... 테스트 짜고... 에러 코드 문서 정리하고 반복했습니다!
<img src="https://velog.velcdn.com/images/shin-jisong/post/4efbae71-9b7a-43b3-aa92-a9c5c09e5f97/image.png" alt=""></p>
<p>버전을 분리할지 말지 고민하다가 Request 받을 때 image 필드가 추가되었기 때문에 <strong>V2로 버전을 분리</strong>하였어요</p>
<p>해당 부분에 image에 null을 받을 수 있도록 해서 있다면 image를 처리하는 방안으로 갈까 고민도 해 보았지만 해당 부분을 적용하게 되면 버전을 나눌 일을 비즈니스 코드에 대입시키는 듯한 구조가 되는 듯하여 지양하고 싶었습니다</p>
<p>없다면 빈 리스트가 오고, 있다면 리스트가 채워서 오도록 설계가 되어있는데 버전에 따라 null이 올 수도 있다는 것을 분기문이라는 비즈니스 로직을 넣어 알게 하기보다는 명확하게 버전을 나누는 것이 더 합리적이라는 생각이 들었습니다! 또한 이 기능을 기점으로 해당 버전에서는 다양한 기능을 추가할 예정이 있었기도 해요 </p>
<p><strong>결론적으로,
API가 &quot;어떤 필드를 기대하는지&quot; 명확하지 않게 만들고, 클라이언트와의 계약이 불분명해지는 것을 지양하고,
버전 관련된 로직 처리를 service 코드에 대입하고 싶지 않았습니다</strong></p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2029aa59-2c75-4db8-8b77-35968abe7cb7/image.png" alt=""></p>
<p>또한 저희 테스트 전반의 환경을 세팅해 주는 클래스가 있는데요!
방끗 내에서는, Service 테스트가 (필요하다면 Repository  테스트까지) 해당 추상화 클래스를 extend 하도록 테스트 규칙을 설정해 주었습니다
이러한 클래스에 Mock 관련 정보를 추가해 주었습니다</p>
<p>테스트마다 awsS3Client로 요청이 날아가는 것을 방지하기 위해서 awsS3Client를  Mocking 해 주었고
테스트 압축은 더 이상 Service 테스트의 영역이 아니기 때문에 테스트의 편리함 그리고 속도를 위해서 이미지 최적화 부분도 Mocking 해 주었어요
<img src="https://velog.velcdn.com/images/shin-jisong/post/d0622d88-24c3-4654-a942-13bab0529c86/image.png" alt=""></p>
<p>테스트 짜다가 지금 내가 생각한 로직이 굉장히 불안정한 로직이라는 점을 깨달았어요
저는 RequestDTO 안에 아래와 같이 MultipartFile도 같이 넣어서 받으려고 했는데 </p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c5ae0b9f-54f2-45bc-b3d4-08ade6d01379/image.png" alt=""></p>
<p>테스트가 너무 까다로워지고 직렬화 쪽에서 에러가 떴습니다...</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/f71e4124-f7f6-46db-b64d-76c8abfed1f6/image.png" alt=""></p>
<p>해당 문제를 더 파보고 싶지만 구현 일정을 맞춰야 해서 구현을 끝내고 천천히 공부해 보려고 해요!
결론적으로는 DTO와 MultipartFile을 분리하면 됩니다...</p>
<ul>
<li><strong>DTO 내부 MultipartFile 포함 vs DTO와 MultipartFile 분리 비교</strong></li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>DTO 안에 MultipartFile 포함</th>
<th>DTO(JSON) + MultipartFile 분리</th>
</tr>
</thead>
<tbody><tr>
<td>바인딩 방식</td>
<td><code>@ModelAttribute</code></td>
<td><code>@RequestPart(&quot;data&quot;)</code>, <code>@RequestPart(&quot;images&quot;)</code></td>
</tr>
<tr>
<td>프론트 요청</td>
<td><code>FormData</code>에 개별 필드 (roomName, content, images)</td>
<td><code>FormData</code>에 <code>data(JSON)</code>, <code>images(File)</code></td>
</tr>
<tr>
<td>직렬화 용이성</td>
<td>어려움 (테스트/직렬화 불편)</td>
<td>JSON으로 DTO 처리 가능</td>
</tr>
<tr>
<td>유지보수</td>
<td>어려움</td>
<td>구조적으로 명확하고 테스트 편함</td>
</tr>
<tr>
<td>추천도</td>
<td>❌ 비추천</td>
<td>✅ 추천</td>
</tr>
</tbody></table>
<hr>
<ul>
<li><p><strong>DTO 안에 MultipartFile 포함</strong><br>DTO가 <code>List&lt;MultipartFile&gt;</code> 같은 파일 필드를 직접 갖는 경우, Spring은 <code>@ModelAttribute</code>를 사용해 바인딩
프론트엔드는 <code>FormData</code>에 각 필드를 개별로 넣어서 전송
그러나 DTO 직렬화(특히 테스트에서 JSON 직렬화)가 어렵고 유지보수가 복잡</p>
</li>
<li><p><strong>DTO(JSON) + MultipartFile 분리</strong><br>DTO는 JSON으로 직렬화 가능하게 설계하고, 파일은 별도의 <code>@RequestPart</code> 파라미터로 분리
프론트엔드는 JSON을 <code>Blob</code>으로 만들어 <code>FormData</code>의 <code>&quot;data&quot;</code>로 넣고, 파일은 <code>&quot;images&quot;</code> 키로 따로 넣어 전송
이 방식은 테스트와 유지보수에 훨씬 유리하며 권장</p>
</li>
</ul>
<p><strong>결론: 다 갈아엎어야 함....... ^^</strong>
지금까지 순수 구현 시간만 최소 30시간 정도... 소요된 것 같은데... 1시간 추가되었네요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/774d6c8c-384f-4eb6-9086-735dea18d650/image.png" alt=""></p>
<p>이렇게 따로 받는 구조로 바꿔 주었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a5735eef-d0ef-415e-ad84-7370ac1a6443/image.png" alt=""></p>
<p>테스트에서도 에러 핸들링을 두 시간 가까이 했는데요</p>
<ol>
<li>직렬화 문제</li>
<li>UTF_8 인코딩 문제</li>
<li>RestAssured에서 Mock 안 됨 문제
세 가지를 한참 해결하다 보니까 두 시간이 훌쩍 지났어요........</li>
</ol>
<p>이렇게 이번 단계도 구현 완료</p>
<h2 id="7-체크리스트-수정-api-리팩토링-및-테스트-작성">7. 체크리스트 수정 API 리팩토링 및 테스트 작성</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/386c039a-b92a-460e-a57f-581283d27370/image.png" alt=""></p>
<p>수정을 구현하면서 저희 기획 단계에서 프론트엔드 개발자분들과 논의했던 내용을 다시 한번 짚어 봐야 해요!
저희는 사용자가 체크리스트를 수정할 때 기존의 사진을 받게 되고 
해당 사진과 새로운 사진을 어떻게 처리할지에 대한 논의를 거쳤습니다</p>
<p>결론적으로, 기존의 사진을 삭제할 경우에는 사진 삭제 API를 호출하게 되고,
새로운 사진을 업로드할 경우 updateImages에 담아 보내게 됩니다</p>
<p>여기서 사진 삭제 API가 필요해요</p>
<h2 id="8-사진-삭제-api-구현">8. 사진 삭제 API 구현</h2>
<p>여기서 API를 설계할 때</p>
<ul>
<li>/checklists/{checklist_id}/images/{image_id}</li>
<li>/images/{image_id}</li>
</ul>
<p>둘 중 어떻게 설계할지 고민이 되었습니다
해당 API를 호출하는 건 사실상 체크리스트를 조회할 때밖에 없고
비즈니스적으로도 계층적인 호출이 필요하기 때문에 해당 부분을 명확히 명시해 줄 수 있습니다
하지만 checklist_id가 필요한 정보가 아니며 image_id로 특정 리소스를 식별할 수 있기 때문에 불필요한 정보이기도 합니다</p>
<p>저는 고민 끝에 <strong>관계적인 형태로 설계</strong>하기로 결심했는데요!</p>
<p>사유는 다음과 같습니다</p>
<p>저희의 도메인에서 사진이라는 데이터는 체크리스트 정보에 독립적일 수 있을지 고민해 보았을 때 현재로서는 그렇지 않다고 판단했습니다</p>
<p>사실 API는 간결한 것도 중요하다고 생각하기에 계속해서 고민을 거쳤는데요
체크리스트 이미지가 재사용되거나 여러 도메인과 관계를 맺을 수 있는 것이 아니며, 독립된 생명주기를 가진다고 보기에도 어려운 부분이 있어 위와 같이 표현하였습니다</p>
<p>(너무 어렵네요... 🥲)</p>
<p>하지만 사진만을 모아보는 필요성이 생겼을 때는 계층형이 아닌 다른 형태로 구현하게 될 것 같습니다 </p>
<p>사진 삭제 API 플로우를 생각해 보면
S3 상으로 사진을 삭제 -&gt; DB에 체크리스트 이미지 삭제 플로우로 가게 됩니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/09329f71-cebc-4b39-85c2-a5dabcbd2cb2/image.png" alt=""></p>
<p>transactional로 묶여 있어 반드시 두개의 동작이 동시에 수행되겠지만
혹시나 S3에 사진이 남아있고, 이미지 레포지토리에서는 삭제될 상황을 방지하기 위해
S3 상으로 사진을 삭제하는 코드를 먼저 기입해 주었습니다!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c003684a-78b1-487b-b60a-8b9f69550fef/image.png" alt=""></p>
<p>2차 고비가 찾아왔어요
기존에는 orderIdx를 포함하여 파일 이름을 만들어 주었는데
생각해보니 orderIdx는 충분히 변할 수 있는 정보이기 때문에 해당 정보를 활용한다면 안정성이 떨어질 우려가 있었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/1d24698d-f298-4d89-942c-ca6d87bdd87f/image.png" alt=""></p>
<p>따라서 이렇게 UUID로 변경하기로 결심했어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/8a168955-74f2-4d29-8741-013c7afe8b73/image.png" alt=""></p>
<p>완성하였습니다...
테스트 코드를 잘 짜두어서 파일 이름 변경이 어렵지 않았습니다</p>
<h2 id="9-체크리스트-삭제-api-리팩토링-및-테스트-작성">9. 체크리스트 삭제 API 리팩토링 및 테스트 작성</h2>
<p>삭제는 따로 버저닝을 해 줄 필요가 없기 때문에 API에서 바로 수정해서 비교적 간단했습니다!</p>
<h2 id="10-체크리스트-조회-api-리팩토링-및-테스트-작성">10. 체크리스트 조회 API 리팩토링 및 테스트 작성</h2>
<p>앞으로 조회 API들만 정리해 주면 대략의 API 코딩 작업들은 끝이 나게 됩니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/81e3d4de-4699-44b0-91fc-cfe59e95662c/image.png" alt=""></p>
<p>v2로 구현해 주었습니다</p>
<h2 id="11-체크리스트-전체-조회-api-리팩토링-및-테스트-작성">11. 체크리스트 전체 조회 API 리팩토링 및 테스트 작성</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/7d9ac2ce-bdb0-4cfc-97f8-73a41ec6fb0d/image.png" alt=""></p>
<p>마찬가지로 V2로 구현을 마무리하였습니다</p>
<h2 id="12-운영-서버-s3-설정">12. 운영 서버 S3 설정</h2>
<p>기존에 dev를 설정한 것과 똑같이 S3를 설정해 주도록 하겠습니다~!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/83caefcb-537e-44c5-8d40-c8c1e22307e7/image.png" alt=""></p>
<p>이렇게 만들어 두었습니다</p>
<h2 id="13-운영-서버-s3-연결-설정-및-테스트-작성">13. 운영 서버 S3 연결 설정 및 테스트 작성</h2>
<p>연결 설정을 완료했으나 테스트 환경에서 운영 서버 S3까지 연결을 확인하는 테스트를 짜려고 보니 여러 문제가 발생하더라고요</p>
<p>따로 YML을 추가해 줘야 하는 문제나,
프로퍼티 이름에 변형을 주어 지정하니 기존 루트를 따라 가서 test access key를 사용하려고 하여 권한 문제가 생기는 등...</p>
<p>현재 잘 접근이 됨을 확인하여 앞으로 운영에 문제는 없을 듯하지만 테스트 가능한 방법을 생각해 보아야 할 듯합니다 </p>
<h2 id="14-운영-서버-db-구조-변경">14. 운영 서버 DB 구조 변경</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/59272136-0f96-48e8-8830-dbaef85764e6/image.png" alt=""></p>
<p>DB도 만들기 완료하였습니다!</p>
<hr>
<p>이후 나머지도 반영하였습니다!
이렇게 작업이 끝났는데요
드디어 사진 반영 작업을 끝내어 기쁩니다 :)</p>
<p>글이 도움이 되었길 바라며 이만 마무리하겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[응집도와 결합도]]></title>
            <link>https://velog.io/@shin-jisong/%EC%9D%91%EC%A7%91%EB%8F%84%EC%99%80-%EA%B2%B0%ED%95%A9%EB%8F%84</link>
            <guid>https://velog.io/@shin-jisong/%EC%9D%91%EC%A7%91%EB%8F%84%EC%99%80-%EA%B2%B0%ED%95%A9%EB%8F%84</guid>
            <pubDate>Wed, 25 Jun 2025 07:45:33 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 이번에는 응집도와 결합도를 정리해 보려고 합니다.
최근 이것에 관한 질문을 받았는데,
머리로는 이해하지만 말로 설명하려고 하니
<em>&quot;응집은 응집이고... 결합은 결합이지...?&quot;</em>
이런 말만 나오게 되는 겁니다... 🥲
아무래도 설명할 수 있어야 제가 인지하고 있는 개념이겠죠!</p>
<p>제대로 설명할 수 있게 정리해 보겠습니다. 👍</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/b462eeee-b24a-4b95-84b2-461b31541cad/image.png" alt=""></p>
<h2 id="응집도cohesion와-결합도coupling의-개념과-차이점">응집도(Cohesion)와 결합도(Coupling)의 개념과 차이점</h2>
<p>소프트웨어 설계에서 좋은 구조를 판단하는 기준 중 가장 핵심적인 두 가지는 <strong>응집도(cohesion)</strong>와 <strong>결합도(coupling)</strong>입니다. 
이 두 개념을 이해하면 유지보수성과 확장성이 뛰어난 코드를 설계할 수 있습니다.</p>
<hr>
<h3 id="✨-응집도cohesion란">✨ 응집도(Cohesion)란?</h3>
<blockquote>
<p>모듈(클래스, 함수 등) 내부 요소들이 얼마나 밀접하게 관련되어 있는지를 나타내는 정도</p>
</blockquote>
<h4 id="✅-응집도의-특징">✅ 응집도의 특징</h4>
<ul>
<li>하나의 모듈이 하나의 책임을 수행할수록 응집도가 높다.</li>
<li>응집도가 높을수록 <strong>모듈의 재사용성과 유지보수성</strong>이 좋아진다.</li>
</ul>
<h4 id="📈-응집도-수준-낮음-→-높음">📈 응집도 수준 (낮음 → 높음)</h4>
<ol>
<li>Coincidental Cohesion (우연적 응집)</li>
<li>Logical Cohesion (논리적 응집)</li>
<li>Temporal Cohesion (시간적 응집)</li>
<li>Procedural Cohesion (절차적 응집)</li>
<li>Communicational Cohesion (통신적 응집)</li>
<li>Sequential Cohesion (순차적 응집)</li>
<li><strong>Functional Cohesion (기능적 응집)</strong> ✅ 최고 수준</li>
</ol>
<h4 id="💡-예시-좋은-응집도">💡 예시 (좋은 응집도)</h4>
<pre><code class="language-java">public class OrderService {
    public void createOrder() { ... }
    public void cancelOrder() { ... }
    public void getOrderById() { ... }
}</code></pre>
<blockquote>
<p><code>OrderService</code>가 오직 주문과 관련된 책임만을 가질 때 높은 응집도를 가진다.</p>
</blockquote>
<hr>
<h3 id="🧷-결합도coupling란">🧷 결합도(Coupling)란?</h3>
<blockquote>
<p>모듈 간의 의존성 정도, 즉 <strong>다른 모듈에 얼마나 영향을 받는지를 나타내는 척도</strong></p>
</blockquote>
<h4 id="❌-결합도가-높으면">❌ 결합도가 높으면?</h4>
<ul>
<li>한 모듈의 변경이 다른 모듈에 영향을 미친다.</li>
<li>유연성과 확장성이 떨어진다.</li>
</ul>
<h4 id="✅-결합도가-낮으면">✅ 결합도가 낮으면?</h4>
<ul>
<li>각 모듈이 독립적이다.</li>
<li>변경에 강하고 테스트가 용이하다.</li>
</ul>
<h4 id="📉-결합도-수준-높음-→-낮음">📉 결합도 수준 (높음 → 낮음)</h4>
<ol>
<li>Content Coupling (내용 결합) ❌</li>
<li>Common Coupling (공통 결합)</li>
<li>External Coupling (외부 결합)</li>
<li>Control Coupling (제어 결합)</li>
<li>Stamp Coupling (스탬프 결합)</li>
<li>Data Coupling (데이터 결합) ✅ 가장 바람직</li>
<li>No Coupling (무결합)</li>
</ol>
<h4 id="💡-예시-나쁜-결합도">💡 예시 (나쁜 결합도)</h4>
<pre><code class="language-java">public class A {
    public B b;
    public void doSomething() {
        b.internalState = 10; // B의 내부 상태 직접 조작 (내용 결합)
    }
}</code></pre>
<blockquote>
<p>B의 내부 구현에 직접 접근하면 결합도가 매우 높아진다.</p>
</blockquote>
<hr>
<h3 id="✅-정리">✅ 정리</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>응집도 (Cohesion)</th>
<th>결합도 (Coupling)</th>
</tr>
</thead>
<tbody><tr>
<td>정의</td>
<td>모듈 내부의 응집력</td>
<td>모듈 간의 의존성</td>
</tr>
<tr>
<td>이상적인 상태</td>
<td>높을수록 좋음</td>
<td>낮을수록 좋음</td>
</tr>
<tr>
<td>목적</td>
<td>하나의 책임 집중</td>
<td>독립성 유지</td>
</tr>
</tbody></table>
<h4 id="🎯-좋은-설계란">🎯 좋은 설계란?</h4>
<ul>
<li><strong>높은 응집도 + 낮은 결합도</strong>를 달성하는 구조</li>
<li>SOLID 원칙, OOP, 클린 코드 등의 설계 원칙도 이 방향을 추구</li>
</ul>
<hr>
<h3 id="🔚-마무리">🔚 마무리</h3>
<p>응집도와 결합도는 단순히 개념을 외우는 것이 아니라, <strong>실제 설계와 리팩토링 과정에서 반복적으로 고민하고 적용해볼 때</strong> 진정한 의미를 체감할 수 있습니다.</p>
<blockquote>
<p>&quot;응집도는 안에서, 결합도는 밖에서.&quot;</p>
</blockquote>
<h2 id="생각">생각</h2>
<p>서두에서 </p>
<p><em>&quot;응집은 응집이고... 결합은 결합이지...?&quot;</em></p>
<p>라고 대답했던 과거를 벗어나서 이제 누군가가 물어보면 이렇게 답할 예정이에요!</p>
<blockquote>
<p>응집도는 객체들이 얼마나 관련이 되어 있는지이다
결합도는 객체들이 얼마나 서로 영향을 받는지이다
응집도는 높게 구성하고, 결합도는 낮게 구성하는 것이 유지보수나 코드 품질에서 좋은 방향이다</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[디자인 패턴]]></title>
            <link>https://velog.io/@shin-jisong/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@shin-jisong/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 23 Jun 2025 07:26:18 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 이번에는 디자인 패턴을 정리해 보려고 합니다.
사실 우테코 6기 시절, 크루 &quot;커찬&quot;의 발표가 저에게는 굉장히 인상 깊었는데요.
따라서 디자인 패턴을 따로 공부하기보다는 프로그래밍을 하며 자연스럽게 익혀왔습니다.</p>
<p>하지만
스프링에서 주요하게 사용되는 싱글톤 패턴,
우테코 체스 미션 중 공부하였던 전략 패턴,
Nginx 트러블 슈팅을 하며 공부하였던 프록시 패턴 등</p>
<p>다양한 패턴을 학습하게 되며 한번쯤은 정리해 보고자 하여 글로 남겨봅니다.</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/53d536ee-9d2f-439f-98f4-a397c216ec5b/image.png" alt=""></p>
<h1 id="디자인-패턴이란">디자인 패턴이란?</h1>
<p>디자인 패턴은 <strong>자주 발생하는 소프트웨어 설계 문제를 해결하기 위한 재사용 가능한 설계 템플릿</strong>입니다.
특정 상황에서 검증된 설계 방법을 패턴으로 정리해두면, 복잡한 문제를 효율적으로 해결할 수 있고 코드의 유지보수성과 확장성을 높일 수 있습니다.</p>
<hr>
<h2 id="주요-디자인-패턴">주요 디자인 패턴</h2>
<h3 id="✅-싱글톤-패턴">✅ 싱글톤 패턴</h3>
<ul>
<li><strong>목적</strong>: 애플리케이션에서 단 하나의 인스턴스만 존재하도록 보장</li>
<li><strong>활용 예시</strong>: 설정 클래스, 로깅, 캐시 등</li>
<li><strong>관련 개념</strong>: 의존성 주입을 통해 싱글톤 객체를 안전하게 주입 가능</li>
</ul>
<pre><code class="language-java">public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}</code></pre>
<hr>
<h3 id="✅-팩토리-패턴">✅ 팩토리 패턴</h3>
<ul>
<li><strong>목적</strong>: 객체 생성 로직을 별도의 클래스(팩토리)로 분리</li>
<li><strong>활용 예시</strong>: 다양한 형태의 객체 생성이 필요한 경우 (예: 인터페이스 기반 객체 생성)</li>
</ul>
<pre><code class="language-java">interface Product {
    void use();
}

class ConcreteProductA implements Product {
    public void use() {
        System.out.println(&quot;Using Product A&quot;);
    }
}

class ProductFactory {
    public static Product createProduct(String type) {
        if (&quot;A&quot;.equals(type)) return new ConcreteProductA();
        throw new IllegalArgumentException(&quot;Unknown type&quot;);
    }
}</code></pre>
<hr>
<h3 id="✅-전략-패턴">✅ 전략 패턴</h3>
<ul>
<li><strong>목적</strong>: 런타임에 알고리즘을 선택할 수 있도록 전략을 인터페이스로 분리</li>
<li><strong>활용 예시</strong>: 정렬 방식 선택, 결제 수단 선택 등</li>
</ul>
<pre><code class="language-java">interface Strategy {
    void execute();
}

class ConcreteStrategyA implements Strategy {
    public void execute() {
        System.out.println(&quot;Strategy A executed&quot;);
    }
}

class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void performStrategy() {
        strategy.execute();
    }
}</code></pre>
<hr>
<h3 id="✅-옵저버-패턴">✅ 옵저버 패턴</h3>
<ul>
<li><strong>목적</strong>: 한 객체의 상태 변화에 따라 여러 객체가 자동으로 갱신되도록 설계</li>
<li><strong>활용 예시</strong>: 이벤트 리스너, 게시판 알림 시스템 등</li>
</ul>
<pre><code class="language-java">interface Observer {
    void update(String message);
}

class Subject {
    private List&lt;Observer&gt; observers = new ArrayList&lt;&gt;();

    public void addObserver(Observer o) {
        observers.add(o);
    }

    public void notifyObservers(String message) {
        for (Observer o : observers) {
            o.update(message);
        }
    }
}</code></pre>
<hr>
<h3 id="✅-프록시-패턴--프록시-서버">✅ 프록시 패턴 &amp; 프록시 서버</h3>
<ul>
<li><strong>목적</strong>: 실제 객체 대신 대리 객체가 요청을 제어하도록 함</li>
<li><strong>활용 예시</strong>: 접근 제어, 캐싱, 지연 로딩</li>
</ul>
<pre><code class="language-java">interface Service {
    void request();
}

class RealService implements Service {
    public void request() {
        System.out.println(&quot;RealService request executed&quot;);
    }
}

class ProxyService implements Service {
    private RealService realService = new RealService();

    public void request() {
        System.out.println(&quot;Proxy before&quot;);
        realService.request();
        System.out.println(&quot;Proxy after&quot;);
    }
}</code></pre>
<hr>
<h3 id="✅-이터레이터-패턴">✅ 이터레이터 패턴</h3>
<ul>
<li><strong>목적</strong>: 컬렉션 내부 구조를 노출하지 않고 순회할 수 있도록 함</li>
<li><strong>활용 예시</strong>: List, Set 등의 반복자</li>
</ul>
<pre><code class="language-java">List&lt;String&gt; list = Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
Iterator&lt;String&gt; iterator = list.iterator();

while (iterator.hasNext()) {
    System.out.println(iterator.next());
}</code></pre>
<hr>
<h3 id="✅-노출-모듈-패턴-javascript-예시">✅ 노출 모듈 패턴 (JavaScript 예시)</h3>
<ul>
<li><strong>목적</strong>: 필요한 것만 외부에 노출하고 나머지는 은닉하여 캡슐화 강화</li>
<li><strong>활용 예시</strong>: 모듈 패턴 기반 JavaScript 코드 구조 등</li>
</ul>
<pre><code class="language-javascript">const MyModule = (function() {
    const privateVar = &#39;secret&#39;;

    function privateFunc() {
        console.log(privateVar);
    }

    return {
        publicFunc: function() {
            privateFunc();
        }
    };
})();

MyModule.publicFunc();</code></pre>
<hr>
<h2 id="ui-설계-패턴">UI 설계 패턴</h2>
<h3 id="✅-mvc-패턴">✅ MVC 패턴</h3>
<ul>
<li><strong>구성</strong>: Model, View, Controller</li>
<li><strong>역할</strong>: 비즈니스 로직과 UI를 분리하여 유지보수 용이</li>
</ul>
<pre><code class="language-text">User → Controller → Model → View → User</code></pre>
<hr>
<h3 id="✅-mvp-패턴">✅ MVP 패턴</h3>
<ul>
<li><strong>구성</strong>: Model, View, Presenter</li>
<li><strong>특징</strong>: View는 Presenter를 통해서만 Model과 통신</li>
</ul>
<pre><code class="language-text">User → View ↔ Presenter ↔ Model</code></pre>
<hr>
<h3 id="✅-mvvm-패턴">✅ MVVM 패턴</h3>
<ul>
<li><strong>구성</strong>: Model, View, ViewModel</li>
<li><strong>특징</strong>: 양방향 데이터 바인딩으로 View와 ViewModel 간 동기화</li>
</ul>
<pre><code class="language-text">View ↔ ViewModel ↔ Model</code></pre>
<hr>
<h2 id="생각">생각</h2>
<p>디자인 패턴이 <strong>항상 좋은 것은 아닙니다.</strong></p>
<p>코드가 복잡해졌을 때, <strong>문제를 분리하고 구조화하는 과정에서 자연스럽게 디자인 패턴이 등장</strong>할 수 있습니다.
패턴을 억지로 외우고 적용하기보다는, <strong>객체 지향적으로 고민하고 리팩토링을 진행하다 보면</strong> 적절한 패턴 형태로 정리되는 경우가 많습니다.</p>
<p>결국, 디자인 패턴은 <strong>문제를 해결하기 위한 도구이지 목표 자체는 아니라는 점</strong>을 잊지 않아야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[절차 지향 vs 순차 지향 vs 객체 지향]]></title>
            <link>https://velog.io/@shin-jisong/%EC%A0%88%EC%B0%A8-%EC%A7%80%ED%96%A5-vs-%EC%88%9C%EC%B0%A8-%EC%A7%80%ED%96%A5-vs-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5</link>
            <guid>https://velog.io/@shin-jisong/%EC%A0%88%EC%B0%A8-%EC%A7%80%ED%96%A5-vs-%EC%88%9C%EC%B0%A8-%EC%A7%80%ED%96%A5-vs-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5</guid>
            <pubDate>Mon, 23 Jun 2025 06:55:42 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c5391835-47e4-4d2f-af88-29206e55fcf6/image.png" alt=""></p>
<p>안녕하세요
오늘은 <strong>프로그래밍 패러다임</strong>을 얘기해 보려고 합니다
저는 자바 스프링을 주요 기술로 공부하면서 늘 <strong>&quot;객체 지향적&quot;</strong> 설계를 중요하게 생각해 왔습니다</p>
<p>객체가 자신의 책임과 역할을 하도록 설계하며,
늘 해당 역할을 잘 수행하고 있는지
네이밍에 한정된 책임 부여는 아닌지 고민하고 
책임이 잘 분리될 수 있도록 설계하기 위해 노력했어요</p>
<p><strong>하지만 생각해 보면 절차 지향이라고 책임이 없을까요?
함수 단위로 나뉜 건 책임이 아닐까요?</strong></p>
<p>제 안의 개념의 혼동이 있다는 사실을 인지하고
잘 정의하기 위해서 글을 정리해 보고자 합니다</p>
<h2 id="정의">정의</h2>
<ul>
<li><p><strong>순차 지향 (Sequential Programming):</strong> 말 그대로 코드가 <strong>작성된 순서</strong>대로 한 줄씩 순차적으로 실행하는 방식</p>
</li>
<li><p><strong>절차 지향 (Procedural Programming)</strong>: 프로그램을 <strong>함수</strong>나 절차 단위로 나누어 순서대로 실행하는 방식</p>
</li>
<li><p><strong>객체 지향 (Object-Oriented Programming, OOP)</strong>: 데이터와 그 데이터를 처리하는 함수를 하나의 객체로 묶어 관리하는 방식, <strong>책임과 역할을 가진 객체 단위</strong></p>
</li>
</ul>
<h2 id="의문점">의문점</h2>
<p>위 정의를 토대로 이렇게 생각해 볼 수 있어요
데이터를 기준으로 책임과 역할을 분리하는 것이 객체 지향입니다
거시적으로 볼 때 함수의 흐름에서 해당 함수를 분리하였을 때, 그것이 역할도 명확이 분리된다면 절차 지향이며 객체 지향으로 볼 수 있을까요?</p>
<blockquote>
<p>객체 지향은 역할과 책임 단위로 객체를 분리하는 것이잖아 만약 어떠한 절차에서 함수를 분리하였을 때 거시적인 관점에서 책임이 분리되었다면 그것이 절차 지향이며 객체 지향으로 볼 수 있는 거야?</p>
</blockquote>
<p>여기서 집중해야 할 점은</p>
<p>객체 지향이 <strong>데이터(상태)와 행동(메서드)을 하나의 단위(객체)로 캡슐화</strong> 한다는 점입니다
단순히 함수로 책임을 분리해도 캡슐화가 되어 있지 않으면 이것은 객체 지향이 아니라는 점이에요</p>
<p>즉, 함수로 인해 책임과 역할이 분리될 수는 있으나 객체가 단순히 책임과 역할뿐만 아니라 <strong>&quot;자신의 상태를 책임지고, 외부에서 직접 상태를 조작하지 않고 메서드를 통해서만 상태를 변경&quot;</strong> 특성까지 고려한다면 단순 책임 분리일 뿐 객체 지향으로 보기는 어렵습니다</p>
<h2 id="생각">생각</h2>
<p><strong>하지만 늘 객체 지향이 좋은 것만은 아닙니다</strong></p>
<p>객체 지향은 설계와 구조가 복잡해질 수 있고, 적절한 추상화와 캡슐화를 위해 많은 고민과 시간이 필요합니다
특히 작은 규모의 프로젝트나 단순한 문제에서는 절차 지향이 더 직관적이고 빠르게 개발할 수 있는 장점이 있습니다
또한, 잘못 설계된 객체 지향 구조는 오히려 유지보수와 확장성을 어렵게 만들 수도 있습니다
따라서 상황과 요구사항에 맞게 적절한 프로그래밍 패러다임을 선택하는 것이 중요합니다</p>
<p>협업을 해 보면 객체 지향이 협업에서 꽤 유용하게 작동되더라~ 정도의 판단일 뿐,
특정 상황과 환경에서 100%로 잘 맞다! 는 절대 아니라고 생각합니다</p>
<p>수많은 경험을 통해서 협업 환경에서 어느정도로 잘 동작하기에 선택한 패러다임이며,
특정 부분에서 객체 지향이 코드 퀄리티를 저해할 경우에는 다른 패러다임을 선택해서 작성할 수 있다고 생각합니다</p>
<p>따라서 개발자는 이러한 패러다임을 잘 인지하고 코드를 작성하는 것이 중요하다고 생각합니다 👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[조회 API에 필터링과 검색이 들어온다면?]]></title>
            <link>https://velog.io/@shin-jisong/%EC%A1%B0%ED%9A%8C-API%EC%97%90-%ED%95%84%ED%84%B0%EB%A7%81%EA%B3%BC-%EA%B2%80%EC%83%89%EC%9D%B4-%EB%93%A4%EC%96%B4%EC%98%A8%EB%8B%A4%EB%A9%B4</link>
            <guid>https://velog.io/@shin-jisong/%EC%A1%B0%ED%9A%8C-API%EC%97%90-%ED%95%84%ED%84%B0%EB%A7%81%EA%B3%BC-%EA%B2%80%EC%83%89%EC%9D%B4-%EB%93%A4%EC%96%B4%EC%98%A8%EB%8B%A4%EB%A9%B4</guid>
            <pubDate>Sat, 10 May 2025 15:26:14 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 방끗의 새로운 기능을 개발하면서 API 설계에 논의한 / 논의할 내용을 글로 정리해 보려고 합니다 👍</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c1b9a7b8-6b52-476e-a083-c4acb90f3868/image.png" alt=""></p>
<p><del>저희 방끗의 디자이너 분께서 열일해 주셨습니다</del></p>
<h2 id="api를-설계해-보자">API를 설계해 보자</h2>
<p>저희가 대대적인 새로운 기능 도입을 앞두고 추가 기능이 많아졌다는 소식은 앞선 게시물을 통해 전달해 드렸는데요</p>
<p>리스트 조회를 하며 유저의 경험 편리성을 향상시키기 위해 다양한 필터링과 검색 조건을 추가하게 되었습니다</p>
<p><strong>필터링 조건들</strong>: 현재 위치 기준, 지하철 기준, 오늘 올라온 건물
<strong>검색 조건들</strong>: 건물명, 위치, 지하철</p>
<p>현재 기능에서는 위와 같은 필터링과 검색이 필요합니다
저는 API를 설계하면서 고민되는 지점이 있었어요</p>
<p><strong>과연 어디까지를 API로 따로 분리해야 될까?</strong></p>
<p>해당 API들은 모두 특정 조건에 따라 건물 리스트 조회를 응답값으로 주게 되고,
모두 응답 포맷은 동일한 API들입니다</p>
<p>파라미터, 혹은 본문을 이용해서 조건을 전달한다면 충분히 하나의 API로 구현이 가능할 것입니다</p>
<p>하지만 그것이 설계적으로도 괜찮을까요? 개발자들이 소통하기에 괜찮은 포맷일까요?</p>
<h2 id="개념적으로-나누어-보자">개념적으로 나누어 보자</h2>
<p>위 조건들을 크게 세 가지로 나누어 볼 수 있을 것 같습니다</p>
<p><strong>1. 오늘 올라온 건물 필터링</strong>
홈 화면에서 오늘 올라온 건물을 보여 주는 API입니다
유저의 직접적인 입력 없이, 요청마다 파라미터가 달라지지 않는 API예요</p>
<p><strong>2. 현재 위치 기준 필터링</strong>
유저의 입력이 아닌 지도에서 위치를 옮길 때마다 특정 반경에 해당하는 건물을 응답값으로 돌려주는 API입니다
유저의 직접적인 입력 없이, 요청마다 파라미터가 달라질 수 있는 API예요</p>
<p><strong>3. 건물명 기준 검색, 위치 기준 검색, 지하철 기준 검색</strong>
_
지하철 기준 필터링과 지하철 기준 검색은 동일한 역할을 하기 때문에 합쳐서 보겠습니다
또한 현재 단계에서는 무엇을 기준으로 검색했는지 서버에서 판단하는 것이 아닌, 유저가 선택해서 검색했다고 가정하겠습니다_</p>
<p>유저가 직접 필터링을 하거나 검색을 할 때 해당하는 건물들을 응답값을 돌려주는 API입니다
유저의 직접적인 입력이 있고, 요청마다 파라미터가 달라질 수 있는 API예요</p>
<h2 id="어떻게-설계해-볼까">어떻게 설계해 볼까?</h2>
<p>먼저 <strong>1번의 경우에는 소통의 직관성과 API의 재활용성을 생각해서 따로 API를 설계</strong>해도 된다고 생각합니다!</p>
<p>&quot;오늘 올라온 건물&quot;이라는 명확한 목적을 가진 API는, 이를 전담하는 엔드포인트를 분리함으로써 클라이언트와 서버 간의 소통이 더 명확해집니다
뿐만 아니라, 명확한 역할을 가진 API는 향후 로직 변경, 캐싱, 로깅 등 유지보수 측면에서도 관리가 수월할 거라고 생각했습니다!</p>
<h2 id="그렇다면-2번과-3번은-어떻게-설계하는-게-좋을까요">그렇다면 2번과 3번은 어떻게 설계하는 게 좋을까요?</h2>
<p><strong>✅ 함께 설계 : 하나의 API + 파라미터 분기 처리</strong></p>
<ul>
<li><p><strong>장점</strong>
클라이언트 입장에서 통일된 방식으로 호출 가능 (/buildings 하나로 통합)
공통 로직(페이징, 정렬 등)을 재사용하기 쉬움
필터 기준이 점점 늘어날 때 구조적으로 유연</p>
</li>
<li><p><strong>단점</strong>
파라미터 조합이 복잡해질 수 있음 (e.g., type=keyword, subwayId, lat/lng, isToday=true 등)
API 문서 가독성 저하 → 각 케이스별 설명이 필요
필터별 로직이 복잡해지면 코드 유지보수가 어려워질 수 있음</p>
</li>
</ul>
<p><strong>✅ 따로 설계 : 기능별로 API를 나누는 방식 (엔드포인트 분리)</strong></p>
<ul>
<li><p><strong>장점</strong>
명확한 역할 분리 → 유지보수 및 API 문서화에 유리
각 API에 맞는 최적화된 처리 가능 (e.g., 캐싱, 쿼리 튜닝)
추후 각 기능을 독립적으로 개선하거나 제거하기 쉬움</p>
</li>
<li><p><strong>단점</strong>
클라이언트는 어떤 API를 호출해야 할지 로직을 더 알아야 함
API 수가 늘어나면서 라우팅 구조가 복잡해질 수 있음</p>
</li>
</ul>
<p><strong>저는 2번(현재 위치 기준)과 3번(검색 기반)은 API를 분리하는 것이 좋다고 생각했어요!</strong></p>
<p>먼저, 위에서 나뉘었다시피 유저의 명시적인 입력 유무와 API의 목적이 뚜렷하게 다릅니다
또한, 2번과 3번의 서버 내부 구현이 아주 다르기 때문에, 파라미터에 따라 완전히 다른 로직을 수행하게 됩니다
UI 구성도 일반적으로 다르며, UX 흐름상 분리된 기능처럼 인식되기 때문에 API를 동일하게 둔다면 개발자 인식상의 혼동이 발생할 수도 있습니다</p>
<h2 id="마무리">마무리</h2>
<p>저는 위와 같이 생각하고 선택을 하였습니다
제가 고려하지 못한 부분이 있는지, 또 다른 시야의 개발자 분들이 계시는지 궁금해요!☺️
글 읽어 주셔서 감사합니다 🙇‍♂️</p>
<hr>
<h2 id="논의-결과">논의 결과</h2>
<p>방끗 팀 내에서 활발한 논의를 해 본 결과, 그리고 제 사고가 바뀌게 되어 
<strong>2번과 3번을 합치게 될 것 같아요</strong></p>
<p>사유는 다음과 같습니다!</p>
<ul>
<li>*<em>2번과 3번의 서버 내부 구현이 아주 다르기 때문에 *</em>
해당 부분을 소통의 측면에서 표시할 필요는 없다고 생각이 바뀌었습니다
백엔드의 구현을 API 상으로 드러낼 필요가 있을까요?
API 상으로 드러나는 부분은 &quot;위치 기준으로 근처 빌딩을 조회&quot;라고 생각이 들어요!</li>
</ul>
<p>저희가 논의할 때 통일된 사안은
필터링, 검색, 정렬을 모두 리스트 조회 API에서 파라미터로 관리하고,
자주 조회되고 따로 사용될 API는 분리하는 것이었어요</p>
<p>이때 위치 기준으로 반경 n 안의 범위의 빌딩을 조회하는 것은 어느 범주로 볼까? 라고 질문을 던져봤을 때 필터링의 범주로 보고 진행하는 것이 흐름상 자연스럽게 느껴졌고,
FE 개발자가 API 문서를 참고할 때 당연히 필터링 파라미터를 보게 되지 않을까? 라는 생각이 들었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/9ac36aee-9d65-45e3-993e-3edfbdc5813f/image.png" alt=""></p>
<p>따라서 이렇게 문서를 정리해 보았습니다~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 변경에 따라 대대적인 코드 공사, 괜찮을까? (기존에 예측한 설계와는 달랐던 기획...)]]></title>
            <link>https://velog.io/@shin-jisong/DB-%EB%B3%80%EA%B2%BD%EC%97%90-%EB%94%B0%EB%9D%BC-%EB%8C%80%EB%8C%80%EC%A0%81%EC%9D%B8-%EC%BD%94%EB%93%9C-%EA%B3%B5%EC%82%AC-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C-%EA%B8%B0%EC%A1%B4%EC%97%90-%EC%98%88%EC%B8%A1%ED%95%9C-%EC%84%A4%EA%B3%84%EC%99%80%EB%8A%94-%EB%8B%AC%EB%9E%90%EB%8D%98-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@shin-jisong/DB-%EB%B3%80%EA%B2%BD%EC%97%90-%EB%94%B0%EB%9D%BC-%EB%8C%80%EB%8C%80%EC%A0%81%EC%9D%B8-%EC%BD%94%EB%93%9C-%EA%B3%B5%EC%82%AC-%EA%B4%9C%EC%B0%AE%EC%9D%84%EA%B9%8C-%EA%B8%B0%EC%A1%B4%EC%97%90-%EC%98%88%EC%B8%A1%ED%95%9C-%EC%84%A4%EA%B3%84%EC%99%80%EB%8A%94-%EB%8B%AC%EB%9E%90%EB%8D%98-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Sun, 04 May 2025 12:32:14 GMT</pubDate>
            <description><![CDATA[<p>오늘은 설계적인 관점에서 얘기해 보고자 합니다!
방끗을 현재 운영하면서 새로운 기능을 하나 도입하기로 했어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/00471bbc-ccdb-4100-98df-9b656a7b6e0c/image.png" alt=""></p>
<p>현재 방끗에서는 &quot;내&quot;가 둘러본 방만 조회할 수 있어요
다른 유저의 체크리스트는 조회할 수 없도록 되어 있습니다</p>
<p>하지만 이러한 체크리스트를 홈에서 둘러볼 수 있는 기능을 도입하며,
고민이 생겼어요</p>
<h2 id="고민-포인트">고민 포인트</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/57227c36-e456-46c1-b448-d80e6970617b/image.png" alt=""></p>
<p>기존에는 위와 같이 체크리스트 각각을 조회하고 있지만 사용자끼리 둘러볼 수 있는 기능을 제공하겠다 결심한 이상 <strong>어느 정도의 기준으로 묶어 두고 그에 해당하는 체크리스트는 모아 두어 제공</strong>하고자 해요</p>
<p>저희는 그러한 기준을 <strong>&quot;건물 단위&quot;</strong>로 잡았습니다</p>
<p>즉, 상세 주소가 아닌 건물 주소까지가 일치하는 경우는 같은 원룸 건물이니 
체크리스트를 통합하여 제공해 주면 방문하는 자취생 분들이 조금 더 다양한 정보를 얻을 수 있을 예정이에요</p>
<p>하지만 이를 구현하기 위해서는 현재 변경해야 되는 부분이 존재합니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/ef9c298c-9065-4f45-a89e-8f26044f2911/image.png" alt=""></p>
<p>기존에는 위와 같은 ERD 구조를 설계했어요
설계 당시에는 유저들끼리 체크리스트를 둘러볼 수 없는 구조였지만, 충분히 들어올 만한 기획이라 생각해서 &quot;부동산&quot;의 &quot;불변&quot;한 정보를 모두 room 테이블에 두고 1:1로 맵핑하여 설계하였어요 반면에 유저 별로 다를 수 있는 정보는 checklist 테이블에 두는 것으로 설계하였습니다</p>
<ol>
<li>해당 정보를 모두 합치기에는 칼럼의 수가 너무 많았고</li>
<li>room이 가지는 정보의 특성과 checklist가 가지는 정보의 특성은 매우 다르기 때문이에요</li>
</ol>
<p>이후, checklist 테이블과 1:N으로 매치해서 해당 기획을 만족할 수 있도록 구현할 생각이었습니다</p>
<p>하지만 실제 기획을 하게 되면서 room의 정보 중 일부만을 활용해서 구현하게 될 예정이 되었어요</p>
<p>*<em>이럴 경우에는 어떻게 해야 될까요? *</em></p>
<h2 id="대안을-고민해-보자">대안을 고민해 보자</h2>
<p>저희는 논의 끝에 두 가지 방안을 생각해 보았어요</p>
<ol>
<li>room에 필요한 정보만을 남기고 불필요한 1:1 구조를 해체한다</li>
<li>상위의 house 테이블을 도입한다</li>
</ol>
<p>해당 부분의 장단점과 상세 설명은 아래에 기재해 보겠습니다</p>
<h3 id="1-room에-필요한-정보만을-남기고-불필요한-11-구조를-해체한다">1. room에 필요한 정보만을 남기고 불필요한 1:1 구조를 해체한다</h3>
<p>사실 1:1 구조는 해당 정보가 나뉘어서 조회될 경우가 많을 때 유의미하다 생각합니다
하지만 이 경우에는 1:1 정보가 불필요해졌기 때문에 해당 구조를 해체하는 것이 좋다 판단하였습니다</p>
<p><strong>즉 1:N 구조로 변경을 하며 DB 구조와 코드를 바꾸는 것입니다</strong></p>
<p>해당 부분으로 진행하게 되면 코드는 깔끔해질 것이라 생각합니다
의도한 대로의 설계가 될 예정이에요</p>
<p>반면에, 이것을 구현하기 위해 지금까지의 코드를 많이 바꿔야 하며 DB 구조까지 변경해야 되기 때문에
현재 운영 중인 서비스에서 최소한의 운영 중단 시간으로 도입할 수 있는 구조는 아닙니다</p>
<p><strong>즉, 유저가 있는 환경에서는 도입하기 힘든 전략일 거라 생각합니다</strong></p>
<h3 id="2-상위의-house-테이블을-도입한다">2. 상위의 house 테이블을 도입한다</h3>
<p>그렇게 해서 나온 전략은 다음과 같아요
필요한 테이블을 추가적으로 도입하는 전략입니다</p>
<p>해당 전략으로 가게 되면 house와 room은 1:N 구조가 될 것이고
room과 checklist는 1:1 구조로 유지하게 됩니다</p>
<p>즉, 새로운 테이블이 추가되고 기존의 테이블 거의 혹은 최소한의 변경만 있기 때문에
운영 중단 시간이 전의 전략에 비해서는 현저히 짧을 것이라 예상됩니다</p>
<p>하지만 기존의 불필요한 1:1 구조는 그대로 유지됩니다</p>
<p>또한, <strong>주소라는 정보가 중복으로 저장되는 문제가 있습니다</strong></p>
<h2 id="무엇을-선택할-것인가">무엇을 선택할 것인가?</h2>
<p>현재로서는 중복 저장되는 칼럼의 수가 수용 가능한 범위이며,
유저가 있는 환경에서 도입 가능한 전략을 수립해야 되기 때문에 <strong>2번의 전략으로 갈 것 같습니다</strong></p>
<p>하지만 어떤 방안이 현업에서 사용되는지 궁금한 생각이 들기도 하고,
더 나은 전략이 있을까 궁금해서 글을 올려 봅니다!</p>
<p>유의미한 토론이 이어지는 글이 되길 바라며 .. 🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우아한테크코스 6기를 수료하며]]></title>
            <link>https://velog.io/@shin-jisong/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-6%EA%B8%B0%EB%A5%BC-%EC%88%98%EB%A3%8C%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@shin-jisong/%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC%EC%BD%94%EC%8A%A4-6%EA%B8%B0%EB%A5%BC-%EC%88%98%EB%A3%8C%ED%95%98%EB%A9%B0</guid>
            <pubDate>Tue, 01 Apr 2025 04:01:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/shin-jisong/post/23c5ff2f-356b-4ee9-a3d6-a82771af5e1c/image.png" alt=""></p>
<p>안녕하세요! 어느덧 수료한 지 한 달 하고도 더 지났네요
(글을 쓸 당시엔 한 달이었으나 마무리가 밀리고 밀려 5달이 되었답니다..........)</p>
<p>우테코에서 함께 한 시간을 떠나보내기가 영 아쉽게 느껴져서 계속 미루다 보니 해가 바뀌었습니다
11월 29일에 수료한 뒤 저는 호주 여행도 다녀오고, 새해도 맞이하고, 크루들도 만나고, 지속해서 방끗 작업을 하며 지냈어요</p>
<p>우아한테크코스에 관해서 하고 싶은 말이 너무 많고, 배운 것도 엄청나게 많은데
이것을 다 전달할 수 있을지가 걱정이에요
차근차근 작성해 보며 아쉬운 부분은 보충해 보겠습니다 👊</p>
<h2 id="우테코에서-배운-것">우테코에서 배운 것</h2>
<p>저는 우테코를 하는 기간 동안 줄곧 엄청나게 성장한 느낌이에요
개발적으로, 인간적으로도 많이 성장할 수 있었던 한해가 아닐까 싶습니다
개발 유토피아에서의 시간을 보낼 수 있어서 아주 좋았어요 👍
<br></p>
<p>우테코에서 가장 크게 배운 것 중 하나는 <strong>개발을 대하는 태도</strong>가 아닐까 싶어요
우테코 이전을 생각해 보면 전 작동하는 코드에 중점을 두어 유지 보수나 테스트에는 그리 큰 힘을 쏟지 않았어요
따라서 지금 보면 굉장히 스파게티인 코드를 보며 뿌듯해하고는 했습니다
하지만 우테코에서 클린 코드, 클린 아키텍처, 역할과 책임을 많이 공부하고 토론하며 <strong>코드에 관한 저만의 가치관</strong>이 생겼어요
<br></p>
<p>또 같은 맥락으로 <strong>학습을 대하는 태도</strong> 또한 배울 수 있었습니다
사실 저는 지금껏 새로운 기술을 익혀야 한다! 하면 무조건 책 하나 사고, 강의 하나 사고 처음부터 끝까지 독파하다 의욕 상실되는 유형이었어요
특히나! 공식 문서는 보기만 해도 알러지가 올라오고....</p>
<p>그래서 돌이켜 보면 내가 도대체 무얼 공부한 거지? 남는 게 있는가? 자괴감이 들기도 했어요
하지만 여기서 크루들과 치열하게 부딪치고 공부하고 탐구하면서 <strong>어떻게 주어진 공부를 해쳐나갈 수 있는지, 어느 정도의 깊이까지 내가 궁금해하는지</strong> 알아가는 시간이었어요
그래서 이제는 새로운 기술을 익히는 것이 두렵지 않아요
Java Spring 개발자가 아닌 개발자로 살아가자 🥳</p>
<p>그리고 <strong>저에 대해 더 잘 탐구하고 생각해 볼 수 있는 시간</strong>이었어요
소프트 스킬 시간에 많이 강조된 것 중 하나인 <strong>학습 마인드셋</strong> (땡스 투 워니, 왼손, 리사 코치 ❣️) 개념을 알고 내가 성과에 많이 메여있는 사람이구나 알게 되고 그것을 탈피하려고 많이 노력했어요
<del>사실 아주 탈피하지는 못했어요 성과... 중요하잖아요?</del></p>
<p>하지만 해당 과정에서 진행한 <strong>유연성 강화 스터디</strong>... 정말 좋았습니다
크루들과 가장 진솔한 시간을 나눌 수 있는 시간이었어요
우테코를 하다 보면 주위에 대단한 크루들이 정말 많고 그래서 많이들 흔들리고 불안한 시간을 가지게 되더라고요
실제로 포비와의 수다 타임 단골 질문이 <code>저만 제대로 못 해내고 있는 것 같아요</code>입니다
저도 해당 감정을 많이 느껴서 늘 이겨내려고 노력했어요
이런 과정에서 하는 유강스 타임은 각자의 고민을 내뱉고 공유하며 어느정도 해소되는 느낌이라 좋아한 시간 중 하나예요!</p>
<p>개인적으로도 올해는 첫 자취, 그리고 우테코 진행이라 큰 도전이었는데요
혼자 살며 나의 생활을 컨트롤하고 사람들과 지내며 많은 감정을 느꼈어요
이런 감정들을 잘 핸들링하고 싶어서 매일 일기를 썼는데요
저를 잘 아는 데 큰 도움이 되었다고 생각해요 추천합니다
<br></p>
<p><strong>아쉬운 점이 있다면</strong> 제가 마인트 컨트롤과 시간 관리상의 이유로 스터디를 많이 하지는 못했어요
가벼운 스터디만 한두 개를 병행하였는데요
우테코에서의 공부만으로 저에게 벅차게 느껴져서 진행하지는 못했지만 잘 진행하는 크루들을 보며 아쉬웠습니다</p>
<p>그리고... 소소한 토로는 제가 칼퇴파 중 하나였는데 하루 종일 같은 공간에서 집중하기 힘들어 저녁 시간에는 집이나 집 근처 카페에서 코딩을 하고는 했어요
그렇다 보니 크루들 간 나누는 양질의 개발 대화, 스터디 결성 등을 약간씩 놓치기는 했다는 점</p>
<p>그렇지만! 그 덕분에 제가 해당 기간동안 아주 크게 번아웃이 오지 않고 꾸준히 할 수 있었다고 생각해서 만족합니다 
<del>쓰다 보니 구구절절 길어졌네요 이만 마무리하고 다음으로 갑니다</del></p>
<h2 id="앞으로의-계획">앞으로의 계획</h2>
<p>이건 올해 다짐 겸으로 적어 봅니다
많은 크루들이 이미 졸업을 한 상태여서 취업 시장으로 직행했어요
현재 친한 크루들이 많이들 취업을 해서 부럽기도 하고 대단하기도 합니다 🎉</p>
<p>하지만 저는 아직 졸업을 하지 못해서 학교로 돌아가요....
지금 취업을 하지 못하는 게 전 여러모로 아쉽게 느껴지더라고요
크루들이랑 같이 취업 준비를 하며 으쌰으쌰하고 싶었기도 하고,
우테코에서 열심히 갈고 닦은 것들을 잃기 전에 취업을 하고 싶기도 했어요
그렇지만 이렇게 된 이상 저의 길을 가 봅니다</p>
<p>올해 상반기에는 복학을 해서 학교 생활을 무사히 해내고, 하반기에는 취업 시장에 뛰어들 예정입니다
코스모스 졸업이 되는 게 마음에 걸려서 상반기에 인턴을 하고, 하반기에 졸업과 취업 준비를 병행하고 싶기도 하지만 요즘 취업 시장이 어려워서 인턴 자리가 거의 없더라고요 🥲</p>
<p>계속해서 방끗 기능 개발과 유지 보수를 해나가고, 블로그 글도 열심히 쓰고, 알고리즘 공부도 하고, 학습들도 해 볼 생각이에요
해야 할 공부가 정말 많아서 무슨 공부부터 해야 할까? 싶네요</p>
<p><strong>올해 꼭 취업 뽀개자 👊</strong></p>
<p>글은 이만 마무리하고 하단에는 활동 정리하고 이만 마무리합니다!</p>
<h2 id="활동-정리">활동 정리</h2>
<h3 id="🔗-woowacourse-archive"><a href="https://github.com/shin-jisong/woowacourse-archive">🔗 woowacourse-archive </a></h3>
<p>실은 이 레포지토리를 정리하는 게 한참 걸렸어요 <del>(나는 미룬이)</del>
감정 회고에 가까웠던 블로그 글 끗</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[S3 이미지 저장 어떤 전략으로 구상할까?]]></title>
            <link>https://velog.io/@shin-jisong/S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5-%EC%96%B4%EB%96%A4-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%EA%B5%AC%EC%83%81%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@shin-jisong/S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5-%EC%96%B4%EB%96%A4-%EC%A0%84%EB%9E%B5%EC%9C%BC%EB%A1%9C-%EA%B5%AC%EC%83%81%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sun, 09 Mar 2025 11:14:52 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
저번 게시물에 이어서 방끗 기능 개발을 위해 고민을 해 보는 게시물입니다
블로그 글의 초반부는 지난 게시물을 갖고 오는 걸로... 하겠습니다 ☺️</p>
<h2 id="새로운-기능을-추가해-보자">새로운 기능을 추가해 보자</h2>
<p>저희 방끗 팀은 11월 수료 이후 간단한 유지 보수를 제외하고는 추가적인 기능 개발을 진행하지 않았어요 홍보와 유저 유치에 집중을 하던 시기였습니다 🎉
그러나! 여러 논의 끝에 이제 새로운 기능을 추가해 보기로 하여 가장 먼저 개발할 두 가지 기능을 픽스하였어요</p>
<p>그 기능 중 두 번째가 체크리스트에서 이미지가 추가 가능하도록 하는 부분이에요</p>
<p>두 가지 기능 중 어느 기능을, 누가 담당할지에 대한 논의가 이루어지기 전에 먼저 설계를 해 보고 싶어 위 글을 작성하게 되었어요 👍</p>
<h2 id="기존의-방끗에서-달라지는-점은">기존의 방끗에서 달라지는 점은?</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/ed2b088d-4290-4303-b3ee-8693e0610f74/image.png" alt=""></p>
<p>기존에는 위와 같이 유저가 이미지를 업로드할 수는 없었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/767447cb-6434-417a-ad81-46be042586e6/image.png" alt=""></p>
<p>저희는 4차례 이상의 데모데이로 직접 유저에게 피드백을 듣는 시간을 가졌고, 이후에도 꾸준히 VoC를 통해 유저의 이야기를 들어왔어요
그 결과 가장 많이 나온 의견 중 하나가 <strong>&quot;집의 이미지를 업로드 할 수 있게 해 주세요&quot;</strong> 였답니다</p>
<p>해당 피드백은 정말 처음부터 꾸준히 나온 의견인데요,
방끗 팀 논의 당시에는 정해진 시간 안에 배포를 해야 했기 때문에 이미지를 업로드한다는 다소 시간이 많이 소요되는 기능은 잠시 미루어두었습니다
하지만 새로운 기능을 추가하기로 한 이상! 해당 부분에 도전해 보려고 합니다</p>
<h2 id="이미지를-업로드하려면-어떻게-해야-할까">이미지를 업로드하려면 어떻게 해야 할까?</h2>
<p>저는 이미지 업로드 기능을 구현해 본 적이 없기 때문에
어떤 플로우로 동작하는지 간단히 알아 보아야 했어요</p>
<h3 id="플로우">플로우</h3>
<p>1) AWS S3 버킷을 생성한다
2) 이미지 업로드 API를 설계한다
이때 서버 업로드 방식과 클라이언트 업로드 방식을 선택해야 한다
3) S3에 업로드된 URL을 DB에 저장한다</p>
<h3 id="고려해야-할-점">고려해야 할 점</h3>
<ul>
<li>이미지 압축 및 리사이징</li>
<li>사용하지 않는 파일 삭제</li>
</ul>
<p>등이 있다고 하여 하나씩 고민해 보겠습니다</p>
<h2 id="s3-비용-산정">S3 비용 산정</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/3a58ef48-c902-417c-93a5-e78d2de00dc1/image.png" alt=""></p>
<p>저희는 현재 프리 티어 계정을 사용하고 있고 5GB, GET 요청 2만건, 2000건의 타 요청 내에서는 무료로 사용 가능하기 때문에 보통은 무료 금액 내에서 처리가 가능할 것이라 판단했어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/fabaa144-ed05-45b8-a29e-75bd1bf64514/image.png" alt=""></p>
<p>혹시나 해당 요청을 넘어서더라도 금액이 크지 않아서 저희 팀 내에서 부담할 수 있는 금액이었습니다!</p>
<h2 id="어디에서-업로드할까">어디에서 업로드할까?</h2>
<p>이미지를 저장할 때는 클라이언트에서 서버를 거쳐 S3에서 업로드하는 방식,
혹은 클라이언트에서 직접 S3로 업로드하는 방식이 있습니다
각각의 장단점을 살펴볼게요!</p>
<h3 id="클라이언트-→-서버-→-s3-서버를-거쳐-업로드">클라이언트 → 서버 → S3 (서버를 거쳐 업로드)</h3>
<p>✅** 장점**</p>
<ul>
<li><p><strong>보안 관리가 용이</strong>: 서버에서 업로드 요청을 검증할 수 있어, 악성 파일이나 인증되지 않은 요청을 차단하기 좋음</p>
</li>
<li><p><strong>일관된 권한 제어</strong>: 클라이언트가 직접 S3에 접근하지 않으므로, S3의 버킷 정책을 복잡하게 설정할 필요 없음</p>
</li>
<li><p><strong>서버에서 추가적인 처리 가능</strong>: 이미지 리사이징, 워터마킹, 메타데이터 추가 등의 작업을 서버에서 처리 가능</p>
</li>
<li><p><strong>로그 관리 및 감사 가능</strong>: 업로드 과정이 서버를 거치므로 로그를 남겨 추적 및 모니터링이 쉬움</p>
</li>
</ul>
<p>❌ <strong>단점</strong></p>
<ul>
<li><p><strong>서버 부하 증가</strong>: 대량의 파일 업로드가 발생하면 서버의 네트워크 및 CPU 부하가 커질 수 있음</p>
</li>
<li><p><strong>비용 증가</strong>: 클라이언트와 서버 간의 트래픽 + 서버와 S3 간의 트래픽이 발생하여 네트워크 비용 증가</p>
</li>
<li><p><strong>지연 시간 증가</strong>: 서버를 한 번 거치기 때문에 업로드 속도가 느려질 수 있음</p>
</li>
</ul>
<h3 id="클라이언트-→-s3-직접-업로드-presigned-url-활용">클라이언트 → S3 직접 업로드 (Presigned URL 활용)</h3>
<p>✅ <strong>장점</strong></p>
<ul>
<li><p><strong>서버 부하 감소</strong>: 서버가 파일을 직접 처리하지 않으므로, 트래픽과 리소스 사용이 줄어듦</p>
</li>
<li><p><strong>빠른 업로드 속도</strong>: 클라이언트가 직접 S3로 올리므로 네트워크 홉이 줄어들어 성능이 개선됨</p>
</li>
<li><p><strong>비용 절감</strong>: 서버를 거치지 않으므로 네트워크 비용을 절감할 수 있음</p>
</li>
</ul>
<p>❌ <strong>단점</strong></p>
<ul>
<li><p><strong>보안 관리 필요</strong>: 클라이언트가 S3에 접근할 수 있어야 하므로, Presigned URL을 안전하게 관리해야 함</p>
</li>
<li><p><strong>권한 관리 복잡</strong>: Presigned URL의 유효 기간과 버킷 정책을 신경 써야 함</p>
</li>
<li><p><strong>추가적인 후처리 어려움</strong>: 서버에서 업로드를 통제하지 않으므로, 업로드된 파일을 가공하려면 별도 Lambda 같은 후처리 과정이 필요할 수 있음</p>
</li>
<li><p><strong>클라이언트 복잡성 증가</strong>: Presigned URL을 받아서 파일을 업로드하는 로직을 구현해야 하므로 클라이언트 개발이 복잡해질 수 있음</p>
</li>
</ul>
<h3 id="결론">결론</h3>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/3054b357-1f14-4ff9-b5a1-7b28f0a2435f/image.png" alt=""></p>
<p>사실 결론을 내기 어려워 Velog는 어떻게 하는지 직접 파일을 붙여넣고 요청을 살펴 보았습니다
위와 같이 요청이 서버로 날아가는 모습입니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/b761cf52-2c4c-4613-9bf2-f44df072eed7/image.png" alt=""></p>
<p>깃허브도 마찬가지로 서버에 요청이 날아가는 모습입니다</p>
<p>해당 개발자들은 왜 저런 선택을 하였을까? 나는 어떤 선택을 해야 할까?
고민 끝에 저는 <strong>서버 업로드 방식을 택하기</strong>로 하였어요!
사유는 아래와 같습니다</p>
<h4 id="보안-문제">보안 문제</h4>
<p>클라이언트에서 Presigned URL 방식으로 직접 S3에 접근한다면,
서버에 해당 URL 발급 요청을 보내고 응답을 받아야 합니다
그 과정에서 해당 URL이 탈취될 여지가 있습니다</p>
<h4 id="리사이징-등-변환">리사이징 등 변환</h4>
<p>클라이언트에서 리사이징 등을 처리하려면 AWS Lambda를 사용해야 합니다
하지만 해당 기능은 사용한 만큼 비용을 지불해야 하기 때문에 저희의 예산상 사용하기 어렵습니다</p>
<p>(추가) 리사이징 등 변환을 프론트에서 주면 해당 문제는 사라지는 것 아닐까요?</p>
<ul>
<li>개발자 도구 등을 통해 직접적으로 보내는 악용 가능성, 프론트엔드 성능 과부하, 파일 변환 불가 등의 이유로 서버에서 처리하는 것이 안전할 것 같아요!</li>
</ul>
<h2 id="어떤-업로드-방식을-사용해야-할까">어떤 업로드 방식을 사용해야 할까?</h2>
<p><a href="https://techblog.woowahan.com/11392/">우아한 기술 블로그</a>를 참고했어요!</p>
<ul>
<li>Stream 업로드</li>
<li>MultipartFile 업로드</li>
<li>AWS Multipart 업로드</li>
</ul>
<table>
<thead>
<tr>
<th>방식</th>
<th>메모리 사용량</th>
<th>네트워크 효율</th>
<th>속도</th>
<th>대용량 파일 지원</th>
<th>구현 난이도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stream 업로드</strong></td>
<td>낮음</td>
<td>높음</td>
<td>보통</td>
<td>비효율적</td>
<td>쉬움</td>
</tr>
<tr>
<td><strong>MultipartFile 업로드</strong></td>
<td>높음</td>
<td>보통</td>
<td>보통</td>
<td>비효율적</td>
<td>매우 쉬움</td>
</tr>
<tr>
<td><strong>AWS Multipart 업로드</strong></td>
<td>낮음</td>
<td>높음</td>
<td>빠름</td>
<td>매우 효율적</td>
<td>어려움</td>
</tr>
</tbody></table>
<p>해당 세 가지 방안 중 저는 전처리를 해야 하고, 대용량은 아니기 때문에 <strong>MultipartFile 업로드 방식</strong>을 택하기로 하였어요</p>
<h2 id="이미지-최적화">이미지 최적화</h2>
<p>만약 사용자가 너무 고화질의, 용량이 큰 이미지를 올릴 경우에는 성능상 문제가 발생합니다
따라서 서버 측에서 리사이징 등의 최적화를 진행해 주어야 해요</p>
<p>아래로 가능한 이미지 최적화 방안을 고민해 봤어요</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>주요 특징</th>
<th>장점</th>
<th>단점</th>
<th>추천 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td><strong>썸네일 생성 및 리사이징</strong></td>
<td>크기 조정 &amp; 용량 줄이기</td>
<td>빠르고 간단, 유지보수 쉬움</td>
<td>품질 저하 가능성, 고정 크기 문제</td>
<td>프로필 이미지, 미리보기 이미지</td>
</tr>
<tr>
<td><strong>WebP/AVIF 변환</strong></td>
<td>차세대 포맷 변환</td>
<td>파일 크기 감소, 웹 최적화</td>
<td>일부 브라우저 미지원, 변환 속도 느림</td>
<td>웹사이트 이미지, 모바일 앱</td>
</tr>
<tr>
<td><strong>이미지 압축 (Lossy/Lossless)</strong></td>
<td>품질 유지하며 용량 감소</td>
<td>S3 비용 절감, 네트워크 최적화</td>
<td>화질 저하 가능, API 비용 발생 가능</td>
<td>사진 갤러리, 사용자 업로드 이미지</td>
</tr>
</tbody></table>
<p>서치해 보니 우테코 6기 동료들이 발표한 <a href="https://www.youtube.com/watch?v=r2arbkCTVUk">테코톡</a>이 있었어요</p>
<p>위 자료를 참고해서 <strong>리사이징과 파일 변환을 하기</strong>로 결정하였어요</p>
<p>이미지 압축은 불필요하다 판단하였는데요,
무손실 압축의 경우에는 압축률이 낮으며 손실 압축의 경우 화질 저하가 발생할 수도 있기 때문에
리사이징과 파일 변환까지로 충분히 이미지 최적화가 적당하다 생각했습니다</p>
<h2 id="사용하지-않는-파일-삭제">사용하지 않는 파일 삭제</h2>
<p>저희 구조에서는 이미지를 업로드할 때마다 업로드 요청을 보내서 링크를 만들어 반환하는 형식이에요
해당 부분에서는 <a href="https://velog.io/@nellroll/S3%EC%9A%A9%EB%9F%89%EA%B4%80%EB%A6%AC">폴라의 블로그</a>를 참고할 수 있었어요</p>
<p>사실 뒤로가기 할 때 삭제하는 것이 가장 간편한 구현 방식이지만 뒤로가기가 아니라 강제로 웹을 꺼버리는 경우에는 인식하기 어려워 결국 사용하지 않는 파일이 생기게 됩니다</p>
<p>따라서 1차 업로드 폴더와 최종 사용 폴더를 분리하고 1차 업로드 폴더는 주기적으로 삭제하는 방식을 선택해 보았어요</p>
<h2 id="필요한-구현">필요한 구현</h2>
<h3 id="파일-업로드">파일 업로드</h3>
<ol>
<li>클라이언트에서 파일 전송</li>
<li>이미지 리사이징</li>
<li>이미지 파일 변환</li>
<li>1차 업로드 폴더에 파일 업로드</li>
</ol>
<h3 id="체크리스트-작성">체크리스트 작성</h3>
<ol>
<li>클라이언트에서 파일 URL 전송</li>
<li>이미지 1차 업로드 폴더에서 제거 후 최종 사용 폴더에 저장</li>
</ol>
<h3 id="s3">S3</h3>
<ol>
<li>1차 업로드 폴더 주기적 삭제</li>
</ol>
<h2 id="결론-1">결론</h2>
<p>이 정도로 고민을 끝낼 수 있을 것 같아요!
사이즈나 사진 개수 정책은 팀에서 논의 후 결정할 듯합니다</p>
<p>혹시 조언 주실 부분이나 논의 주실 사항은 언제나 환영이에요 🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지금부터 유저의 질문 추가가 가능합니다 설계하세요!]]></title>
            <link>https://velog.io/@shin-jisong/%EC%A7%80%EA%B8%88%EB%B6%80%ED%84%B0-%EC%9C%A0%EC%A0%80%EA%B0%80-%EC%A7%88%EB%AC%B8-%EC%B6%94%EA%B0%80%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%A9%EB%8B%88%EB%8B%A4-%EC%84%A4%EA%B3%84%ED%95%98%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@shin-jisong/%EC%A7%80%EA%B8%88%EB%B6%80%ED%84%B0-%EC%9C%A0%EC%A0%80%EA%B0%80-%EC%A7%88%EB%AC%B8-%EC%B6%94%EA%B0%80%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%A9%EB%8B%88%EB%8B%A4-%EC%84%A4%EA%B3%84%ED%95%98%EC%84%B8%EC%9A%94</guid>
            <pubDate>Sat, 08 Mar 2025 17:26:35 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
오늘은 설계하며 든 고민들을 정리하면 어떨까 싶어서 작성해 보는 글입니다</p>
<h2 id="새로운-기능을-추가해-보자">새로운 기능을 추가해 보자</h2>
<p>저희 방끗 팀은 11월 수료 이후 간단한 유지 보수를 제외하고는 추가적인 기능 개발을 진행하지 않았어요 홍보와 유저 유치에 집중을 하던 시기였습니다 🎉
그러나! 여러 논의 끝에 이제 새로운 기능을 추가해 보기로 하여 가장 먼저 개발할 두 가지 기능을 픽스하였어요</p>
<p>그 기능 중 첫번째가 <strong>체크리스트 질문을 유저가 추가 가능</strong>하도록 하는 부분이에요</p>
<p>두 가지 기능 중 어느 기능을, 누가 담당할지에 대한 논의가 이루어지기 전에 먼저 설계를 해 보고 싶어 위 글을 작성하게 되었어요 👍</p>
<h2 id="기존의-방끗에서-달라지는-점은">기존의 방끗에서 달라지는 점은?</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/749f261b-1b68-482b-9ec8-b85bafbb0dbd/image.png" alt=""></p>
<p>기존에는 위와 같이 서비스에서 제공해 주는 질문을 추가하거나 빼는 것으로 유저 개인의 체크리스트를 커스텀할 수 있었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/45c1c4b1-dbc9-4bac-a699-c4990bd34b5e/image.png" alt=""></p>
<p>저희는 4차례 이상의 데모데이로 직접 유저에게 피드백을 듣는 시간을 가졌고, 이후에도 꾸준히 VoC를 통해 유저의 이야기를 들어왔어요
그 결과 가장 많이 나온 의견 중 하나가 <strong>&quot;유저가 질문을 추가할 수 있게 해 주세요&quot;</strong> 였답니다</p>
<p>해당 피드백은 정말 처음부터 꾸준히 나온 의견인데요,
방끗 팀 논의 당시에는 정해진 시간 안에 배포를 해야 했기 때문에 최대한 많은 질문을 저희가 제공하는 것으로 보완하여 출시하였습니다
하지만 새로운 기능을 추가하기로 한 이상! 해당 부분에 도전해 보려고 합니다</p>
<h2 id="api">API</h2>
<p>해당 부분이 변경될 경우 크게 영향을 받는 API를 정리해 봅시다</p>
<ul>
<li><strong>커스텀 체크리스트 수정</strong>
해당 API는 커스텀 체크리스트에 질문을 넣거나 뺄 때 요청이 가는 API입니다</li>
<li><strong>커스텀 체크리스트 전체 질문 조회</strong>
해당 API는 커스텀 체크리스트를 편집하기 전에 전체 질문을 조회할 때 요청이 가는 API입니다 
이때 유저 개인의 질문이 추가된다면 공통 질문과 개인 질문을 모두 보내 주어야 할 것 같아요</li>
<li><strong>체크리스트 질문 조회</strong>
해당 API는 체크리스트를 작성하는 시점에 나의 질문들을 요청하는 API입니다
이때 유저 개인의 질문이 추가된다면 공통 질문과 개인 질문들 중 내가 체크한 질문들만 보내 주어야 해요</li>
</ul>
<p>추가로 구현되어야 할 API를 정리해 봅시다</p>
<ul>
<li><strong>커스텀 질문 추가</strong></li>
<li><strong>커스텀 질문 삭제</strong></li>
</ul>
<p>저는 해당 부분을 고민할 때 시스템 제공 질문은 추가, 삭제가 불가하다는 가정 하에 설계했습니다
개인이 자율적으로 추가한 질문만 추가, 삭제가 가능한 방향이에요</p>
<p>시스템 제공 질문의 경우, 가장 기본적인 것들로 추가를 해 두어서 질문의 역할 뿐만 아니라 집을 봐야 할 때 어떤 것을 봐야할지 알려 주는 가이드라인의 역할 또한 하고 있습니다
시스템 질문의 삭제하거나 수정하게 되면 해당 부분의 정확도가 떨어진다고 판단하여 코어 질문들은 유지하는 방향으로 가고자 합니다!</p>
<h3 id="커스텀-질문-수정"><strong>커스텀 질문 수정</strong></h3>
<p>해당 부분은 고민이 되는 API 중 하나입니다</p>
<p><strong>생각해 본 시나리오</strong> 중 하나가 있는데요</p>
<p>만약, 유저가 커스텀 질문을 추가한 후 해당 질문을 바탕으로 체크리스트를 작성하고 해당 질문을 수정한 후 체크리스트를 조회하게 되면</p>
<p>저희의 설계상으로 작성 당시의 체크리스트 질문이 아닌 수정 후의 질문이 보여지게 됩니다 
유저가 간단한 어휘를 바꾼 정도라면 해당 답변에 문제가 없지만 아예 다른 내용으로 바꾼 경우에는 해당 체크리스트의 신뢰도가 떨어지게 됩니다 </p>
<p>이러한 설계를 유저가 모르기 때문에 충분히 발생 가능한 시나리오라고 판단하였습니다</p>
<p>그럼 <strong>설계상으로 이렇게 안 되도록 변경하면 되지 않나?</strong> 를 고민해 보자면</p>
<p>체크리스트를 작성할 때마다 해당 질문의 내용까지 저장하는 방식을 떠올릴 수 있는데요
하나의 작은 시나리오를 예방하고자 많은 불필요한 데이터를 저장하는 것과 커스텀 질문 수정 기능을 유지하는 것 중 무엇이 더 합리적일까? 커스텀 질문 수정 기능을 그만큼 유저가 필요로 할까? 를 고민해 봤을 때 질문 추가, 삭제를 두고 질문 복사 버튼을 추가하는 것으로 충분히 대체 가능하다 판단하였습니다</p>
<p>그 외에도 다양한 방안이 떠올랐으나 실현 불가능하다 생각하여 보류하였습니다</p>
<p>따라서 해당 API는 구현을 하지 않는 방향으로 가는 것이 적합할 거라고 생각해요</p>
<h3 id="커스텀-질문-삭제"><strong>커스텀 질문 삭제</strong></h3>
<p>위의 내용을 생각해 보다가, 그렇다면 커스텀 질문을 삭제할 때도 문제가 생기겠는데? 라는 생각이 문득 들었어요</p>
<p>만약, 유저가 커스텀 질문을 추가한 후 해당 질문을 바탕으로 체크리스트를 작성하고 해당 질문을 삭제한 후 체크리스트를 조회하게 되면 현재의 경우에는 질문이 삭제되어 해당 질문과 답변을 조회할 수 없습니다</p>
<p>사실 현재 저희 서비스에서는 일부 부분에 한정하여 <strong>논리적 삭제</strong>를 도입하였어요
저희의 판단 하에 유의미하다 생각한 데이터 일부는 논리적 삭제를 통해 처리하고 있습니다
이러한 논리적 삭제된 데이터는 다른 History DB에 옮겨지고 있지 않고, 같은 테이블 내에서 관리하고 있어요
따라서 해당 부분은 논리적 삭제를 잘 조절한다면 해결 가능한 부분입니다</p>
<p>하지만, <strong>논리적 삭제가 된다는 사실을 비즈니스 코드가 알게 되어도 괜찮을까요?</strong>
저희가 기존에 논리적 삭제를 도입한 것은 유의미하다 생각한 데이터를 분석하기 위함이었고, 다른 DB로 옮기지 않은 이유는 저희가 다른 환경을 구축할 만큼 많은 유저가 있는 서비스가 아니었기 때문입니다
만약 유저가 많아진다면 충분히 DB를 분리할 수도 있는 상황이에요
현재 환경적 특성을 이용하는 비즈니스 코드를 작성해도 괜찮을지 고민이 되었어요</p>
<p>그렇다고 해서 커스텀 질문 삭제 기능 자체를 구현하지 않는 것은 유저 사용에 있어서 큰 불편을 초래할 것임이 분명했어요
따라서 고민 끝에 <code>user_deleted</code> 와 같은 논리적 삭제와 비슷하지만, 좀 더 비즈니스 코드에서 사용하기 위한 플래그 칼럼을 추가하는 것이 어떨까라는 생각이 들었습니다
해당 플래그를 통해 <strong>유저는 이 질문을 삭제했지만 체크리스트를 볼 때는 해당 질문을 보여 주어야 해</strong>라는 의도를 드러낼 수 있다 생각해요</p>
<p>물론, 해당 방안에도 문제점이 떠올라요
사실상 논리적 삭제 플래그와 동일하게 동작하기 때문에 현 시점에서는 불필요한 코드예요
따라서 논리적 삭제 DB를 분리하는 시점해서 해당 칼럼을 추가해 줘도 될 듯합니다
하지만 그렇게 한다면 내가 아닌 다른 개발자가 DB를 옮길 때에 이를 인지할 수 있을까? 라는 부분에서는 코드에 의도가 명확히 드러나지 않아 파악하기 힘들고, 이로 인해 치명적인 에러가 발생할 수도 있을 것 같다는 생각이 듭니다</p>
<p>따라서 저는 <strong>플래그 칼럼을 추가하는 것이 가장 합리적이라고 판단</strong>하였습니다</p>
<h2 id="db">DB</h2>
<p>그렇다면 기존의 시스템 질문만 있는 테이블과 개인 질문이 있는 테이블은 같이 사용해도 될까요 분리해야 할까요?
사실 이 부분을 고민할 때 굳이 합칠 이유도, 굳이 분리해야 할 이유도 명확히 떠오르지 않았습니다
그래서 각각의 경우를 비교해보고, 데이터 조회 및 관리의 복잡성을 고려했을 때 어떤 방식이 더 적절한지 정리해보겠습니다</p>
<h3 id="테이블을-합칠-경우-장점">테이블을 합칠 경우 장점</h3>
<h4 id="질문-id를-일관되게-유지할-수-있음">질문 ID를 일관되게 유지할 수 있음</h4>
<p>질문 ID를 공통적으로 사용하기 때문에 한 테이블에서 관리하는 것이 조회하기 쉽습니다
만약 테이블을 분리할 경우, 커스텀 질문 테이블에서 해당 질문이 어느 테이블에서 왔는지 식별해야 하는 문제가 발생합니다</p>
<p>이를 해결하려면 아래와 같은 방법이 필요합니다</p>
<ul>
<li>질문 ID 앞에 SYS_1, PER_1 같은 접두사를 붙여서 관리</li>
<li>question_type 같은 플래그 칼럼을 추가하여 구분</li>
</ul>
<p>하지만 이런 방식은 조회할 때 IF문으로 분기 처리를 해야 하고, 데이터 정합성을 유지하는 것도 복잡해지므로 성능 저하가 발생할 가능성이 높습니다</p>
<p>반면, 한 테이블에서 관리하면 이러한 불필요한 로직이 필요하지 않아 쿼리가 단순해지고, ID 체계를 유지하기도 훨씬 수월합니다</p>
<h4 id="질문-조회가-단순해짐">질문 조회가 단순해짐</h4>
<p>질문을 조회할 때 단일 테이블에서 검색할 수 있어 쿼리가 간결해집니다</p>
<p>반대로 테이블이 분리되어 있으면, 시스템 질문과 개인 질문을 함께 조회할 경우 UNION을 사용하거나 두 개의 쿼리를 실행해야 합니다
이는 성능 저하뿐만 아니라 데이터를 조회하는 로직이 복잡해지는 원인이 됩니다</p>
<h3 id="테이블을-합칠-경우의-단점">테이블을 합칠 경우의 단점</h3>
<h4 id="불필요한-칼럼이-저장될-수-있음">불필요한 칼럼이 저장될 수 있음</h4>
<p>개인 질문의 경우 생성한 유저 ID, 삭제 여부 등의 칼럼이 필요하지만, 시스템 질문에는 불필요합니다
따라서 테이블을 통합하면 일부 칼럼이 비어 있는 상태로 저장될 수 있습니다</p>
<p>하지만 시스템 질문의 개수가 많지 않다면, 이로 인한 저장 비용이 크게 문제 되지 않을 가능성이 높습니다
즉, 공간 낭비는 존재하지만, 성능과 관리의 복잡성 측면에서 얻는 이점이 더 크다면 감수할 수 있는 부분입니다</p>
<h3 id="결론">결론</h3>
<p>이로 인해 저는 테이블을 합치는 것이 더 합리적이라고 판단하였습니다!</p>
<hr>
<p>이렇게 고려할 부분을 고려하여 설계를 해 보았는데요
제가 어느 부분을 담당할지는 모르겠지만!
몇 시간에 걸쳐 고민하고 선택을 하니 어렵기도 하면서 즐겁게 느껴져요</p>
<p>설계에 대한 조언이나 토론 댓글은 언제나 환영입니다 🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[캐싱된 아티클에 조회수를 추가해 보자 (캐시가 불러온 나비효과 🦋 Help...)]]></title>
            <link>https://velog.io/@shin-jisong/%EC%BA%90%EC%8B%B1%EB%90%9C-%EC%95%84%ED%8B%B0%ED%81%B4%EC%97%90-%EC%A1%B0%ED%9A%8C%EC%88%98%EB%A5%BC-%EC%B6%94%EA%B0%80%ED%95%B4-%EB%B3%B4%EC%9E%90-%EC%BA%90%EC%8B%9C%EA%B0%80-%EB%B6%88%EB%9F%AC%EC%98%A8-%EB%82%98%EB%B9%84%ED%9A%A8%EA%B3%BC-Help</link>
            <guid>https://velog.io/@shin-jisong/%EC%BA%90%EC%8B%B1%EB%90%9C-%EC%95%84%ED%8B%B0%ED%81%B4%EC%97%90-%EC%A1%B0%ED%9A%8C%EC%88%98%EB%A5%BC-%EC%B6%94%EA%B0%80%ED%95%B4-%EB%B3%B4%EC%9E%90-%EC%BA%90%EC%8B%9C%EA%B0%80-%EB%B6%88%EB%9F%AC%EC%98%A8-%EB%82%98%EB%B9%84%ED%9A%A8%EA%B3%BC-Help</guid>
            <pubDate>Wed, 08 Jan 2025 13:10:50 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
오늘도 방끗 ☺️ 프로젝트를 진행하며 만난 문제와 해결 과정을 갖고 와 보았습니다
그러나 제목에 Help... 를 통해 혹시나 글을 보시는 분께 SOS를 곁들여 봅니다</p>
<p>지금부터 저의 8시간 구현 여정을 함께 가 봅시다 👊</p>
<h2 id="아티클에-조회수를-넣으시오">아티클에 조회수를 넣으시오!</h2>
<p>새로운 기능을 조금씩 구현하며 사용자 유치에 집중하던 그때,
홍보 방식뿐만 아니라 서비스적으로도 사용자 유치에 효과적인 방안을 모색 중이었어요
저희의 서비스는 지극히 개인화되어 있는 서비스라 하나의 유저가 다른 유저를 인식하지는 못하는 구조예요</p>
<p><strong>하지만!</strong></p>
<p>가시적으로 사용자가 있음을 보여주어 우리 유저 이렇게 많다~ 그리고 이 아티클 진짜 인기 많다~ 를 보여 주기 위해 조회수를 도입하기로 했어요</p>
<p>아주 간단한 필드 하나 추가, 증가 코드 한 줄 작성... 정도가 되리라 예측했어요
다른 크루가 해당 기능을 맡아 구현하다가 갑자기 연락이 옵니다</p>
<p>👨: 시소~ 저번에 캐시 도입했었지? <strong>캐시가 있어서 조회수가 업데이트가 안 되네</strong> DB에만 반영되고 있어 시간 관계상 다른 방안을 도입하진 못하겠어서... 캐시를 떼야 될 것 같은데?</p>
<p>🎡: 뭐? 하지만 아티클은 모든 유저에게 똑같이 보여주는 거라 매번 DB를 통하기 비효율적인 것 같은데 분명 방안이 있을 것 같아! <strong>내가 해 볼게</strong></p>
<p>이렇게... 제가 하게 되었습니다</p>
<h2 id="조회수-어떻게-관리할래">조회수 어떻게 관리할래?</h2>
<p>저는 조회수를 다른 곳에 저장해 두었다가 주기적으로 해당 데이터를 DB에 옮기는 방법을 생각해 보았습니다
두 가지 정도의 방안이 떠오르더라고요</p>
<h3 id="1-아티클-id와-조회수를-map-형태로-관리">1. 아티클 ID와 조회수를 Map 형태로 관리</h3>
<p>이 방식은 로컬 메모리에 Map 형태로 아티클 ID와 조회수를 저장하는 구조입니다.
요청이 들어오면 Map에서 해당 아티클 ID의 값을 찾아 +1만 수행하면 되기 때문에 속도가 빠르고 간단합니다
추가적인 네트워크 호출이나 데이터 일관성 관리에 대한 부담이 없습니다
특히, 기존 코드의 변경이 거의 필요하지 않다는 점에서 도입하기가 매우 쉽습니다</p>
<p>그러나, 이 방식은 데이터가 서버의 로컬 메모리에 저장되기 때문에, 분산 서버 환경에서는 문제가 발생합니다
예를 들어, 여러 서버에서 동일한 데이터를 관리할 경우, 조회수의 일관성이 깨질 수 있으며, 서버가 재시작되면 데이터가 손실될 위험이 있습니다</p>
<p>따라서 단일 서버 환경이나 간단한 테스트/개발 환경에서 유용하지만, 확장성이나 안정성이 중요한 환경에서는 적합하지 않습니다</p>
<h3 id="2-캐시-데이터-자체를-수정하여-관리">2. 캐시 데이터 자체를 수정하여 관리</h3>
<p>이 방식은 외부 캐시를 활용하여 데이터를 변경하는 구조입니다
요청이 들어오면 캐시에 저장된 데이터를 읽어와 조회수를 증가시키고, 다시 캐시에 저장하는 작업을 수행합니다
이 방식은 데이터를 중앙화하여 관리하기 때문에, 분산 서버 환경에서도 데이터 일관성을 보장할 수 있습니다</p>
<p>예를 들어, 모든 서버가 동일한 캐시에 접근하기 때문에, 여러 서버에서 동시에 조회수가 증가해도 최종 결과가 정확히 반영됩니다
그러나, 이 방식을 도입하려면 캐시와 통신하는 로직을 추가해야 하므로 코드 수정 범위가 넓어질 가능성이 높습니다
또한, 캐시 서버의 부하, 네트워크 지연, 캐시 만료 정책 등 운영 중 추가적으로 고려해야 할 요소가 많아집니다</p>
<p>따라서 도입 코스트가 높은 편이지만, 대규모 트래픽이나 분산 서버 환경에서는 안정성과 확장성 측면에서 더 적합합니다</p>
<h3 id="결론적으로">결론적으로</h3>
<p>저는 2번 방안을 선택하였습니다
기존에 분산 서버 환경을 도입한 경험이 있어, 해당 상황을 고려할 필요가 있었습니다
또한, 조회수를 관리하는 데이터가 Map으로 분산될 경우, 관리 포인트가 증가하여 유지보수가 어려워질 우려가 있었습니다</p>
<h2 id="layer-분리해야지">Layer... 분리해야지?</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2054db5c-0216-400f-bd13-b0a0819b7d7a/image.png" alt=""></p>
<p>기존 코드에는 다음과 같은 구조가 적용되어 있습니다</p>
<p><code>@Cacheable</code> 어노테이션은 함수 실행이 종료될 때 결과를 캐시에 저장하며, 이후 동일한 입력값으로 함수가 호출되면 AOP를 통해 캐시된 값을 반환합니다
즉, 해당 함수는 캐시가 비어 있을 경우 처음 한 번만 실행되고, 이후에는 캐시 데이터가 삭제될 때까지 실행되지 않습니다</p>
<h3 id="articlereponse에서-조회수-값-수정">ArticleReponse에서 조회수 값 수정?</h3>
<p>현재 코드에서는 ArticleResponse 객체가 캐시 데이터로 저장되고 있습니다</p>
<p>이와 같은 구조에서 조회수 증가를 캐시에 반영하려면 ArticleResponse 객체의 값을 수정해야 합니다
하지만, <strong>DTO 객체인 Response의 값을 직접 수정하는 것은 비즈니스 로직이 DTO로 침투하는 상황</strong>으로 적절하지 않다고 판단했습니다</p>
<h3 id="repository에서-캐시">Repository에서 캐시?</h3>
<p>그렇다면 현재 도메인은 Repository가 반환하고 있으니 Repository를 캐시해야 할까요?</p>
<p><strong>하지만, 캐시 로직을 Repository에 추가하는 것도 적합하지 않았습니다</strong></p>
<ul>
<li>Repository는 데이터베이스와의 상호작용을 책임지는 역할에 집중해야 하며, 캐시 관리까지 맡게 되면 역할이 모호해집니다</li>
<li>트랜잭션이 완료되지 않은 데이터를 캐싱할 경우, 데이터의 일관성이 깨질 우려도 있습니다</li>
</ul>
<p>일반적으로 캐시는 비즈니스 로직을 처리하는 Service 레이어에 적용하는 것이 가장 적합하기 때문에, Repository에 캐싱 로직을 추가하는 방식은 권장되지 않습니다</p>
<p><strong>이러한 이유로, 캐시 로직을 관리할 별도의 Service를 분리하는 것이 더 적합하다고 판단했습니다</strong></p>
<h3 id="articlemanageservice의-도입">ArticleManageService의 도입</h3>
<p>새로운 구조에서는 ArticleService와는 별도로 <strong>조회수 증가 및 캐시 관리 기능을 담당하는 ArticleViewService</strong>를 추가하였습니다
이는 기존의 ArticleService가 본래 맡고 있던 비즈니스 로직에 영향을 주지 않으면서, 캐시에 관한 부분만 별도로 관리할 필요성이 있다고 판단했기 때문이에요</p>
<p>또한, 이 두 Service를 통합적으로 관리하는 <code>ArticleManageService</code>를 도입하여, 역할과 책임을 분리하고 코드의 가독성과 유지보수성을 높였습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/be040dcc-324a-4ec7-9bf4-f28b00e4602a/image.png" alt=""></p>
<p>결론적으로, Service 레이어를 분리하여 <code>ArticleService</code>와 <code>ArticleViewService</code>를 각각의 역할에 맞게 설계한 구조로 개선하였습니다</p>
<h2 id="캐시-값-변경을-구현해-보자">캐시 값 변경을 구현해 보자</h2>
<p>드디어 정책적인 부분 고려가 끝나고 구현에 들어왔습니다
<del>사실 전 위의 시행착오를 겪는 동안 그것을 모두 구현해 보아서 실제로 더 오랜 시간이 걸렸습니다</del></p>
<ul>
<li><p>ArticleManageService에서 readArticle 요청을 받으면
<img src="https://velog.velcdn.com/images/shin-jisong/post/88e44ebb-8ba5-416f-897f-ffd5df2b901d/image.png" alt=""></p>
</li>
<li><p>ArticleService에서 Repository로 데이터를 조회한 뒤 캐시합니다
이 과정에서 이미 캐시된 데이터가 있으면 해당 함수는 실행되지 않습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/6c759db3-d558-4230-a701-9917d11f830b/image.png" alt=""></p>
</li>
<li><p>그 후 ArticleViewService에서 캐시된 데이터를 조회해 값을 증가시키고 다시 저장합니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/e4d97818-6c0a-4c40-b2b9-b10c17df5fa3/image.png" alt=""></p>
</li>
</ul>
<h2 id="주기적으로-db에-반영해야지">주기적으로 DB에 반영해야지</h2>
<p>조회수 값을 캐싱에 성공하였어요
테스트도 아주 잘 돌아갑니다 👍</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/88334c1f-f700-49e7-ae0e-7d5894ab36af/image.png" alt=""></p>
<p>이제 주기적으로 캐시에 있는 모든 값을 DB에 반영할 차례입니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/522f59c8-50bd-4e94-83d2-00ee68a0eee0/image.png" alt=""></p>
<p>그러나 눈을 씻고 찾아 봐도 보이지 않는 모든 데이터 조회 😲</p>
<p>key가 있어야만 값을 가져오는 것이 가능하고, 존재하는 모든 key를 조회한다든가 모든 value를 조회하는 API가 아예 존재하지 않더라고요
<del>이럴 수가 나는 어떡하라고</del></p>
<p>따라서 <code>ArticleViewService</code>에 </p>
<p><code>private final Set&lt;Long&gt; cacheArticleIds = new HashSet&lt;&gt;();</code> 변수를 추가하여 캐싱되어 있는 id들을 관리하는 집합을 두었습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/fd94f988-80cc-42f0-be6d-773a698348ac/image.png" alt=""></p>
<p>그리고 매일 자정마다 해당 집합을 조회에 DB로 데이터를 반영하는 로직을 작성해 주었습니다
이때, 자정으로 한 이유는 트래픽이 적은 시간대이며, 하루 단위로 조회수를 관리하기 용이하다는 점을 들 수 있습니다</p>
<h3 id="해당-로직이-service에-있어도-될까">해당 로직이 Service에 있어도 될까?</h3>
<p>@Scheduled(cron = &quot;0 0 0 * * ?&quot;) 함수가 Service에 위치해도 되는지에 대한 고민에서 다음과 같은 논리로 결론을 내렸습니다</p>
<ul>
<li><p><strong>동기화 로직을 비즈니스 로직에 명시</strong>
Service 계층에 해당 로직을 두면, 조회수 데이터를 매일 자정에 동기화한다는 의도를 명확하게 드러낼 수 있습니다
이는 비즈니스 로직에 포함되는 작업으로 간주할 수 있으며, 다른 개발자들이 해당 로직을 이해하고 유지보수하는 데 혼란을 줄일 수 있습니다</p>
</li>
<li><p><strong>Repository와의 상호작용</strong>
해당 로직은 articleRepository를 사용하여 데이터를 업데이트합니다
Repository 계층은 데이터베이스와의 직접적인 상호작용을 담당하지만, 비즈니스 로직을 다루는 Service 계층에서 Repository를 호출하여 데이터 동기화를 처리하는 것은 자연스러운 설계입니다
따라서, 이 작업은 비즈니스 로직의 일환으로 볼 수 있습니다</p>
</li>
<li><p><strong>책임 분리 원칙 준수</strong>
Service 계층은 비즈니스 로직을 구현하는 계층으로, 특정 주기에 실행되는 동기화 작업을 포함할 수 있습니다
Scheduler가 Repository에 직접 접근하지 않고, Service 계층에서 처리되도록 설계함으로써 책임의 분리와 코드의 가독성을 유지할 수 있습니다</p>
</li>
</ul>
<p>결론적으로, @Scheduled 함수가 Service 계층에 위치하는 것은 협업, 설계 원칙과 비즈니스 로직의 역할을 고려했을 때 적합하다고 판단하였습니다</p>
<h2 id="scheduled-로직을-테스트해-보자">@Scheduled 로직을 테스트해 보자</h2>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/9239146f-080a-4091-a289-73a1054f851e/image.png" alt=""></p>
<p>기존에는 단순히 함수 호출을 통해 테스트를 진행하며 <code>@Scheduled</code>를 적용하지 않았습니다
Spring이 구현한 영역이니 굳이 테스트를 안 해도 되는 부분이라 생각했기 때문이에요</p>
<p>하지만 스케줄링이 제대로 동작하는지까지 확인하고 싶어서 구글링을 통해 해결 방안을 찾았습니다
그 결과, <code>taskScheduler</code>를 활용하면 스케줄링 테스트가 가능하다는 것을 알게 되었고, 이를 활용하여 테스트를 완료했습니다</p>
<h2 id="여전히-남은-고민">여전히 남은 고민</h2>
<p>현재 상황에서는 아티클 리스트를 조회할 때는 Repository에서 데이터를 가져오고, 개별 아티클 조회 시에는 캐시를 활용하고 있습니다</p>
<p>문제는 조회수가 캐시에만 반영되며, 캐시 데이터가 매 정각마다 DB에 업데이트되기 때문에 리스트 조회 시점과 캐시 데이터 간의 조회수 불일치가 발생한다는 점입니다</p>
<p>이를 해결하기 위해 다음과 같은 방안을 고민해 보았습니다:</p>
<ul>
<li><p><strong>조회수만 별도로 조립</strong>
리스트를 조회할 때, 각 아티클의 조회수를 캐시에서 가져와 조립하는 방식입니다.
그러나 이 방식은 서버에서 처리해야 할 작업이 지나치게 많아지는 단점이 있습니다.</p>
</li>
<li><p><strong>리스트 조회 시 캐시를 갱신</strong>
리스트를 조회할 때마다 캐시 데이터를 DB와 동기화하여 최신 상태로 갱신하는 방식입니다.
그러나 이 방식은 캐시의 장점인 성능 향상을 활용할 수 없게 되어 캐시를 사용하는 의미가 사라집니다</p>
</li>
</ul>
<p>이와 같은 상황에서는 캐시를 효율적으로 사용하는 것이 어려운 것일까? 캐시를 적용할 수는 없는 걸까? 하는 고민이 있어요
현재로서는 해결 방안이 명확하지 않아, 비슷한 경험이 있는 유경험자의 조언이 필요합니다 🥲</p>
<p><del>현재 아티클 리스트에는 조회수를 보여주지 않고 있지만 언젠가 보여줘야 하는 상황이 왔을 때에는 어떻게 하면 좋을지...
아 참 동시성도 언젠가는</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[localhost:8080을 입력하면 어떻게 내가 실행한 스프링으로 요청이 들어올까요?]]></title>
            <link>https://velog.io/@shin-jisong/localhost8080%EC%9D%84-%EC%9E%85%EB%A0%A5%ED%95%98%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%82%B4%EA%B0%80-%EC%8B%A4%ED%96%89%ED%95%9C-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%9A%94%EC%B2%AD%EC%9D%B4-%EB%93%A4%EC%96%B4%EC%98%AC%EA%B9%8C%EC%9A%94</link>
            <guid>https://velog.io/@shin-jisong/localhost8080%EC%9D%84-%EC%9E%85%EB%A0%A5%ED%95%98%EB%A9%B4-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%82%B4%EA%B0%80-%EC%8B%A4%ED%96%89%ED%95%9C-%EC%8A%A4%ED%94%84%EB%A7%81%EC%9C%BC%EB%A1%9C-%EC%9A%94%EC%B2%AD%EC%9D%B4-%EB%93%A4%EC%96%B4%EC%98%AC%EA%B9%8C%EC%9A%94</guid>
            <pubDate>Mon, 23 Dec 2024 02:53:59 GMT</pubDate>
            <description><![CDATA[<p>오늘은 평소에 궁금했던, 다소 기본적인 내용을 다뤄볼 거예요
우리가 스프링 프로젝트를 실행한 후 인터넷 브라우저에 <code>localhost:8080</code>으로 요청을 보내면 해당 요청이 내가 실행한 스프링 서버로 들어오게 됩니다
과연 어떤 과정을 거쳐 나의 서버로 전달되는지 알아보겠습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/7e8d9078-930c-44c9-82a3-6f37931ea397/image.png" alt=""></p>
<blockquote>
<p>루프백 주소를 어떻게 처리하냐를 중심으로 글을 전개합니다</p>
</blockquote>
<h2 id="1-브라우저에서-요청-생성">1. 브라우저에서 요청 생성</h2>
<p>브라우저에서 사용자가 localhost:8080을 입력하면,
브라우저는 먼저 localhost라는 이름을 IP 주소로 변환해야 합니다. </p>
<p>이를 보통의 경우에는 DNS(Domain Name System) 시스템을 통해 해결할 수 있습니다. 
DNS는 도메인 이름을 IP 주소로 변환해주는 시스템이지만, 
localhost는 로컬 시스템에서 특별하게 다뤄집니다. </p>
<p><strong>따라서 DNS 서버에 의존하지 않고, 로컬 호스트 파일을 먼저 확인합니다.</strong></p>
<p>운영 체제는 localhost를 IP 주소 127.0.0.1로 변환하기 위해 hosts 파일을 사용합니다. 이 파일은 운영 체제에 직접 설정된 이름과 IP 주소 매핑을 포함하고 있습니다. 예를 들어, 다음과 같은 항목이 포함되어 있습니다:</p>
<pre><code>127.0.0.1   localhost</code></pre><p>Windows에서는 C:\Windows\System32\drivers\etc\hosts에 localhost가 127.0.0.1로 정의되어 있습니다.
Linux/macOS에서는 /etc/hosts에 localhost가 127.0.0.1로 정의되어 있습니다.</p>
<p>이 설정은 localhost라는 이름이 항상 127.0.0.1이라는 루프백 IP 주소와 매핑되도록 보장합니다.
hosts 파일은 DNS 서버보다 우선적으로 조회되기 때문에,
로컬에서만 사용되는 주소(localhost)는 이 파일에서 처리됩니다.</p>
<p>따라서 DNS 서버를 거치지 않고 바로 내부의 host 파일을 거쳐 요청이 이루어지기 때문에 네트워크에 연결되어 있지 않아도 됩니다.</p>
<h2 id="2-네트워크-계층-처리">2. 네트워크 계층 처리</h2>
<p>IP 주소 127.0.0.1은 루프백 주소로, 이는 외부 네트워크 장치 없이도 내 컴퓨터의 네트워크 스택을 통해 연결될 수 있게 해주는 특별한 주소입니다. 루프백 주소는 내부 통신을 위한 용도로, 외부 네트워크와의 연결 없이도 로컬에서만 네트워크 서비스를 주고받을 수 있게 해줍니다. 이 주소를 사용하면, <strong>컴퓨터의 네트워크 인터페이스 카드(NIC)</strong>를 거치지 않고, 로컬 시스템 내에서만 데이터를 전송하고 받을 수 있게 됩니다.</p>
<h3 id="osi-layer에서-루프백-주소의-동작">OSI Layer에서 루프백 주소의 동작</h3>
<p>루프백 주소(127.0.0.1 또는 ::1)는 네트워크를 통과하지 않고 컴퓨터 내부에서 데이터 통신을 처리합니다. OSI 모델의 각 계층에서 루프백 주소가 어떻게 처리되는지 상세히 살펴보면 다음과 같습니다.</p>
<ul>
<li><p><strong>애플리케이션 계층</strong>
소프트웨어(예: 웹 브라우저, 서버, 클라이언트)가 요청(예: HTTP 요청)을 생성합니다.
데이터는 사용하려는 프로토콜(TCP/UDP)을 설정하고 OS에 전달됩니다.</p>
</li>
<li><p><strong>전송 계층</strong>
TCP 또는 UDP 프로토콜을 사용해 데이터를 세그먼트 단위로 분리하거나 재조립합니다.
전송 계층은 포트를 기반으로 소프트웨어 간 통신을 설정하며, 요청이 루프백 주소로 전송되므로 외부 연결을 시도하지 않습니다.</p>
</li>
<li><p><strong>네트워크 계층</strong>
데이터를 패킷으로 캡슐화하며, 발신지 IP와 수신지 IP 주소를 확인합니다.
루프백 주소(127.0.0.1 또는 ::1)는 OS에 특별히 예약된 IP로, 네트워크 인터페이스(NIC)를 우회하고 내부 네트워크 스택에서 처리됩니다.
따라서, 이 계층에서 외부 라우팅으로 전송하지 않습니다.</p>
</li>
<li><p><strong>데이터 링크 계층</strong>
일반적인 통신에서는 이 계층에서 MAC 주소와 NIC를 통해 물리적 전송을 준비하지만, 루프백 주소의 경우 데이터 링크 계층으로 내려가지 않고 OS 내부에서 바로 상위 계층으로 응답을 돌려줍니다.</p>
</li>
<li><p><strong>물리 계층</strong>
루프백 주소는 물리 계층으로 도달하지 않습니다. 데이터가 네트워크를 통해 전송되지 않고, 내부적으로 처리되기 때문입니다.</p>
</li>
</ul>
<h3 id="네트워크-계층에서-루프백-주소의-처리">네트워크 계층에서 루프백 주소의 처리</h3>
<p>루프백 주소(127.0.0.1 또는 ::1)는 네트워크 계층에서 특별히 처리됩니다. 이 단계에서는 외부 네트워크로 데이터를 전송하지 않고, 컴퓨터 내부에서만 패킷을 전달하기 위해 설정된 프로세스가 수행됩니다.</p>
<ul>
<li><p><strong>IP 주소 확인 및 패킷 생성</strong>
발신지 IP와 수신지 IP가 할당됩니다.
발신지 IP: 요청을 보낸 로컬 머신의 IP 주소 (일반적으로 127.0.0.1 또는 ::1)
수신지 IP: 응용 프로그램이 지정한 목적지 주소 (127.0.0.1 또는 ::1)
이 IP 주소는 루프백 주소로 예약되어 있으므로 라우팅 과정이 달라집니다.</p>
</li>
<li><p><strong>라우팅 테이블 확인</strong>
네트워크 계층은 라우팅 테이블을 조회해 수신지 IP에 맞는 경로를 결정합니다.</p>
</li>
</ul>
<p>127.0.0.1 및 ::1은 기본적으로 <strong>로컬 호스트(Localhost)</strong>로 매핑되며, 패킷을 외부 네트워크로 전달하지 않도록 설정됩니다.
라우팅 테이블은 루프백 주소에 대한 엔트리를 포함하며, 이 엔트리는 데이터가 컴퓨터 내부에서만 처리되도록 지정합니다.</p>
<ul>
<li><p><strong>패킷 전달</strong>
네트워크 계층은 이 패킷을 <strong>루프백 인터페이스(가상 네트워크 인터페이스)</strong>로 전달합니다.
루프백 인터페이스는 NIC와 달리 물리적 하드웨어가 아니라 운영체제에서 제공하는 소프트웨어 인터페이스입니다.
운영체제는 루프백 인터페이스를 통해 패킷을 다시 상위 계층(전송 계층)으로 전달합니다.</p>
</li>
<li><p><strong>NIC를 우회</strong>
루프백 주소로 설정된 패킷은 네트워크 카드(NIC)로 전달되지 않고, 시스템 내부에서 전송이 종료됩니다.
데이터 링크 계층과 물리 계층으로 내려가지 않으므로 네트워크 트래픽이 발생하지 않습니다.</p>
</li>
</ul>
<h3 id="루프백-인터페이스-vs-네트워크-인터페이스-카드-비교">루프백 인터페이스 vs 네트워크 인터페이스 카드 비교</h3>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>루프백 인터페이스</strong></th>
<th><strong>네트워크 인터페이스 카드(NIC)</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>정의</strong></td>
<td>컴퓨터 내부에서 네트워크 프로토콜 테스트를 위한 가상 인터페이스.</td>
<td>컴퓨터를 외부 네트워크에 연결하는 물리적 또는 가상 장치.</td>
</tr>
<tr>
<td><strong>사용 목적</strong></td>
<td>내부 통신, 테스트, 디버깅, 로컬 호스트 간 데이터 송수신.</td>
<td>외부 네트워크와의 데이터 송수신 및 인터넷 연결.</td>
</tr>
<tr>
<td><strong>IP 주소 범위</strong></td>
<td>IPv4: <code>127.0.0.0/8</code> (주로 <code>127.0.0.1</code>)<br>IPv6: <code>::1</code></td>
<td>실제 네트워크 제공자(ISP)나 DHCP 서버에서 할당된 IP 주소.</td>
</tr>
<tr>
<td><strong>물리적 존재 여부</strong></td>
<td>없음 (운영체제에서 소프트웨어로 구현).</td>
<td>유선(이더넷) 또는 무선(Wi-Fi) 연결을 위한 물리적 장치.</td>
</tr>
<tr>
<td><strong>데이터 흐름</strong></td>
<td>데이터가 외부로 나가지 않고 시스템 내부에서만 처리.</td>
<td>데이터를 네트워크를 통해 외부 장치로 송수신.</td>
</tr>
<tr>
<td><strong>OSI 모델 역할</strong></td>
<td>전송 계층 및 네트워크 계층에서만 동작, 데이터 링크 계층 이하로 내려가지 않음.</td>
<td>OSI 전 계층을 통과하며, 실제 데이터 송수신은 물리 계층에서 이루어짐.</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>내부 처리이므로 매우 빠름.</td>
<td>네트워크 속도(유선/무선)에 따라 다름.</td>
</tr>
<tr>
<td><strong>응용 사례</strong></td>
<td>- 로컬 서버 개발 및 테스트 (예: 웹 서버 <code>localhost</code>).<br>- 네트워크 구성 확인 (핑 테스트).</td>
<td>- 외부 인터넷 연결.<br>- LAN/WAN 연결.<br>- 클라이언트-서버 간 통신.</td>
</tr>
<tr>
<td><strong>구현 방식</strong></td>
<td>운영체제가 제공하는 가상 인터페이스.</td>
<td>하드웨어 장치 및 드라이버로 구현.</td>
</tr>
<tr>
<td><strong>MAC 주소</strong></td>
<td>없음 (물리적 주소 필요하지 않음).</td>
<td>물리적 NIC에 고유한 MAC 주소가 있음.</td>
</tr>
<tr>
<td><strong>고장 여부</strong></td>
<td>소프트웨어로 구현되므로 고장 가능성이 거의 없음.</td>
<td>하드웨어 고장 가능성 존재 (포트 손상, 전원 문제 등).</td>
</tr>
<tr>
<td><strong>트래픽 제어</strong></td>
<td>외부 네트워크와 독립적이므로 트래픽 제어 필요 없음.</td>
<td>QoS, 대역폭 제한 등 트래픽 관리 필요.</td>
</tr>
</tbody></table>
<h3 id="루프백-인터페이스">루프백 인터페이스</h3>
<p>글을 작성하다 보니 루프백 인터페이스가 어떻게 동작하는지에 대해 너무 모호하게 다가와서 해당 부분을 추가해 봅니다</p>
<p>루프백 인터페이스는 물리적 네트워크 장치나 다른 시스템과 연결되지 않고, 로컬 시스템 내에서만 데이터가 송수신되는 논리적이고 가상의 네트워크 인터페이스입니다.</p>
<p>&#39;인터페이스&#39;라는 용어가 <code>네트워크에서 통신을 위한 물리적 연결 지점</code>을 의미하는 경우가 많지만, 루프백 인터페이스는 이러한 <code>물리적 연결 없이 자기 자신과의 연결 포인트</code>를 제공합니다.</p>
<p>즉, 루프백 인터페이스는 외부 네트워크와의 상호작용 없이 컴퓨터 내부에서만 통신할 수 있는 가상적 연결 포인트입니다. 이로 인해 시스템은 물리적 네트워크를 거치지 않고, 빠르고 안정적으로 데이터를 송수신할 수 있습니다. 루프백 인터페이스는 보통 127.0.0.1(IPv4)이나 ::1(IPv6) 주소로 지정되며, 이를 통해 시스템은 네트워크 기능을 테스트하거나 디버깅할 수 있습니다.</p>
<p>결국 루프백 인터페이스는 시스템 내부에서 자기 자신과의 통신을 위해 설계된 연결 포인트로, 외부와의 상호작용 없이 독립적으로 동작하면서도, 네트워크 프로토콜을 통한 데이터를 안전하게 처리할 수 있는 중요한 역할을 수행합니다.</p>
<h2 id="3-스프링-애플리케이션-내부에서-처리">3. 스프링 애플리케이션 내부에서 처리</h2>
<p>내부 네트워크 인터페이스를 통해 들어오는 요청은 스프링 부트 애플리케이션이 <strong>임베디드 서버(예: Tomcat)</strong>에서 수신합니다.</p>
<p>스프링 부트는 자동으로 포트 8080을 기본으로 설정하며,
해당 포트를 리스닝(listen)하여 HTTP 요청을 받습니다.</p>
<p>예를 들어, localhost:8080으로 들어오는 요청은 스프링 애플리케이션의 컨트롤러(Controller)에서 처리됩니다.
이 컨트롤러는 요청에 대한 핸들러 메서드를 정의하여, 요청에 맞는 처리를 수행하고 응답을 생성합니다.</p>
<h2 id="4-요청과-응답-흐름">4. 요청과 응답 흐름</h2>
<p>브라우저가 요청을 보내면,
스프링 애플리케이션에서 해당 요청을 매핑된 URL과 일치하는 컨트롤러 메서드로 전달합니다.</p>
<p>예를 들어, @RequestMapping(&quot;/home&quot;)으로 정의된 메서드는 /home 경로로 들어오는 요청을 처리합니다.</p>
<p>이때, 스프링은 서블릿 컨테이너(기본적으로 Tomcat)를 사용하여 요청을 받아들이고 처리하는데,
Tomcat은 요청을 스프링의 DispatcherServlet에 전달합니다. DispatcherServlet은 요청을 해당하는 핸들러로 분배하고,
최종적으로 HTTP 응답을 생성하여 브라우저에 반환합니다.</p>
<h2 id="5-응답-처리">5. 응답 처리</h2>
<p>스프링 애플리케이션은 처리한 데이터를 HTML, JSON, XML 등 다양한 형식으로 변환하여 응답을 보냅니다. 이 응답은 다시 루프백 인터페이스를 통해 브라우저로 돌아가고, 사용자는 결과를 확인할 수 있습니다.</p>
<h2 id="마치며">마치며</h2>
<p>사실 제가 가장 알고 싶었던 건 루프백 주소를 어떻게 처리하느냐? 라서 해당 부분에 글이 집중되어 있습니다!
아직 네트워크에서 공부할 것이 많다는 것을 느끼며... 🥲</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[무분별한 로그, 서버를 죽였습니다 (로그 전략, 기록뿐만 아니라 관리까지)]]></title>
            <link>https://velog.io/@shin-jisong/%EB%AC%B4%EB%B6%84%EB%B3%84%ED%95%9C-%EB%A1%9C%EA%B7%B8-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%A3%BD%EC%98%80%EC%8A%B5%EB%8B%88%EB%8B%A4-%EB%A1%9C%EA%B7%B8-%EC%A0%84%EB%9E%B5-%EA%B8%B0%EB%A1%9D%EB%BF%90%EB%A7%8C-%EC%95%84%EB%8B%88%EB%9D%BC-%EA%B4%80%EB%A6%AC%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@shin-jisong/%EB%AC%B4%EB%B6%84%EB%B3%84%ED%95%9C-%EB%A1%9C%EA%B7%B8-%EC%84%9C%EB%B2%84%EB%A5%BC-%EC%A3%BD%EC%98%80%EC%8A%B5%EB%8B%88%EB%8B%A4-%EB%A1%9C%EA%B7%B8-%EC%A0%84%EB%9E%B5-%EA%B8%B0%EB%A1%9D%EB%BF%90%EB%A7%8C-%EC%95%84%EB%8B%88%EB%9D%BC-%EA%B4%80%EB%A6%AC%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Tue, 17 Dec 2024 06:42:14 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요 
오늘은 로그 전략에 대해 이야기해 보고자 합니다</p>
<p>사실 제목을 </p>
<ul>
<li>무분별한 로그, 서버를 죽였습니다
로그 전략, 기록뿐만 아니라 관리까지</li>
</ul>
<p>둘 중 하나로 고민하다가 둘 다 넣었습니다
하나만 넣기에는 제목에서 제가 전하려는 게 다 전달되지 않을 것 같았어요 🤔</p>
<p>하지만 좀 더 정돈된 제목으로 글 시작하겠습니다</p>
<h1 id="로그-전략-기록뿐만-아니라-관리까지">로그 전략, 기록뿐만 아니라 관리까지</h1>
<p>때는 우테코 Lv3으로 돌아갑니다
방끗을 한창 개발 중에 이러한 미션이 주어졌어요
<img src="https://velog.velcdn.com/images/shin-jisong/post/22cf514b-2846-42b4-9ac3-e2f4435d3188/image.png" alt=""></p>
<p>저희의 스프링 서버에 적용할 프레임워크를 정하고 전략을 수립해야 했습니다
로깅 프레임워크를 다양한 조건에 맞춰 정하고 (해당 부분에도 많은 고려가 있었으나 글의 호흡이 너무 길어질 것 같아서 생략합니다)
로깅 전략을 수립했어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/47353c44-9c95-4c0f-84f7-034b5eb83e21/image.png" alt=""></p>
<p>그때 당시에 작성한 <a href="https://github.com/woowacourse-teams/2024-bang-ggood/wiki/%E2%9C%8F%EF%B8%8F-BE-LOGGING">WIKI</a>에서 주요 부분만 갖고 왔습니다
자세한 사항은 들어가시면 확인할 수 있어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/c86bd598-1915-4c95-a5ec-226a68bc7032/image.png" alt=""></p>
<p>다양한 로깅 전략을 참고하여 우리만의 전략을 수립하고 필요 시 추가 혹은 수정을 거칠 예정이었습니다
해당 로그들은 <code>server.log</code> 파일로 기록되도록 배포 파일에서 설정해 두었습니다</p>
<h3 id="하지만">하지만</h3>
<p>어느 순간 갑자기 죽어버리는 서버를 보며 발등에 불이 붙었어요
도대체 왜? 어째서?</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a46b6368-5c02-4db2-a859-9f27e0a670dd/image.png" alt=""></p>
<p>부랴부랴 EC2에 들어가서 원인을 파악해 보니
해당 Use% 부분이 100%가 된 것을 볼 수 있었습니다</p>
<p><em>용량이 부족한 것이었어요 😵‍💫</em></p>
<p>임시방편으로 EC2의 볼륨을 추가 할당한 후 서버를 재실행할 수 있었지만
저희는 재발 방지를 위해서 대책을 마련해야 했습니다</p>
<p><del>사실 사용자가 그리 많지 않은 서비스에서 로그 때문에 서버가 죽는 상황은 아주 비정상적이었죠</del></p>
<h3 id="따라서">따라서</h3>
<p>비로소 깨달았습니다
로그 전략을 수립할 때에 로그를 남기는 것뿐만 아니라 &quot;어떻게&quot; 관리할지도 수립했어야 된다는 것을... 🤦‍♂️</p>
<p>로그를 보존, 분석 나아가 삭제까지 하기 위해 해당 전략을 수립하기로 마음 먹었어요
함께 진행한 제제와 다양한 회의를 하였는데 일부를 공유해 봅니다
모두 다 읽지 않아도 하단에 요약하여 적을게요!</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/84ca5f78-6e41-49a2-be9f-01b338fc509c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/8df7e375-3c40-44e3-911b-f46c9133bf84/image.png" alt=""></p>
<p>위와 같이 정말 많은 고민을 거치다가 솔라 코치와 긴 토크 타임을 가지기도 했습니다
<del>코치님 그립네요 🥲</del></p>
<p>결론적으로, 저희는 로그 전략을 다음과 같이 변경하였습니다</p>
<h4 id="info-log">INFO LOG</h4>
<ul>
<li><p><strong>기존에 Service Method 이름을 남기던 방식에서 Controller API, User ID를 남기도록 변경 (Base Log는 동일하게)</strong>
기존에 Service Method의 이름을 남겼던 이유는 오류 상황이 발생했을 때 앞뒤로 어느 method까지 도달했는지 알기 위함이었어요
하지만 로그의 양이 너무 방대해지고 저희가 해당 정보를 간접적으로 활용한 적이 1번, 직접적으로 활용한 적은 사실상 없었기 때문에 필요 없는 정보라 생각했어요
그렇다면 <strong>도대체 무슨 정보를 남겨야 하지?</strong> 라는 고민에 코치님께서는 유저가 들어오고 응답을 받았는 것을 기본적으로 개발자가 살펴야 한다는 답변을 주셔서 해당 부분을 로그에 남기도록 하였어요</p>
</li>
<li><p><strong>매일 새로운 파일 생성 (ex. INFO-2024-12-17.log)</strong></p>
</li>
<li><p><strong>3일에 한 번씩 로그 파일 압축</strong>
해당 전략을 도입할 때에는 그럼** 압축이 일어난 직후에는 해당 로그를 보지 못하는 것 아닌가?** 라는 고민 지점이 있었는데요
코치님과의 대화를 통해 <strong>어쩔 수 없는 트레이드 오프의 영역</strong>이라고 결론을 내렸어요
서버의 용량이 커지는 것을 방지하기 위해서는 압축 혹은 삭제를 해야 하는데 삭제 시기를 길게 잡기 위해서는 압축을 하여 보관할 수밖에 없습니다
따라서 부득이하게 압축 직후 로그를 확인하기 위해서는 불편함을 감수하고 압축을 푼 뒤에 로그를 확인해야 하지만 그것이 서버가 다운되거나, 로그가 아예 없는 것보다는 나은 선택지라고 생각해요</p>
</li>
<li><p><strong>압축된 파일은 2주 후 삭제</strong>
해당 전략을 도입할 때에는 <strong>로그를 남겨 유의미한 분석을 하고 싶은데 어느 정도의 기간이 좋을까?</strong> 라는 고민이 있었어요
그때 코치님께서 주신 조언은 <strong>결국 중요한 정보는 DB에 담는다는 전제 하에 서비스에 따라 다른 기간을 잡아야 한다</strong>라는 조언을 주셨어요
저희는 사실 유의미한 분석은 모두 DB의 데이터로 진행할 수 있다 생각하였고,
&quot;방 구하는 체크리스트 서비스&quot;라는 것을 고려하였을 때 일주일을 사이클을 보아야 하였으며,
사용할 수 있는 용량을 고려하여 2주로 잡았습니다</p>
</li>
</ul>
<h4 id="error--warn-log">ERROR &amp; WARN LOG</h4>
<ul>
<li><strong>기록 전략은 동일</strong></li>
<li><strong>매일 새로운 파일 생성 (ex. ERROR-2024-12-17.log)</strong></li>
<li><strong>3일 후 삭제</strong>
ERROR 로그와 WARN 로그는 개발자 혹은 사용자의 실수로 발생할  수 있는 에러들을 기록하는 로그예요
따라서 바로바로 대처를 해야 하는 부분이기 때문에 짧은 기간으로 잡았습니다
단, 주말 내내 가용 인원이 없을 수도 있을 것을 고려해 3일의 기간으로 잡았어요</li>
</ul>
<h3 id="이렇게">이렇게</h3>
<p>저희의 전략을 서버에 적용해 보았습니다</p>
<h4 id="로깅-aop-수정">로깅 AOP 수정</h4>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/00e0df81-e19a-4954-89a6-49c2bc1f1f0b/image.png" alt="">
INFO LOG에서 기록 전략을 반영하였습니다</p>
<h4 id="logback-수정">LOGBACK 수정</h4>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/31ba6a0a-5696-410d-a81e-c03bba4b979d/image.png" alt=""></p>
<p>로그백 설정을 변경해 주었습니다
해당 설정에서는 날짜별 파일 생성, ERROR 및 WARN 로그의 삭제 시기를 관리할 수 있습니다</p>
<p>자세한 코드는 저희의 <a href="https://github.com/woowacourse-teams/2024-bang-ggood/blob/dev/backend/bang-ggood/src/main/resources/logback.xml">레포지토리</a>에서 확인 가능합니다</p>
<h4 id="ec2-환경에서-압축-및-삭제-스케줄링">EC2 환경에서 압축 및 삭제 스케줄링</h4>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/238a09bc-6849-4bb3-9375-f957ac539fca/image.png" alt="">
INFO 로그의 압축 및 삭제는 sh 명령어 파일을 작성하여 crontab으로 관리하고 있습니다</p>
<pre><code>0 0 * * * /path/to/log_cleanup.sh</code></pre><p>해당 명령어를 베이스로 커스텀할 수 있는데
저희는 3일에 한 번, 매일 새벽 3시에 실행하도록 설정하였어요</p>
<h3 id="마침내">마침내</h3>
<p>저희는 일정 용량 이상 넘어가지 않는 로그 관리 전략을 수립할 수 있었습니다
수료 이후 저희의 자체 EC2로 이전하면서 금전 문제로 인해 스케일도 더 낮은 EC2로 서버를 이전하였는데요
그럼에도 불구하고 로그 용량으로 인해 서버가 종료되는 일이 없었어요</p>
<p>저와 함께, 이건 무조건 해야 한다며 페어로 해 준 제제에게 감사하며</p>
<p>오늘도 방끗 웃는 하루 보내세요 😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[캐시 도입한 후 응답 속도 47% 향상?]]></title>
            <link>https://velog.io/@shin-jisong/%EC%BA%90%EC%8B%9C-%EB%8F%84%EC%9E%85%ED%95%9C-%ED%9B%84-%EC%9D%91%EB%8B%B5-%EC%86%8D%EB%8F%84-47-%ED%96%A5%EC%83%81</link>
            <guid>https://velog.io/@shin-jisong/%EC%BA%90%EC%8B%9C-%EB%8F%84%EC%9E%85%ED%95%9C-%ED%9B%84-%EC%9D%91%EB%8B%B5-%EC%86%8D%EB%8F%84-47-%ED%96%A5%EC%83%81</guid>
            <pubDate>Sun, 24 Nov 2024 06:16:01 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요!
오늘은 방끗 프로젝트에 캐싱을 도입한 얘기를 하려고 합니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/53f1d0d0-ea30-4b4b-bc98-6a5525412004/image.png" alt=""></p>
<p>저희는 기존에 ENUM으로 관리하던 카테고리와 질문 데이터를 데이터베이스로 옮기는 작업을 진행했습니다
이 작업은 카테고리와 질문이 기획에 따라 자주 변경될 가능성이 있는 정보이므로, <strong>기획 변경 시마다 해당 ENUM을 수정하여 프로젝트를 재배포하는 방식은 올바른 설계가 아니다</strong>라는 판단에서 이루어진 결정이었습니다</p>
<p>그러나, 모든 유저가 질문과 카테고리를 조회할 때마다 데이터베이스에 접근하는 방식은 성능 면에서 비효율적이라고 느꼈습니다
이 부분을 개선하면 사용자들에게 더 빠르고 효율적으로 정보를 제공할 수 있지 않을까 하는 고민이 생겼습니다</p>
<p>따라서 사용자마다 변치 않는 정보인 <code>전체 질문</code> <code>전체 카테고리</code> <code>아티클</code>을 캐싱하기로 결정하였어요</p>
<h3 id="로컬-캐시-vs-글로벌-캐시">로컬 캐시 vs 글로벌 캐시</h3>
<p>그렇다면 캐시를 도입할 전략을 택해야 해요
로컬 캐시와 글로벌 캐시 중 무엇을 도입하면 좋을까요?
특징을 비교해 봅시다</p>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>로컬 캐시</strong></th>
<th><strong>글로벌 캐시</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>속도</strong></td>
<td>매우 빠름</td>
<td>상대적으로 느림 (네트워크 비용)</td>
</tr>
<tr>
<td><strong>일관성</strong></td>
<td>서버별로 다를 수 있음</td>
<td>모든 서버가 동일한 데이터 참조</td>
</tr>
<tr>
<td><strong>설정 및 운영</strong></td>
<td>간단함</td>
<td>복잡함 (클러스터 관리 등)</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>서버 증가 시 동기화 어려움</td>
<td>서버 증가와 무관</td>
</tr>
<tr>
<td><strong>데이터 크기</strong></td>
<td>제한적 (서버 메모리 의존)</td>
<td>대용량 처리 가능</td>
</tr>
</tbody></table>
<p>저희는 위 사안들을 고려했을 때 <strong>로컬 캐시</strong>를 도입하기로 했어요</p>
<p>방끗 서버는 2대의 서버를 로드 밸런서로 조율하기 때문에 글로벌 캐시를 사용해야 하는 것 아닌가? 생각이 들 수도 있지만
저희가 캐시를 사용하는 목적은 DB에 있는 데이터를 그대로 캐시에 올려 사용하는 것이기 때문에 데이터 문제가 발생하지 않아요</p>
<h3 id="캐시-전략">캐시 전략</h3>
<p>다만, 저희가 DB의 데이터를 변경할 경우에 캐시에 반영이 되지는 않는다는 단점이 있죠
해당 데이터들은 critical한 데이터가 아니라고 판단하여 하루마다 캐시 만료를 통해 길어도 하루가 지나면 새로운 데이터가 반영되도록 설계하였어요</p>
<p>사실 기획적으로도 생각해 보자면 사용자가 과거에 작성한 체크리스트를 조회했을 때 기존 질문과 바뀌어 있다면 혼돈이 있을 거라 판단하여 질문을 수정, 삭제하는 것은 배제하고 최신 질문 태그를 두어 관리하려고 했어요</p>
<p>이러한 고려 끝에 <strong>캐시는 하루가 지나면 만료시키는 전략</strong>을 수립하였습니다</p>
<h3 id="ehcache-vs-caffeine-cache">ehcache vs caffeine cache</h3>
<p>로컬 캐시 중에서도 저희가 선택할 수 있는 캐시는 꽤 여러 개 있어요
저희는 그중에서도 ehcache와 caffeine cache 둘 중 하나를 선택하고자 결심했는데요</p>
<p>기술이나 도구를 도입할 때 가장 중요한 요소 중 하나는 <code>커뮤니티가 잘 형성되어 있나?</code> 라고 생각해요
문제가 발생했을 때 도움을 받을 수 있는 다양한 자료가 많으며,
많은 유저들이 사용하는 만큼 경쟁력이 있음은 빼놓을 수 없는 요소라고 생각합니다</p>
<p>검색 결과 두 캐시의 자료가 가장 많았기에 두 개를 두고 고민하기 시작했습니다</p>
<p><a href="https://medium.com/naverfinancial/%EB%8B%88%EB%93%A4%EC%9D%B4-caffeine-%EB%A7%9B%EC%9D%84-%EC%95%8C%EC%95%84-f02f868a6192">니들이 caffeine 맛을 알아?</a>
<a href="https://techblog.uplus.co.kr/%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%9C-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0-e394202d5c87">로컬 캐시 선택하기</a></p>
<p>해당 글들을 참고한 결과 단순하고 성능이 좋은 caffeine으로 결정하여 진행하기로 했습니다!
위 블로그에 두 개의 성능과 기타 사항들을 비교한 것이 잘 기록되어 있어서 참고하시면 좋을 듯해요 👍</p>
<h3 id="카페인을-도입해-보자">카페인을 도입해 보자</h3>
<p>캐시를 도입하는 것은 사실 엄청나게 쉬운데요</p>
<ol>
<li><p><strong>config 작성하기</strong>
<img src="https://velog.velcdn.com/images/shin-jisong/post/0dd9e7de-1f01-4c67-87ae-ee1cac052281/image.png" alt=""></p>
</li>
<li><p><strong>캐시 적용하기</strong>
<img src="https://velog.velcdn.com/images/shin-jisong/post/b94a3cf8-c4cd-4342-a501-9d4d4589fce5/image.png" alt=""></p>
</li>
<li><p><strong>캐시 이름 관리하기</strong>
cacheNames에 일일히 String을 적는 것보다는 관리 변수를 하나 두어 일괄적으로 적용하는 것이 더 좋은 설계라고 생각하여 이름을 관리하는 클래스에 public static 변수로 두었어요
<br>원래는 ENUM으로 관리하고 싶었는데 어노테이션 안에서는 name() 함수 적용이 안 되어서 아래 방향으로 구현하였습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/6752170e-8526-43f2-bb6d-a84a86f4737c/image.png" alt=""></p>
</li>
</ol>
<ol start="4">
<li><strong>캐시 테스트 해 보기</strong>
그 다음 두 번 조회 시 한 번만 repository 호출이 된 것 또한 테스트하였습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/c32ae3bd-c3a7-4ed8-9fa9-7502007f5726/image.png" alt=""></li>
</ol>
<p>이렇게 캐시를 도입하였습니다 </p>
<h3 id="응답-속도-측정">응답 속도 측정</h3>
<p>캐시를 도입하고 나서 얼마나 유의미한 개선이 있었는지 파악하고 싶었어요
따라서 k6를 통해서 부하 테스트를 진행하였습니다</p>
<p><strong>측정 조건</strong></p>
<ul>
<li>동시 접속자 수 20명</li>
<li>60초 동안 요청</li>
</ul>
<p><strong>측정할 API들</strong></p>
<ul>
<li>@GetMapping(&quot;/checklists/questions&quot;) → 체크리스트 질문 <strong>7.59% 향상</strong></li>
<li><em>캐싱 전*</em>에는 95%의 사용자가 응답을 받는 데 165.67ms가 걸렸으나
<img src="https://velog.velcdn.com/images/shin-jisong/post/3e4ae008-e013-4c6e-a0c6-83f1c0e91254/image.png" alt=""></li>
<li><em>캐싱 후*</em>에는 153.09ms 소요되었습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/ce7ea40e-c2c4-49b0-b5f7-6a0d063517b2/image.png" alt=""></li>
</ul>
<ul>
<li><p>@GetMapping(&quot;/custom-checklist/all&quot;) → 초반에 커스텀 체크리스트 모두 가져올 때 <strong>39.08% 향상</strong></p>
</li>
<li><p><em>캐싱 전*</em>에는 95%의 사용자가 응답을 받는 데 423.52ms가 걸렸으나
<img src="https://velog.velcdn.com/images/shin-jisong/post/f0aa19cc-8974-46dc-8772-f7f513a645bc/image.png" alt=""></p>
</li>
<li><p><em>캐싱 후*</em>에는 257.99ms 소요되었습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/9e325a2e-67fc-40ac-bce7-6d2f8b09f4c1/image.png" alt=""></p>
</li>
<li><p>@GetMapping(&quot;/articles&quot;) <strong>47.22% 향상</strong></p>
</li>
<li><p><em>캐싱 전*</em>에는 95%의 사용자가 응답을 받는 데 416.04ms가 걸렸으나
<img src="https://velog.velcdn.com/images/shin-jisong/post/7f89b09f-1cda-45c9-a9bc-3ecb730b0de8/image.png" alt=""></p>
</li>
<li><p><em>캐싱 후*</em>에는 219.58ms 소요되었습니다
<img src="https://velog.velcdn.com/images/shin-jisong/post/ec4ce8a8-a7c8-47d8-a353-65d3f4cb09cc/image.png" alt=""></p>
</li>
</ul>
<h3 id="마무리">마무리</h3>
<p>전략 수립부터 캐시 도입, 응답 속도 측정까지 일련의 과정을 담아보았는데요
유의미한 응답 속도 개선으로 이어져 뿌듯한 시간이었습니다!</p>
<p>글 읽어 주셔서 감사합니다 🙇‍♂️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포트 포워딩, "잘" 해 봅시다]]></title>
            <link>https://velog.io/@shin-jisong/%ED%8F%AC%ED%8A%B8-%ED%8F%AC%EC%9B%8C%EB%94%A9-%EC%9E%98-%ED%95%B4-%EB%B4%85%EC%8B%9C%EB%8B%A4</link>
            <guid>https://velog.io/@shin-jisong/%ED%8F%AC%ED%8A%B8-%ED%8F%AC%EC%9B%8C%EB%94%A9-%EC%9E%98-%ED%95%B4-%EB%B4%85%EC%8B%9C%EB%8B%A4</guid>
            <pubDate>Wed, 13 Nov 2024 06:44:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>저는 프로젝트에서 포트포워딩 관련 문제를 만나고 관련 발표를 진행한 경험이 있습니다!
<a href="https://youtu.be/uZdUO9Oi7DA?si=iUZF4qrbYanhPWTa">포트포워딩, 그랬구나</a>
해당 발표의 후속글로 해당 글을 준비하였습니다 👍
궁금한 거 있으면 댓글 남겨 주세요!</p>
</blockquote>
<hr>
<p>개발을 하다 보면 <strong>포트 포워딩</strong>이라는 용어를 종종 접하게 됩니다. 특히 서버 개발자에게 인프라를 구성하며 포트를 분리하는 일은 필수적입니다. 서버를 외부와 연결하거나 특정 네트워크 설정이 필요할 때 이 기술을 사용하게 될 수도 있습니다. 하지만 막상 포트 포워딩을 시도하면 개념이 낯설고 복잡하게 느껴질 수 있습니다. 매우 low level의 지식이니 부담감이 들기도 합니다. 저 역시 “방끗” 프로젝트를 진행하며 포트 포워딩을 경험했고, 그 과정에서 학습의 필요성을 느끼고 진행하였습니다.</p>
<p>이 글에서는 프로젝트를 진행하며 얻은 경험과 그 과정에서 궁금했던 점들을 공유하고자 합니다. 포트 포워딩의 개념을 간단히 설명하고, 이를 효과적으로 설정하는 방법을 실용적인 예시와 함께 살펴보겠습니다.</p>
<h2 id="포트-포워딩"><strong>포트 포워딩?</strong></h2>
<h3 id="포트-포워딩이란-무엇인가"><strong>포트 포워딩이란 무엇인가</strong></h3>
<p><strong>포트 포워딩(Port Forwarding)</strong> 은 직역하면 “포트를 전달하다”라는 의미를 담고 있습니다. 좀 더 자세한 의미를 살펴 보자면, <code>하나의 IP 주소와 포트 번호 결합의 통신 요청을 다른 곳으로 넘겨 주는 네트워크 주소 변환의 응용</code>이라고 정의할 수 있습니다. 다양한 Level에서 해당 개념을 볼 수 있겠지만 오늘은 서버 개발자의 측면에서 포트 포워딩을 다뤄 보도록 하겠습니다. </p>
<p>포트 포워딩은 <strong>서버 개발자</strong>가 원격 네트워크 또는 로컬 네트워크에서 서비스에 접근하기 위해 사용되는 중요한 기법입니다. 특정 포트로 들어오는 트래픽을 다른 포트나 호스트로 전달함으로써 외부 클라이언트가 올바른 서버에 연결되도록 합니다. 서버 개발자들은 방화벽, NAT(Network Address Translation) 등의 네트워크 장비를 통과할 때 클라이언트의 요청이 특정 서버로 도달할 수 있도록 포트 포워딩을 구성합니다. 이를 활용하여 네트워크 구조에 구애받지 않고 애플리케이션과 서비스를 안정적으로 제공할 수 있습니다.</p>
<p><strong>포트 포워딩이 필요한 예</strong>로는 여러 가지 상황이 존재합니다. 예를 들어 가정용 네트워크에서 NAS(Network Attached Storage) 서버를 외부에서 접근할 때, 사무실 내 IP 카메라에 외부에서 접속할 때, 게임 서버를 열어 다른 사용자들이 접속할 수 있도록 할 때, VPN 서버를 구성할 때 등이 있습니다. 특히 클라우드 서버에서는 인바운드 규칙과 더불어 포트 포워딩이 클라이언트와 서버 간의 통신을 원활하게 합니다.</p>
<h3 id="나의-상황에서-포트-포워딩이-필요했던-이유"><strong>나의 상황에서 포트 포워딩이 필요했던 이유</strong></h3>
<p><img src="https://github.com/user-attachments/assets/7314ba06-6983-4c01-8ed0-b3fa951a75a4" alt="image"></p>
<p><strong>AWS EC2 인스턴스</strong>에서 <strong>보안 그룹-Security Group</strong>은 특정 인바운드 및 아웃바운드 트래픽을 허용하는 일종의 가상 방화벽 역할을 합니다. 인바운드 규칙은 서버로 들어오는 요청을 필터링하며, 이를 활용하여 서버가 공격으로부터 보호받을 수 있습니다.</p>
<p>인바운드 보안 설정은 서버가 외부와 통신하는 방식을 제어하여 <strong>보안을 강화</strong>하는 데 중요한 역할을 합니다. 불필요한 포트를 차단하고 신뢰할 수 있는 IP만 접근을 허용함으로써 해커의 침입 경로를 줄이고 무단 접근을 방지할 수 있습니다.</p>
<p>일반적으로 웹 서비스에 사용되는 80(HTTP)과 443(HTTPS) 포트를 열어 외부 사용자가 웹사이트에 접근할 수 있도록 하며, 원격 관리 시에는 SSH의 22 포트를 특정 IP에만 허용하여 관리의 안전성을 높입니다. 또한, 이러한 설정을 사용하면 어떤 IP에서 어떤 요청이 들어오는지 모니터링할 수 있어 이상 징후를 조기에 탐지하고 대응할 수 있으며, 필요 없는 트래픽을 차단함으로써 네트워크 성능을 최적화할 수 있습니다. 결과적으로 인바운드 보안 설정은 서버와 네트워크의 안전을 보장하고 관리 효율성을 높이는 필수적인 단계입니다. </p>
<p><img src="https://github.com/user-attachments/assets/8c39a9a0-5ee2-4f21-ad93-175bd648c850" alt="image"></p>
<p>위와 같은 보안을 고려하여, 저의 인프라 구성에서는 인바운드 보안 설정으로 그림과 같이 80 포트, 443 포트, 특정 IP에 대한 22 포트를 허용해 준 상태입니다. 이러한 인바운드 보안 설정으로 spring 서버가 설정되어 있는 8080 포트로 요청을 전달해 주기 위해서는 <strong>포트 포워딩이 필요</strong>합니다.</p>
<p><img src="https://github.com/user-attachments/assets/aaf9f4bb-ca2a-4894-9f80-56981a155d4b" alt="image"></p>
<p>포트 포워딩을 하기 위해서는 대표적으로 OSI 7계층에서 동작하는 <strong>Nginx</strong>와 OSI 3계층과 4계층에 걸쳐 동작하는 <strong>iptable</strong>을 활용하는 방법이 있습니다. 아래 단락에서 좀 더 상세히 해당 방법들을 알아 보도록 하겠습니다.</p>
<h2 id="포트-포워딩-방법과-원리"><strong>포트 포워딩 방법과 원리</strong></h2>
<h3 id="iptable-포트-포워딩"><strong>iptable 포트 포워딩</strong></h3>
<p><img src="https://github.com/user-attachments/assets/311be996-aa65-473e-8665-945c25790a8c" alt="image"></p>
<p><strong>iptable</strong>은 규칙 집합을 정의할 수 있는 일반 방화벽 소프트웨어입니다. 해당 소프트웨어는 리눅스 커널의 네트워크 스텍에 내장된 프레임 워크인 netfilter의 기능을 활용하는 인터페이스로 동작합니다.  iptable는 네트워크 패킷을 필터링하고 포트나 IP 주소를 변경하는 데 활용됩니다. 포트 포워딩을 설정하기 위해서는 다음과 같은 명령어를 사용할 수 있습니다.</p>
<p><code>iptables -t nat -A PREROUTING -p tcp --dport {타켓 포트} -j REDIRECT --to-port {대상 포트}</code></p>
<p>이 <strong>명령어</strong>는 <strong>외부에서 들어오는 트래픽을 특정 포트로 리다이렉트 시키는 역할</strong>을 합니다. 해당 명령어는 iptable을 활용하여 nat 테이블을 지정합니다. ‘-A’ 명령어는 규칙을 append한다는 의미로 사용하고 있습니다. 만약, ‘-D’를 사용할 경우, 규칙을 delete할 수 있습니다. PREROUTING 규칙은 패킷이 라우팅 되기 전, 패킷이 목적지에 도달하기 전에 뒤에 나오는 규칙을 적용할 수 있습니다. 뒤에 나오는 명령어는, TCP 프로토콜로 들어오는 패킷에 대하여 Destination인 dport가 타겟 포트가 맞다면 패킷을 대상 포트로 리다이렉트하라는 명령어입니다. iptables -h 명령어를 사용한다면 옵션에 대한 설명을 좀 더 자세히 볼 수 있습니다.</p>
<p><img src="https://github.com/user-attachments/assets/d8c2bd93-b1b4-4f16-ae86-944d874d4acb" alt="image"></p>
<p>iptable은 여러 테이블과 체인으로 구성되며, 각 테이블과 체인에는 패킷을 처리하는 규칙이 존재합니다. FILTER 테이블, NAT 테이블, MANGLE 테이블, RAW 테이블 등이 있습니다. 우리가 사용하는 NAT 테이블은 네트워크 주소 변환을 처리하는 테이블입니다. 주로 패킷의 출발지 또는 목적지 IP 주소를 변환합니다. 여기에는 패킷이 라우팅 되기 전에 처리하는 PREROUTING 체인, 패킷이 라우팅된 후에 처리하는 POSTROUTING 체인, 로컬에서 생성된 패킷을 대상으로 처리하는 OUTPUT 체인이 있습니다.</p>
<p><img src="https://github.com/user-attachments/assets/39006c17-3d2b-4046-92f3-10aedf1de6be" alt="image"></p>
<p>반복적으로 등장하는 NAT 키워드에 대해 좀 더 살펴보겠습니다. <strong>NAT - Network Address Translation</strong>은 네트워크에서 패킷의 출발지 또는 목적지 IP 주소를 변환하는 기술입니다. 주로 사설 네트워크와 공용 네트워크 사이의 통신을 가능하게 하기 위해 사용됩니다. 사설 네트워크의 기기들이 인터넷에 접근할 때, NAT는 사설 IP 주소를 공인 IP 주소로 변환해줍니다. 이 과정을 거쳐 사설 네트워크 내부의 IP 주소들이 외부에 노출되지 않으며, IP 주소의 부족 문제도 해결할 수 있습니다. NAT는 라우터와 같은 네트워크 장비에서 동작하며, 공용 네트워크로 나가는 패킷은 NAT를 거치면서 IP 주소가 변환됩니다. 반대로, 외부 네트워크에서 들어오는 패킷도 NAT를 거쳐 내부 IP 주소로 변환됩니다.</p>
<p>NAT는 주로 두 가지 방식으로 작동합니다. 첫 번째는 <strong>Static NAT</strong>로, 이 방식에서는 특정 사설 IP 주소가 항상 같은 공인 IP 주소로 매핑됩니다. 주로 서버와 같이 고정된 IP 주소가 필요한 경우에 사용됩니다. 예를 들어, 기업의 웹 서버가 항상 외부에서 접근 가능한 203.0.113.1이라는 공인 IP 주소를 필요로 할 때, Static NAT를 사용하여 해당 서버의 사설 IP 주소인 192.168.1.10을 항상 같은 공인 IP 주소에 매핑할 수 있습니다. 이러한 방식은 신뢰할 수 있는 외부 접근을 보장하는 데 유리하지만, 할당된 공인 IP 주소를 비효율적으로 사용할 수 있는 단점이 있습니다.</p>
<p>두 번째는 <strong>Dynamic NAT</strong>로, 이 경우 NAT 장비는 내부 사설 IP 주소를 사용 가능한 공인 IP 주소 풀에서 동적으로 할당합니다. 이를 이용해 하나의 공인 IP 주소가 여러 사설 IP 주소와 공유될 수 있습니다. 예를 들어, 회사의 직원들이 사내 네트워크에서 인터넷에 접근할 때, NAT 장비는 각각의 직원 기기의 사설 IP 주소를 공인 IP 주소 203.0.113.2, 203.0.113.3 등으로 변환하여 인터넷에 연결합니다. 이러한 동적 할당은 네트워크의 유연성을 높이고, IP 주소의 효율적인 사용을 가능하게 합니다. 그러나, 이 방식은 공인 IP 주소가 부족한 경우에 사용될 수 있으며, 특정 연결이 항상 같은 공인 IP 주소를 필요로 할 때는 적합하지 않을 수 있습니다.</p>
<p>또한, NAT의 확장 개념으로 <strong>PAT - Port Address Translation</strong>가 있습니다. PAT는 여러 내부 사설 IP 주소가 단일 공인 IP 주소로 변환되면서, 각 연결을 식별하기 위해 포트 번호를 추가로 사용하는 방식입니다. 예를 들어, 내부 네트워크의 여러 장치가 모두 공인 IP 주소 203.0.113.1을 사용하여 인터넷에 접속한다고 가정할 때, NAT 장비는 각 장치의 요청을 관리하기 위해 요청마다 다른 포트 번호를 할당합니다. 이를 이용하여, 단일 공인 IP 주소를 통해 수많은 클라이언트가 동시에 인터넷에 접근할 수 있으며, 이는 IP 주소의 효율적인 활용을 더욱 극대화합니다.</p>
<p>NAT의 이러한 다양한 작동 방식은 네트워크의 요구사항과 구성에 따라 적절히 선택될 수 있으며, 서버 개발자는 이를 이해함으로써 더 효과적인 네트워크 설계 및 관리가 가능합니다.</p>
<h3 id="nginx-포트-포워딩"><strong>Nginx 포트 포워딩</strong></h3>
<p><img src="https://github.com/user-attachments/assets/9c0327da-1d86-4a58-9700-30b7dd6e48ba" alt="image"></p>
<p><strong>Nginx</strong>는 웹 서버 소프트웨어로, 고성능과 효율성 때문에 널리 사용됩니다. 웹 서버 소프트웨어는 클라이언트(브라우저)로부터 HTTP 요청을 받아 HTML 페이지, 이미지, 비디오, 파일 등의 자원을 전달하는 역할을 합니다. 일반적으로 웹 서버는 정적 콘텐츠를 제공하며, 사용자의 요청에 따라 동적 콘텐츠를 제공하는 애플리케이션 서버와 협력할 수 있습니다.</p>
<p>Nginx는 특히 높은 동시 접속을 효율적으로 처리하는 비동기 이벤트 기반 아키텍처를 채택해 빠른 응답 속도와 적은 리소스 소비로 유명합니다. 이를 사용하여 트래픽이 많은 웹 사이트에서도 빠르고 안정적으로 콘텐츠를 제공할 수 있습니다.</p>
<p>포트 포워딩 설정으로 외부 요청을 내부 포트로 전달하여, Nginx는 리버스 프록시로서 동작하면서 네트워크 트래픽을 관리하는 로드 밸런싱의 역할을 수행할 수 있고 내부 애플리케이션 서버의 요청 처리를 보조할 수 있습니다. Nginx에서 포트 포워딩을 적용하기 위해서는 <code>nginx.conf</code> 파일에 리버스 프록시 설정을 추가해야 합니다. 예를 들어, 외부에서 80번 포트로 들어오는 요청을 내부의 8080번 포트로 전달하려면 다음과 같은 설정을 추가할 수 있습니다:</p>
<p><img src="https://github.com/user-attachments/assets/15f2f3d1-74b5-4c2b-8a6b-c734eaa076ef" alt="image"></p>
<p>이 설정으로 Nginx는 클라이언트의 요청을 내부 서버의 8080번 포트로 전달하여 처리합니다.</p>
<p><img src="https://github.com/user-attachments/assets/bccdec22-c91b-4854-9b4d-b9dd9e88e1da" alt="image"></p>
<p>Nginx의 포트 포워딩은 리버스 프록시를 이용하여 외부 클라이언트가 직접 내부 서버에 접근하지 않도록 중계해주는 방식입니다. <strong>리버스 프록시 - Reverse Proxy</strong>란 클라이언트의 요청을 받아 실제 서버로 전달하고, 서버로부터 받은 응답을 다시 클라이언트로 전달하는 서버입니다. 클라이언트와 서버 간의 중개자로서 동작합니다. 이를 바탕으로 클라이언트는 실제로 응답을 제공하는 서버의 존재를 알지 못한 채, 리버스 프록시를 서버로 인식하게 됩니다. 따라서 클라이언트는 Nginx에 요청을 보내고, Nginx는 그 요청을 내부 서버로 전달한 후, 내부 서버의 응답을 다시 클라이언트에게 반환합니다. 이 방식은 서버 구조를 숨기고, 트래픽을 효율적으로 분산시키는 데 유용합니다.</p>
<p>리버스 프록시의 가장 큰 장점 중 하나는 <strong>보안 강화</strong>입니다. 리버스 프록시는 클라이언트와 내부 서버 간의 직접적인 연결을 차단하여, 내부 서버의 IP 주소와 구성을 숨길 수 있습니다. 이것을 이용하여 외부의 공격으로부터 내부 서버를 보호할 수 있으며, DDOS(Distributed Denial of Service) 공격 같은 보안 위협을 완화할 수 있습니다. 또한 Nginx는 다수의 내부 서버 간에 클라이언트의 요청을 분산시키는 <strong>로드 밸런싱 기능</strong>을 제공합니다. 이 기능은 서버의 부하를 고르게 분산시키고, 고가용성을 유지할 수 있습니다. 즉, 서버가 과부하에 걸리지 않도록 지원하여 애플리케이션의 성능과 안정성을 향상시킵니다.</p>
<p>리버스 프록시는 <strong>정적 콘텐츠를 캐싱</strong>하여 반복적인 요청에 대해 내부 서버에 대한 직접적인 요청을 줄일 수 있습니다. 이것은 응답 시간을 줄이고, 서버의 부하를 감소시키며, 클라이언트에게 더 빠른 응답을 제공합니다. 또한 SSL/TLS 암호화를 처리하여 내부 서버가 암호화된 트래픽을 직접 처리하지 않도록 할 수 있습니다. 이로 인해 내부 서버의 성능을 향상시키고, 관리의 복잡성을 줄일 수 있습니다.</p>
<p>이와 같은 이유로, Nginx의 리버스 프록시 기능은 서버 개발자에게 필수적인 도구가 되어 주며, 네트워크 및 애플리케이션 성능을 극대화하는 데 기여합니다. 다양한 사용 사례에서 리버스 프록시는 대규모 웹 애플리케이션에서 여러 대의 백엔드 서버를 운영하면서 클라이언트의 요청을 효율적으로 관리할 수 있도록 돕고, 마이크로서비스 아키텍처에서는 서비스 간의 통신을 간소화하며 보안을 강화하는 데 활용될 수 있습니다.</p>
<h2 id="나의-경험과-사견-정리"><strong>나의 경험과 사견 정리</strong></h2>
<h3 id="각각-방법의-장단점"><strong>각각 방법의 장단점</strong></h3>
<table>
<thead>
<tr>
<th>iptables</th>
<th>Nginx</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점</strong></td>
<td><strong>장점</strong></td>
</tr>
<tr>
<td><strong>고성능</strong></td>
<td><strong>설정 간편성</strong></td>
</tr>
<tr>
<td><code>iptables</code>는 리눅스 커널 수준에서 작동하므로, 네트워크 트래픽을 효율적으로 처리할 수 있으며, 성능 오버헤드가 적고 대규모 트래픽 처리에 적합합니다. 패킷을 빠르게 필터링하고, 최소한의 지연으로 패킷을 전달할 수 있어 성능이 뛰어납니다.</td>
<td>Nginx는 간단한 설정 파일로 쉽게 포트 포워딩과 리버스 프록시를 설정할 수 있으며, HTTP/HTTPS 기반의 트래픽 처리에 최적화되어 있습니다. 설정 예제가 명확하게 문서화되어 있어, 사용자 친화적인 인터페이스를 제공합니다.</td>
</tr>
<tr>
<td><strong>유연성</strong></td>
<td><strong>애플리케이션 레벨 기능</strong></td>
</tr>
<tr>
<td>복잡한 라우팅 규칙을 설정할 수 있으며, 특정 네트워크 인터페이스나 IP 기반의 세밀한 제어가 가능하여 네트워크 전반에 대한 정책 관리를 강화할 수 있습니다. 다양한 규칙을 조합하여 특정 요구사항에 맞춰 세밀한 네트워크 관리가 가능합니다.</td>
<td>SSL/TLS 암호화 처리, 로드 밸런싱, 캐싱 기능 등을 제공하여 웹 애플리케이션의 성능과 보안을 향상시킬 수 있습니다. 이러한 기능들은 트래픽의 신뢰성과 속도를 높이는 데 기여하며, 사용자의 요구에 맞춘 다양한 설정을 지원합니다.</td>
</tr>
<tr>
<td><strong>보안 강화</strong></td>
<td><strong>로드 밸런싱</strong></td>
</tr>
<tr>
<td>방화벽 기능으로 외부에서 들어오는 트래픽을 필터링하여 내부 네트워크를 보호할 수 있습니다. 특정 IP 주소나 포트를 차단하여 악의적인 접근을 차단하고, 보안 규칙을 세분화하여 더욱 강력한 방어 체계를 구축할 수 있습니다.</td>
<td>여러 서버로 트래픽을 분산시켜 고가용성을 보장하고 서버 부하를 줄이는 데 유리합니다. 이를 이용해해 단일 서버의 과부하를 방지하고, 장애 발생 시에도 다른 서버가 요청을 처리하여 서비스 연속성을 유지할 수 있습니다.</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td><strong>단점</strong></td>
</tr>
<tr>
<td><strong>설정 복잡성</strong></td>
<td><strong>성능 한계</strong></td>
</tr>
<tr>
<td>네트워크 지식이 필요하며, 설정이 복잡하고 관리하기 어려울 수 있습니다. 잘못된 설정은 네트워크에 문제를 일으킬 수 있으며, 특히 복잡한 환경에서는 오류가 발생하기 쉬워 관리에 어려움을 겪을 수 있습니다.</td>
<td>Nginx는 애플리케이션 레벨에서 동작하므로 <code>iptables</code>보다 성능 오버헤드가 발생할 수 있으며, 대규모 네트워크 트래픽 처리에 적합하지 않을 수 있습니다. 이로 인해 대량의 동시 연결 처리 시 성능 저하가 발생할 수 있습니다.</td>
</tr>
<tr>
<td><strong>애플리케이션 레벨 기능 부족</strong></td>
<td><strong>프로토콜 제한</strong></td>
</tr>
<tr>
<td><code>iptables</code>는 주로 네트워크 레벨에서 작동하므로 SSL 처리나 로드 밸런싱 같은 고급 애플리케이션 기능을 제공하지 않습니다. 따라서, 애플리케이션의 복잡한 요구를 충족시키기 위해서는 추가적인 도구나 설정이 필요할 수 있습니다.</td>
<td>주로 HTTP/HTTPS 트래픽에 초점을 맞추고 있어 다른 프로토콜(UDP, ICMP 등) 지원이 제한적입니다. 따라서, 다양한 프로토콜을 활용해야 하는 경우에는 대체 솔루션을 고려해야 할 수 있습니다.</td>
</tr>
</tbody></table>
<h3 id="나의-의견-최종-정리"><strong>나의 의견 최종 정리</strong></h3>
<p><img src="https://github.com/user-attachments/assets/b4733647-38c2-48c5-9f33-22ae0e0943d8" alt="image"></p>
<p>저의 경우, 두 방식 중 <strong>Nginx를 사용하여 포트 포워딩을 할 것</strong>이라고 결정하였습니다. 사유는 다음과 같습니다.</p>
<ol>
<li><p><strong>명령어 설정에서의 오류 가능성 감소</strong> <br>
포트 포워딩을 명령어 기반으로 설정할 경우, 특히 iptables와 같이 세밀한 옵션과 플래그를 사용하는 터미널 명령어는 실수를 초래할 가능성이 큽니다. 예를 들어, 옵션의 입력 실수는 예상치 못한 결과를 초래하고 디버깅을 어렵게 할 수 있습니다. 실제로 포트 포워딩을 설정하는 과정에서 여러 번 디버깅에 어려움을 겪었고, 이러한 경험으로 안정적이고 명확하게 설정을 유지할 수 있는 Nginx로 전환하게 되었습니다.</p>
</li>
<li><p><strong>관리 포인트의 효율성</strong> <br>
iptable을 사용하는 경우, 서버 내부에서 기본적으로 사용할 수 있는 포트 포워딩 기능을 제공하기 때문에 추가적인 소프트웨어 도입 없이 설정할 수 있습니다. 그러나 Nginx는 이미 로드 밸런싱과 같은 기능을 위한 선택지로 도입된 상태였기에, 두 가지 방식의 포트 포워딩 방식을 병행하는 것은 관리 포인트를 불필요하게 늘리는 일이 되었습니다. Nginx로 포트 포워딩과 관련된 설정을 일원화함으로써 운영 부담을 줄이고, 모든 포워딩 설정을 Nginx 설정 파일에서 쉽게 관리할 수 있게 되었습니다.</p>
</li>
<li><p><strong>설정 보존 및 유지 관리의 간편성</strong> <br>
EC2와 같은 환경에서 서버를 중지하고 재시작할 때 iptables의 설정은 일시적으로 사라지기 때문에, 매번 동일한 명령어를 반복적으로 입력해야 하는 불편함이 있습니다. 반면, Nginx는 설정 파일(nginx.conf)에 포워딩 관련 설정이 영구적으로 저장됩니다. 서버 재부팅 시에도 Nginx 서비스만 다시 시작하면 이전 설정이 그대로 유지되기 때문에 보다 간편한 설정 복원이 가능합니다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[윈도우만 최신 순서로 조회가 안 된다? 서러운데요🥲]]></title>
            <link>https://velog.io/@shin-jisong/%EC%9C%88%EB%8F%84%EC%9A%B0%EB%A7%8C-%EC%B5%9C%EC%8B%A0-%EC%88%9C%EC%84%9C%EB%A1%9C-%EC%A1%B0%ED%9A%8C%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4-%EC%84%9C%EB%9F%AC%EC%9A%B4%EB%8D%B0%EC%9A%94</link>
            <guid>https://velog.io/@shin-jisong/%EC%9C%88%EB%8F%84%EC%9A%B0%EB%A7%8C-%EC%B5%9C%EC%8B%A0-%EC%88%9C%EC%84%9C%EB%A1%9C-%EC%A1%B0%ED%9A%8C%EA%B0%80-%EC%95%88-%EB%90%9C%EB%8B%A4-%EC%84%9C%EB%9F%AC%EC%9A%B4%EB%8D%B0%EC%9A%94</guid>
            <pubDate>Fri, 01 Nov 2024 08:18:18 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요
오늘은 우아한테크코스 6기 방끗 팀 프로젝트를 진행하며 
BE 팀에서 유일한 윈도우로 겪었던 문제 상황에 대해 말씀드려 보고자 합니다 </p>
<p>제목으로 유추하기 힘든 오늘의 주제는 바로 아래와 같습니다</p>
<blockquote>
<p>LocalDateTime NonoSecond와 OS 간의 상관 관계</p>
</blockquote>
<p>사실 우테코를 진행하며 수많은 윈도우 동지들이 있었는데요
Lv1을 진행하며 그 수는 급감해 버렸어요 
거의 아래 함수와 같은 수렴률...</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/545a0189-594b-43de-87b8-8c1f674d945d/image.png" alt=""></p>
<p>그럼에도 불구하고 저는 꿋꿋하게 윈도우 유저로써 자리를 지켰는데요
방끗 팀을 만나고 BE 구성에서 윈도우 3 맥북 1 조합에 내심 기뻐했었죠
하지만 한 달이 지나자</p>
<pre><code>어라? 왜 나 혼자 윈도우야?</code></pre><p>맥북 2명이 자연 발생하여 윈도우 1 맥북 3 조합이 되었습니다
그리하여 슬퍼진 <strong>윈도우 유저의 글</strong> 시작합니다</p>
<h2 id="localdatetime-nonosecond와-os-간의-상관-관계">LocalDateTime NonoSecond와 OS 간의 상관 관계</h2>
<p>문제 상황은 다음과 같습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/d999c619-e8c4-43b1-9082-ba69fcff8659/image.png" alt=""></p>
<p>이렇게 간헐적으로 최신식으로 조회하는 테스트가 깨지는 상황이 발생하였습니다
최신순으로 조회하는 테스트가 대략 4개정도 있는데,
1개부터 4개까지 랜덤하게 깨지는 문제점이 있었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/5c7bdfe3-e5ca-42bd-ab7c-32ce2dd46daf/image.png" alt=""></p>
<p>이러한 테스트가 있으면 아티클이 1번부터 4번까지 차례대로 저장되고
조회했을 때는 4번부터 1번까지 최신순인 역순으로 조회되어야 하는데
실제로 조회할 때는 순서가 뒤죽박죽인 문제가 있었어요</p>
<p>특히 이 문제는 저에게만 발생하고 방끗 BE 타 크루에게는 발생하지 않으며
간헐적 발생으로 재현이 쉽지도 않아서 해결에 어려움을 겪었습니다</p>
<p><del>저는 실제로 블로그 글 작성을 위해 10번 테스트를 돌렸으나 한 번만 재현에 성공했으며</del>
<img src="https://velog.velcdn.com/images/shin-jisong/post/7fecb2fd-5529-45a4-8212-782a7bc60b3e/image.png" alt=""></p>
<p>위와 같이 createdAt으로 정렬하기 때문에 해당 값의 문제일 거라 생각하고 로그를 찍어 보았습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a02021f0-203d-4b5f-93e6-1e2d4ca4fd2b/image.png" alt=""></p>
<p>재현하지 못한 관계로 지금은 정상적으로 값이 식별 가능하게 찍히지만
당시에는 뒤에 부분 00을 제외한 모든 시간이 일치했어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/68a6151e-5aa7-4b82-b1ec-cfe835fa69e0/image.png" alt=""></p>
<p>반면 맥북에서 로그를 찍었을 때는 6자리만 노출되는 것을 볼 수 있습니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/de4d788a-7e9b-4c75-a9c5-de047df70c05/image.png" alt=""></p>
<p>위 사안들로 추측해 볼 때 <strong><code>LocalDateTime</code> 클래스가 OS 별로 다르게 동작하고 이에 관련한 문제가 있구나!</strong> 를 알 수 있었어요</p>
<p>이를 위해서 <code>AuditingEntityListener.class</code>를 파 보았습니다
해당 클래스는 </p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/2feb1b7b-34e4-4d2c-95c5-33e91c72db56/image.png" alt=""></p>
<p>영속성을 하기 전에 <code>markedCreated</code> 함수를 호출하는데요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/a533d3d9-3511-45b4-b26a-d6b72a8cc98f/image.png" alt=""></p>
<p>해당 함수는 <code>AuditingHandler</code>에 구현되어 있고 이것은 <code>AuditingHandlerSupport</code>를 extends 합니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/0d74e6d0-5646-4b86-8fbe-73872515c897/image.png" alt=""></p>
<p><code>AuditingHandlerSupport</code> 함수에는 <code>setDateTimeProvider</code>가 있는데 
이 함수는 <code>CurrentDateTimeProvider</code>를 사용하고 있고</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/3c965b68-10fc-4171-a16d-fdf6cb7ab464/image.png" alt=""></p>
<p>해당 함수는 바로 <code>LocalDateTime.now()</code>를 사용하고 있어요!</p>
<p>그렇다면 이것이 OS에 종속적인 걸까? 추측해 볼 수 있는데요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/98b2ac0d-153d-4b87-b641-7b5c5c35689a/image.png" alt=""></p>
<p>구현체를 보면 <code>LocalDateTime.now()</code>를 구현할 때 해당 함수를 사용하고 있는데 해당 함수는 &quot;the granularity of the value depends on the underlying operating system&quot; 라는 언급에 따라 정밀도가 OS에 따라 달라질 수 있음을 알 수 있어요</p>
<p>그렇다면 WINDOW와 MAC은 어떻게 다를까요?</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/cca4951c-f6cd-48f2-bf44-22b2f8244dd0/image.png" alt=""></p>
<p>Window의 경우 default timer가 15.6ms 임을 알 수 있습니다 
즉, 15.6ms의 오차가 발생할 가능성이 있으며 위 로그와 같이 아래 2자리수는 반올림되어 00으로 채워집니다</p>
<p>반면에 MacOS의 경우, 오차에 관한 자료를 찾지 못하였으나 대략 1ms까지 정확성을 보장해 준다는 자료를 보았습니다
<a href="https://en.wikipedia.org/wiki/Mach_(kernel)">MacOS에서 Mach라는 커널을 이용해서 TIMER를 측정한다고 합니다</a>
하지만 위 로그와 같이 아래 3자리수는 반올림되어 000으로 채워집니다</p>
<blockquote>
<p><strong>따라서 OS에 따라 정밀도가 달라 오류가 발생했다</strong></p>
</blockquote>
<p>위와 같이 정리하고 갈 수 있을 것 같네요</p>
<p>macOS에서 타이머 해상도가 1ms 단위까지 측정되기 때문에, 반올림 과정에서 insert 활동에 따른 지연이 발생하고 이로 인해 값이 식별 가능할 정도의 오차가 있었던 것으로 추측돼요</p>
<p>반면, 윈도우에서는 기본 타이머 해상도가 15.6ms로, 이로 인해 측정 시 발생하는 오차가 크기 때문에 여러 값이 동일하게 나타날 가능성이 높아졌을 것이라고 추측됩니다</p>
<p>해당 부분에 대해 더 탐구해 보고 싶었으나 low level의 자료라 서치가 쉽지 않았어요
혹시 자료를 보충해 주실 수 있는 분은 댓글 남겨 주세요</p>
<h3 id="그렇다면-이-문제를-어떻게-해결했나">그렇다면 이 문제를 어떻게 해결했나?</h3>
<p>시간이 모두 같으면 정렬을 랜덤으로 해 주어서 테스트가 실패했습니다
해당 상황을 방지하기 위해 저리는 쿼리에 ID로 정렬한다는 부분을 추가해 주었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/fd265269-b04c-4dac-b358-ebed932ce412/image.png" alt=""></p>
<p>이를 통해 늘 같은 순서를 보장할 수 있었습니다</p>
<p>추후 자료 보충이 있을 예정입니다! (확실치는 않아요)
탐구 끝!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Transactional(readOnly = true)를 작성 안 해도 DB 분리가 된다?]]></title>
            <link>https://velog.io/@shin-jisong/TransactionalreadOnly-true%EB%A5%BC-%EC%9E%91%EC%84%B1-%EC%95%88-%ED%95%B4%EB%8F%84-DB-%EB%B6%84%EB%A6%AC%EA%B0%80-%EB%90%9C%EB%8B%A4</link>
            <guid>https://velog.io/@shin-jisong/TransactionalreadOnly-true%EB%A5%BC-%EC%9E%91%EC%84%B1-%EC%95%88-%ED%95%B4%EB%8F%84-DB-%EB%B6%84%EB%A6%AC%EA%B0%80-%EB%90%9C%EB%8B%A4</guid>
            <pubDate>Wed, 30 Oct 2024 04:18:45 GMT</pubDate>
            <description><![CDATA[<p>평화로운 우테코 6시 레벨5 미션!
DB를 분리하고 복제 지연을 해결하는 미션입니다
<a href="https://github.com/shin-jisong/java-coupon/tree/step1">원활한 배경 설명을 위해 깃헙 링크 첨부합니다</a></p>
<p>미션을 진행하며 리뷰이에게 리뷰를 남기는 와중에 뜻밖의 사실을 알게 되었어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/651df5fe-fc44-4de5-84b1-9dfd07aae443/image.png" alt=""></p>
<p>사실 저도 궁금해서 여쭤봤는데...
슬쩍 여쭤보고 지식만 쏙쏙 흡수하려고 했는데 딱 걸렸어요</p>
<p>짐작가는 게 있다면</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/db0c9b49-3ee6-4f7e-bfa9-1f4c617d37a7/image.png" alt=""></p>
<p>JPA에서는 대부분의 것들을 지원해 주기 때문에 당연히 이런 부분까지 지원해 줄 수도 있겠다 추측하였지만 공식 문서를 뒤져보니 광활한 공식 문서에서 찾지 못하였어요</p>
<p>그래서 넘겼다가 여유가 생겼을 때 작성해 봅니다</p>
<p>사실 원리는 간단해요
저는 아래 코드와 같이 JpaRepository를 extends하여 사용하고 있습니다</p>
<pre><code>public interface CouponRepository extends JpaRepository&lt;Coupon, Long&gt; </code></pre><p>이 JpaRepository를 타고타고 들어가면 SimpleJpaRepository가 있어요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/8824352f-c597-4eef-889f-a918768741f7/image.png" alt=""></p>
<p>여기 옵션을 보시면 @Transactional에 ReadOnly 옵션이 있는게 보이시나요?
해당 class에 이미 readOnly 옵션이 걸려 있어서 Service단에 Transaction을 걸지 않아도 DB가 분리되어 나갑니다
(물론 Service 단에 Transactional을 거냐마냐는 여러 논의가 있지만 해당 글에서 다루지는 않습니다)</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/87c3cdf3-3e74-4b7d-9f69-6eb516e6e6ff/image.png" alt=""></p>
<p>위 사진과 같이 delete 혹은 save 같은 WRITE DB로 가야 할 쿼리들은 따로 @Transactional을 걸어 주어 <code>readOnly = false</code> 옵션이 적용됩니다</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/285d0e70-1fe3-4bb5-adb8-97b8cca05bf8/image.png" alt=""></p>
<p>find 함수에는 따로 Transactional을 걸지 않아 <code>readOnly = true</code> 옵션이 적용되고요</p>
<p>그렇다면 여기서 하나의 의문이 생깁니다</p>
<blockquote>
<p>❓ Transaction의 전파를 공부할 때에 <code>@Transactional</code>의 기본 전파 레벨은 <code>Required</code>라고 배웠어요 즉, 상위 트랜잭션에 묶이게 됩니다 하지만 클래스 단위로 <code>readOnly = true</code> 옵션을 적용하고 함수 단위로 <code>readOnly = false</code>  옵션을 적용했을 때에는 왜 함수의 트랜잭션 먼저 적용이 되는가?</p>
</blockquote>
<p>이건 <a href="https://docs.spring.io/spring-framework/docs/4.3.14.RELEASE/spring-framework-reference/html/transaction.html">공식 문서</a>를 참고하면 알 수 있는 답이에요</p>
<p><img src="https://velog.velcdn.com/images/shin-jisong/post/9c02940c-f7cd-44d6-9aa1-a110052acbd4/image.png" alt=""></p>
<p>바로 클래스 레벨과 메서드 레벨에 <code>@Transactional</code>이 둘 다 걸려 있을 경우 메서드 레벨에 걸려 있는 것이 우선적으로 적용되기 때문입니다</p>
<p>해당 구현은 TransactionAspectSupport의 invokeWithinTransaction 메서드 부분을 보면 알 수 있습니다</p>
<pre><code class="language-JAVA">// 트랜잭션 경계 내에서 메서드를 호출하는 메서드
protected Object invokeWithinTransaction(Method method, Class&lt;?&gt; targetClass, InvocationCallback invocation) throws Throwable {
    // 메서드와 클래스의 트랜잭션 속성 가져오기
    TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);

    // 트랜잭션을 설정할 트랜잭션 관리자 선택
    PlatformTransactionManager tm = determineTransactionManager(txAttr);

    // 트랜잭션 식별을 위한 메서드 이름 가져오기
    String joinpointIdentification = methodIdentification(method, targetClass);

    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // 타겟 메서드를 실제로 호출
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        // 예외 발생 시 트랜잭션 처리 완료
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        // 트랜잭션 정보 정리
        cleanupTransactionInfo(txInfo);
    }

    // 성공적으로 메서드가 완료된 경우 트랜잭션 커밋
    commitTransactionAfterReturning(txInfo);
    return retVal;
}</code></pre>
<p>위 부분에서 <code>메서드와 클래스의 트랜잭션 속성 가져오기</code> 코드를 살펴보면 메서드와 클래스 중 우선 적용할 트랜잭션을 찾음을 알 수 있어요!</p>
<pre><code>해당 글을 작성하는 데 도움을 준 우아한테크코스 6기 BE 폴라에게 감사드리며 글을 마무리합니다 😊</code></pre>]]></description>
        </item>
    </channel>
</rss>