<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>zo_meong.log</title>
        <link>https://velog.io/</link>
        <description>Server Developer</description>
        <lastBuildDate>Sun, 10 Aug 2025 17:28:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>zo_meong.log</title>
            <url>https://velog.velcdn.com/images/zo_meong/profile/5f20705f-eea3-4f60-9c95-c7543d5e1797/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. zo_meong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/zo_meong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Project] 반경 내 조회 기능 성능 비교 테스트 (DB vs Redis GEO) #2]]></title>
            <link>https://velog.io/@zo_meong/Project-%EB%B0%98%EA%B2%BD-%EB%82%B4-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-DB-vs-Redis-GEO-2</link>
            <guid>https://velog.io/@zo_meong/Project-%EB%B0%98%EA%B2%BD-%EB%82%B4-%EC%A1%B0%ED%9A%8C-%EA%B8%B0%EB%8A%A5-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-%ED%85%8C%EC%8A%A4%ED%8A%B8-DB-vs-Redis-GEO-2</guid>
            <pubDate>Sun, 10 Aug 2025 17:28:50 GMT</pubDate>
            <description><![CDATA[<p>OhDelivery 프로젝트의 반경 내 조회 기능은 배달 주문이 들어왔을 때, 해당 가게의 반경 Nkm 이내에 있는 배달 가능한 상태의 라이더들을 조회하고 알림을 보내는 기능이다.</p>
<p>이 때, 라이더들의 위치가 실시간으로 변하므로 기존에 구현한 DB에서 조회하는 방식은 성능 저하가 발생할 것으로 예상하였다. 이에 Redis GEO 도입후 Jmeter를 통해 성능을 비교해보고자 한다.</p>
<h1 id="테스트-시나리오">테스트 시나리오</h1>
<h2 id="환경">환경</h2>
<p>테스트 도구 : Apache JMeter
라이더 수 : 50 / 100 (실시간 위치 업데이트)
조회 요청 스레드 수 : 100 / 200 (동시 조회 요청)</p>
<p>라이더 수와 조회 요청 스레드 수를 조절하며 3가지 테스트 케이스를 진행하고 비교한다.</p>
<h2 id="시나리오">시나리오</h2>
<blockquote>
<p>Thread Group 1 : Rider 위치 변경 API 부하
  → 랜덤 라이더 ID 선택 후 랜덤 좌표를 생성하여 실시간으로 위치 업데이트</p>
</blockquote>
<p>Thread Group 2 : 라이더 조회 API 부하
  → 가게 좌표 반경 내 10km 이내의 라이더들을 조회
  → 50% DB 조회 / 50% Redis 조회
  → 동시 실행하여 같은 환경에서 비교 </p>
<p>실시간으로 변하는 라이더 위치를 업데이트 해야하고 배달 주문이 하나 들어올 때 마다 가게 주변의 라이더를 매번 새롭게 조회해야 하므로 위와 같이 두 개의 테스트 그룹을 설정하여 진행한다.</p>
<h2 id="측정-지표">측정 지표</h2>
<pre><code>•    Average Response Time (ms)
•    P50 / P90 / P95 / P99 Response Time (ms)
•    Error %
•    Throughput (TPS)</code></pre><h1 id="테스트">테스트</h1>
<p>Case 1.</p>
<ul>
<li>라이더 수 : 50</li>
<li>조회 요청 스레드 수 : 100</li>
<li>지속 시간 : 10분</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/399399a3-c4a4-4bcf-b053-6621876c7efe/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/81358c17-1567-4aaf-b7eb-92726a6ae9eb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/16354969-84ad-4ad2-852e-60bedce97cab/image.png" alt=""></p>
<p>Case 2.</p>
<ul>
<li>라이더 수 : 50</li>
<li>조회 요청 스레드 수 : 200</li>
<li>지속 시간 : 10분</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/eda960ab-c574-4f7c-aa2d-e0a6e53335b3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/8c72ae00-6e5d-4430-bbad-42b1f1ae291a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/1e53595d-297a-40ea-aaec-53efa3f6a60b/image.png" alt=""></p>
<p>Case 3.</p>
<ul>
<li>라이더 수 : 100</li>
<li>조회 요청 스레드 수 : 100</li>
<li>지속 시간 : 10분</li>
</ul>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/4e94b4d4-931e-496c-a28a-2f636bd89f79/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/53cbb827-d479-43c1-b52f-73e82d057860/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/95570065-f463-4a3d-a582-57cd34f365c8/image.png" alt=""></p>
<h1 id="인사이트">인사이트</h1>
<p>P95는 95%의 요청이 몇 ms안에 처리되는지 나타낸 지표로 실사용자 경험과 가장 가깝기 때문에 이를 기준으로 세가지 테스트 케이스에 대한 속도 개선을 비교해보도록 하자</p>
<table>
<thead>
<tr>
<th>P95</th>
<th align="right">DB (ms)</th>
<th align="right">Redis (ms)</th>
<th align="right">개선율 (%)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Case #1</strong></td>
<td align="right">1130</td>
<td align="right">518</td>
<td align="right">54.16%</td>
</tr>
<tr>
<td><strong>Case #2</strong></td>
<td align="right">4123</td>
<td align="right">843</td>
<td align="right">79.55%</td>
</tr>
<tr>
<td><strong>Case #3</strong></td>
<td align="right">1084</td>
<td align="right">738</td>
<td align="right">31.92%</td>
</tr>
<tr>
<td><strong>평균</strong></td>
<td align="right">-</td>
<td align="right">-</td>
<td align="right"><strong>55.21%</strong></td>
</tr>
</tbody></table>
<p>결과를 보면, DB를 통한 조회 방식보다 Redis GEO를 활용한 방식이 <strong>평균적으로 55%의 속도 개선</strong> 을 이루었음을 확인할 수 있다. 특히나 조회 요청 스레드가 2배 많은 케이스에서 DB 조회 방식이 눈에 띄게 느려진 것을 확인할 수 있고 이 경우 Redis GEO가 80% 빠른 것을 확인할 수 있었다. </p>
<p>다만 TPS는 모든 테스트 케이스에서 거의 동일한 수치를 보였다.
또한 에러율은 모든 테스트 케이스에서 0.01% 미만으로 안정적인 수치를 보였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 면접 대비 - 파이썬 #1]]></title>
            <link>https://velog.io/@zo_meong/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-%ED%8C%8C%EC%9D%B4%EC%8D%AC-1</link>
            <guid>https://velog.io/@zo_meong/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-%ED%8C%8C%EC%9D%B4%EC%8D%AC-1</guid>
            <pubDate>Fri, 08 Aug 2025 14:45:21 GMT</pubDate>
            <description><![CDATA[<h1 id="리스트와-튜플의-차이">리스트와 튜플의 차이</h1>
<p>리스트는 가변, 튜플은 불변이다. 튜플은 딕셔너리 키로 사용할 수 있다.</p>
<pre><code class="language-python">lst = [1, 2, 3]
tpl = (1, 2, 3)

lst[0] = 9       # 가능
tpl[0] = 9       # TypeError 발생</code></pre>
<h1 id="gil이란">GIL이란?</h1>
<p>GIL(Global Interpreter Lock)은 하나의 스레드만 파이썬 바이트 코드를 실행하도록 하는 락이다. 멀티 스레딩의 성능을 제한하지만, IO-bound 작업에는 유리하다. 파이썬은 스레드에 대해 안정성 있는 접근을 보장하기 위해 GIL을 사용한다.</p>
<h1 id="global과-nonlocal의-차이">global과 nonlocal의 차이</h1>
<p>global은 전역 변수를 참조, 수정할 때 사용한다.
nonlocal은 중첩 함수에서 상위 함수의 지역 변수를 참조, 수정할 때 사용한다.</p>
<pre><code class="language-python">x = 0

def outer():
    y = 1
    def inner():
        nonlocal y
        y = 10
    inner()
    print(y)  # 10

def change():
    global x
    x = 100

outer()
change()
print(x)  # 100</code></pre>
<h1 id="데코레이터란">데코레이터란?</h1>
<p>데코레이터는 기존의 함수를 수정하지 않고 기능을 확장하는 함수이다.</p>
<pre><code class="language-python">def logger(func):
    def wrapper(*args, **kwargs):
        print(f&quot;Calling {func.__name__}&quot;)
        return func(*args, **kwargs)
    return wrapper

@logger
def greet():
    print(&quot;Hello&quot;)

greet()

# 결과
Calling greet
Hello</code></pre>
<h1 id="예외-처리-실행-순서">예외 처리 실행 순서</h1>
<p>Q. try-except-else-finally의 실행 순서는?</p>
<p>try : 예외 발생의 가능성이 있는 코드
except : 예외 발생시 실행
else : 예외가 없을 때 실행
finally : 예외와 무관하게 실행</p>
<h1 id="컴프리헨션의-장점">컴프리헨션의 장점</h1>
<p>리스트, 딕셔너리 컴프리헨션은 간결하고 효율적으로 새로운 컬렉션을 생성 할 수 있도록 한다. 반복문보다 빠르고 직관적이다.</p>
<pre><code class="language-python">squares = [x*x for x in range(5)]
evens = {x: x%2 == 0 for x in range(5)}</code></pre>
<h1 id="call-by">call by</h1>
<p>함수의 파라미터를 받을 때, call by value는 변수에 담긴 값 자체를 스택에 복사하여 넘겨준다. call by reference는 변수가 가리키는 메모리 주소(참조) 값을 전달한다. 파이썬은 <strong>Call by Object Reference</strong> 또는 Call by Assignment 방식을 사용한다. Call by Object Reference란 함수에 객체의 주소를 전달하되, 그 참조 자체는 값처럼 복사되는 것이다. 이로 인해 가변 객체는 함수 내부에서 변경이 원본에 반영되어 변경 가능한 것이고, 불변 객체는 그렇지 않은 것이다.</p>
<pre><code class="language-python">def change(x):
    x = x + 1
    print(&quot;inside:&quot;, x)

a = 10
change(a)
print(&quot;outside:&quot;, a)

# 결과
inside: 11        # 새로운 int 객체를 생성하여 x에 바인딩
outside: 10        # int는 불변 객체이므로 outside의 a는 변경되지 않음

---

def modify(lst):
    lst.append(4)
    print(&quot;inside:&quot;, lst)

my_list = [1, 2, 3]
modify(my_list)
print(&quot;outside:&quot;, my_list)

# 결과
inside: [1, 2, 3, 4]
outside: [1, 2, 3, 4]    # list는 가변 객체이므로 outside에 수정 사항이 반영됨</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 반경 내 조회 기능 성능 비교 테스트 (DB vs Redis GEO) #1]]></title>
            <link>https://velog.io/@zo_meong/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-DB-vs-Redis-GEO</link>
            <guid>https://velog.io/@zo_meong/%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0-DB-vs-Redis-GEO</guid>
            <pubDate>Sat, 17 May 2025 11:41:50 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>OhDelviery 프로젝트의 라이더 알림 기능을 구현하던 중 주변의 라이더를 조회하는 로직에 대한 성능 테스트를 진행해보기로 하였다. 해당 로직은 배달 이벤트 발생 시 가게 반경 10km이내의 라이더들을 조회해오는 로직으로 1. DB에서 현재 상태가 배달 가능인 라이더들을 모두 조회한 뒤, 가게에서 라이더의 현재 위치까지의 거리를 각각 계산하고 필터를 걸어 최종 결과 리스트를 반환하는 방식과 2. 배달 가능한 라이더만 올려놓은 Redis에서 Redis GEO를 통해 설정한 거리 이내의 라이더 리스트를 반환하는 방식으로 구현하였다.
유의미한 결과의 테스트를 위해서 가게 위치 주변에 3000명의 라이더가 있는 것으로 가정하고 mock 데이터를 만들어서 테스트하였다.</p>
<h1 id="테스트">테스트</h1>
<h2 id="1-db">1. DB</h2>
<pre><code class="language-java">private List&lt;Rider&gt; getRidersWithDB(Double sLat, Double sLon) {
    // 상태가 AVAILABLE인 라이더 DB에서 전체 조회
    List&lt;Rider&gt; riders = riderRepository.findAllByStatus(RiderStatus.AVAILABLE);

    // 조회해온 라이더 중 현재 위치가 가게에서 반경 10KM 이내인 라이더 필터링 (위경도로 계산)
    riders = riders.stream()
        .filter(rider -&gt; {
          double dis = calculateDistance(sLat, sLon, rider.getLatitude(), rider.getLongitude());
          return dis &lt;= 10.0;
        })
        .toList();

    return riders;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/670dbc2e-242e-41d9-9db3-c3f82bdff0ac/image.png" alt=""></p>
<h2 id="2-redis-geo">2. Redis Geo</h2>
<p>Redis Geo로 반경 10km 이내의 라이더 조회 후, <strong>조회한 라이더 ID 리스트로 DB에서 라이더 정보 조회</strong></p>
<pre><code class="language-java">private List&lt;Rider&gt; getRidersWithRedis(Double sLat, Double sLon) {
    // Redis GEO로 라이더 조회
    GeoResults&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; results = redisRiderLocRepository
        .searchByLoc(sLon, sLat);

    List&lt;Long&gt; riderIds = results.getContent().stream()
        .map(geoLocation -&gt; geoLocation.getContent().getName())
        .map(Long::valueOf)
        .toList();

    // 조회한 라이더 ID 리스트로 DB에서 라이더 정보 조회
    return riderRepository.findAllByRiderIdIn(riderIds);
  }

public GeoResults&lt;GeoLocation&lt;String&gt;&gt; searchByLoc(Double lon, Double lat) {
    return redisTemplate.opsForGeo().search(
        RIDER_LOC_PREFIX,
        GeoReference.fromCoordinate(lon, lat),
        new Distance(10.0, RedisGeoCommands.DistanceUnit.KILOMETERS)
    );
  }</code></pre>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/2961d17e-32cb-4180-b18f-887f0bea6ddb/image.png" alt=""></p>
<p>Redis Geo를 활용하는 것이 DB 조회보다 빠를 것이라고 예상했는데 반대의 결과가 나옴. 라이더 id 리스트로 다시 DB에서 조회해오는 로직 때문일 것이라고 생각하고 라이더의 정보도 레디스에 저장한 뒤 테스트를 진행해보기로 함.</p>
<hr>
<p>Redis Geo로 반경 10km 이내의 라이더 조회 후, <strong>조회한 라이더 ID 리스트로 Redis에서 라이더 정보 조회</strong></p>
<pre><code class="language-java">private List&lt;Rider&gt; getRidersWithRedis(Double sLat, Double sLon) {
    GeoResults&lt;RedisGeoCommands.GeoLocation&lt;String&gt;&gt; results = redisRiderLocRepository
        .searchByLoc(sLon, sLat);

    List&lt;String&gt; riderIds = results.getContent().stream()
        .map(geoLocation -&gt; geoLocation.getContent().getName())
        .toList();

    // 라이더 정보를 DB가 아닌 Redis에서 조회
    List&lt;Rider&gt; riders = new ArrayList&lt;&gt;();
    for (String riderId : riderIds) {
      Rider rider = redisRiderLocRepository.getRiderInfo(riderId);
      riders.add(rider);
    }

    return riders;
  }</code></pre>
<p><del>라이더 테이블을 한 번 밀고 다시 저장했더니 위치가 조금 달라져서 조회해오는 라이더 수도 좀 달라졌지만 30명 차이라 별로 상관없을듯</del>
<img src="https://velog.velcdn.com/images/zo_meong/post/daebe66e-a542-4ecb-b8a0-5e64c3973cba/image.png" alt=""></p>
<p>근데 이 로직은 조회 시간이 엄청나게 늘어버리는 문제 발생...
로직을 다시 살펴보면 </p>
<pre><code class="language-java">public Rider getRiderInfo(String riderId) {
    String riderInfo = redisTemplate.opsForValue().get(RIDER_INFO_PREFIX + riderId);</code></pre>
<p>라이더 한 명마다 매번 조회를 하고 있어서 사실상 3000번의 조회가 일어나고 있으므로 당연히 성능 저하가 발생한다...</p>
<hr>
<p><strong>mget을 통해 멀티 조회</strong>를 하는 방식으로 변경해보자</p>
<pre><code class="language-java">public List&lt;Rider&gt; getRidersInfo(List&lt;String&gt; riderIds) {
    List&lt;String&gt; keys = riderIds.stream()
        .map(id -&gt; RIDER_INFO_PREFIX + id)
        .toList();

    List&lt;String&gt; riderInfos = redisTemplate.opsForValue().multiGet(keys);

    List&lt;Rider&gt; riders = new ArrayList&lt;&gt;();
    for (String riderInfo : riderInfos) {
      if (riderInfo != null) {
        try {
          riders.add(objectMapper.readValue(riderInfo, Rider.class));
        } catch (JsonProcessingException e) {
          throw new RuntimeException(&quot;Failed to deserialize rider info&quot;, e);
        }
      }
    }
    return riders;
  }</code></pre>
<p>이렇게 하니 시간이 줄긴했는데 결론적으로 맨 처음의 DB조회랑 별로 차이가 안남.
<img src="https://velog.velcdn.com/images/zo_meong/post/dd3eb8cd-3803-4aed-bd0f-a8687ad3cbc3/image.png" alt=""></p>
<hr>
<p>왜 이런 결과가 나타날까 궁금해서** Redis GEO로 라이더의 id리스트만 조회**하는 부분을 속도 측정했더니 평균 10ms 정도로 빠른 속도를 보였다. 결국 라이더 정보 조회를 위해 한 번 더 레디스나 DB에 왔다 갔다 하면서 총 두 번의 I/O가 일어나기 때문에, 한 번의 I/O로 처리하는 맨 처음의 로직과 성능의 차이가 별로 없었던 듯 하다.</p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/a0dc3098-8f5e-4f77-91ed-068f5169916d/image.png" alt=""></p>
<p>다만, 지금의 테스트는 라이더의 위치 정보가 고정적인 상태로 진행되었기 때문에, 실제의 서비스 환경과는 좀 다를 것 같아서 <em>라이더의 위치 정보가 실시간으로 변하고 있는 상황에서</em> 2차 테스트를 다시 진행해보도록 하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] @Qualifier와 Lombok 생성자]]></title>
            <link>https://velog.io/@zo_meong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Qualifier%EC%99%80-Lombok-%EC%83%9D%EC%84%B1%EC%9E%90</link>
            <guid>https://velog.io/@zo_meong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Qualifier%EC%99%80-Lombok-%EC%83%9D%EC%84%B1%EC%9E%90</guid>
            <pubDate>Sun, 20 Apr 2025 17:24:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/zo_meong/post/e85f108c-2379-41a4-9286-f1ef0e573cfa/image.png" alt=""></p>
<h1 id="문제">문제</h1>
<p>이번 스프링부트 프로젝트에서 인터페이스의 구현체를 선택해서 주입해야하는 부분이 있었다. 구현체를 주입하는건 간단하고 다양한 방법이 존재하지만, 스프링 빈 주입시 어떤 구현체를 주입할지 명확하게 지정할 수 있도록 해주는 <code>@Qualifier</code> 애너테이션을 적용해보기로 하였다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class AgentMatchingService {
    private final ApplicationEventPublisher eventPublisher;

      @Qualifier(&quot;leastBusy&quot;)
      private final AgentMatchingStrategy strategy;
}

@Component(&quot;leastBusy&quot;)
public class LeastBusyStrategy implements AgentMatchingStrategy {
}</code></pre>
<p>처음엔 이런식으로 구현했다. 
<em>그런데 <code>@Qualifier</code>로 지정한 구현체가 주입되지 않는 문제가 발생했다.</em></p>
<h1 id="원인">원인</h1>
<p>문제의 원인은 롬복 생성자 때문이었다. 롬복의 자동 생성 기능 (<code>@AllArgsConstructor</code>, <code>@RequiredArgsConstructor</code> 등)은 단순히 생성자를 자동 생성할 뿐, <strong>필드에 붙어있는 <code>@Qualifier</code>를 인식해서 주입되는 파라미터에 붙이지 못한다.</strong></p>
<h1 id="해결">해결</h1>
<h2 id="1-생성자-정의">1. 생성자 정의</h2>
<p>가장 명확하고 빠른 해결 방법으로는 롬복의 생성자를 사용하지 않고 그냥 직접 생성자를 정의하면 된다.</p>
<pre><code class="language-java">public AgentMatchingService(
      ApplicationEventPublisher eventPublisher,
      @Qualifier(&quot;leastBusy&quot;) AgentMatchingStrategy strategy) 
  {
    this.eventPublisher = eventPublisher;
    this.strategy = strategy;
  }</code></pre>
<h2 id="2-config">2. config</h2>
<p>이 방법은 구글링하다가 찾은 방법인데 롬복의 설정 파일을 추가해 <code>@Qualifier</code>를 인식하도록 하는 방법도 있다고 한다.</p>
<p>루트 디렉토리에 <code>lombok.config</code> 파일을 생성하고 아래의 내용을 추가한다.</p>
<pre><code class="language-properties">lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier</code></pre>
<p>다만 이 방법을 적용하지 않은 이유는, 우리 프로젝트는 멀티 모듈로 구성된 MSA 프로젝트이므로 설정 파일을 추가하는 것보단 명시적으로 생성자를 사용하는게 더 간단하고 안정적인 해결 방법이라 생각했기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 알림 서비스 기술적 의사 결정]]></title>
            <link>https://velog.io/@zo_meong/%EC%95%8C%EB%A6%BC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC-%EA%B2%B0%EC%A0%95</link>
            <guid>https://velog.io/@zo_meong/%EC%95%8C%EB%A6%BC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC-%EA%B2%B0%EC%A0%95</guid>
            <pubDate>Thu, 10 Apr 2025 09:10:44 GMT</pubDate>
            <description><![CDATA[<h1 id="기획">기획</h1>
<p>이번 프로젝트는 라이더용 배달 어플로, 주문이 발생하면 가게 근처의 라이더들에게 주문 배달 알림을 전송하는 기능을 구현해야한다. 이 때, 이 알림 기능을 어떤 기술로 구현할지에 대해 고민해보았다.</p>
<h2 id="메시지-큐">메시지 큐</h2>
<p>우선 이번 알림 서비스 구현은 이벤트 기반의 메시지 큐를 활용하여 구현하기로 결정했다. 메시지 큐 활용시 알림 서비스가 비동기적으로 실행되어 빠르고 안정적으로 처리될 수 있을 뿐만 아니라 시스템 간의 결합도를 낮춰 유지보수와 확장에 용이하기 때문이다.</p>
<p>그렇다면 메시지 큐를 어떤 기술로 구현할 것인가에 대한 고민을 하게 되었는데 가장 일반적인 메시지 큐인 RabbitMQ, kafka와 Redis Pub/Sub까지 함께 고려해보았다.</p>
<h3 id="redis-pubsub">Redis Pub/Sub</h3>
<p>Redis Pub/Sub은 큐에 메시지를 저장하는 형태가 아닌, 메시지 발행과 함께 구독자들에게 즉시 전달 후 소멸되는 형태이다. 메시지가 저장되지 않으므로 메시지 전달에 대한 보장이 약하다. 즉 신뢰성이 떨어진다. 그렇지만 구현이 가장 간단하다.</p>
<p>Redis Pub/Sub은 실시간 처리에만 적합하고 알림 전달 보장이 필요한 시스템에서는 적합하지 않다고 할 수 있다. 우리 서비스는 라이더들에게 배달에 대한 알림 전송의 안정성이 중요하다고 볼 수 있으므로 Redis Pub/Sub은 고려 대상에서 제외하기로 결정했다.</p>
<h3 id="rabbitmq-vs-kafka">RabbitMQ VS Kafka</h3>
<p>RabbitMQ와 Kafka는 둘 다 발행된 메시지를 큐에 저장하는 방식이다. 전송에 실패하더라도 사용자가 알림을 다시 받을 수 있다. 즉 신뢰성 높은 메시지 처리가 가능하다.</p>
<p>RabbitMQ와 Kafka의 가장 큰 차이는 대용량 스트림인데 Kafka는 고용량 대용량 처리에 강하여 메시지 내구성이 매우 좋다. 다만 우리 서비스에서 한 번에 수십만건의 알림을 처리할 것도 아니고 단순 알림 서비스에 Kafka를 적용하기에는 구현 난이도, 비용, 스펙 측면에서 모두 과하다고 생각했기 때문에 RabbitMQ로 결정했다.</p>
<p>그러나 이 결정을 번복하게 되는데.... 이유는 이벤트를 발행하는 로직에서 이미 Kafka로 이벤트를 발행하고 있기 때문이었다. Kafka는 producer에서 이벤트를 한 번 발행하면 consumer쪽에서 group id를 다르게 설정하여 여러 다른 도메인에서 하나의 이벤트에 대해 각각 처리할 수 있다는 특징이 있다. 즉, 알림 쪽에서 RabbitMQ를 사용하게 되면 이벤트를 두 번 발행해야하지만, kafka를 사용하게 되면 추가적인 이벤트 발행 로직 없이 이미 발행된 이벤트를 활용하면 된다.</p>
<p>뿐만 아니라, 프로젝트 전반적으로 Kafka를 사용하고 있었으나 RabbitMQ를 사용하고 있는 도메인은 없다. 여기서 알림 기능에 RabbitMQ를 도입하게 되면 RabbitMQ 도커 컨테이너를 추가로 띄우고 설정해야하므로 추가적인 비용이 발생하게 된다.</p>
<p>이러한 이유로 Kafka로 통일하여 구현하기로 결정하였다.</p>
<h2 id="클라이언트">클라이언트</h2>
<p>처음에는 우리 서비스에 프론트가 존재하지 않아서 Slack 알림으로 구현하였다. 그러나 후에 WebSocket을 활용한 알림 기능도 구현하면 좋을 것 같아서 WebSocket Stomp를 활용해 두 가지 알림이 비동기로 전송될 수 있도록 구현하기로 결정햇다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 기술 면접 대비 #2]]></title>
            <link>https://velog.io/@zo_meong/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-2</link>
            <guid>https://velog.io/@zo_meong/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B8%B0%EC%88%A0-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-2</guid>
            <pubDate>Mon, 07 Apr 2025 02:50:03 GMT</pubDate>
            <description><![CDATA[<h1 id="기본-질문">기본 질문</h1>
<h3 id="1-영속성-컨텍스트의-범위는">1. 영속성 컨텍스트의 범위는?</h3>
<p>➡️ 영속성 컨텍스트의 범위는 일반적으로 HTTP 요청이 시작될 때 생성되고, 응답이 완료될 때 소멸된다. JPA에서는 @Trancational이 적용된 메서드 내에서 유지된다.</p>
<p>영속성 컨텍스트는 엔티티 객체를 관리하는 공간으로 엔티티 객체의 생명 주기를 관리하고 변경 감지를 통해 자동으로 DB에 반영하는 역할을 한다. </p>
<h3 id="2-spring의-bean-scope">2. Spring의 Bean Scope?</h3>
<p>➡️ @Componet로 등록된 Bean이 어떤 범위로 관리될지 설정할 수 있다. 기본값은 Singleton으로 애플리케이션에서 하나의 인스턴스만 생성되고 DI 컨테이너가 생성될 때 한 번만 초기화 된다.</p>
<h3 id="3-jpa-에서-fetchtype-이-lazy-로-설정된-엔티티를-조회할-때-프록시-객체-동작-방식은">3. JPA 에서 FetchType 이 LAZY 로 설정된 엔티티를 조회할 때, 프록시 객체 동작 방식은?</h3>
<p>➡️ 엔티티를 조회할 때 실제 데이터를 로드하는 것이 아니라 프록시 객체가 생성된다. 실제 데이터가 필요한 시점에 영속성 컨텍스트를 통해 엔티티를 조회하고 데이터를 가져온다.</p>
<h3 id="4-spring-에서-transactional-이-적용된-메서드가-실행될-때-프록시-객체-동작-방식은">4. Spring 에서 <code>@Transactional</code> 이 적용된 메서드가 실행될 때, 프록시 객체 동작 방식은?</h3>
<p>➡️ 트랜잭션이 적용된 메서드는 AOP 기반의 프록시 객체를 생성하여 실행된다. 메서드 실행 전 트랜잭션이 시작되며 메서드가 종료될 때 트랜잭션이 커밋되거나 롤백된다.</p>
<h3 id="5-transaction-isolation-레벨">5. Transaction isolation 레벨?</h3>
<p>➡️ 트랜잭션 격리 수준 동시에 실행되는 트랜잭션 간의 데이터 격리 수준에 대한 설정이다. 기본값은 REPEATABLE READ로, 트랜잭션 동안 동일한 데이터를 읽을 수 있다. READ UNCOMITTED(커밋되지 않은 데이터 읽기 가능), READ COMITTED(커밋된 데이터만 읽기 가능), SEIALIZALBE(동시 실행 차단) 등의 격리 수준이 존재한다.</p>
<h3 id="6-mysql-에서-인덱스-사용-시-쓰기-작업이-발생할-때-인덱스의-자료구조-재정렬-방식">6. MySQL 에서 인덱스 사용 시, 쓰기 작업이 발생할 때 인덱스의 자료구조 재정렬 방식?</h3>
<p>➡️ MySQL의 B-Tree 인덱스는 삽입/삭제 시 자동으로 정렬된다. B-Tree 특성에 따라 적절한 위치에 삽입되고 균형 유지를 위한 재정렬 작업이 발생한다.</p>
<h3 id="7-java-에서-객체를-직렬화한-후-역직렬화할-때-클래스의-classpath가-변경되면classnotfoundexception-오류가-발생하는-이유">7. Java 에서 객체를 직렬화한 후 역직렬화할 때, 클래스의 classpath가 변경되면<code>ClassNotFoundException</code> 오류가 발생하는 이유?</h3>
<p>(클래스 버전 관리/SerialVersionUID 에 대해 알고 있는지)
➡️ Java에서 객체를 직렬화할 때, 클래스의 메타데이터가 함께 저장된다. 역직렬화 시점에 데이터의 클래스 메타데이터와 일치하는 클래스를 찾을 수 없으면 ClassNotFoundException이 발생한다.</p>
<h3 id="8-spring-내부에서-di-과정이-이루어지는-방식">8. Spring 내부에서 DI 과정이 이루어지는 방식?</h3>
<p>➡️ 스프링은 ComponentScan을 통해 Bean을 찾고 인스턴스를 생성한 뒤, @Autowired, @Inject 등의 애너테이션, 생성자 등을 통해 의존성을 주입한다. Bean 인스턴스를 스프링 컨테이너에 등록하고 필요에 따라 제공한다.</p>
<h3 id="9-ddd에서-이벤트-리스너-패턴-구성-방식은">9. DDD에서 이벤트 리스너 패턴 구성 방식은?</h3>
<p>➡️ 도메인 객체의 상태 변화가 발생하면 이벤트가 발행된다. 이벤트 리스너가 해당 이벤트를 감지하고 처리한다.</p>
<h1 id="프로젝트-질문">프로젝트 질문</h1>
<h3 id="1-의존역전원칙dip은-무엇이고-고수준-모듈을-어떻게-보호하는지">1. 의존역전원칙(DIP)은 무엇이고 고수준 모듈을 어떻게 보호하는지?</h3>
<p>➡️ DIP는 고수준 모듈이 저수준 모듈에 의존하지 않고 추상화에 의존하도록 인터페이스를 활용하여 의존성을 역전하는 개념이다.</p>
<p>저수준 모듈이 아니라 인터페이스나 추상 클래스를 참조하여 의존성을 분리한다. 인터페이스를 주입하여 고수준 모듈이 특정 구현체에 의존하지 않도록 보호할 수 있게 된다.</p>
<h3 id="2-트래픽이-많은-서비스에서-redis를-많이-사용하는-이유를-redis의-내부-구조와-연관-지어-설명">2. 트래픽이 많은 서비스에서 Redis를 많이 사용하는 이유를 Redis의 내부 구조와 연관 지어 설명?</h3>
<p>➡️ 레디스는 인메모리 데이터베이스로 디스크보다 빠른 속도를 제공한다. 다양한 자료 구조를 지원하여 상황에 맞게 적용할 수 있다. </p>
<h3 id="3-주문-시스템을-개발했는데-선점-기능을-도입한다면-어떻게-개발-할지">3. 주문 시스템을 개발했는데, 선점 기능을 도입한다면 어떻게 개발 할지?</h3>
<p>➡️ 1. 버전 정보를 사용하여 충돌을 감지하는 낙관적 락
2. 하나의 트랜잭션만 데이터에 접근 가능하도록 제어하는 비관적 락
3. 다중 인스턴스 환경에서 선점을 구현하는 분산 락</p>
<h3 id="4-프로젝트를-다중-인스턴스-또는-msa-구조로-변경한다고-했을-때-고려해야할-점은">4. 프로젝트를 다중 인스턴스 또는 MSA 구조로 변경한다고 했을 때 고려해야할 점은?</h3>
<p>➡️ 데이터 일관성, 서비스 간 통신, 로드 밸련싱, 보안 등</p>
<h3 id="5-jwt-기반-인증에서-토큰을-즉시-만료시킬-수-있는-방법은">5. JWT 기반 인증에서 토큰을 즉시 만료시킬 수 있는 방법은?</h3>
<p>➡️ 1. 레디스에 로그아웃된 토큰을 저장하고 인증시 체크하는 블랙리스트 설계
2. 토큰 무효화 API 제공
3. 엑세스 토큰의 유효시간을 짧게 하고 리프레쉬 토큰 무효화</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Error] RecursionError : 재귀 깊이 제한]]></title>
            <link>https://velog.io/@zo_meong/Error-RecursionError-%EC%9E%AC%EA%B7%80-%EA%B9%8A%EC%9D%B4-%EC%A0%9C%ED%95%9C</link>
            <guid>https://velog.io/@zo_meong/Error-RecursionError-%EC%9E%AC%EA%B7%80-%EA%B9%8A%EC%9D%B4-%EC%A0%9C%ED%95%9C</guid>
            <pubDate>Mon, 31 Mar 2025 09:05:47 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<p><strong>프로그래머스</strong>: 콜라 문제
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/132267">https://school.programmers.co.kr/learn/courses/30/lessons/132267</a></p>
<p>해당 문제를 해결하기 위해 재귀 함수를 사용했다.</p>
<pre><code class="language-python">def bottle(a, b, n, result):
    if n &lt; a:
        return result

    give = n // a * a
    receive = n // a * b
    return bottle(a, b, n - give + receive, result + receive)</code></pre>
<h1 id="에러">에러</h1>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/9aa18778-c3b1-4895-b9d4-6b273ac958e1/image.png" alt=""></p>
<p>그런데 특정 테스트 케이스에서만 런타임 에러가 발생했다. 재귀 함수를 사용하고 있으므로 이와 관련된 것일 거라 생각했다. 찾아본 결과, 파이썬에서는 기본적으로 재귀 깊이 제한 (sys.getrecursionlimit())이 약 1000으로 설정되어 있기 때문에 재귀 호출이 1000번 이상 반복되어 RecursionError (maximum recursion depth exceeded)가 발생했을 가능성이 높은 것으로 보인다.</p>
<h1 id="해결">해결</h1>
<p>이를 해결하기 위한 방법으로는 반복문으로 변경하거나 재귀의 깊이를 늘리는 방법이 있다. 하지만 재귀의 깊이를 늘리게 되면 메모리 소비가 커지고 무한 루프에 빠질 수 있어서 메모리를 절약할 수 있도록 반복문으로 변경하였다.</p>
<pre><code class="language-python">while n &gt;= a:
   give = n // a * a
   receive = n // a * b
   n -= give - receive
   result += receive
return result</code></pre>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/a9219cd9-adbd-4e18-bf2a-cac3792947a0/image.png" alt=""></p>
<p>반복문으로 변경하니 런타임 예외가 발생하지 않고 통과되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] 물류 프로젝트 4L 팀 회고]]></title>
            <link>https://velog.io/@zo_meong/Project-%EB%AC%BC%EB%A5%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4L-%ED%8C%80-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@zo_meong/Project-%EB%AC%BC%EB%A5%98-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4L-%ED%8C%80-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 26 Mar 2025 04:18:08 GMT</pubDate>
            <description><![CDATA[<h3 id="📝-프로젝트-개요"><strong>📝 프로젝트 개요</strong></h3>
<ul>
<li><strong>프로젝트명: 13friday</strong></li>
<li><strong>진행 기간:  2025.03.12 ~ 2025.03.26</strong></li>
<li><strong>팀원: 박동휘,서현재,김지현,진강훈</strong></li>
<li><strong>목표: MSA 기반 B2B 물류 관리 및 배송 시스템 개발</strong></li>
</ul>
<hr>
<h3 id="📊-4l-회고"><strong>📊 4L 회고</strong></h3>
<h3 id="1-liked-좋았던-점-장점"><strong>1. Liked (좋았던 점, 장점)</strong></h3>
<ul>
<li>현재 : MSA를 경험할 수 있어서 좋았다</li>
<li>강훈: 새로운 아키텍쳐 경험을 쌓고 다양한 기술을 적용시켜 볼 수 있어서 좋았다.</li>
<li>지현: 처음 MSA 프로젝트를 하며 새로운 경험을 할 수 있었다.</li>
</ul>
<hr>
<h3 id="2-learned-배운-점"><strong>2. Learned (배운 점)</strong></h3>
<ul>
<li>현재 : 인증,인가 흐름에 대해서 진짜 명확하게 알게 되었다</li>
<li>지현: MSA 환경에서 설계, 통신, 구현 방법 등을 배움</li>
<li>강훈: MSA 의 설계 방식과 DDD 적용하는법, feginClient로 다른 서비스의 API를 호출하는 방법을 배웠다.</li>
<li>동휘: feignClient로 통신하는 방식에 대해 배웠다.</li>
</ul>
<hr>
<h3 id="3-lacked-부족했던-점-단점"><strong>3. Lacked (부족했던 점, 단점)</strong></h3>
<ul>
<li>지현: 초반의 의사소통, 컨벤션이 부족해서 프로젝트 마무리 시점에 시간이 부족하였다.</li>
<li>현재 : docker라던가 CRUD를 제외한 기타 항목들에 대한 지식이 없어서 다른 것들을 적용 못했다.</li>
<li>강훈: MSA 나 DDD, docker의 개념이 부족하여 초반 설계단계에서 시간을 너무 많이 쓰게되어 프로젝트 시작이 늦어졌다.</li>
<li>동휘: 프로젝트를 정확히 이해하는데 시간이 걸렸고 지식이 부족해 오류를 해결하는데 시간이 걸림. 에러처리를 제대로 못했다.</li>
</ul>
<hr>
<h3 id="4-longed-for-아쉬웠던-점-더-바랐던-것"><strong>4. Longed for (아쉬웠던 점, 더 바랐던 것)</strong></h3>
<ul>
<li>현재 : kafka, jmeter, zipkin등 부하테스트라던가, 성능고도화에 대한 고려조차 못한게 아쉽다</li>
<li>지현: 일정 관리를 통해 미리 테스트를 진행하고 보완점을 찾아 개선했으면 좋았을 듯 합니다.</li>
<li>강훈: 이동경로를 P2P로 했는 데 난이도가 제일 쉬운 모델이였어서 다른 모델로 적용시켜봤으면 좋았을것같다.</li>
<li>동휘: 여러 전략들에 대해 깊이 고민하지 못하였고 성능 테스트를 제대로 못한 점이 아쉬웠다.</li>
</ul>
<hr>
<h3 id="개선-방향"><strong>개선 방향</strong></h3>
<ul>
<li>현재 : 말을 많이 해야겠다. 적극적으로 의사소통을 해야 프로젝트가 더 원활하게 진행 할 수 있을 것 같</li>
<li>동휘: 부하 테스트등 성능 테스트를 통해서 기능을 더 개선하면 좋을 듯 합니다.</li>
<li>지현: 초반부터 꼼꼼한 설계와 일정, 시나리오 작성을 하면 더 수월하게 진행할 수 있을 것 같다.</li>
<li>강훈: 초반 설계 단계에서 확실하게 마무리하고, 단위 테스트를 더 많이 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis 데이터 타입]]></title>
            <link>https://velog.io/@zo_meong/Redis-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@zo_meong/Redis-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85</guid>
            <pubDate>Mon, 10 Mar 2025 08:35:03 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/zo_meong/post/5bd7c1fb-f3ac-4b2c-a937-7a231f67e57f/image.png" alt=""></p>
