<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>gwangmin-kim.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 07 Oct 2026 14:58:28 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>gwangmin-kim.log</title>
            <url>https://velog.velcdn.com/images/gwangmin-kim/profile/395d4ad7-809d-4001-8f4d-eae32bb98089/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. gwangmin-kim.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/gwangmin-kim" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 19일차] 최종 프로젝트 서버 로그인 및 인증]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%EC%9D%B8%EC%A6%9D</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B0%8F-%EC%9D%B8%EC%A6%9D</guid>
            <pubDate>Wed, 07 Oct 2026 14:58:28 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>연결되지 않은 Directed Graph에서 가장 긴 경로를 찾는 문제였다. 다만, 문제 조건이 상당히 단순해서(모든 노드가 1개의 inbound 엣지와 outbound 엣지를 가져야 해서, 모든 노드가 루프를 구성한다) O(N)의 간단한 알고리즘으로도 문제가 해결되었다.</p>
<p><strong>2. 최종 프로젝트</strong></p>
<p><strong>서버 로그인 및 인증</strong> 로직을 구현했다. 현재는 <strong>게스트 로그인</strong>(서버 발급, 클라이언트는 별도의 로그인 화면 없이 로딩 중에 백그라운드로 로그인 처리)만 구현했고, 이후 여유가 된다면 소셜 연동을 고려 중이다. 인증은 <strong>JWT</strong>를 사용하되, JWT의 유효 기간은 짧게 두고, 서버가 DB에 보관 및 명시적으로 폐기할 수 있는 <strong>리프레시 토큰</strong>을 별도로 두어 관리하도록 했다. 이러면 로그인 재시도 횟수를 줄일 수 있는데, 당장은 성능적인 이득이 크지는 않다. 게스트 로그인 방식은 무작위 문자열로 시크릿을 생성하고, 이는 레인보우 테이블 공격 등에 비교적 덜 취약하다. (많이 쓰이는 비밀번호라는 개념이 없으니까) 그래서 현재는 느린 bcrypt 해시를 쓰지 않아 로그인과 리프레시 토큰 검증 간에 퍼포먼스 차이가 크지 않다. 영구 자격 증명 도구인 시크릿이 통신을 타고 오가는 횟수가 줄어드는 것도, 어차피 HTTPS 통신을 사용하니 실질적인 위험은 아니다. 현재에는 성능이나 보안 상의 이점보다는, <strong>로그인 상태가 유지될 수 있으며, 서버가 명시적으로 사용자의 로그인 상태를 끊을 수도 있는</strong> 구조라는 데에 의의가 있다. 다만, bcrypt를 굳이 안 쓸 이유는 없어 보여서 전환을 검토해보려고 한다. 느리기 때문에 공격을 어렵게 한다는 장점도 있으니까.</p>
<p><strong>서버 저장 및 로드</strong> 로직을 일부 구현했다. 단계별로 나눠서 구현할 예정이며, 현재는 1단계: 클라이언트가 올려주는 진행도를 믿고 서버에 저장하는 방식이다. 즉, 데이터가 서버에도 저장되지만, 클라이언트도 가지고 있는 구조이다. 점진적으로 서버만이 원본을 가지고 있고, 클라이언트는 인증 토큰을 가지고 요청해서 값을 받아오는 구조로 변경할 예정이다.</p>
<p>JWT를 학습한 지 시간이 좀 지나서 많이 내용을 잊어버렸는데, 이번 작업을 통해 다시 복습하는 기회가 되었다.</p>
<h1 id="내일-학습할-내용">내일 학습할 내용</h1>
<ul>
<li><strong>서버 저장 및 로드 기능:</strong> 클라이언트 책임에서 점차 서버 책임으로 이동. 정보 원본을 서버가 갖고 있고, 클라이언트는 인증 토큰을 가지고 요청만 하는 방식</li>
<li><strong>클라이언트 시크릿 저장 방식 검토:</strong> 현재는 평문으로 저장되는데, 보다 안전한 방식 조사</li>
<li><strong>클라이언트 연동 테스트:</strong> Unity REST 살펴보기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 혼자 놀기의 달인]]></title>
            <link>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%98%BC%EC%9E%90-%EB%86%80%EA%B8%B0%EC%9D%98-%EB%8B%AC%EC%9D%B8</link>
            <guid>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%98%BC%EC%9E%90-%EB%86%80%EA%B8%B0%EC%9D%98-%EB%8B%AC%EC%9D%B8</guid>
            <pubDate>Wed, 07 Oct 2026 00:43:23 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/131130">문제 링크</a></p>
<p><strong>제출 코드(통과)</strong></p>
<pre><code class="language-csharp">using System;

public class Solution {
    public int solution(int[] cards)
    {
        var opened = new bool[cards.Length];
        var max1 = 0;
        var max2 = 0;
        for (var i = 0; i &lt; cards.Length; i++)
        {
            if (opened[i]) continue;
            opened[i] = true;
            var j = cards[i] - 1;
            var count = 1;
            while (!opened[j])
            {
                opened[j] = true;
                j = cards[j] - 1;
                count++;
            }

            if (count &gt; max1)
            {
                max2 = max1;
                max1 = count;
            }
            else if (count &gt; max2)
            {
                max2 = count;
            }
        }
        return max1 * max2;
    }
}</code></pre>
<p>카드에 중복이 없기 때문에, 상자 관계를 그래프로 그리면 모든 상자가 루프를 만든다. (연결된 그래프의 일부만 루프에 갇히는 경우가 존재할 수 없음. 그러기 위해선 한 상자를 가리키는 상자가 여러 개여야 하기 때문) 따라서 이미 열려있는 상자가 속한 경로의 길이는 루프의 길이로 고정되어 변하지 않기 때문에 다시 볼 필요가 없다. 루프의 길이를 재면서 가장 긴 두 개만 남긴다. 만약 루프가 하나면 <code>max2</code>가 0으로 남기 때문에 자연스럽게 0을 반환하게 된다.</p>
<p><strong>내부 루프 진입을 좀 더 깔끔하게 수정(로직엔 차이 없음)</strong></p>
<pre><code class="language-csharp">using System;

public class Solution {
    public int solution(int[] cards)
    {
        var opened = new bool[cards.Length];
        var max1 = 0;
        var max2 = 0;
        for (var i = 0; i &lt; cards.Length; i++)
        {
            if (opened[i]) continue;
            var j = i;
            var count = 0;
            while (!opened[j])
            {
                opened[j] = true;
                j = cards[j] - 1;
                count++;
            }

            if (count &gt; max1)
            {
                max2 = max1;
                max1 = count;
            }
            else if (count &gt; max2)
            {
                max2 = count;
            }
        }
        return max1 * max2;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 19일차] 최종 프로젝트 서버 기획 시작]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%9C%EB%B2%84-%EA%B8%B0%ED%9A%8D-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%9C%EB%B2%84-%EA%B8%B0%ED%9A%8D-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Tue, 06 Oct 2026 12:23:45 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>BFS를 활용하는 문제였는데, 어렵게 생각하다가 다소 시간을 많이 잡아먹었다. &#39;최단경로를 찾는다&#39;는 목표에 매몰되어 A* 알고리즘을 사용해보았는데, 알고리즘을 정확히 구현하지 못해서 처음 제출은 실패했었다. 연결된 노드 간 가중치(거리)가 모두 1이기 때문에, 굳이 A* 알고리즘이 필요하지 않았고, BFS 트리 구축만으로도 최단 경로를 얻어낼 수 있었다. 제출 성공 이후 A* 알고리즘도 올바르게 구현하여 통과했지만, 성능면에서는 BFS보다 불리하기 때문에 이 문제에서 적절한 풀이는 아니다.</p>
<p>지금까지의 경향을 보면 그래프 기반 문제에 약한 것 같아서, 해당 유형의 문제들을 조금 더 집중적으로 풀어보려고 한다.</p>
<p><strong>2. 최종 프로젝트</strong></p>
<p>클라이언트는 거의 마무리 단계에 들어섰고, 나는 오늘부터 서버 설계를 맡게 되었다. 우리 게임에서, 서버가 맡아야 할 핵심 기능은 <strong>사용자 인증</strong>, <strong>진행상황 저장/로드 기능 제공</strong> 및 <strong>클라이언트 조작 검증</strong>으로 잡아두었다.</p>
<p>현재 클라이언트와 비교적 무관한 사용자 인증 API를 구현 중이다. 계정은 명시적인 id/pw 입력 없이, 첫 접속 시 서버로부터 게스트 계정을 발급받아 클라이언트에 저장해두고, 이후부터 해당 계정으로 접속하는 방식을 택했다. 이렇게 하면 명시적인 로그인을 요구하지 않고 사용자를 구별할 수 있다. 추후 가능하다면 소셜 플랫폼 연동을 통한 로그인으로의 확장을 고려 중이다. (구글, 애플, 카카오 등)</p>
<p>저장 기능은 먼저 클라이언트에서 진행 상황 상태를 정의하는 방법을 결정짓고, 해당 구조에 맞게 서버가 값을 내려주는 방식으로 구현할 예정이다. 우리 게임에서의 핵심 정보는 <strong>업그레이드 노드 상태</strong>와 <strong>보유 골드량</strong>, <strong>기존 최대 진행도(블랙홀 레벨)</strong> 정도로 구성된다. 노드 구매 요청 처리는 서버로 이관해야 하며, 구매 시점에는 &quot;노드 구매 정보&quot;만 반영되고, 실제 수치가 달라지지는 않는다. 수치 조립은 전투 시작 시 이루어진다. csv 파일 등 업그레이드 세부 정보 역시 서버가 원본을 가지고 있어야 하며, 클라이언트는 그걸 읽고 ui를 그리는 데에만 사용해야 한다.</p>
<p>클라이언트 조작 검증은 아직 그 강도를 확정짓지 못했다. 약하게는 전투 결과의 합리성 검증부터, 강하게는 결정론적 전투 시뮬레이션 재현까지 고려해볼 수 있다. 다만 후자의 경우 C#과 Java라는 환경 차이로 인해 두 세계에서 완벽히 동일한 결과를 재현하는 데에 한계가 있을 수 있고, 서버 부하가 커지며 싱글 플레이 게임에서 과한 구현이 될 수 있다. 지금은 전자와 후자의 중간 정도에 위치한, <strong>이벤트 로그 검증 방식</strong> 정도가 적정선이라고 생각하고 있는데, 적정선에 대해서는 실무 경험이 있으신 튜터님들께 조언을 구해보는 게 좋을 것 같다.</p>
<h1 id="내일-학습할-내용">내일 학습할 내용</h1>
<ul>
<li>최종 프로젝트 서버 API 명세 구축<ul>
<li>유저 로그인(인증) - 게스트 기반</li>
<li>세이브 구조 확인 및 DB 저장 스키마 기획</li>
</ul>
</li>
<li>외부 Unity 팀프로젝트 개발<ul>
<li>절차적 맵 생성기 개선</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 미로 탈출]]></title>
            <link>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AF%B8%EB%A1%9C-%ED%83%88%EC%B6%9C</link>
            <guid>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%AF%B8%EB%A1%9C-%ED%83%88%EC%B6%9C</guid>
            <pubDate>Tue, 06 Oct 2026 06:16:28 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/159993">문제 링크</a></p>