<h1 id="redis란">Redis란?</h1>
<p><strong>Redis(Remote Dictionary Server)</strong>는 인메모리 NoSQL 데이터베이스로, 키-값(key-value) 저장소 형태로 데이터를 빠르게 저장하고 검색할 수 있다.</p>
<p>가장 큰 특징은 <strong>인메모리</strong> 데이터 베이스라는 점이다. 데이터를 디스크가 아닌 메모리(RAM)에 저장하여 매우 빠른 성능을 제공한다. 따라서 높은 트래픽을 처리하는 시스템에 적합하다.</p>
<h2 id="데이터-타입">데이터 타입</h2>
<p>Redis는 단순 문자열, 리스트, 집합, hash, set 등 다양한 데이터 구조의 저장 방식을 지원한다. key-value 데이터베이스이므로 value로 사용하고자 하는 자료형에 따라 다른 명령어를 사용하게 된다. 대부분의 명령이 존재하지 않는 key 값에 사용되어도 정상적으로 동작하며, 존재하지 않는 key에 데이터를 저장하면 key를 생성한다.</p>
<h3 id="string">String</h3>
<pre><code class="language-shell"># key에 value 문자열 저장
SET &lt;key&gt; &lt;value&gt;
MSET &lt;key&gt; &lt;value&gt; &lt;key&gt; &lt;value&gt;...