<p><strong>첫 제출(실패)</strong></p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;

public class Solution {
    public int solution(string[] maps)
    {
        (int x, int y) s = (-1, -1);
        (int x, int y) l = (-1, -1);
        (int x, int y) e = (-1, -1);

        for (var i = 0; i &lt; maps.Length; i++)
        {
            for (var j = 0; j &lt; maps[i].Length; j++)
            {
                switch (maps[i][j])
                {
                    case &#39;S&#39;: s = (i, j); break;
                    case &#39;L&#39;: l = (i, j); break;
                    case &#39;E&#39;: e = (i, j); break;
                }
            }
        }

        var toLever = Calc(maps, s, l);
        if (toLever == -1) return -1;
        var toExit = Calc(maps, l, e);
        if  (toExit == -1) return -1;
        return toLever + toExit;
    }

    private int Calc(string[] maps, (int x, int y) from, (int x, int y) to)
    {
        (int x, int y) size = (maps.Length, maps[0].Length);
        var nexts = new PriorityQueue&lt;(int x, int y), int&gt;();
        nexts.Enqueue(from, Manhattan(from, to));

        var visited = new bool[size.x, size.y];
        var dists = new int[size.x, size.y];

        while (nexts.Count &gt; 0)
        {
            var now = nexts.Dequeue();
            visited[now.x, now.y] = true;
            if (now == to) return dists[now.x, now.y];

            var currentDist = dists[now.x, now.y];
            var candidates = new[]
            {
                (now.x - 1, now.y),
                (now.x + 1, now.y),
                (now.x, now.y - 1),
                (now.x, now.y + 1)
            };

            foreach (var (x,y) in candidates)
            {
                if (x &lt; 0 || x &gt;= size.x || 
                    y &lt; 0 || y &gt;= size.y || 
                    visited[x, y] ||
                    maps[x][y] == &#39;X&#39;) continue;

                dists[x, y] = currentDist + 1;
                var priority = dists[x, y] + Manhattan((x, y), to);
                nexts.Enqueue((x, y), priority);
            }
        }

        return -1;
    }

    private int Manhattan((int x, int y) from, (int x, int y) to)
    {
        return Math.Abs(from.x - to.x) + Math.Abs(from.y - to.y);
    }
}</code></pre>
<p><strong>A* 알고리즘</strong>으로 해결하려고 했는데, 단순히 알고리즘을 잘못 구현해서 실패했다. A* 알고리즘은 같은 위치를 한 번 방문한다고 끝나면 안되고, 더 짧은 경로가 발견되면 갱신해주어야 한다.</p>
<p>사실 이 문제는 굳이 최단 경로 알고리즘을 사용할 필요는 없었다. 가중치가 없기 때문에, 단순히 시작 지점을 기준으로 BFS 트리를 만들어주면 모든 칸까지의 최단 경로 트리가 완성이 된다.</p>
<p><strong>수정된 코드(통과)</strong></p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;

public class Solution {
    public int solution(string[] maps)
    {
        (int x, int y) s = (-1, -1);
        (int x, int y) l = (-1, -1);
        (int x, int y) e = (-1, -1);

        for (var i = 0; i &lt; maps.Length; i++)
        {
            for (var j = 0; j &lt; maps[i].Length; j++)
            {
                switch (maps[i][j])
                {
                    case &#39;S&#39;: s = (i, j); break;
                    case &#39;L&#39;: l = (i, j); break;
                    case &#39;E&#39;: e = (i, j); break;
                }
            }
        }

        var toLever = Calc(maps, s, l);
        if (toLever == -1) return -1;
        var toExit = Calc(maps, l, e);
        if  (toExit == -1) return -1;
        return toLever + toExit;
    }

    private int Calc(string[] maps, (int x, int y) from, (int x, int y) to)
    {
        (int x, int y) size = (maps.Length, maps[0].Length);
        var nexts = new Queue&lt;(int x, int y)&gt;();
        var visited = new bool[size.x, size.y];
        var dists = new int[size.x, size.y];

        nexts.Enqueue(from);
        visited[from.x, from.y] = true;

        while (nexts.Count &gt; 0)
        {
            var now = nexts.Dequeue();
            if (now == to) return dists[now.x, now.y];

            var candidates = new[]
            {
                (now.x - 1, now.y),
                (now.x + 1, now.y),
                (now.x, now.y - 1),
                (now.x, now.y + 1)
            };

            foreach (var (x, y) in candidates)
            {
                if (x &lt; 0 || x &gt;= size.x || 
                    y &lt; 0 || y &gt;= size.y || 
                    visited[x, y] || 
                    maps[x][y] == &#39;X&#39;) continue;

                visited[x, y] = true;
                dists[x, y] = dists[now.x, now.y] + 1;
                nexts.Enqueue((x, y));
            }
        }

        return -1;
    }
}</code></pre>
<p>물론, A* 알고리즘도 올바르게 쓰면 통과할 수 있다. 더 이상 방문 기록은 없고, 대신 처음에는 거리가 무한(여기선 <code>int</code>를 사용하니 <code>int.MaxValue</code>로 대체)한 것으로 초기화하고, 더 짧은 경로가 나타날때마다 갱신해주는 방식을 써야 한다.</p>
<p>아래 코드로도 통과는 했지만, BFS보다 수행 시간이 오래 걸렸다. 알고리즘 자체가 더 무겁기 때문에 당연한 결과. 핵심적인 원인은 우선순위 큐가 단순 큐보다 삽입/삭제 복잡도가 높다는 것이다.</p>
<p><strong>올바른 A* 알고리즘 코드</strong></p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;

public class Solution {
    public int solution(string[] maps)
    {
        (int x, int y) s = (-1, -1);
        (int x, int y) l = (-1, -1);
        (int x, int y) e = (-1, -1);

        for (var i = 0; i &lt; maps.Length; i++)
        {
            for (var j = 0; j &lt; maps[i].Length; j++)
            {
                switch (maps[i][j])
                {
                    case &#39;S&#39;: s = (i, j); break;
                    case &#39;L&#39;: l = (i, j); break;
                    case &#39;E&#39;: e = (i, j); break;
                }
            }
        }

        var toLever = Calc(maps, s, l);
        if (toLever == -1) return -1;
        var toExit = Calc(maps, l, e);
        if (toExit == -1) return -1;

        return toLever + toExit;
    }

    private int Calc(string[] maps, (int x, int y) from, (int x, int y) to)
    {
        (int x, int y) size = (maps.Length, maps[0].Length);

        var dists = new int[size.x, size.y];
        for (int i = 0; i &lt; size.x; i++)
        {
            for (int j = 0; j &lt; size.y; j++)
            {
                dists[i, j] = int.MaxValue;
            }
        }

        var nexts = new PriorityQueue&lt;(int x, int y), int&gt;();

        dists[from.x, from.y] = 0;
        int startPriority = dists[from.x, from.y] + Manhattan(from, to);
        nexts.Enqueue(from, startPriority);

        while (nexts.Count &gt; 0)
        {
            var now = nexts.Dequeue();

            if (now == to) return dists[now.x, now.y];

            var candidates = new[]
            {
                (now.x - 1, now.y),
                (now.x + 1, now.y),
                (now.x, now.y - 1),
                (now.x, now.y + 1)
            };

            foreach (var (x, y) in candidates)
            {
                if (x &lt; 0 || x &gt;= size.x || 
                    y &lt; 0 || y &gt;= size.y || 
                    maps[x][y] == &#39;X&#39;) continue;

                int newDist = dists[now.x, now.y] + 1;

                if (newDist &lt; dists[x, y])
                {
                    dists[x, y] = newDist;
                    int priority = newDist + Manhattan((x, y), to);
                    nexts.Enqueue((x, y), priority);
                }
            }
        }

        return -1;
    }

    private int Manhattan((int x, int y) from, (int x, int y) to)
    {
        return Math.Abs(from.x - to.x) + Math.Abs(from.y - to.y);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 19일차] 최종 프로젝트 클라이언트 개발 진행]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%EC%A7%84%ED%96%89</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-19%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EA%B0%9C%EB%B0%9C-%EC%A7%84%ED%96%89</guid>
            <pubDate>Fri, 02 Oct 2026 12:13:07 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>최근 며칠 레벨 5 문제가 순조롭게 풀려서, 레벨 6의 첫번째 문제를 풀어봤다. 하노이탑 문제였는데, 난이도를 떠나서 너무 널리 알려진 문제라 그런지 오히려 더 쉽게 풀어낸 것 같다. 굳이 따지자면 신경썼던 부분은 재귀 호출마다 새 배열을 만들지 않고, 처음부터 총 시행 횟수를 미리 계산해서(점화식으로 일반항 도출) 한번만 할당한 점.</p>
<p><strong>2. Spring 심화 완강</strong></p>
<p>챕터 4까지 모두 들었다. 챕터 4는 전반적으로 실습보다는 클라우드 컴퓨팅, 운영 및 배포의 여러 요소들을 가볍게 소개하는 내용에 가까웠다. 최종 프로젝트 배포 환경을 구성하는 데에 활용할 수 있기를 기대하지만, 당장은 다소 막연한 느낌이라 서버 앱 개발 후 직접 실습해보며 배워가야 할 것 같다.</p>
<p><strong>3. 최종 프로젝트 MVP 발표</strong></p>
<p>다른 팀의 MVP 발표를 구경할 수 있었다. 뱀서라이크, 파티/보드게임이 많이 보였다. 우리 조의 진행 상황 및 클라이언트 완성도가 나쁘지 않다는 생각이 들었다. 아트 부담이 적은 추상적 미니멀리즘 그래픽을 레퍼런스로 택한 것이 좋았던 것 같다. 마피아 게임을 개발하는 팀은 서버를 벌써 구축하기까지 해서 인상적이었다.</p>
<h1 id="내일연휴간-학습할-내용">내일(연휴간) 학습할 내용</h1>
<ul>
<li>최종 프로젝트 클라이언트 개발<ul>
<li>블랙홀 성장도 관련 시스템 구조 수정</li>
<li>전투 세션 구축 구조 점검 및 개선</li>
<li>(가능하다면) 서버 구축 계획</li>
</ul>
</li>
<li>Unity 외부 팀 프로젝트 개발<ul>
<li>절차적 맵 생성 로직 구체화<ul>
<li>스폰 지점 근처 강제 지형 규칙</li>
<li>여러 종류의 타일을 경계를 구분짓지 않고 이어지게 설치하는 오토타일 규칙</li>
<li>(선택) 인위적 지형 추가</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (CDN)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CDN</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CDN</guid>
            <pubDate>Fri, 02 Oct 2026 08:46:55 GMT</pubDate>
            <description><![CDATA[<h1 id="cdn-content-delivery-network">CDN (Content Delivery Network)</h1>
<blockquote>
<p>지리적 제약 없이 전 세계 사용자에게 웹 콘텐츠를 빠르고 안전하게 전송하기 위해 <strong>분산 배치된 서버 네트워크</strong></p>
</blockquote>
<ul>
<li>전 세계 분산된 서버에 콘텐츠를 복사해두고, 사용자와 가장 가까운 서버에서 응답</li>
<li>정적 파일(이미지 등)은 누가 요청해도 같은 내용<ul>
<li>미국, 유럽, 아시아 등 곳곳에 미리 복사해두면, 사용자는 가까운 서버에서 응답을 받아 물리적 거리에 따른 지연을 최소화</li>
</ul>
</li>
</ul>
<h2 id="cloudfront">CloudFront</h2>
<blockquote>
<p>AWS의 CDN 서비스</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/1e041307-b1da-4daa-a2df-5206e7cc70d2/image.png" alt=""></p>
<ul>
<li>전 세계 750개 이상 지역에 배치된 캐시 서버</li>
<li>일종의 프록시<ul>
<li>S3에 직접 접근하지 못하고, CloudFront를 거쳐 접근하게됨</li>
<li>보안에서의 이점</li>
</ul>
</li>
<li>일반적으로, 같은 지역이라고 해도 S3에서 파일을 가져오는 것보다 CloudFront에서 데이터를 가져오는 것이 훨씬 빠르고, 비용도 효율적</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (CloudWatch)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CloudWatch</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CloudWatch</guid>
            <pubDate>Fri, 02 Oct 2026 02:47:20 GMT</pubDate>
            <description><![CDATA[<h1 id="aws-cloudwatch">AWS CloudWatch</h1>
<blockquote>
<p>AWS 리소스들의 상태를 수집하고 모니터링하는 서비스</p>
</blockquote>
<h2 id="핵심-개념">핵심 개념</h2>
<ul>
<li><strong>Metric:</strong> 시간에 따라 변하는 숫자(CPU 사용률, 요청 수)</li>
<li><strong>Log Group:</strong> 로그 저장 단위</li>
<li><strong>Dashboard:</strong> 지표 시각화 화면</li>
</ul>
<h3 id="alarm">Alarm</h3>
<blockquote>
<p>특정 지표에 기준을 설정해두고 자동화된 대응 조치를 수행하는 것</p>
</blockquote>
<p><strong>예시</strong></p>
<ul>
<li>&#39;CPU 사용률이 80%를 넘으면 즉시 담당자 슬랙이나 이메일로 경고를 보내라&#39;</li>
<li>&#39;트래픽이 폭주하면 자동으로 서버를 여러 대 더 띄워라(Auto Scaling)&#39;</li>
</ul>
<h2 id="cloudwatch-지표-확인-예">CloudWatch 지표 확인 예</h2>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/7f29d3e0-7cf1-4012-ac66-04ff6e62b426/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/de0c3a0a-8651-4dbf-afff-ee090490ba8d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (로드 밸런싱)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1</guid>
            <pubDate>Fri, 02 Oct 2026 02:42:24 GMT</pubDate>
            <description><![CDATA[<h1 id="로드밸런서">로드밸런서</h1>
<blockquote>
<p>트래픽을 받아 서버에 분배하는 장치</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/702ed927-2b1a-40d6-a576-33bb6c70e5b9/image.png" alt="">
(Target은 EC2 혹은 Fargate 등)</p>
<ul>
<li>EC2 IP를 직접 노출하지 않음 (프록시)</li>
<li>HTTPS 처리</li>
<li>헬스체크</li>
<li>서버 확장 용이</li>
</ul>
<h1 id="alb-application-load-balancer">ALB (Application Load Balancer)</h1>
<blockquote>
<p>AWS의 관리형 로드 밸런서(ELB, Elastic Load Balancer) 중 하나
HTTP를 이해하는 어플리케이션 전용 로드 밸런서</p>
</blockquote>
<ul>
<li>헬스체크: <code>/actuator/health</code> 등으로 수행</li>
<li>HTTPS: ACM 인증서 연동으로 수행</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (서버리스)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4</guid>
            <pubDate>Fri, 02 Oct 2026 02:08:34 GMT</pubDate>
            <description><![CDATA[<h1 id="서버리스">서버리스</h1>
<blockquote>
<p>개발자가 <strong>서버 인프라를 직접 관리하거나 설정할 필요가 없는</strong> 클라우드 컴퓨팅 모델
개발자는 코드 작성과 비즈니스 로직에만 집중할 수 있음
<strong>서버가 없다는 뜻이 아니다!</strong></p>
</blockquote>
<h2 id="aws-fargate">AWS Fargate</h2>
<blockquote>
<p><strong>AWS의 서버리스 컴퓨팅 서비스</strong>
EC2처럼 인스턴스를 직접 프로비저닝/관리할 필요 없이 컨테이너를 바로 실행할 수 있게 해줌</p>
</blockquote>
<ul>
<li>Docker가 내장되어 있음</li>
<li>SSH(<code>.pem</code>)같은 방식으로 서버 내부에 접속할 수 없음<ul>
<li>서버가 존재는 하지만, 관리할 수 없음</li>
</ul>
</li>
</ul>
<blockquote>
<p>반드시 Fargate가 더 좋은 것은 아님!</p>
</blockquote>
<ul>
<li>간편하게 백엔드 앱을 띄운다면: Fargate</li>
<li>게임 데디케이트 서버 등: EC2</li>
</ul>
<h2 id="다른-aws-서버리스-서비스">다른 AWS 서버리스 서비스</h2>
<ul>
<li>AWS S3</li>
<li>AWS Lambda</li>
<li>AWS DynamoDB</li>
<li>Amazon Aurora Serverless</li>
<li>AWS EventBridge</li>
<li>...</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (ASG)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-ASG</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-ASG</guid>
            <pubDate>Fri, 02 Oct 2026 01:59:03 GMT</pubDate>
            <description><![CDATA[<h1 id="asg-auto-scaling-group">ASG (Auto Scaling Group)</h1>
<blockquote>
<p>Launch Template 기반으로 EC2를 자동 생성/삭제/관리하는 서비스</p>
</blockquote>
<h4 id="launch-template이란">Launch Template이란?</h4>
<p>ASG가 EC2를 만들 때 사용하는 설계도</p>
<ul>
<li>어떤 OS를 사용할지(Amazon Linux 2023)</li>
<li>어떤 사양인지(t4g.small)</li>
<li>어떤 트래픽을 허용할지(보안 그룹)</li>
<li>어떤 AWS 권한을 가질지(IAM)</li>
<li>EC2 시작 시 실행할 스크립트(User Data)</li>
</ul>
<h2 id="핵심-설정">핵심 설정</h2>
<ul>
<li><strong>Desired:</strong> 지금 유지하고 싶은 인스턴스 수(인스턴스 장애 시 새 EC2 생성)</li>
<li><strong>Min:</strong> 최소 인스턴스 수(트래픽 줄면 Min 수준까지 인스턴스 제거)</li>
<li><strong>Max:</strong> 최대 인스턴스 수(부하 발생 시 Max 범위 내에서 자원 추가)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (NAT Gateway)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-NAT-Gateway</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-NAT-Gateway</guid>
            <pubDate>Fri, 02 Oct 2026 01:52:05 GMT</pubDate>
            <description><![CDATA[<h1 id="nat-gateway">NAT Gateway</h1>
<blockquote>
<p><strong>프라이빗 서브넷 내의 인스턴스가 외부 인터넷과 통신할 수 있도록</strong> 네트워크 주소 변환(NAT)을 수행하는 클라우드 네트워킹 서비스</p>
</blockquote>
<ul>
<li><strong>아웃바운드 전용:</strong> 프라이빗 서브넷 내부에서 외부 인터넷을 호출할 수는 있지만, 외부 이넡넷에서 프라이빗 서브넷 내부 인스턴스에 접속할 수는 없다.</li>
</ul>
<h4 id="zonal-gateway-regional-gateway">Zonal Gateway, Regional Gateway</h4>
<ul>
<li>Zonal NAT Gateway<ul>
<li>기존에 있던 방식</li>
<li>AZ마다 설정해주어야 함</li>
</ul>
</li>
<li>Regional NAT Gateway<ul>
<li>최신 버전</li>
<li>Region 전체를 커버하는 NAT Gateway</li>
<li>하나만 만들면 별다른 설정 없이도 AWS가 AZ별로 알아서 확장해줌</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>ECR에서 Docker Image를 pull하기 위해서 NAT Gateway가 필요하다!</strong>
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/f462d2ea-6afe-402b-9f00-23a42e85a97a/image.png" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (CI/CD)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CICD</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-CICD</guid>
            <pubDate>Fri, 02 Oct 2026 01:42:35 GMT</pubDate>
            <description><![CDATA[<h1 id="cicd란">CI/CD란</h1>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/54fcc66a-994b-48a1-934b-1e791737907e/image.png" alt=""></p>
<blockquote>
<p><strong>CI:</strong> 개발된 코드를 자동으로 빌드/테스트 (무결성 검증)
<strong>CD:</strong> 완성된 앱을 실제 환경에 자동으로 배포</p>
</blockquote>
<h2 id="ci-continuous-integration">CI (Continuous Integration)</h2>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/7742f1f7-8d7d-4827-9ec6-0c859a8a512f/image.png" alt=""></p>
<blockquote>
<p><strong>지속적인 통합</strong>
작업한 코드를 지속적으로 빌드 및 테스트하여 레포지토리에 통합(merge)하는 것</p>
</blockquote>
<h2 id="cd-continuous-deployment">CD (Continuous Deployment)</h2>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/e5195f3d-0894-4580-a612-f1e8feb38f01/image.png" alt=""></p>
<blockquote>
<p><strong>지속적인 배포</strong>
통합된 코드를 지속적으로 릴리스하여 운영 환경에 배포(deploy)하는 것</p>
</blockquote>
<ul>
<li>Continuous Delivery라고도 쓰인다</li>
</ul>
<h1 id="ecr-elastic-container-registry">ECR (Elastic Container Registry)</h1>
<blockquote>
<p>AWS에서 제공하는 <strong>Docker 이미지 저장소</strong></p>
</blockquote>
<ul>
<li><p>한 쪽에서 이미지 push, 다른 쪽애서 pull하여 실행</p>
</li>
<li><p>Docker Hub 등 외부 저장소를 사용할 때와 달리, 별도의 인증(로그인) 없이 이미지를 가져올 수 있다. (IAM으로 권한 관리)</p>
</li>
<li><p>기존 실습: <code>.jar</code>를 직접 EC2로 옮김</p>
</li>
<li><p>실무: Docker 이미지를 통해 배포</p>
</li>
<li><p>Docker 이미지 전용 S3라고 생각해도 된다. </p>
<ul>
<li>도커 이미지 전용 저장소가 없다면, S3에 이미지를 올리고 그걸 EC2에서 가져다 사용하는 방식도 가능할 것이다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>사실 ECR이 내부적으로는 S3와 같다. (S3를 래핑해서 도커 이미지 전용으로 만든 것)</p>
</blockquote>
<h1 id="github-actions">Github Actions</h1>
<blockquote>
<p>코드 push를 트리거로 자동으로 무언가를 실행하도록 하는 도구
빌드/배포 자동화 가능</p>
</blockquote>
<ul>
<li>로컬에서 <code>./gradlew bootJar</code> 실행</li>
<li><code>docker build</code> 실행</li>
<li><code>docker tag</code> 실행</li>
<li>ECR 로그인</li>
<li><code>docker push</code> 실행</li>
</ul>
<p>Github Actions를 사용하면 항상 일관된 절차로 실수 없이 테스트 및 배포를 진행할 수 있다.</p>
<blockquote>
<p>코드를 push하면, GitHub Actions에서 자동 테스트, 빌드, Docker Image 생성, ECR에 Docker Image push까지 자동으로 이루어지도록 할 수 있다.
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/0f020973-32c2-4197-8201-05311877e321/image.png" alt=""></p>
</blockquote>
<h2 id="핵심-용어">핵심 용어</h2>
<ul>
<li><strong>Workflow:</strong> 자동화 작업 전체를 정의한 파일</li>
<li><strong>Job:</strong> 워크플로우 안에서 독립적으로 실행되는 작업 단위</li>
<li><strong>Step:</strong> 잡 안에서 순서대로 실행되는 명령어</li>
<li><strong>Runner:</strong> 실제로 명령어를 실행하는 서버(Github 제공)</li>
</ul>
<h2 id="실행-흐름">실행 흐름</h2>
<ol>
<li>코드 push</li>
<li>GitHub이 Workflow 파일 발견</li>
<li>Runner (임시 서버) 생성</li>
<li>Job 시작 (Step 순서대로)</li>
<li>결과 리포트 (성공/실패)</li>
<li>Runner 삭제</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 하노이의 탑]]></title>
            <link>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%95%98%EB%85%B8%EC%9D%B4%EC%9D%98-%ED%83%91</link>
            <guid>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%95%98%EB%85%B8%EC%9D%B4%EC%9D%98-%ED%83%91</guid>
            <pubDate>Fri, 02 Oct 2026 00:48:21 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/12946">문제 링크</a></p>
<p><strong>제출 코드(통과)</strong></p>
<pre><code class="language-csharp">using System;

public class Solution {
    public int[,] solution(int n)
    {
        var size = Power(2, n) - 1;
        var answer = new int[size, 2];
        Iter(n, 0, 1, 3, answer);
        return answer;
    }

    private static int Power(int baseNum, int exp)
    {
        var result = 1;
        for (var i = 0; i &lt; exp; i++)
            result *= baseNum;
        return result;
    }

    private int Iter(int n, int step, int from, int to, int[,] answer)
    {
        if (n == 1)
        {
            answer[step, 0] = from;
            answer[step, 1] = to;
            return step + 1;
        }

        var nextTo = 6 - from - to;
        var next = Iter(n - 1, step, from, nextTo, answer);
        answer[next, 0] = from;
        answer[next, 1] = to;
        next++;
        return Iter(n-1, next, nextTo, to, answer);
    }
}</code></pre>
<p>전형적인 재귀 문제 중 하나인 하노이 탑이다. 총 시행 횟수의 점화식이 <code>x(n+1) = 2x(n) + 1</code>이므로 일반항은 <code>x(n) = 2^n - 1</code>이다. 크기부터 계산해 전체 배열을 한 번 할당하고, 내부를 채워나가는 방식으로 작성했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 18일차] 최종 프로젝트 클라이언트 MVP]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-18%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-MVP</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-18%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-MVP</guid>
            <pubDate>Thu, 01 Oct 2026 14:15:34 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>원형 큐 개념을 이용하는 문제였고, 큰 문제 없이 해결하였다. 최근 며칠간의 문제가 비교적 쉽게 해결되고 있는데, 5레벨 치고 쉬운 문제들이었던 건지 실력이 늘어서인건지 아직 잘 모르겠다. 우선 스택/큐를 메인으로 활용하는 문제는 어느 정도 해결이 가능한 것 같다. 순차적으로 계속 풀기보단, 6레벨로 넘어가거나 다른 유형의 문제를 더 찾아보는 것도 좋을 것 같다.</p>
<p><strong>2. Spring 심화 챕터 3</strong></p>
<p>챕터 3 마지막 항목인 Webcraft 배포 실습을 진행했다. EC2, RDS, Elasticache, Parameter Store를 모두 활용하여 배포 환경에 자바 서버 앱을 올리고, 환경변수를 분리하여 주입하도록 했다. 연결 과정에서 암호화 관련 작은 이슈가 있었다. 로컬 환경에선 암호화 없이 통신하더라도, 배포 환경에선 암호화를 반드시 사용하여야 한다.</p>
<p>챕터 4는 아직 듣지 못했다. 내일 발표 후 약간의 여유가 있으니 내일은 정말 수강하려고 한다.</p>
<p><strong>3. 최종 프로젝트 MVP</strong></p>
<p>게임의 겉모습은 어느 정도 완성되었다. 나는 git 병합 요청에 대한 리뷰 및, 적 파괴 특수 효과 구조화 및 시각화를 맡아 진행하였다.</p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/ea3ebe9a-d84a-4a8d-8a4e-0029ee0d6633/image.png" alt=""></p>
<p>반복 게임의 핵심이 되는 업그레이드 노드 구현이 필요하고, 핵심적인 시스템 구현에서 역기획 내용과 다른 점이 있어 해당 부분을 수정해야 한다.</p>
<h1 id="내일-학습할-내용">내일 학습할 내용</h1>
<ul>
<li>클라이언트 개발 발표 준비</li>
<li>적 생성 구조 점검 및 수정</li>
<li>Spring 심화 챕터 4까지 진행(완강)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (AWS 배포 실습)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-AWS-%EB%B0%B0%ED%8F%AC-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-AWS-%EB%B0%B0%ED%8F%AC-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Thu, 01 Oct 2026 12:28:48 GMT</pubDate>
            <description><![CDATA[<h1 id="webcraft-배포-실습">Webcraft 배포 실습</h1>
<h2 id="jar-빌드-로컬">jar 빌드 (로컬)</h2>
<p>과제로 제출한 버전이 아닌 <a href="https://github.com/f-api/webcraft-server">제공된 버전</a>을 사용한다.</p>
<h3 id="설정-파일-확인-applicationproperties">설정 파일 확인 (<code>application.properties</code>)</h3>
<pre><code>spring.application.name=webcraft
spring.datasource.url=jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:webcraft}?createDatabaseIfNotExist=true
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:12345678}
spring.jpa.hibernate.ddl-auto=update
spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=${REDIS_PORT:6379}
spring.data.redis.ssl.enabled=${REDIS_SSL:false}

webcraft.chat.pubsub-enabled=true</code></pre><ul>
<li>기본적으로 로컬의 DB를 사용하도록 Fallback 설정이 되어있다.</li>
</ul>
<h2 id="보안그룹-생성">보안그룹 생성</h2>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/9fc36454-719d-4a04-bdd2-db53342fee8d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/91d9b73f-f149-410e-9bee-3ac75bca7f62/image.png" alt=""></p>
<h2 id="rds-생성">RDS 생성</h2>
<blockquote>
<p>이전 실습과 달리, <strong>프라이빗 서브넷으로만 서브넷 그룹을 구성</strong>한다.
외부 액세스를 허용하지 않고, 앞서 생성한 db용 보안그룹을 할당해준다.
이렇게 하면 <code>webcraft-app</code> 보안 그룹을 할당받은 EC2에서만 RDS에 접근이 가능해진다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/e969c23c-722d-4e1c-8e20-c296ed56d128/image.png" alt=""></p>
<h2 id="elasticache-생성">Elasticache 생성</h2>
<blockquote>
<p>이전 실습과 동일한 세팅으로 생성(Elasticache는 이전에도 프라이빗 액세스만 가능하도록 생성하였음)
단, <strong>보안그룹은 RDS와 마찬가지로 webcraft-app으로 지정</strong>해야 함</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/b746d676-ae4c-4545-8569-0750842e345a/image.png" alt=""></p>
<h2 id="ec2-설정">EC2 설정</h2>
<blockquote>
<p>자세한 방법은 <a href="https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-AWS-%EC%8B%A4%EC%8A%B5#ec2-%EC%83%9D%EC%84%B1">이 글</a> 참조</p>
</blockquote>
<h3 id="기존-설정-유지">기존 설정 유지</h3>
<ul>
<li>AMI: Amazon Linux 2023 (Arm)</li>
<li>인스턴스: t4g.small</li>
<li>퍼블릭 IP 자동 할당</li>
</ul>
<h3 id="보안-그룹-수정">보안 그룹 수정</h3>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/79c49ba0-ccd5-4b0a-a5dc-67765af92bee/image.png" alt=""></p>
<h3 id="키-페어-교체">키 페어 교체</h3>
<ol>
<li><p>새로운 키페어 생성
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/62749ee7-608e-46f9-a50f-922bbce1ce91/image.png" alt=""></p>
</li>
<li><p>공개 키값 확인
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/409a7ca3-9a02-4037-abee-df53d75206e3/image.png" alt=""></p>
</li>
<li><p>기존 키페어로 접속, 편집기로 <code>.ssh/authorized_keys</code> 열기
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/85587e40-b81f-468d-88c9-fb148dc5f563/image.png" alt=""></p>
</li>
<li><p>기존 값 제거 후 새 공개키 값으로 교체
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/622f145c-1227-40f8-8953-46282c269fc0/image.png" alt=""></p>
</li>
<li><p>기존 키로는 접속되지 않고, 새 키로 접속이 가능한 것을 확인
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/9c87babf-fdbb-4797-8564-64fd17025131/image.png" alt=""></p>
</li>
</ol>
<h2 id="jar-ec2에-올리기">jar EC2에 올리기</h2>
<blockquote>
<p>자세한 방법은 <a href="https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-EC2-%EB%B0%B0%ED%8F%AC-%EC%8B%A4%EC%8A%B5#ec2-%EC%84%9C%EB%B2%84%EC%97%90-java-%EC%84%A4%EC%B9%98">이 글</a> 참조</p>
</blockquote>
<ul>
<li>이전 실습에서 올린 app.jar는 삭제하였음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/b40c76e4-3d6c-4b16-b44a-aa6483f6bbd3/image.png" alt=""></p>
<h2 id="parameter-store-설정">Parameter Store 설정</h2>
<blockquote>
<p>자세한 방법은 <a href="https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-AWS-Parameter-Store">이 글</a> 참조</p>
</blockquote>
<ul>
<li><code>application.properties</code>에 정의되어있던 모든 환경변수들에 대해 파라미터를 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/3b50fdd9-6881-41f9-a4e7-46b34f15448f/image.png" alt=""></p>
<h2 id="앱-실행">앱 실행</h2>
<blockquote>
<p>파라미터를 Parameter Store에서 가져와 환경변수로 주입한 뒤 앱을 실행한다.</p>
</blockquote>
<pre><code class="language-bash">#!/bin/bash

# 1. AWS Systems Manager Parameter Store에서 설정값 불러오기
export DB_HOST=$(aws ssm get-parameter --name &quot;/webcraft/prod/DB_HOST&quot; --query &quot;Parameter.Value&quot; --output text)
export DB_PORT=$(aws ssm get-parameter --name &quot;/webcraft/prod/DB_PORT&quot; --query &quot;Parameter.Value&quot; --output text)
export DB_NAME=$(aws ssm get-parameter --name &quot;/webcraft/prod/DB_NAME&quot; --query &quot;Parameter.Value&quot; --output text)
export DB_USERNAME=$(aws ssm get-parameter --name &quot;/webcraft/prod/DB_USERNAME&quot; --query &quot;Parameter.Value&quot; --output text)
export DB_PASSWORD=$(aws ssm get-parameter --name &quot;/webcraft/prod/DB_PASSWORD&quot; --query &quot;Parameter.Value&quot; --output text)

export REDIS_HOST=$(aws ssm get-parameter --name &quot;/webcraft/prod/REDIS_HOST&quot; --query &quot;Parameter.Value&quot; --output text)
export REDIS_PORT=$(aws ssm get-parameter --name &quot;/webcraft/prod/REDIS_PORT&quot; --query &quot;Parameter.Value&quot; --output text)
export REDIS_SSL=$(aws ssm get-parameter --name &quot;/webcraft/prod/REDIS_SSL&quot; --query &quot;Parameter.Value&quot; --output text)

# 2. 불러온 환경 변수를 적용하여 애플리케이션 실행
java -jar webcraft.jar</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/5808f510-23a3-4b13-9839-96162b1e6752/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/b9ce411e-aa3f-42d4-90af-8278126e94d7/image.png" alt=""></p>
<p>다중 접속까지 이루어지는 것을 확인할 수 있었다.</p>
<h3 id="트러블슈팅-redis_ssl">트러블슈팅: <code>REDIS_SSL</code></h3>
<ul>
<li>위 스크립트로 스프링 앱 실행 시 Redis 연결 타임아웃이 발생했다.</li>
</ul>
<pre><code>RedisCommandTimeoutException: Connection initialization timed out after 1 minute(s)</code></pre><ul>
<li><p><code>nc -zv</code> 테스트 결과 EC2에서 ElastiCache 6379 포트로의 네트워크 통신은 정상이었으나, ElastiCache에 설정된 암호화 방식과 애플리케이션의 접속 방식이 일치하지 않아 핸드셰이크 단계에서 응답이 멈춘 것으로 파악되었다.</p>
</li>
<li><p>문제는 <code>REDIS_SSL</code>을 <code>false</code>로 주입했다는 것이다. 옵션을 <code>false</code>로 두면 일반 Plain TCP로 연결을 시도하고, <code>true</code>일 경우 TLS 기반 암호화 터널로 접속을 시도한다.</p>
</li>
<li><p>Elasticache는 전송 중 암호화 옵션이 활성화 되어 있었다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/d55b0bbf-24b9-4584-91df-43a8808a9c43/image.png" alt=""></p>
<ul>
<li><p>이 경우 Elasticache는 TLS 연결 요청만 수락한다. 따라서 클라이언트가 평문 패킷을 보내면 응답하지 않아 타임아웃이 발생했던 것이다.</p>
</li>
<li><p>Parameter Store에서 해당 값을 <code>true</code>로 수정하여 문제를 해결하였다.</p>
</li>
<li><p>기존 Fallback은 로컬 환경을 상정한 것이라 암호화 없는 옵션으로 설정되어있던 것으로 보인다. 배포 환경에서는 암호화된 통신을 활성화하는 것이 일반적이다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 두 큐 합 같게 만들기]]></title>
            <link>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%91%90-%ED%81%90-%ED%95%A9-%EA%B0%99%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%91%90-%ED%81%90-%ED%95%A9-%EA%B0%99%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 01 Oct 2026 00:43:07 GMT</pubDate>
            <description><![CDATA[<p><a href="">문제 링크</a></p>
<p><strong>제출 코드(통과)</strong></p>
<pre><code class="language-csharp">using System;

public class Solution {
    public int solution(int[] queue1, int[] queue2)
    {
        var queue = new int[queue1.Length + queue2.Length];
        queue1.CopyTo(queue, 0);
        queue2.CopyTo(queue, queue1.Length);

        long sum1 = 0;
        foreach (var i in queue1) sum1 += i;
        long sum2 = 0;
        foreach (var i in queue2) sum2 += i;
        var sum = sum1 + sum2;
        if (sum % 2 != 0) return -1;

        var index1 = 0;
        var index2 = queue1.Length;


        var answer = 0;

        while (sum1 != sum2 &amp;&amp; answer &lt; queue.Length * 2)
        {
            if (sum1 &gt; sum2)
            {
                var num = queue[index1];
                sum1 -= num;
                sum2 += num;
                index1 = (index1 + 1) % queue.Length;
            }
            else
            {
                var num = queue[index2];
                sum1 += num;
                sum2 -= num;
                index2 = (index2 + 1) % queue.Length;
            }

            answer++;
        }

        return sum1 == sum2 ? answer : -1;
    }
}</code></pre>
<p>한 쪽 큐에서 꺼낸 걸 즉시 다른 쪽 큐에 넣어야 하므로, 둘을 하나의 원형 큐로 이은 뒤 인덱스를 옮기는 것으로 작업 하나를 나타낼 수 있다. <code>index1</code>에서 <code>index2</code>-<code>1</code>까지는 첫 번째 큐, 나머지는 두 번째 큐로 간주된다. 완성될 수 없음을 판단하는 작업 상한은 두 인덱스가 한 바퀴를 돌아 원래 자리로 돌아오는 경우, 즉 전체 길이의 2배 횟수만큼 작업을 수행하는 경우로 설정했다. 더 타이트한 상한이 있을 수도 있겠지만, 우선은 이걸로 충분했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 17일차] 최종 프로젝트 작업]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-17%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-17%EC%9D%BC%EC%B0%A8-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%91%EC%97%85</guid>
            <pubDate>Wed, 30 Sep 2026 12:31:11 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>투포인터를 사용한 비교적 간단한 문제였고, 문제 조건(동일한 조건일 경우 가장 먼저 나온 답을 선택)을 놓쳐 한 번 실수한 것을 빼면 큰 시행착오 없이 해결할 수 있었다.</p>
<p><strong>2. 최종 프로젝트</strong></p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/6c0b696c-233d-45fa-8e5b-a5e693cdc08d/image.png" alt=""></p>
<p>내가 맡은 부분은 플레이어 공격 효과(브레이커) 시각화였다. 스프라이트 없이 셰이더를 활용하였고, 시각화 자체는 큰 어려움 없이 진행되었다.</p>
<p>작업 중 버프 효과 구조 구현이 잘못되어있어 수정하였다. 중첩 가능하고, 각 효과가 갱신되지 않고 독립적으로 적용/종료되도록 큐 형태의 버프 홀더를 정의하였다. 버프 수치 책임을 적으로부터 플레이어로 옮기고, 수치 업그레이드 정의를 추가하였다.</p>
<p>Git 관련한 이슈가 있었고, 그나마 Git 협업에 익숙한 내가 협업 담당으로 작업 규칙 설정 및 관련 이슈 대응을 주로 맡았다.</p>
<p>우선 PR없이 <code>dev</code> 브랜치로 머지하는 것을 금지했다. 초기에 단순 merge로 병합하는 경우가 있었고, 브랜치 보호 규칙을 걸어 직접 수정을 금지하였다. 작업 인원이 많은 만큼, <code>dev</code>는 누가 언제 무엇을 수정했는지 명확한 기록을 남겨야 한다고 생각했기 때문이다. PR 템플릿은 레포 구축 시 함께 정의해두었기에 간단하게라도 작업 내용을 기록하도록 규칙을 정했다.</p>
<p>PR 시 반드시 본인의 브랜치에서 <code>dev</code>의 최신 상태를 받아와 충돌을 모두 해결한 뒤 커밋하도록 했고, 이를 통해 <code>dev</code> 브랜치에서 충돌이 발생하는 것을 막을 수 있었다.</p>
<p>다만 생각지 못한 문제가 있었는데, <code>dev</code> 병합 후 충돌 해결 과정에서 <code>dev</code>를 날리고 본인 코드만 반영해서 코드가 누락되는 사고가 있었다. 이후 팀원들에게 충돌 해결 원칙을 고지했다. (일반적인 코드 충돌의 경우 양측 다 적용, 한쪽을 지워야 한다면 명확한 이유를 보고할 것)</p>
<p>코드 리뷰를 제대로 하고 PR해야 한다는 점을 느꼈다. 기간이 촉박해 대강 확인하고 병합했고 사실상 코드 리뷰가 정상적으로 이루어지지 못했는데, 이번 사고는 결국 리뷰에서 잡아낼 수 있었던 문제였다.</p>
<h1 id="내일-학습할-내용">내일 학습할 내용</h1>
<ul>
<li>Spring 심화 챕터 4까지 진행(완강)</li>
<li>블랙홀 키우기 적 생성 구조 수정</li>
<li>Unity 팀프로젝트(캠프와 별개) 내 담당 부분 작업하기 (스킬 효과 구현 등)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 - 연속된 부분 수열의 합]]></title>
            <link>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-</link>
            <guid>https://velog.io/@gwangmin-kim/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-</guid>
            <pubDate>Wed, 30 Sep 2026 01:06:24 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/178870">문제 링크</a></p>
<p><strong>제출 코드(통과)</strong></p>
<pre><code class="language-csharp">using System;

public class Solution {
    public int[] solution(int[] sequence, int k) { 
        var i = sequence.Length - 1;
        var j = sequence.Length - 1;
        var sum = sequence[j];
        while (i &gt;= 0 &amp;&amp; sum != k)
        {
            if (sum &gt; k)
            {
                if (i == j)
                {
                    i--;
                    j--;
                    sum = sequence[i];
                }
                else
                {
                    sum -= sequence[j--];
                }
            }
            else
            {
                sum += sequence[--i];
            }
        }

        while (i &gt; 0 &amp;&amp; sequence[i - 1] - sequence[j] == 0)
        {
            i--;
            j--;
        }

        return [i, j];
    }
}</code></pre>
<p>비교적 간단한 투포인터 문제였다. 문제 조건 덕에 예외처리를 고려할 필요가 없어서 더욱 쉬웠다. 비내림차순 배열이므로 뒤에서부터 구간을 탐색하면 첫 번째로 발견한 부분 수열이 곧 최소 길이가 된다. 거기서 구간 길이를 유지하면서 합이 일정하도록 앞으로 이동할 수 있다면 (그러려면 배열 내부 및 그 앞 칸 원소의 값이 모두 같아야 한다.) 구간을 앞으로 한 칸씩 당긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[내일배움캠프 백엔드 16일차] Spring 심화, 최종 프로젝트 확정]]></title>
            <link>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-16%EC%9D%BC%EC%B0%A8-Spring-%EC%8B%AC%ED%99%94-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%95%EC%A0%95</link>
            <guid>https://velog.io/@gwangmin-kim/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-16%EC%9D%BC%EC%B0%A8-Spring-%EC%8B%AC%ED%99%94-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%99%95%EC%A0%95</guid>
            <pubDate>Tue, 29 Sep 2026 12:25:02 GMT</pubDate>
            <description><![CDATA[<h1 id="오늘-학습한-내용">오늘 학습한 내용</h1>
<p><strong>1. 코드카타</strong></p>
<p>특정 알고리즘/자료구조를 이용하기보다는 논리 수학 문제를 풀듯이 해결하는 문제였다. 달팽이 모양으로 나열한 숫자를 다시 일자 배열로 펼쳤을 때의 모습을 계산하는 문제였고, 나는 숫자 순서대로 일자 배열 속에서 들어갈 위치를 찾는 방식으로 문제를 해결했다. locality 관점에서는 효율적이지 못한 방식이지만, 배열 인덱스 순서대로 거기에 들어갈 숫자를 계산하는 방법을 떠올리지 못했다. 입력의 최대 크기도 1000으로 작기 때문에 문제 없이 통과할 수 있었다.</p>
<p><strong>2. Spring 심화 챕터 3</strong></p>
<p>챕터 3의 마지막 강의를 제외하고 완강했다. 마지막 강의는 그전에 다룬 AWS 활용법을 토대로 이전 프로젝트 과제였던 Webcraft를 배포해보는, 과제 성격의 강의였다. 캠프 시간 상 곧바로 수행하기 어려울 것 같아 내일 진행하려고 한다.</p>
<p>오늘은 AWS의 DB 서비스에 대해 배웠다. 후술할 컴퓨터공학 특강에서도 강조된 내용으로, Stateless한 서버를 만들기 위해 서버(EC2) 안에 DB를 두지 않고 별도의 DB 클라우드 서비스를 활용해야 한다. 서버와의 책임을 분리하고, 서버의 확장성을 용이하게 하여 유지 관리에도 이점이 있고, 장애 대응 역시 유연하게 가능하기 때문이다.</p>
<p>기존의 MySQL에 대응되는 서비스인 RDS와 Redis에 대응되는 서비스인 Elasticache를 Spring 서버에 연동해서 사용하는 방법을 배웠는데, 아직은 기초적인 실습만 해봤기에 내일 배포 실습을 통해 더 잘 이해해보려고 한다.</p>
<p><strong>3. 컴퓨터공학 특강: 게임 서버 아키텍쳐</strong></p>
<p>실제 온라인 게임을 운영할 때 서버 구조를 어떻게 설계하고 확장하는지에 대해 배울 수 있었다. 서버의 수평 확장성(스케일아웃)을 위해 무상태로 구현하는 것이 중요하고, DB는 데이터(상태)를 담고 있는 것이 필연적이라 서버와 달리 무한정 확장하는 것이 어렵다. 조회 성능을 위해 병렬로 배치하는 Replication에 대해서는 알고 있었는데, 아예 특정 기준으로 데이터를 나눠서 보관하는 Shading에 대해서는 새롭게 배웠다. 대규모 게임에서 서버를 나누는 방식이 샤딩이라는 걸 알게 되었고, 서버 간 통합 컨텐츠(거래 등)가 구현하기 어려운 이유가 물리적으로 별도의 DB에 있기 때문이라는 걸 이해할 수 있었다.</p>
<p>부수적으로 메시지 큐나 MSA, 모니터링, 장애 대응 등 대규모 게임을 운영하는 데 있어 필요한 지식들을 접할 수 있었다. 자세히 다루지는 않았지만, 이러한 개념이 있다는 걸 알고 있는 것만으로도 의미가 있다고 느꼈다. 우리 최종 프로젝트는 멀티 게임도 아니고 아주 규모가 작게 만들어지겠지만, 학습 목적으로 이런 요소들을 적용해볼 수 있을 것 같다.</p>
<p><strong>4. 최종 프로젝트 주제 확정</strong></p>
<p>최종 결정까지 난항이 있었지만, 결국 기존에 잡은 레퍼런스를 그대로 밀고 나가기로 했다. 규모가 작은 게임을 고른 대신, 완벽하게 해당 게임을 복제해내는 것을 목표로 잡았다. 클라이언트 레벨에서는 특히 시각적인 연출 요소를 다듬는 것이 중요할 것 같고, 서버 레벨에서는 클라이언트 검증을 핵심적인 기능으로 잡고 개발해나가면 좋을 것 같다. 만약 오늘 배운 서버 아키텍쳐 요소들을 작게나마 적용해볼 수 있다면 조금 더 포트폴리오로써 가치있는 결과물이 나올 것 같다. 적어도 서버의 무상태 유지 등 핵심적인 원칙만큼은 신경써서 개발해보려고 한다.</p>
<h1 id="내일-학습할-내용">내일 학습할 내용</h1>
<ul>
<li>Spring 심화 챕터 4까지 진행(완강)</li>
<li>블랙홀 키우기 브레이커 시각 효과 구현<ul>
<li>기본 및 타격 애니메이션</li>
<li>달 버프 및 혜성 버프 시각 효과</li>
</ul>
</li>
<li>Unity 팀프로젝트(캠프와 별개) 내 담당 부분 작업하기 (스킬 효과 구현 등)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 심화 (Stateless, AWS 상태 분리)]]></title>
            <link>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-Stateless-AWS-%EC%83%81%ED%83%9C-%EB%B6%84%EB%A6%AC</link>
            <guid>https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-Stateless-AWS-%EC%83%81%ED%83%9C-%EB%B6%84%EB%A6%AC</guid>
            <pubDate>Tue, 29 Sep 2026 07:51:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>앱을 운영 환경에서 안정적으로 운영하려면 Stateless 패턴이 중요!</p>
</blockquote>
<ul>
<li>서버 유지 보수성</li>
<li>서버 확장성</li>
</ul>
<table>
<thead>
<tr>
<th>패턴</th>
<th>의미</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stateless</strong> (상태 없음)</td>
<td>앱이 상태를 저장하지 않음</td>
<td>서버 교체/확장 용이</td>
</tr>
<tr>
<td><strong>Stateful</strong> (상태 있음)</td>
<td>앱이 상태를 저장함</td>
<td>서버 교체 시 데이터 유실 위험</td>
</tr>
</tbody></table>
<p><strong>Stateless 패턴의 핵심</strong></p>
<ol>
<li>앱은 언제든지 교체 가능하게</li>
<li>데이터는 외부에</li>
</ol>
<h1 id="s3와-rds-elasticache를-이용한-상태-분리">S3와 RDS, Elasticache를 이용한 상태 분리</h1>
<table>
<thead>
<tr>
<th>서비스</th>
<th>역할</th>
<th>해결하는 문제</th>
</tr>
</thead>
<tbody><tr>
<td><strong>S3</strong> (Simple Storage Service)</td>
<td>외부 파일 저장소</td>
<td>파일 유실, 확장 문제</td>
</tr>
<tr>
<td><strong>RDS</strong> (Relational Database Service)</td>
<td>외부 DB 서비스</td>
<td>DB 관리 부담</td>
</tr>
<tr>
<td><strong>Elasticache</strong></td>
<td>외부 Redis 서비스</td>
<td>Redis 관리 부담</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>Stateful</th>
<th>Stateless</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/178335f3-6320-4f57-b0c8-05ffbef957eb/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/4f4c26a5-6424-4c5b-9f7f-ec63fada6e1e/image.png" alt=""></td>
</tr>
</tbody></table>
<h1 id="s3">S3</h1>
<blockquote>
<p>Simple Storage Service</p>
</blockquote>
<p><strong>구조 예시</strong></p>
<pre><code>s3://my-app-bucket/
    ├── uploads/                       
    │   ├── profile/user1.jpg          
    │   └── docs/report.pdf            
    └── static/
        └── logo.png                   </code></pre><ul>
<li><strong>버킷:</strong> 파일을 담는 저장소 컨테이너(폴더)</li>
<li><strong>오브젝트:</strong> 저장된 파일 자체(파일)</li>
<li><strong>키:</strong> 오브젝트의 고유 경로/이름(파일 경로)</li>
</ul>
<p><code>user1.jpg</code>에 접근할 때, <code>uploads/profile/user1.jpg</code> 전체가 하나의 키로 쓰인다.</p>
<h4 id="버킷의-종류">버킷의 종류</h4>
<ul>
<li><strong>범용 버킷</strong></li>
<li>디렉터리 버킷</li>
<li>테이블 버킷</li>
<li>벡터 버킷</li>
</ul>
<p>보편적으로 범용 버킷을 사용한다. (다른 버킷은 특수한 용도에 최적화된 형태라, 해당 목적 이외에는 거의 사용되지 않는다)</p>
<h2 id="s3-보안">S3 보안</h2>
<h4 id="1-퍼블릭-액세스-차단">1. 퍼블릭 액세스 차단</h4>
<blockquote>
<p>기본적으로 모든 퍼블릭 접근을 차단</p>
</blockquote>
<ul>
<li>버킷 생성 시 기본값으로 적용 (따로 안건드려도 됨)</li>
<li>과거 퍼블릭 버킷으로 대량의 데이터가 유출된 사고 사례가 있음<ul>
<li>2017년: 미국 유권자 1억 9천만 명 데이터 노출</li>
<li>2019년: 페이스북 사용자 5억 4천만 명 데이터 노출</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>ACL(Access Control List)</strong></p>
</blockquote>
<ul>
<li>버킷/오브젝트별 접근 권한을 설정하는 기능</li>
<li>레거시 방식으로, 현재는 IAM 정책 사용 권장</li>
</ul>
<h4 id="2-최소-권한">2. 최소 권한</h4>
<blockquote>
<p>필요한 권한만 부여</p>
</blockquote>
<ul>
<li>기본적인 보안 원칙</li>
</ul>
<h2 id="버킷-생성-실습">버킷 생성 실습</h2>
<ul>
<li>버킷 이름: <code>camp-health-p4ngmin-files</code></li>
<li>글로벌 네임스페이스로 생성<ul>
<li>계정 리전 네임스페이스는 비교적 최근에 추가된 새 방식</li>
<li>큰 차이는 없고, 이름 규칙 관련한 내용이 다른 것으로 보임</li>
<li>기존 방식인 글로벌 네임스페이스가 보다 널리 쓰이고 자료가 많아 이걸로 진행</li>
</ul>
</li>
<li>모든 퍼블릭 액세스 차단<ul>
<li>이미 기본값으로 설정되어 있으니 건드리지 않고 확인만</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/fd01e4ba-1175-4124-a47e-e95ee0ce9147/image.png" alt=""></p>
<ul>
<li>이제 파일 수동 업로드 가능 (다른 클라우드 스토리지 서비스처럼 사용할 수도 있다)</li>
</ul>
<h4 id="presigned-url">Presigned URL</h4>
<blockquote>
<p><strong>임시로 접근을 허용하는 서명된 URL</strong></p>
</blockquote>
<ul>
<li>유효 시간 설정 가능</li>
<li>유효 시간 내에는 누구나 해당 URL로 접근 가능</li>
</ul>
<p>S3 버킷은 기본적으로 퍼블릭 액세스 차단이 되어있어(프라이빗 버킷), URL로 직접 접근할 수 없다.</p>
<h2 id="spring-연동-aws-의존성-통합-관리">Spring 연동: AWS 의존성 통합 관리</h2>
<p>이전에 <a href="https://velog.io/@gwangmin-kim/Spring-%EC%8B%AC%ED%99%94-AWS-Parameter-Store">Parameter Store</a>를 연동할 때와 같이, S3도 의존성을 필요로 한다.</p>
<p>이때, Parameter Store와 S3는 모두 aws에서 제공하는 서비스인데, 의존성 버전을 따로 명시하면 관리가 번거롭다. 버전 업그레이드 시 여러 의존성을 함께 수정해야 하고, 실수할 경우 버전이 달라 충돌할 수도 있다.</p>
<pre><code class="language-groovy">// 버전을 각각 명시 - 관리가 어려움
implementation &#39;io.awspring.cloud:spring-cloud-aws-starter-parameter-store:0.0.0&#39;
implementation &#39;io.awspring.cloud:spring-cloud-aws-starter-s3:0.0.0&#39;</code></pre>
<h3 id="bom-bill-of-materials">BOM (Bill Of Materials)</h3>
<blockquote>
<p>관련 의존성 라이브러리들의 버전을 한 곳에서 관리하는 명세서</p>
</blockquote>
<pre><code class="language-groovy">// BOM으로 Spring Cloud AWS Starter 버전 통합 관리
implementation platform(&#39;io.awspring.cloud:spring-cloud-aws-dependencies:4.1.1-RC1&#39;)
implementation &#39;io.awspring.cloud:spring-cloud-aws-starter-parameter-store&#39;
implementation &#39;io.awspring.cloud:spring-cloud-aws-starter-s3&#39;</code></pre>
<p>BOM을 불러와 버전을 명시해주면, 해당 명세서에 속한 의존성들은 자동으로 해당 버전으로 설정된다.</p>
<blockquote>
<p>BOM도 Maven Repository에서 찾아서 사용하면 된다.
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/a9f82706-cb28-40be-8d4f-d34dd06212bb/image.png" alt=""></p>
</blockquote>
<h2 id="s3template">S3Template</h2>
<blockquote>
<p>Spring Cloud AWS가 S3Template를 빈으로 등록해주므로, DI해서 바로 사용 가능하다.</p>
</blockquote>
<pre><code class="language-java">@RequiredArgsConstructor
public class XxxClass {
        private final S3Template s3Template; // DI
}</code></pre>
<p>코드 한 줄로 간단히 파일을 업로드할 수 있다.</p>
<pre><code class="language-java">// 파일 업로드
s3Template.upload(&quot;my-bucket&quot;, &quot;uploads/file.txt&quot;, inputStream);</code></pre>
<h3 id="실습용-사전-설정">실습용 사전 설정</h3>
<ul>
<li><code>application.properties</code><pre><code class="language-yaml">server.port=8080
</code></pre>
</li>
</ul>
<h1 id="aws-region">AWS Region</h1>
<p>spring.cloud.aws.region.static=ap-northeast-2</p>
<h1 id="파일-업로드-크기-제한">파일 업로드 크기 제한</h1>
<p>spring.servlet.multipart.max-file-size=10MB</p>
<pre><code>
- `application-local.properties`
```yaml
# 버킷 이름
spring.cloud.aws.s3.bucket=camp-health-p4ngmin-files</code></pre><ul>
<li><code>.gitignore</code>에 <code>application-local.properties</code> 추가<ul>
<li>로컬 설정은 깃으로 관리되어선 안됨<pre><code class="language-gitignore"># properties
application-local.properties</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="dto-정의">DTO 정의</h3>
<pre><code class="language-java">@Getter
@RequiredArgsConstructor
public class FileUploadResponse {
    private final String key;
}

@Getter
@RequiredArgsConstructor
public class FileDownloadUrlResponse {
    private final String url;
}</code></pre>
<h3 id="서비스-클래스">서비스 클래스</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class S3Service {
    private static final Duration PRESIGNED_URL_EXPIRATION = Duration.ofMinutes(10); // 서명 URL 유효 시간 10분

    private final S3Template s3Template; // DI해서 사용

    @Value(&quot;${spring.cloud.aws.s3.bucket}&quot;)
    private String bucket;

    public String upload(MultipartFile file) {
        try {
            String key = &quot;uploads/&quot; + UUID.randomUUID() + &quot;_&quot; + file.getOriginalFilename();
            // 파일명이 중복되지 않도록 랜덤한 UUID를 붙여서 사용
            // UUID는 긴 랜덤값을 O(1)로 생성할 수 있음
            // UUID 자체가 유일성을 보장하지는 않음
            // 이론상 겹칠 수도 있지만, 확률이 희박함
            s3Template.upload(bucket, key, file.getInputStream());
            return key;
        } catch (IOException e) {
            // 적절한 커스텀 예외로 바꾸고, GlobalExceptionHandler로 핸들링 필요
            throw new RuntimeException(&quot;파일 업로드 실패&quot;, e);
        }
    }

    public URL getDownloadUrl(String key) {
        return s3Template.createSignedGetURL(bucket, key, PRESIGNED_URL_EXPIRATION);
    }
}</code></pre>
<h3 id="컨트롤러-클래스">컨트롤러 클래스</h3>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class FileController {
    private final S3Service s3Service;

    @PostMapping(&quot;/files/upload&quot;)
    public ResponseEntity&lt;FileUploadResponse&gt; upload(@RequestParam(&quot;file&quot;) MultipartFile file) {
        String key = s3Service.upload(file);
        return ResponseEntity.ok(new FileUploadResponse(key));
    }

    @GetMapping(&quot;/files/download-url&quot;)
    public ResponseEntity&lt;FileDownloadUrlResponse&gt; getDownloadUrl(@RequestParam String key) {
        URL url = s3Service.getDownloadUrl(key);
        return ResponseEntity.ok(new FileDownloadUrlResponse(url.toString()));
    }
}</code></pre>
<h2 id="s3-연동-로컬-테스트">S3 연동 로컬 테스트</h2>
<h3 id="iam-정책-생성">IAM 정책 생성</h3>
<ul>
<li><code>S3CampHealthPolicy</code>라는 이름으로 생성</li>
</ul>
<pre><code class="language-json">{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [&quot;s3:PutObject&quot;, &quot;s3:GetObject&quot;],
            &quot;Resource&quot;: &quot;arn:aws:s3:::camp-health-p4ngmin-files/uploads/*&quot;
        }
    ]
}</code></pre>
<h3 id="iam-사용자-생성">IAM 사용자 생성</h3>
<ul>
<li>생성 시 직전에 생성한 권한과 연결</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/128a54b7-ab14-4cc0-a809-069f7acacbf3/image.png" alt=""></p>
<h3 id="액세스-키-생성">액세스 키 생성</h3>
<ul>
<li>로컬 코드 용으로 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/bdbc599b-8a2f-463f-aca8-b68d2c856d5e/image.png" alt=""></p>
<p>이 화면 이후 다시 확인할 수 없기에 복사해두어야 함</p>
<h3 id="설정값application-localproperties에-액세스-키환경-변수-추가">설정값(<code>application-local.properties</code>)에 액세스 키(환경 변수) 추가</h3>
<pre><code>spring.cloud.aws.credentials.access-key=AKIAROC57E2O6SYC3FF3
spring.cloud.aws.credentials.secret-key=...</code></pre><h3 id="postman-api-호출-테스트">Postman API 호출 테스트</h3>
<blockquote>
<p>트러블슈팅: 실행 프로필을 <code>local</code>로 설정해야 환경변수가 적용된다. 해당 설정을 누락하여 서버 앱 실행이 안되는 문제를 겪었다.</p>
</blockquote>
<table>
<thead>
<tr>
<th>업로드할 파일</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/399668a4-56e6-441d-ae33-b352816665ab/image.jpg" alt=""></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>업로드 API 요청</th>
<th>다운로드 URL 요청</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/0cd11315-d2f9-4147-8dd8-fefb7ccd9253/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/45f69bc6-9c6f-45cd-814a-b05102a4c158/image.png" alt=""></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>S3 확인</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/90f6d428-c0d1-434e-b3b2-dc5613a91fc3/image.png" alt=""></td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>응답받은 서명 URL 접속</th>
<th>나중에(10분 이상 흐른 뒤) 재접속 시</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/7a925134-bc9d-4e26-ab45-840016922360/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/fde5a3d4-871e-45a1-b2fe-2690aa2bfe37/image.png" alt=""></td>
</tr>
</tbody></table>
<h1 id="rds">RDS</h1>
<h2 id="ec2-db-vs-rds">EC2 DB vs RDS</h2>
<table>
<thead>
<tr>
<th>운영부담</th>
<th>EC2 DB</th>
<th>AWS RDS</th>
</tr>
</thead>
<tbody><tr>
<td>백업</td>
<td>cron으로 백업 스크립트 작성</td>
<td>자동 데이터 백업</td>
</tr>
<tr>
<td>패치</td>
<td>MySQL 보안 업데이트 직접 적용</td>
<td>유지 관리 기간에 자동 적용</td>
</tr>
<tr>
<td>장애</td>
<td>서버 다운 시 복구 직접 수행</td>
<td>Multi-AZ(여러 가용 영역에 복제본 유지) 구성 시 자동 장애 조치</td>
</tr>
</tbody></table>
<h2 id="mysql-rds-인스턴스-생성">MySQL RDS 인스턴스 생성</h2>
<h3 id="서브넷-그룹-생성">서브넷 그룹 생성</h3>
<blockquote>
<p><strong>프라이빗 서브넷으로 설정해야 한다</strong>
현재는 로컬에서 해보는 간단한 실습이라 접근을 위해 퍼블릭 서브넷 그룹으로 생성한다</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/d9b3b3c4-f3ed-489d-87f8-3d9166944cdf/image.png" alt=""></p>
<h3 id="rdb-생성">RDB 생성</h3>
<ul>
<li>MySQL</li>
<li>퍼블릭 액세스 허용 (퍼블릭 서브넷과 별도로 설정해주어야 함)</li>
<li>default 보안 그룹 (모든 트래픽 허용)<ul>
<li>여기서 모든 트래픽은 소스가 자기 자신인 경우에만 해당</li>
<li>MySQL 포트인 3306에 한해 모든 소스 허용하도록 규칙 추가
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/cb464b6f-dd44-4779-8bd3-80b5c25001de/image.png" alt=""></li>
</ul>
</li>
<li>3306 포트 (기본 포트)</li>
<li>초기 데이터베이스 이름 (미리 <code>CREATE DATABASE ...</code> 하는 개념)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/2ce7e936-48c0-4210-a684-ea942f2f5784/image.png" alt=""></p>
<h3 id="자바-프로젝트-생성-db-접속-확인">자바 프로젝트 생성, DB 접속 확인</h3>
<p>이제 localhost가 아닌 aws rds 엔드포인트로 지정해주어야 함</p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/0bcc101f-3f43-4357-b10e-6a180d50dbc4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/gwangmin-kim/post/328e7727-dc14-4945-9a1a-d167557843fd/image.png" alt=""></p>
<h4 id="트러블슈팅-aws-rds-호스트-차단-에러-error-1129">트러블슈팅: AWS RDS 호스트 차단 에러 (Error 1129)</h4>
<p><strong>접속 시도 시 다음 에러 발생</strong></p>
<pre><code>Host &#39;14.57.166.223&#39; is blocked because of many connection errors; unblock with &#39;mysqladmin flush-hosts&#39;</code></pre><p><strong>원인 파악</strong>
아래는 AI가 파악한 문제 상황 시나리오이다.</p>
<ul>
<li>드라이버 불일치로 인한 다량의 연결 실패: 최초 접속 시 IntelliJ에서 실제 RDS 엔진(일반 MySQL)과 맞지 않는 Amazon Aurora MySQL (AWS Wrapper) 드라이버를 선택한 상태로 접속을 시도했다. IntelliJ가 해당 드라이버를 제안했기에 해당 </li>
<li>보안 임계값 초과: 호환되지 않는 드라이버와 잘못된 설정값으로 인해 단시간에 연속적인 접속 실패가 발생했다.</li>
<li>서버 측 IP 차단: MySQL은 보안을 위해 동일 IP에서 연속 실패 횟수가 임계값(max_connect_errors, 기본값 100회)을 넘으면, 해당 호스트를 비정상적인 공격 시도로 간주하고 자동으로 접속을 차단(Block)한다. 이후에 MySQL 드라이버로 접속을 시도했을 때도 동일한 에러가 발생한 것이 이 때문이다.</li>
</ul>
<p><strong>해결</strong></p>
<ul>
<li>드라이버 정상화: IntelliJ의 데이터베이스 드라이버를 일반 MySQL로 올바르게 수정했다.</li>
<li>호스트 캐시 초기화: AWS RDS는 OS CLI 접근이 불가능하므로, RDS 인스턴스를 재부팅하여 메모리에 남아있던 호스트 차단 블랙리스트 캐시를 초기화하여 해결했다.</li>
</ul>
<h2 id="rds-연동-로컬-테스트">RDS 연동 로컬 테스트</h2>
<blockquote>
<p>앱 실행은 로컬에서 하지만, 클라우드에 배포된 라이브 상황(prod 프로필)이라는 가정 하에 진행
<img src="https://velog.velcdn.com/images/gwangmin-kim/post/61e05e0a-2838-4b4d-9b36-b90c84210eb2/image.png" alt=""></p>
</blockquote>
<h3 id="환경-변수-주입">환경 변수 주입</h3>
<p><code>application.properties</code></p>
<pre><code>spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect</code></pre><p><code>application-prod.properties</code></p>
<ul>
<li>각 항목에 실습용으로 생성한 RDS 정보를 입력</li>
</ul>
<pre><code>spring.datasource.url=jdbc:mysql://{RDS엔드포인트}:3306/{데이터베이스명}
spring.datasource.username={유저네임}
spring.datasource.password={비밀번호}
spring.jpa.hibernate.ddl-auto=validate</code></pre><h3 id="코드-작성">코드 작성</h3>
<p><strong>엔티티(<code>User</code>)</strong></p>
<pre><code class="language-java">@Getter
@Entity
@Table(name = &quot;users&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String name;

    public User(String name) {
        this.name = name;
    }
}</code></pre>
<p><strong><code>UserRepository</code></strong></p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {
}</code></pre>
<p><strong><code>UserService</code></strong></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public void save() {
        User user = new User(
                UUID.randomUUID().toString()
        );
        userRepository.save(user);
    }
}</code></pre>
<p><strong><code>UserController</code></strong></p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping(&quot;/users&quot;)
    public void create() {
        userService.save();
    }
}</code></pre>
<h3 id="스키마-적용">스키마 적용</h3>
<ul>
<li>라이브 환경에서는 <code>spring.jpa.hibernate.ddl-auto</code>를 <code>create</code>나 <code>update</code>가 아닌 <code>validate</code>로 둔다. 의도치 않은 테이블의 변경을 방지하기 위함이다.</li>
<li>현재 prod 프로필에서의 설정도 그렇게 되어 있으니, 이대로는 다음과 같은 에러 로그와 함께 실행이 멈춘다.<pre><code>SchemaManagementException: Schema validation: missing table [users]</code></pre></li>
</ul>
<p>따라서 다음 쿼리를 통해 <code>User</code> 엔티티에 맞는 스키마를 DB에 만들어줘야 한다.</p>
<pre><code class="language-sql">CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);</code></pre>
<blockquote>
<p>개발 시 변경된 테이블 내역에 대해 운영 서버 DB에 변경분을 업데이트하는 것을 <strong>마이그레이션</strong>이라고 한다.</p>
</blockquote>
<h3 id="실행-후-postman-테스트">실행 후 Postman 테스트</h3>
<table>
<thead>
<tr>
<th>헬스체크</th>
<th>유저 생성</th>
<th>생성된 유저 확인</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/45592849-f4d3-4dae-a2c2-ee1a0d9d89a4/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/f11284f0-742f-4f67-be5a-bf0f3924f2c3/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/gwangmin-kim/post/eaa3b1e5-5b13-469a-bde7-9faaca408d50/image.png" alt=""></td>
</tr>
</tbody></table>
<h1 id="elasticache">Elasticache</h1>
<blockquote>
<p>RDS의 Redis 버전!</p>
</blockquote>
<ul>
<li>AWS에서는 Redis와 호환되는 Valkey를 사용한다.<ul>
<li>Redis는 구버전만 지원한다.</li>
</ul>
</li>
</ul>
<h2 id="생성-실습">생성 실습</h2>
<blockquote>
<p>RDS와 다르게 퍼블릭 액세스 자체를 허용하지 않는다
따라서 이번엔 프라이빗 서브넷으로 그룹을 생성한다.</p>
</blockquote>
<h2 id="elasticache-로컬-연동">Elasticache 로컬 연동</h2>
<blockquote>
<p>실제 실습은 다음 강의에서 진행</p>
</blockquote>
<h3 id="환경-변수-주입-1">환경 변수 주입</h3>
<pre><code>spring.data.redis.host={Valkey Endpoint}
spring.data.redis.port=6379
spring.data.redis.ssl.enabled=true</code></pre>]]></description>
        </item>
    </channel>
</rss>