# key에 저장된 문자열 반환
GET &lt;key&gt;
MGET &lt;key&gt; &lt;key&gt;...

# 정수 저장시
INCR &lt;key&gt; # 1 증가
DECR &lt;key&gt; # 1 감소</code></pre>
<h3 id="list">LIST</h3>
<p>여러 문자열 데이터를 리스트로 저장한다. 레디스에서 리스트는 Linked List 형태로 저장된다.</p>
<pre><code class="language-shell"># 리스트 앞/뒤에 저장
LPUSH &lt;key&gt; &lt;value&gt;
RPUSH &lt;key&gt; &lt;value&gt;

# 리스트 맨 앞/뒤 값 반환 및 제거
LPOP &lt;key&gt;
RPOP &lt;key&gt;

LLEN &lt;key&gt; # 리스트 길이 반환
LRANGE &lt;key&gt; &lt;start end&gt; # start부터 end까지 원소 반환</code></pre>
<h3 id="set">SET</h3>
<p>문자열의 집합이며 중복을 허용하지 않고 순서가 존재하지 않는다.</p>
<pre><code class="language-shell">SADD &lt;key&gt; &lt;value&gt;    # 값 추가
SREM &lt;key&gt; &lt;value&gt;    # 값 삭제
SMEMBERS &lt;key&gt;        # set에 저장된 모든 원소 반환
SCARD &lt;key&gt;            # set 크기 반환

SINTERCARD &lt;key1&gt; &lt;key2&gt;..    # 교집합 크기 반환
SINTER &lt;key1&gt; &lt;key2&gt;        # 교집합 원소 반환
SUNION &lt;key1&gt; &lt;key2&gt;        # 합집합 원소 반환</code></pre>
<h3 id="sorted-set">SORTED SET</h3>
<p>정렬된 집합으로 set에 <strong>score 실수</strong>를 함께 저장한다. 데이터를 가져올 때 score를 기준으로 정렬하여 값을 가져올 수 있다.
순위와 관련된 리더보드 등의 기능을 만들 때 주로 사용된다.</p>
<pre><code>ZADD &lt;key&gt; &lt;score&gt; &lt;meber&gt;    # key의 set에 score 점수를 가진 member 추가
ZINCRBY &lt;key&gt; &lt;inc&gt; &lt;member&gt;    # member의 score를 inc만큼 증가
ZRANK &lt;key&gt; &lt;member&gt;    # member의 순위 반환
ZRANGE &lt;key&gt; &lt;start end&gt;    # start부터 end 순위 까지의 member 반환</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Amazon ECS 개요]]></title>
            <link>https://velog.io/@zo_meong/Amazon-ECS-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@zo_meong/Amazon-ECS-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Fri, 28 Feb 2025 11:42:10 GMT</pubDate>
            <description><![CDATA[<h1 id="ecs">ECS</h1>
<p>ECS(Amazon Elastic Container Service)는 AWS에서 제공하는 <strong>컨테이너 오케스트레이션</strong> 서비스이다. 컨테이너 기반 애플리케이션을 AWS 환경에서 쉽게 배포하고 관리할 수 있도록 도와준다. 쿠버네티스와 비슷한 역할을 하지만 보다 저렴하고 사용하기 쉬우며 AWS에 최적화된 관리형 서비스로 제공된다. 또한 서버리스로도 구성 가능하다.</p>
<p>주요 기능으로는 컨테이너 관리, 클러스터 관리, 로드 밸런싱, 자동 스케일링, IAM으로 권한 세분화 등이 있다. </p>
<h2 id="실행-방식">실행 방식</h2>
<p>ECS는 컨테이너 실행에 2가지 방식을 제공한다. 하나는 EC2 기반의 방식 또 다른 하나는 Fargate를 사용한 서버리스 방식이다.</p>
<ul>
<li><strong>EC2</strong> : EC2 인스턴스에서 컨테이너를 실행하는 방식. 더 많은 제어가 가능하지만 인스턴스를 직접 관리해야한다.</li>
<li><strong>Fargate</strong> : 서버 관리 없이 컨테이너만 실행하는 방식. AWS가 자동으로 인프라를 관리하며 클러스터 인스턴스도 필요하지 않다.</li>
</ul>
<h2 id="구성-요소">구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/56c7a5f7-9703-4c55-b627-307f5f394ade/image.png" alt=""></p>
<ul>
<li>Task Definition : 컨테이너를 실행하기 위해 정의한 설정</li>
<li>Task : ECS에서 실제로 실행되는 도커 컨테이너</li>
<li>Service : 컨테이너를 실행, 유지 관리하는 역할, 테스트가 두 개 이상 모인 것</li>
<li>Cluster : 컨테이너를 실행할 수 있는 인프라 그룹 (EC2, Fargate)</li>
</ul>
<h2 id="워크플로우">워크플로우</h2>
<p><strong>초기 설정</strong></p>
<ol>
<li>컨테이너 이미지 빌드 &amp; 저장</li>
<li>클러스터 인스턴스 생성 (EC2 방식일 경우)</li>
<li>Task Definition 생성</li>
<li>Service 생성</li>
</ol>
<p><strong>실행 과정</strong></p>
<ol>
<li>컨테이너 레지스트리에 저장된 도커 이미지를 가져옴</li>
<li>ECS에서 이미지를 실행하여 컨테이너(task) 생성</li>
<li>ECS에서 서비스 및 클러스터 관리, 모니터링</li>
</ol>
<h1 id="ecr">ECR</h1>
<p>ECR(Amazon Elastic Container Registry)은 Docker 컨테이너 이미지를 저장하고 관리하는 AWS의 <strong>컨테이너 레지스트리</strong> 서비스이다. 도커 허브와 비슷한 기능을 하지만 AWS 환경에 최적화되어 있다. 주요 기능으로는 컨테이너 이미지 저장 및 관리, CI/CD 연동, 보안 및 스캔 기능 등이 있다.</p>
<p><strong>ECR 이미지 업로드 및 배포 과정</strong></p>
<ol>
<li>도커 이미지 빌드</li>
<li>ECR 로그인</li>
<li>ECR에 이미지 태깅</li>
<li>ECR에 이미지 푸시</li>
<li>ECR에서 컨테이너 배포</li>
</ol>
<h1 id="cicd">CI/CD</h1>
<p>ECS과 ECR을 활용하여 다음과 같은 과정으로 CI/CD 파이프라인을 구성 할 수 잇다.</p>
<ol>
<li>코드 변경</li>
<li>Docker 이미지 빌드</li>
<li>ECR에 푸시</li>
<li>ECS가 자동으로 새로운 이미지 배포</li>
<li>서비스 무중단 업데이트</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Delivery 프로젝트 SWOT 개인 회고]]></title>
            <link>https://velog.io/@zo_meong/Project-Delivery-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-SWOT-%EA%B0%9C%EC%9D%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@zo_meong/Project-Delivery-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-SWOT-%EA%B0%9C%EC%9D%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 27 Feb 2025 08:04:30 GMT</pubDate>
            <description><![CDATA[<h1 id="📝-프로젝트-개요">📝 프로젝트 개요</h1>
<ul>
<li><strong>프로젝트명: AI를 활용한 배달 REST API SERVER</strong></li>
<li><strong>진행 기간: 2025.02.12 ~ 2025.02.25</strong></li>
<li><strong>팀원: 이승욱, 김정환, 김지현, 오연주</strong></li>
<li><strong>목표:</strong> 광화문 근처에서 운영될 음식점들의 배달 및 포장 주문 관리, 결제, 그리고 주문 내역 관리 기능을 제공하는 플랫폼 개발</li>
</ul>
<hr>
<h1 id="📊-개인-swot-분석">📊 개인 SWOT 분석</h1>
<h2 id="✅-1-strengths-강점---내가-잘한-점">✅ 1. Strengths (강점 - 내가 잘한 점)</h2>
<ul>
<li>Spring Security에서 예외 처리를 진행한 점</li>
<li>배포를 담당한 점</li>
<li>다른 팀원들의 질문에 적극적으로 대답해드리고 오류 사항을 같이 확인하며 해결함</li>
</ul>
<h2 id="⚠️-2-weaknesses-약점---부족했던-점">⚠️ 2. Weaknesses (약점 - 부족했던 점)</h2>
<ul>
<li>Refresh 토큰과 로그아웃 부분을 구현하려 했으나 시간 관계상 담당하지 않은 점</li>
<li>추가 기능을 더 적극적으로 구현하지 못한 점</li>
</ul>
<h2 id="🔍-3-opportunities-기회---배울-기회가-있었던-부분">🔍 3. Opportunities (기회 - 배울 기회가 있었던 부분)</h2>
<ul>
<li>그동안의 팀 프로젝트에서는 Spring security와 유저 관련 기능 구현을 맡은 부분</li>
<li>스프링 시큐리티와 JWT에 대한 기술적인 이해도, 구현 실력이 증가함</li>
<li>내가 어려워하던 부분에 도전하여 스스로의 성장을 도모함</li>
</ul>
<h2 id="⚡-4-threats-위협---나의-성장을-방해한-요소">⚡ 4. Threats (위협 - 나의 성장을 방해한 요소)</h2>
<ul>
<li>이 정도하면 되지 않았을까하는 안일한 마음이 순간적으로 들 때가 가장 나의 성장에 방해가 되는 것 같음</li>
</ul>
<h1 id="🎯-개선-방향-및-액션-플랜">🎯 개선 방향 및 액션 플랜</h1>
<ul>
<li>개발 일정을 더 계획적으로 작성하여 더 많은 작업을 체계적으로 할 수 있도록 해야겠음</li>
<li>더 적극적으로 프로젝트에 임해서 내가 맡은 부분 이상의 성과를 내야겠다는 마음가짐</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Delivery 프로젝트 4L 팀 회고]]></title>
            <link>https://velog.io/@zo_meong/Project-Delivery-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8C%80-%ED%9A%8C%EA%B3%A04L</link>
            <guid>https://velog.io/@zo_meong/Project-Delivery-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%8C%80-%ED%9A%8C%EA%B3%A04L</guid>
            <pubDate>Wed, 26 Feb 2025 05:35:54 GMT</pubDate>
            <description><![CDATA[<h1 id="📝-프로젝트-개요">📝 프로젝트 개요</h1>
<ul>
<li><strong>프로젝트명: AI를 활용한 배달 REST API SERVER</strong></li>
<li><strong>진행 기간: 2025.02.12 ~ 2025.02.26</strong></li>
<li><strong>팀원: 이승욱, 김정환, 김지현, 오연주</strong></li>
<li><strong>목표:</strong> 광화문 근처에서 운영될 음식점들의 배달 및 포장 주문 관리, 결제, 그리고 주문 내역 관리 기능을 제공하는 플랫폼 개발</li>
</ul>
<hr>
<h1 id="📊-4l-회고">📊 4L 회고</h1>
<h2 id="✅-1-liked-좋았던-점-장점">✅ 1. Liked (좋았던 점, 장점)</h2>
<ul>
<li>팀원분들이 각자 일을 가지고 하는게 다툼이 없었다.</li>
<li>공평하게 사다리타기로 진행되는 것도 나쁘지 않았다.</li>
<li>초반에 code with me를 통해 프로젝트의 세팅을 같이하여 오류를 줄인 것 같다.</li>
<li>오류가 생겼을 때 같이 해결하는 것이 좋았다.</li>
</ul>
<h2 id="📚-2-learned-배운-점">📚 2. Learned (배운 점)</h2>
<ul>
<li>refresh token에 대해서 공부하여 구현해봄</li>
<li>docker를 사용하여 편리성을 깨달음</li>
<li>코드 convention의 중요성</li>
<li>Scheduler의 사용법과 이점</li>
<li>GlobalException의 사용법</li>
<li>DDD 구조</li>
<li>ERD설계에 관계형 DB와 비관계형 DB의 차이</li>
<li>Spring security 예외 처리 방법</li>
</ul>
<h2 id="⚠️-3-lacked-부족했던-점-단점">⚠️ 3. Lacked (부족했던 점, 단점)</h2>
<ul>
<li>blacklist로 토큰 관리를 하지 못했었던 점</li>
<li>test code를 작성하지 못했던 점</li>
</ul>
<h2 id="🔍-4-longed-for-아쉬웠던-점-더-바랐던-것">🔍 4. Longed for (아쉬웠던 점, 더 바랐던 것)</h2>
<ul>
<li>필수 요구사항만 구현했는데 조금 더 확장시켜보았으면 좋았을 것 같다.</li>
<li>개발 진행 중 필수 요구 사항을 발견하여 수정한 것</li>
<li>JWT와 사용했던 기술들이 현업에서 어떻게 사용하는지 깊이 알아보지 못한 것</li>
<li>테스트 시간을 충분히 잡지 못한 것</li>
</ul>
<h2 id="🎯-다음-프로젝트를-위한-개선-방향">🎯 다음 프로젝트를 위한 개선 방향</h2>
<ul>
<li>프로젝트에 사용될 기술 스택(MSA, CI/CD, Kafka)들을 좀 더 미리 공부해보는 게 좋을 것 같다.</li>
<li>일정 관리를 체계적으로 하여 더 많은 작업을 효율적으로 할 수 있도록 계획</li>
<li>PR을 좀 더 세부적으로 작성할 수 있도록 연습해보는 것</li>
<li>매번 하는 CRUD가 아닌 다른 것도 도전해볼 수 있게 일정 조율을 하는 것이 좋을 것 같다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] EC2 배포 - Docker compose]]></title>
            <link>https://velog.io/@zo_meong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-EC2-Docker-compose-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@zo_meong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-EC2-Docker-compose-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Tue, 25 Feb 2025 12:05:25 GMT</pubDate>
            <description><![CDATA[<h1 id="트러블슈팅">트러블슈팅</h1>
<p>Delivery 프로젝트 기능 구현을 마무리 하고 배포 단계에서 CI/CD를 할 것인지 의논하였으나 프로젝트 규모가 작아 그냥 배포만 하기로 결정하였다. 그런데 배포 과정에서 예상치 못한 오류를 만났으니... 트러블 슈팅을 정리해보자</p>
<h2 id="gradle-wrap">gradle wrap</h2>
<p>첫 번째 문제는 우리의 프로젝트 파일에 <code>gradle-wrapper.jar</code> 파일이 포함되지 않아서 발생했다. 이 파일이 존재하지 않으면 간단히 Gradle Wrapper를 초기화해서 파일을 생성해주면 된다. ec2 환경이니 gradle 설치부터 해주어야 한다.</p>
<p>우분투 기반의 EC2에서 그냥 apt install로 gradle을 설치하면 4.X의 낮은 버전이 설치된다. 우리 프로젝트는 JDK 17, Gradle 8.12.1 버전을 사용하므로 버전을 맞추기 위해 수동으로 설치해주어야 했다.</p>
<pre><code>//gradle 설치
sudo wget https://services.gradle.org/distributions/gradle-8.12.1-bin.zip 

// 압축 해제
sudo unzip -d /opt/gradle gradle-8.12.1-bin.zip

// 환경 변수 설정
export PATH=$PATH:/opt/gradle/gradle-8.12.1/bin</code></pre><p>gradle 설치 후 권한을 부여하고 wrap을 실행한다.</p>
<pre><code>sudo chmod 777  gradle-wrapper.properties
sudo chmod 777  gradlew.bat

gradle wrap</code></pre><p>...이렇게 하면 된다고 하는데 우리의 조그마한 t2.micro가 wrap 명령어 사용과 함께 CPU 사용률 100%를 찍고 뻗어버렸다. Gradle Daemon을 종료하고 시도한다던가 하는 방법을 GPT가 추천하긴 했지만 이 시점에 제출이 얼마 남지않아.. 시간이 얼마 없었으므로 <code>gradle-wrapper.jar</code> 파일을 그냥 로컬에서 직접 쏴서 때려 넣어버렸다. 아무튼 그렇게 해서 빌드는 성공했다.</p>
<h2 id="docker-compose-리소스-부족">Docker compose 리소스 부족</h2>
<p>이제 docker compose up 명령어로 서버와 DB, 레디스를 띄웠는데.... CPU 사용률이 또 100%를 찍고 뻗어버렸다. 요청을 보내도 무한 로딩에 걸리질 않나 SSH 접속도 안되질 않나
<img src="https://velog.velcdn.com/images/zo_meong/post/2754db81-2943-47e6-8633-7b4d4965d6f8/image.png" alt=""></p>
<p>문제 해결을 위해 인스턴스 종료 후 재시작한 뒤, <code>docker-compose.yml</code> 파일에 리소스 설정을 추가해주었다. t2.micro는 vCPU 1개, RAM 1GB를 제공하므로 이에 맞춰서 각각의 컨테이너의 CPU와 메모리 사용량을 제한하였다.</p>
<pre><code class="language-yaml">    deploy:
      resources:
        limits:
          cpus: &quot;0.5&quot;  # 최대 CPU 50% 사용
          memory: &quot;400M&quot;  # 최대 400MB RAM 사용</code></pre>
<p>리소스 설정 후 재시작하니 잘 작동하여 배포가 성공적으로 이루어짐을 확인하였다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Spring Security 인증/인가 예외 처리]]></title>
            <link>https://velog.io/@zo_meong/Spring-Security-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@zo_meong/Spring-Security-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Tue, 18 Feb 2025 14:31:17 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/zo_meong/post/a47c9649-bea4-4635-ba96-24d45cbf91b2/image.png" alt=""></p>
<h1 id="예외-처리">예외 처리</h1>
<p>이번 delivery 프로젝트에서 Spring Security를 사용하여 인증/인가 과정을 구현하고 Global Exception을 정의해 비지니스 로직에서의 전역적인 예외 처리를 구현해두었다. 그런데 필터에서 발생한 예외는 @ContollerAdvice에 잡히지 않으며 상태 코드만 클라이언트로 반환해준다.</p>
<p>우선 필터의 예외가 @RestControllerAdvice에 잡히지 않는 이유는 시큐리티 필터 체인의 구조 때문이다. 일반적인 비지니스 로직에서의 예외는 서버가 받은 요청이 컨트롤러에 도달한 이후 발생한다. 그러나 스프링 시큐리티는 요청이 컨트롤러에 도달하기 전, 필터 체인에서 예외를 발생시킨다. 즉 @RestControllerAdvice가 처리 가능한 영역이 아니다.</p>
<p>그래서 필터 체인에서 예외가 발생하면 시큐리티가 예외를 자체적으로 처리하고 status code만 클라이언트로 전송한다. 예외의 발생 원인을 클라이언트에서 알 수 있도록 커스터마이징하기 위해 예외 처리를 시도했다.</p>
<h2 id="authentication-entry-point">Authentication Entry Point</h2>
<p>우선 인증(Authentication) 실패 시의 예외를 처리하기 위해서 AuthenticationEntryPoint을 구현해준다.
인증 실패시 401 Unauthorized 에러 코드와 메시지를 반환한다.</p>
<pre><code class="language-java">// 인증 예외 처리
@Component
@Slf4j(topic = &quot;JwtAuthenticationEntryPoint&quot;)
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {

    String errorMessage = authException.getMessage();
    log.error(&quot;Unauthorized error: {}&quot;, errorMessage);

    // 응답 에러 메시지 반환
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setCharacterEncoding(&quot;UTF-8&quot;);
    response.getWriter().write(errorMessage);
  }
}</code></pre>
<h2 id="access-denined-handler">Access Denined Handler</h2>
<p>인가(Authorization) 과정에서의 예외 처리는 AccessDeninedHandler에서 구현한다.
인가 실패 시 403 Forbidden 상태 코드와 에러 메시지를 반환한다.</p>
<pre><code class="language-java">// 인가 예외 처리
@Component
@Slf4j(topic = &quot;JwtAccessDeniedHandler&quot;)
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

  @Override
  public void handle(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException, ServletException {

    String errorMessage = accessDeniedException.getMessage();
    log.error(&quot;Forbidden error: {}&quot;, errorMessage);

    response.setStatus(HttpStatus.FORBIDDEN.value());
    response.setCharacterEncoding(&quot;UTF-8&quot;);
    response.getWriter().write(errorMessage);
  }
}</code></pre>
<h1 id="트러블슈팅">트러블슈팅</h1>
<p>여기까진 이론이고 실제로 프로젝트에 적용하면서는 원하는 대로 응답이 오지 않아 꽤 많은 시행착오를 거쳤다. 그 중 가장 골치 아팠던 문제들이 아래와 같다.</p>
<h2 id="1-unsuccessfulauthentication">1. unsuccessfulAuthentication()</h2>
<p>첫 번째는 validateToken()에서 던져진 예외들이 잡히지 않아서 응답이 제대로 오지 않는 다는 것이었다.</p>
<p>로그인 실패 로그를 보니 JwtAuthenticationEntryPoint까지 예외가 전파되지 않고 JwtAuthenticationFilter에서 처리되어 끝나버렸는데 이유는 내가 이 필터 안에 unsuccessfulAuthentication() 메서드를 오버라이딩 해놨기 때문이었다..... 여기서 401로 response를 던졌기 때문에 예외 처리가 이미 된 것이다... (안돼서 삽질을 엄청나게 했는데 말이다</p>
<pre><code class="language-java">  @Override
  protected void unsuccessfulAuthentication
      (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
      throws IOException, ServletException {
    log.error(failed.getMessage());
    response.setStatus(401);
  }</code></pre>
<p>resopnse 를 던지는 대신 entrypoint의 commence를 호출하도록 수정해서 예외를 전파시켰다.</p>
<pre><code class="language-java">@Override
  protected void unsuccessfulAuthentication
      (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)
      throws IOException, ServletException {
    jwtAuthenticationEntryPoint.commence(request, response, failed);
  }</code></pre>
<h2 id="2-global-exception-처리">2. Global Exception 처리</h2>
<p>두 번째는 response에 Global Exception의 메시지가 응답으로 오는 것이 아니라 spring security의 기본 에러 메시지가 온다는 문제였다. 에러 로그를 보면 발생시킨 exception이 확인이 되는데 정작 메시지는 <code>Full authentication is required to access this resource</code> 이런게 오더라... 이걸 해결하기 위해 Global Exception을 던지지 않고 AuthenticationException을 상속받은 CustomAuthenticationException을 정의해서 던졌는데도 잡히지 않았다. </p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/96d7168a-31db-42da-83e2-4da72b416fef/image.png" alt="">
<img src="https://velog.velcdn.com/images/zo_meong/post/0937942f-57a0-42b2-b185-f510ad267d61/image.png" alt=""></p>
<p>그래서 디버깅을 돌렸더니 AuthenticationException의 인스턴스가 InsufficientAuthenticationException인 걸 발견하게 되었다. 찾아보니 스프링 시큐리티의 예외 처리 흐름에서 내 custom exception이 wrap 되어 버려서 커스텀 예외처리의 의미가 없다나... 간단하게 말하자면 덮어쓰기 된거다.</p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/a0d33387-d05f-4552-b879-f2951b6d2146/image.png" alt=""></p>
<p>이걸 해결하고 원하는 응답값을 주려면 InsufficientAuthenticationException을 직접 또 정의해서 던져주면 되기야 하겠지만 이렇게 되면 커스텀 응답 처리의 의미가 있나 싶어졌고... 이 부분은 그냥 Exception Filter를 통해 처리하기로 결정했다.</p>
<p>Exceptoin Filter를 가장 앞단에 배치시켜서 Authorization 인가 과정에서 발생하는 Global Exception을 잡아 처리했다.</p>
<pre><code class="language-java">// web security config
http.addFilterBefore(jwtExceptionFilter, UsernamePasswordAuthenticationFilter.class);

// jwt exception filter
try {
      filterChain.doFilter(request, response);
    } catch (GlobalException e) {
      log.error(&quot;ERROR: {}, URL: {}, MESSAGE: {}&quot;, e.getStatus(),
          request.getRequestURI(), e.getMessage());

      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.setCharacterEncoding(&quot;UTF-8&quot;);
      response.getWriter().write(e.getMessage());</code></pre>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/9aced0da-5c61-40c4-add6-85fad2ca61be/image.png" alt=""></p>
<p>드디어 원하는 응답으로 예외 처리를 성공했다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] JWT 토큰 저장 : 쿠키 VS 헤더]]></title>
            <link>https://velog.io/@zo_meong/Project-JWT-%ED%86%A0%ED%81%B0-%EC%A0%80%EC%9E%A5-%EC%BF%A0%ED%82%A4-VS-%ED%97%A4%EB%8D%94</link>
            <guid>https://velog.io/@zo_meong/Project-JWT-%ED%86%A0%ED%81%B0-%EC%A0%80%EC%9E%A5-%EC%BF%A0%ED%82%A4-VS-%ED%97%A4%EB%8D%94</guid>
            <pubDate>Mon, 17 Feb 2025 12:35:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/zo_meong/post/4cb07670-a510-4d66-9d06-86caa42dc912/image.png" alt=""></p>
<p>이번 delivery 프로젝트에서 유저 기능 구현을 담당하게 되어 JWT 토큰 방식의 로그인을 구현 하던 중 토큰을 쿠키에 저장할지 헤더에 저장할지 고민이 생겼다. 두 가지 방식은 각각의 장단점이 존재한다.</p>
<h1 id="cookie">Cookie</h1>
<p>쿠키(Cookie)에 저장하는 방식은 서버가 JWT를 발급하여 쿠키에 저장한 뒤, 클라이언트로 전송하는 방식으로, 클라이언트는 자동으로 이후 요청에 쿠키를 포함한다.</p>
<h2 id="장점">장점</h2>
<ul>
<li>자동 전달 : 클라이언트가 자동으로 쿠키를 요청에 포함시키므로, 별도의 토큰 관리 로직이 필요하지 않다.</li>
<li>XSS 공격 방지 : HttpOnly 쿠키를 사용하면 JavaScript로 쿠키에 접근할 수 없다.</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li>CSRF 공격에 취약 : 쿠키가 자동으로 요청에 포함되므로 CSRF 공격에 노출될 수 있다.</li>
<li>도메인 제약 : 쿠키는 도메인 단위로 관리되므로 여러 도메인 간에 토큰 공유가 어렵다.</li>
<li>사이즈 제한 : 쿠키에 크기 제한 (약 4KB)이 있어 토큰의 크기가 커질 경우 문제가 될 수 있다.</li>
</ul>
<h1 id="header">Header</h1>
<p>헤더(Header)에 저장하는 방식은 클라이언트가 JWT를 로컬 저장소에 저장해두고 HTTP 요청 헤더에 JWT를 포함하여 전송힌다.</p>
<h2 id="장점-1">장점</h2>
<ul>
<li>CSRF 위험 감소 : 쿠키를 사용하지 않으므로 상대적으로 CSRF 공격에 안전하다.</li>
<li>유연성 : 클라이언트 측에서 토큰을 직접 관리하므로 토큰의 저장 및 전송 방식을 원하는 대로 커스터마이징할 수 있다.</li>
<li>도메인 독립성(CORS) : 헤더에 저장된 토큰은 특정 도메인에 묶이지 않고 사용할 수 있으므로 특히 MSA와 같은 환경에서 다양한 도메인간의 통신 시 유리하다.</li>
</ul>
<h2 id="단점-1">단점</h2>
<ul>
<li>XSS 위험 : 로컬이나 세션 스토리지에 저장하는 경우, JavaScript로 접근 가능하므로 XSS 공격에 취약할 수 있다.</li>
<li>복잡성 : 클라이언트에서 토큰을 저장하고 요청마다 헤더에 추가하는 로직을 직접 구현해야 한다.</li>
</ul>
<h1 id="결론">결론</h1>
<p>사실 쿠키와 헤더 방식 중에서는 일반적으로 헤더 방식이 더 많이 사용된다. 헤더 방식은 서버에 부담이 덜 하고 쿠키 방식은 도메인의 제약이 크기 때문이다. 특히나 서버의 확장성과 유지보수성을 고려한다면 헤더 방식으로 선택하는 것이 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] Checked Exception]]></title>
            <link>https://velog.io/@zo_meong/CS-Checked-Exception</link>
            <guid>https://velog.io/@zo_meong/CS-Checked-Exception</guid>
            <pubDate>Fri, 14 Feb 2025 10:53:31 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Q. 자바에서 Checked Exception과 Unchecked Exception에 대해 설명해주세요.</p>
</blockquote>
<p>A. Checked Exception은 컴파일 예외로, 컴파일 시점에 확인되며 반드시 예외 처리를 해야한다. Unchecked Exception은 런타임 예외로, 프로그램 실행 중에 발생하며 예외 처리를 강제하지 않는다.</p>
<h1 id="checked-exception">Checked Exception</h1>
<p>컴파일러가 체크하는 예외이며 개발자가 명시적으로 <code>try-catch</code> 블럭 또는 <code>throws</code>을 사용해 예외를 처리해야한다. 컴파일 에외는 예외 처리를 하지 않으면 프로그램을 실행할 수 없다.</p>
<h1 id="runtime-exception">Runtime Exception</h1>
<p>런타임 시점에 발생하는 예외로, 프로그래머의 실수나 코드의 논리적인 오류로 인해 발생한다. 자바에서는 RuntimeException을 상속한 예외들이 해당된다. 컴파일러가 예외 처리를 강제하지는 않지만 프로그래머가 적절히 처리해야한다.</p>
<h1 id="error-vs-exception">Error VS Exception</h1>
<p>Error는 주로 JVM에서 발생하는 회복이 어려운 문제나 오류로, 시스템 레벨에서 발생하는 오류이다. 일반적으로 프로그램에서 처리하지 않으며 애플리케이션 코드에서 복구할 수 없는 심각한 문제를 나타낸다.
Exception은 프로그램 실행 중 발생할 수 있는 오류 상황을 나타낸다. 대부분 회복 가능성이 있으며 예외 처리를 통해 프로그램 내에서 오류 상황을 제어할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Project] Delivery 프로젝트 설계]]></title>
            <link>https://velog.io/@zo_meong/Delivery-Project-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@zo_meong/Delivery-Project-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Thu, 13 Feb 2025 14:11:58 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발자 4명이 스프링부트로 진행하는 배달/포장 서비스 서버 개발 프로젝트 설계이다. 광화문 근처에서 운영될 음식점들의 배달 및 포장 주문 관리, 결제, 그리고 주문 내역 관리 기능을 제공하는 플랫폼 개발한다. </p>
<h1 id="api-명세">API 명세</h1>
<p>도메인 : 사용자(User), 음식점(Store), 음식(Food), 주문(Order), 리뷰(Review), 주소(Address)</p>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/caec6a1f-134c-4252-af31-67807293b4bb/image.png" alt="">
<img src="https://velog.velcdn.com/images/zo_meong/post/c29ac1d4-8f36-4d70-987a-6c79777348ab/image.png" alt=""></p>
<h1 id="erd">ERD</h1>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/087e097b-aaa0-4788-af81-47ac490a4b45/image.png" alt=""></p>
<h1 id="아키텍처">아키텍처</h1>
<p><img src="https://velog.velcdn.com/images/zo_meong/post/70fa989f-5eb8-4535-9b25-a30309748a79/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Cloud란 무엇인가]]></title>
            <link>https://velog.io/@zo_meong/Spring-Cloud%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@zo_meong/Spring-Cloud%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 12 Feb 2025 11:37:25 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-cloud">Spring Cloud</h1>
<p>스프링 클라우드는 마이크로서비스 아키텍쳐(MSA) 기반의 시스템 구축을 도와주는 스프링 프레임워크의 확장이다. MSA 환경에서는 서비스 간 통신, 로드 밸런싱, 분산 구성 관리 등의 다양한 기능이 필요한데 스프링 클라우드는 이러한 기능을 손쉽게 구현할 수 있도록하는 다양한 모듈을 제공한다.</p>
<h2 id="주요-기능">주요 기능</h2>
<h3 id="1-서비스-디스커버리-service-discovery">1. 서비스 디스커버리 (Service Discovery)</h3>
<p>MSA 환경에서는 각 서비스의 위치(IP 및 포트)가 동적으로 변할 수 있다. 따라서 각각의 서비스들을 등록 및 검색할 수 있는 중앙 저장소 역할의 도구가 필요하다. 이를 통해 서비스 간의 동적 연결을 지원한다.</p>
<ul>
<li><strong>Eureka</strong>
Netflix OSS 기반의 서비스 디스커버리 도구. 서비스 인스턴스가 실행될 때 자동으로 등록되며,클라이언트는 Eureka 서버를 통해 서비스의 위치를 조회할 수 있다.
로드 밸런싱과 장애 감지를 통한 서비스 회복 기능을 제공한다.</li>
</ul>
<h3 id="2-api-게이트웨이-api-gateway">2. API 게이트웨이 (API Gateway)</h3>
<p>모든 클라이언트의 요청을 한 곳에서 관리하는 진입점 역할. 클라이언트와 마이크로서비스 간의 통신을 중재하고, 요청을 적절한 서비스로 라우팅한다. 또한 인증/인가, 로깅, 부하 분산 등의 기능을 제공한다.</p>
<ul>
<li><strong>Zuul</strong>
Netflix OSS 기반의 API 게이트웨이. 요청 라우팅, 필터링, 보안 등의 기능을 제공한다.</li>
<li><strong>Cloud Gateway</strong>
스프링 클라우드에서 제공하는 API 게이트웨이. 비동기 및 논블로킹 방식으로 동작하여 Zuul 보다 더 높은 성능을 제공한다.</li>
</ul>
<h3 id="3-로드-밸런싱-load-balancing">3. 로드 밸런싱 (Load Balancing)</h3>
<p>클라이언트 요청을 여러 인스턴스에 분배하여 부하를 균형있게 조절한다. </p>
<ul>
<li><strong>Ribbon</strong>
Eureka와 연동하여 서비스 인스턴스 목록을 가져와 동적으로 로드 밸런싱을 수행할 수 있다. 클라이언트는 Ribbon을 통해 서비스에 대한 여러 개의 인스턴스 중 하나를 선택하여 요청을 보내는 방식으로, 클라이언트 사이드 로드 밸런싱 방식이라고도 한다. 분산 전략으로 라운드 로빈, 가중치 기반 등 다양한 로드밸런싱 알고리즘을 지원한다.</li>
</ul>
<h3 id="4-서킷-브레이커-circuit-breaker">4. 서킷 브레이커 (Circuit Breaker)</h3>
<p>서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지하고 지연이나 실패를 처리하여 시스템의 전체적인 안전성을 유지하는 역할을 하며 빠르게 장애 부분을 격리하고 시스템의 다른 부분에 영향을 주지 않도록 한다.</p>
<ul>
<li><strong>Hystrix</strong>
Netflix OSS 기반의 서킷 브레이커 라이브러리.</li>
<li><strong>Resilience4j</strong>
Hystrix의 대체 도구로, 경량화되고 모듈화된 자바 기반의 서킷 브레이커 라이브러리. 서킷 브레이커가 open되면 특정 임계치를 초과한 요청을 차단하고, 일정 시간이 지나면 다시 정상 동작을 시도할 수 있다. 실패 시 Failback 매커니즘을 통해 대체 동작을 수행한다.</li>
</ul>
<h3 id="5-설정-관리-config-server">5. 설정 관리 (Config Server)</h3>
<p>여러 마이크로서비스의 설정을 중앙에서 관리하고, 변경 사항을 실시간으로 반영하도록 할 수 있다.</p>
<ul>
<li><strong>Spring Cloud Config</strong>
중앙 집중식 구성 관리를 제공하는 프레임워크. Spring Cloud Bus를 사용하면 서비스 재시작 없이도 설정 반영이 가능하다.</li>
</ul>
<h3 id="6-분산-추적">6. 분산 추적</h3>
<p>분산 시스템에서 서비스 간의 요청 흐름을 추적하여 모니터링하는 기능이다.</p>
<ul>
<li><strong>Zipkin</strong>
각 요청에 고유한 추적 ID를 부여하여 요청의 흐름을 시각화 할 수 있다. 요청의 시작부터 끝까지의 경로를 추적하여 지연 시간, 오류 등의 정보를 수집한다. 또한 특정 요청을 검색하고 필터링하여 문제를 진단할 수 있다.</li>
</ul>
<h3 id="7-이벤트-드리븐-아키텍처-event-driven-architecture">7. 이벤트 드리븐 아키텍처 (Event-Driven Architecture)</h3>
<p>이벤트 드리븐 아키텍처는 시스템에서 발생하는 이벤트를 기반으로 동작하는 소프트웨어 설계 방식이다. 이벤트는 비동기적으로 처리되며, 서비스 간의 결합도를 낮추고 확장성을 높인다.</p>
<ul>
<li><strong>Spring Cloud Stream</strong>
이벤트 드리븐 아키텍처를 구현하기 위한 프레임워크. 메시지 브로커(Kafka, RabbitMQ 등)를 통해 이벤트 스트리밍을 처리한다. 예시로 알림 기능 등이 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ MSA란 무엇인가]]></title>
            <link>https://velog.io/@zo_meong/MSA%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@zo_meong/MSA%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Tue, 11 Feb 2025 04:28:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/zo_meong/post/c54063e4-8a1c-457b-9c15-39fc08b61e9a/image.png" alt=""></p>
<h1 id="msa">MSA</h1>
<p>MSA(Microservices Architecture)란 하나의 애플리케이션을 <strong>여러 개의 독립적인 서비스</strong>로 분리하여 개발, 배포, 유지보수를 용이하게 하는 소프트웨어 아키텍처 스타일로, 모든 기능이 하나의 애플리케이션 내에 포함되는 모놀리식 아키텍처(Monolithic Architecture)와 반대되는 개념이다. 각 서비스는 특정 비지니스 기능을 수행하며 서로 독립적으로 동작하고 API를 통해 통신한다.</p>
<h2 id="주요-특징">주요 특징</h2>
<ul>
<li><strong>서비스 분리</strong> : 애플리케이션을 여러 개의 독립적인 마이크로 서비스로 분리하며 각 서비스는 특정 기능을 담당</li>
<li><strong>독립성</strong> : 각 서비스는 독립적으로 배포 및 운영이 가능하며 특정 서비스만 개별적으로 확장이 가능하다. 서비스 간의 의존성을 최소화하여 하나의 서비스에 문제가 생겨도 다른 서비스에 영향을 미치지 않는다.</li>
<li><strong>분산 데이터 관리</strong> : 서비스마다 각각의 데이터베이스를 가질 수 있다. 데이터의 일관성을 유지하기 위해 이벤트 기반 아키텍처를 사용할 수 있다.</li>
<li><strong>서비스 간 통신</strong> : 주로 RESTful API, gRPC, 메시지 큐 등을 통해 이루어진다.</li>
</ul>
<h2 id="구성-요소">구성 요소</h2>
<ul>
<li>API Gateway : 클라이언트의 요청을 받아 내부 서비스로 라우팅한다. 인증, 로깅, 로드 밸런싱의 역할을 수행한다.</li>
<li>Eureka (Service Discovery) : 각 서비스의 위치를 동적으로 찾도록 도와주는 서비스 레지스트리의 역할을 한다.</li>
<li>Config Server : 각 마이크로서비스들의 설정 정보를 중앙 관리한다.</li>
</ul>
<pre><code>[외부 클라이언트]
       │
       ▼
[ API Gateway ] 
   ┌───┴───┐
   ▼       ▼
[User]  [Order]  [Payment]  ← 마이크로서비스
   ▲       ▲         ▲
   │       │         │
[ Eureka (Service Discovery) ]  ← 서비스 등록 및 검색
   ▲
   │
[ Config Server ]  ← 중앙 구성 관리</code></pre><h2 id="장점">장점</h2>
<ul>
<li>독립성 : 특정 서비스만 업데이트 가능하며 각각의 서비스를 독립적으로 개발 할 수 있다.</li>
<li>확장성 : 트래픽이 많은 서비스만 개별적으로 확장 할 수 있어 비용 절감이 가능하다.</li>
<li>장애 격리 : 특정 서비스에 장애가 발생해도 전체 시스템이 다운되지 않는다.</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li>복잡성 : 서비스가 많아질수록 구현, 배포, 모니터링, 로깅이 어렵다. 또한 여러 서비스를 관리하기 위한 추가적인 인프라가 필요하다.</li>
<li>네트워크 지연 : 각 서비스의 호출이 많아지면 성능 저하가 발생할 수 있다.</li>
<li>데이터 일관성 : 각 서비스가 독립된 DB를 가지면 트랜잭션 처리가 어렵다.</li>
</ul>
<h2 id="ma-vs-msa">MA VS MSA</h2>
<table>
<thead>
<tr>
<th></th>
<th>MA(모놀리식 아키텍처)</th>
<th>MSA(마이크로서비스)</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>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2로 jar 배포하기]]></title>
            <link>https://velog.io/@zo_meong/AWS-EC2%EB%A1%9C-jar-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@zo_meong/AWS-EC2%EB%A1%9C-jar-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Feb 2025 08:31:42 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 배포할 땐 CI/CD 환경을 구성해서 배포하는 편이지만 간단하고 빠르게 배포를 하고 싶을 때는 jar 파일을 직접 올려 배포하기도 한다.
EC2와 RDS 인스턴스를 설정하고 스프링부트 서버를 배포해보자.</p>
<h1 id="rds">RDS</h1>
<h2 id="rds-인스턴스-생성">RDS 인스턴스 생성</h2>
<ol>
<li>AWS RDS 대시보드에서 &quot;데이터베이스 생성&quot; 클릭</li>
<li>데이터베이스 엔진 선택 (EX. MySQL)</li>
<li>템플릿 - 프리티어 선택</li>
<li>DB 인스턴스 식별자, 마스터 사용자 이름, 암호 설정</li>
<li>퍼블릭 엑세스 설정</li>
</ol>
<ul>
<li>허용시 : 외부(로컬 컴퓨터, EC2 등)에서 접속 가능 / 다만, AWS 정책 변경으로 프리티어에서도 과금됨</li>
<li>비허용시 : EC2에서 RDS의 프라이빗 엔드포인트로 접속</li>
</ul>
<ol start="6">
<li>VPC 보안 그룹 생성</li>
<li>추가 구성 - 초기 데이터베이스 설정 (필요시)</li>
</ol>
<h2 id="보안-그룹-설정">보안 그룹 설정</h2>
<p>RDS 보안그룹의 인바운드 규칙을 편집하여 EC2 또는 외부에서 접근 가능하도록 한다.</p>
<ul>
<li>유형 : <code>MYSQL/Auora</code></li>
<li>포트 : 3306</li>
<li>소스(퍼블릭) : <code>0.0.0.0/0</code> </li>
<li>소스(프라이빗) : (EX. EC2 보안그룹으로 제한)
⚠️ 프라이빗 접근으로 설정했다면 EC2와 RDS를 같은 VPC&amp;서브넷에 배치하면 된다.</li>
</ul>
<h2 id="파라미터-그룹-설정">파라미터 그룹 설정</h2>
<p>RDS 환경에 맞는 파라미터를 설정하여 타임존, 문자 인코딩, 데이터 정렬 기준 등에 대한 설정을 할 수 있다. 아주 간단한 프로젝트 환경이라면 꼭 필요하진 않다.</p>
<ol>
<li>RDS 파라미터 그룹 생성 및 세부 정보 설정</li>
<li>파라미터 편집에서 세부 설정 진행</li>
</ol>
<ul>
<li><code>time_zone</code> : <code>Asia/Seoul</code></li>
<li><code>character_set_*</code> : <code>utf8mb4</code></li>
<li><code>collation_*</code> : <code>utf8mb4_general_ci</code></li>
</ul>
<ol start="3">
<li>RDS 데이터베이스 옵션에서 파라미터 그룹 선택</li>
</ol>
<p>🔗 Spring Boot의 설정 파일에 DB URL 설정</p>
<pre><code class="language-yml">url = jdbc:mysql://[RDS 엔드포인트]:3306/[Database]</code></pre>
<h1 id="ec2">EC2</h1>
<h2 id="ec2-인스턴스-생성">EC2 인스턴스 생성</h2>
<ol>
<li>AWS EC2 대시보드에서 &quot;인스턴스 시작&quot; 클릭</li>
<li>AMI 선택 (이하 Ubuntu 선택 기준)</li>
<li>인스턴스 유형 선택 (프리티어 - t2.micro)</li>
<li>키 페어 발급 후 저장</li>
<li>보안 그룹 생성</li>
</ol>
<h2 id="보안-그룹-설정-1">보안 그룹 설정</h2>
<p>인바운드 규칙을 추가하여 접근 허용</p>
<ul>
<li>포트 22 : SSH</li>
<li>포트 80 : HTTP</li>
<li>포트 443 : HTTPS</li>
<li>포트 8080 : Spring Boot</li>
</ul>
<h2 id="ssh로-ec2-접속">SSH로 EC2 접속</h2>
<p>로컬 컴퓨터(mac) 터미널에서 접근 가능하다.</p>
<pre><code class="language-bash"># 1. 키페어 권한 변경
sudo chmod 600 [key.pem]

# 2. EC2 접속
ssh -i key.pem [ubuntu]@[ec2-public-ip]</code></pre>
<p>EC2 인스턴스에서 &quot;연결&quot;을 선택하여 웹 콘솔로도 접속할 수 있다.</p>
<h1 id="spring-boot">Spring Boot</h1>
<p>스프링부트 애플리케이션을 빌드하여 JAR 파일을 생성 한뒤 EC2에 전송해서 실행시키면 배포가 완료된다.</p>
<ol>
<li>인텔리제이 Gradle 탭의 bootjar를 클릭하면 <code>build &gt; libs</code> 폴더에 jar 파일이 생성된다.</li>
<li>로컬(mac)에서 EC2로 jar 파일을 전송한다.<pre><code class="language-bash">scp -i key.pem app.jar ubuntu@ec2-public-ip:/home/ubuntu/</code></pre>
</li>
<li>EC2에 JDK를 설치한다.<pre><code class="language-bash">sudo apt-get update
sudo apt install openjdk-17-jdk</code></pre>
</li>
<li>jar 파일을 실행시킨다.<pre><code class="language-bash">java -jar [app.jar]
</code></pre>
</li>
</ol>
<h1 id="백그라운드-실행">백그라운드 실행</h1>
<p>nohup java -jar [app.jar] &amp;</p>
<h1 id="실행-종료">실행 종료</h1>
<p>ps -ef | grep -java
kill -9 [pid]</p>
<pre><code>
이렇게 배포 한뒤 `http://[public ip]:8080/` 으로 접속하면 확인할 수 있다.

## 환경 변수
설정 파일의 민감 정보를 환경 변수로 관리하고 있다면 EC2 터미널에서 직접 환경 변수를 설정하여 사용할 수 있다.
```bash
# .bashrc 파일에 추가하여 환경 변수 영구 설정
sudo echo &#39;export EX_VAR=value&#39; &gt;&gt; ~/.bashrc
source ~./bashrc</code></pre><h2 id="포트포워딩">포트포워딩</h2>
<p>jar 파일을 실행시킨 뒤 배포하여 웹 서비스에 접속하려면 주소 뒤에 8080 포트 번호를 항상 붙여주어야 한다. HTTP 요청에서는 80 포트가 기본으로 사용되므로 80 포트로 전달되는 요청을 자동으로 8080으로 전달하는 포트포워딩(port forwarding)을 설정하면 포트 번호를 입력하지 않아도 된다.</p>
<p>EC2 터미널에서 포트포워딩 설정</p>
<pre><code class="language-bash">sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080</code></pre>
<ul>
<li><code>iptables</code> : 리눅스의 네트워크 관리 도구</li>
<li><code>-t nat</code> : nat 주소 변환 테이블</li>
<li><code>-A PREOUTING</code> : PREOUTING 체인에 규칙 추가</li>
<li><code>-i eth0</code> : eth0 네트워크 인터페이스</li>
<li><code>--dport</code> : 패킷의 목적지 포트</li>
<li><code>--to--port</code> : 리다이렉션할 포트</li>
</ul>
<p>⚠️ 네트워크 인터페이스가 다를 경우 확인 후 수정</p>
<pre><code class="language-bash"># 네트워크 인터페이스 확인
ip -a</code></pre>
<p>포트포워딩 후 <code>http://[public ip]</code> 로 접속하여 확인</p>
]]></description>
        </item>
    </channel>
</rss>