<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>win-luck.log</title>
        <link>https://velog.io/</link>
        <description>Discover Tomorrow</description>
        <lastBuildDate>Wed, 16 Oct 2024 10:17:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. win-luck.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/win-luck" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[현대자동차 소프티어 부트캠프 4기 백엔드 수료 및 후기]]></title>
            <link>https://velog.io/@win-luck/%ED%98%84%EB%8C%80%EC%9E%90%EB%8F%99%EC%B0%A8%EA%B7%B8%EB%A3%B9-%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-%EC%9B%B9%EB%B0%B1%EC%97%94%EB%93%9C-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/%ED%98%84%EB%8C%80%EC%9E%90%EB%8F%99%EC%B0%A8%EA%B7%B8%EB%A3%B9-%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-%EC%9B%B9%EB%B0%B1%EC%97%94%EB%93%9C-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 16 Oct 2024 10:17:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/win-luck/post/6d38fbb2-f5c4-4eab-90c3-7c47cc87f71d/image.png" alt="">
이번 7~8월 여름방학 동안 참여했던 <a href="https://softeerbootcamp.hyundaimotorgroup.com/">현대자동차그룹 소프티어 부트캠프</a> 4기 웹백엔드 후기를 남깁니다. 5기 분들에게 조금이라도 도움이 되었으면 합니다.</p>
<p>Java Spring 백엔드 개발자로서 많은 것을 배우고 더욱 성숙해질 수 있었던, 대학생활 중 가장 의미있던 외부 활동이었습니다!
<img src="https://velog.velcdn.com/images/win-luck/post/ad1b5a66-d0f2-4b48-a6a3-c54a6d051e45/image.png" alt=""></p>
<p>4기 기준 지원서 접수 - 코딩테스트 - CS테스트 순서로 모집이 진행되었으며, 기획/디자인/FE/BE/DE를 합하여 총 85명 가량 선발하였습니다.</p>
<p>(구체적으로 수치를 밝힐 수는 없지만 지원자 수가 꽤 많았다고 합니다.)</p>
<h1 id="지원서-접수">지원서 접수</h1>
<ul>
<li>4기 기준 <strong>2024년 2월 졸업자 ~ 2025년 2월 졸업예정자만 선발</strong>하였습니다.</li>
<li>간단한 인적사항과 졸업 증빙 서류를 제출했습니다.</li>
</ul>
<h1 id="1차-온라인-평가-코딩테스트">1차 온라인 평가: 코딩테스트</h1>
<ul>
<li>2시간 동안 진행되었고, <strong>4기 기준 웹백엔드는 C++과 Java만 허용</strong>되었습니다.</li>
<li>총 5문제였고, 1-2-5번 3솔 후 합격하였습니다.</li>
<li>전반적으로 BOJ 실버 상위 ~ 골드 하위 티어 문제로 구성되었습니다.</li>
</ul>
<h1 id="2차-온라인-평가-소프트웨어-지식테스트cs테스트">2차 온라인 평가: 소프트웨어 지식테스트(CS테스트)</h1>
<ul>
<li>2시간 30분 동안 진행되었습니다.</li>
<li>운영체제/네트워크/데이터베이스 등 다양한 CS지식을 다루었습니다.</li>
<li><strong>정보처리기사 + SQLD를 기본적으로 학습하시길 권장</strong>합니다. (개발자 기술면접 README도 좋습니다.)</li>
<li>약 30문제 중 17~18문제를 확실하게 맞혔고 다행히 합격하였습니다. (정확한 커트라인은 잘 모르겠습니다.)</li>
</ul>
<h1 id="14주차-개인-과제-및-기획">1~4주차: 개인 과제 및 기획</h1>
<ul>
<li>부트캠프 개발자 직무 교육은 네이버 부스트캠프를 담당하는 코드스쿼드에서 주관하며, 현대자동차그룹에서는 직무 멘토링, 프로젝트 심사, 피드백 등을 진행해주셨습니다.</li>
<li>부트캠프 기간 동안 Mac M1 16인치를 사용할 수 있습니다. 14인치를 사용하는 입장에서 넓은 모니터가 너무 만족스러웠습니다.</li>
<li>Spring/Springboot이 아닌 순수 Java로 개인 과제를 수행했습니다.</li>
<li>과제 자체는 유명한 과제이기에 Github를 찾아보면 여러 코드가 있지만, 혼자서 학습하며 다양한 시행착오를 겪는 게 큰 도움이 됩니다.</li>
<li>이 과정에서 Java 백엔드 개발자라면 반드시 알아야 할 HTTP 프로토콜과 JVM, 멀티스레드 및 동시성 문제에 대해 체계적으로 복습할 수 있었습니다.</li>
<li>3~4주차부터는 과제 수행 중간중간 기획+디자인+프런트엔드 분들과 팀 프로젝트 기획에 참여하였습니다.</li>
</ul>
<h1 id="59주차-팀-프로젝트">5~9주차: 팀 프로젝트</h1>
<ul>
<li>주제 및 요구사항에 맞게 완성된 기획 및 디자인을 바탕으로 프로젝트 개발을 진행합니다.</li>
<li>절대적인 개발 결과물에 집착하기보다는 팀 전체적인 방향성에 집중하면 좋습니다. 협업 역량이 훨씬 더 중요합니다.</li>
<li>부트캠프 마지막 날에 최종 발표 + 수료식을 진행합니다. 현대/기아의 실무진 분들이 프로젝트 결과를 심사해주시고 다양한 피드백을 남겨주셨습니다.</li>
</ul>
<h1 id="후기">후기</h1>
<ul>
<li>4학년 1학기 여름방학에 할 수 있는 대외활동 중 인턴십만큼 가치가 있는 외부 활동이라고 생각합니다.</li>
<li>특히 기본적인 개발 역량 및 프로젝트 경험은 쌓았으나 <strong>본격적인 취업 준비를 앞두고 코어 활동이 부족한 사람들은 촉박한 시간 속에서 내실 있는 팀 프로젝트를 통해 빠르게 성장할</strong> 수 있습니다.</li>
<li>또한 개발 직무를 이끌어주시는 마스터님들은 많은 경험을 갖추신 베테랑이시기에 다양한 질문을 통해 의문을 해소할 수 있습니다.</li>
<li>남의 돈으로(?) 로드 밸런서나 오토 스케일링 등 클라우드 내의 여러 시스템을 활용할 수 있어 한층 더 발전하는 소중한 경험을 얻을 수 있었습니다.</li>
<li>단순 부트캠프보다는 <strong>채용 프로세스의 일부라고 생각하고 적극적으로 임한다면, 더 많은 것을 얻어갈 수 있는 활동입니다.</strong> (저는 2개월 간의 과제 테스트 + 실무진 면접이라고 생각했습니다.)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코드트리] 삼성 SW역량테스트 기출 - 메이즈 러너 (C++)]]></title>
            <link>https://velog.io/@win-luck/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%82%BC%EC%84%B1-SW%EC%97%AD%EB%9F%89%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EC%B6%9C-%EB%A9%94%EC%9D%B4%EC%A6%88-%EB%9F%AC%EB%84%88-C</link>
            <guid>https://velog.io/@win-luck/%EC%BD%94%EB%93%9C%ED%8A%B8%EB%A6%AC-%EC%82%BC%EC%84%B1-SW%EC%97%AD%EB%9F%89%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B8%B0%EC%B6%9C-%EB%A9%94%EC%9D%B4%EC%A6%88-%EB%9F%AC%EB%84%88-C</guid>
            <pubDate>Sat, 21 Sep 2024 16:30:24 GMT</pubDate>
            <description><![CDATA[<p><strong>삼성 코딩테스트 2023년 상반기 오후 1번</strong>이었다.</p>
<p>24년 인턴십 지원으로 기회를 얻었던 서천연수원 코딩테스트를 포함하여 삼성 코딩테스트는 느낀 점이 3개 정도 있다.</p>
<ul>
<li>문제 똑바로 읽고 요구사항을 명확하게 정리한 뒤에 시작해도 늦지 않다. (요구사항을 하나라도 흘리고 시작하면 최소 1시간 뻘짓을 하게 된다.)</li>
<li>기능별로 함수를 분리하여 런타임 에러가 났을 때 어디가 문제인지 빠르게 확인할 수 있어야 한다. (현장에서 진짜 피봤던 기억이..)</li>
<li>배열 90도 돌리기, 달팽이 모양처럼 돌리기 등 2차원 배열의 회전이 정말 자주 나온다. <a href="https://velog.io/@danbibibi/2%EC%B0%A8%EC%9B%90-%EB%B0%B0%EC%97%B4%EC%97%90%EC%84%9C-90%EB%8F%84-%ED%9A%8C%EC%A0%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98">이런 테크닉을 알아두면 좋을 것 같다.</a></li>
</ul>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/74d667d8-c375-4e30-9ddf-b500bf0f0d21/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/31741163-c46d-49f1-a7be-f80d7ff3d485/image.png" alt=""></p>
<p>문제에서 알 수 있는 핵심 정보는 다음과 같다.</p>
<ul>
<li>좌상단은 (1,1) -&gt; 인덱스 설정 주의</li>
<li>참가자는 <strong>벽이 없으면서 항상 출구와 가까운 방향으로 움직여야 하며, 이 때 상하 이동이 우선권</strong>을 갖는다.</li>
<li>참가자는 움직일 수 없다면 가만히 있으며, 한 좌표에 2명 이상의 참가자가 존재할 수 있다.</li>
<li><strong>움직임을 마친 후</strong> 1명 이상의 참가자와 출구를 포함한 가장 작은 최적의 정사각형을 찾는다.</li>
<li>정사각형 내부 벽, 사람, 출구를 90도 시계방향으로 회전한다.</li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<h2 id="main">main</h2>
<p>문제의 핵심 동작은 크게 3가지로 나눌 수 있다.</p>
<ul>
<li>참가자의 움직임 (move)</li>
<li>조건에 맞는 최적의 정사각형을 추적 (rotateFunc)</li>
<li>정사각형 내부 개체(벽, 사람, 출구) 시계 방향 90도로 회전 (rotate)</li>
</ul>
<p>출구 좌표를 {ex, ey}로 잡고 main에 주요 함수를 세팅한다.</p>
<pre><code class="language-cpp">int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    cin &gt;&gt; n &gt;&gt; m &gt;&gt; t;
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            cin &gt;&gt; arr[i][j];
        }
    }
    for(int i=1; i&lt;=m; i++){
        int a, b;
        cin &gt;&gt; a &gt;&gt; b;
        v.push_back(make_pair(a-1, b-1));
    }
    cin &gt;&gt; ex &gt;&gt; ey;
    arr[--ex][--ey] = -1;

    for(int i=1; i&lt;=t; i++){
        move(); // 참가자의 움직임
        rotateFunc(); // 정사각형 결정 및 회전
    }
    cout &lt;&lt; answer &lt;&lt; &#39;\n&#39;;
    cout &lt;&lt; ex+1 &lt;&lt; &quot; &quot; &lt;&lt; ey+1;
    return 0;
}</code></pre>
<h2 id="move">move</h2>
<p>참가자들의 좌표를 담아둔 vector를 순회하며 <strong>현재 좌표에서 벽이 아니면서 출구와 가까운 방향으로 이동할 수 있는지 검증</strong>한다. 이후 이동이 가능하다면 <strong>상하 이동을 우선</strong>으로 하여 움직이고, 이를 answer로 카운트한다.</p>
<p>이 때 참가자들 중 <strong>출구에 서 있는 친구는 erase로 제거</strong>한다. 
이 때 순방향으로 erase 실행 시 인덱스를 당겨줘야 하기 때문에 편의를 위해 역방향으로 처리한다.</p>
<pre><code class="language-cpp">// 참가자들이 이동
void move(){
    for(int i=v.size()-1; i&gt;=0; i--){
        if(v[i].first == ex &amp;&amp; v[i].second == ey) {
            v.erase(v.begin() + i);
            continue;
        }
        pair&lt;int, int&gt; now = v[i];
        int nowdis = abs(ex - now.first) + abs(ey - now.second); // 현재 출구까지의 거리

        for(int a=0; a&lt;4; a++){
            int nx = now.first + dx[a];
            int ny = now.second + dy[a];

            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue; // 이탈
            if(arr[nx][ny] &gt; 0) continue; // 벽
            int nextdis = abs(ex - nx) + abs(ey - ny); // 다음 좌표 기준 출구까지의 거리
            if(nowdis &gt; nextdis){ // 이동 가능
                answer++;
                v[i] = make_pair(nx, ny);
                break;
            }
        }
    }
    if(!v.empty())
        sort(v.begin(), v.end(), cmp2); // 최적의 정사각형 추적을 위한 정렬
}</code></pre>
<h2 id="rotatefunc">rotateFunc</h2>
<p>이제 조건에 맞는 최적의 정사각형을 찾아 해당 영역에서 시계 방향으로 90도 회전을 실행하면 된다.
정사각형의 핵심 정보는 시작점(왼쪽 위)의 좌표와 변의 길이이다.
n = 10이기에 완전탐색으로 해도 문제가 없다.</p>
<p>&quot;최적의 정사각형&quot;이란, 변의 길이가 제일 작고, 행과 열의 값이 가장 작은 정사각형이다.</p>
<pre><code class="language-cpp">// 사각형 크기 오름차순, 행/열 번호 오름차순 정렬
bool cmp(pair&lt;int, pair&lt;int, int&gt; &gt; p1, pair&lt;int, pair&lt;int, int&gt; &gt; p2){
    if(p1.first == p2.first){
        if(p1.second.first == p2.second.first){
            return p1.second.second &lt; p2.second.second;
        }
        return p1.second.first &lt; p2.second.first;
    }
    return p1.first &lt; p2.first;
}</code></pre>
<p>k를 통해 변의 길이 2부터(1이면 조건을 절대 만족할 수 없다.) 2중 for문을 통해 특정 시작점에서 해당 정사각형이 유효한지 검증한다. 
이를 위해 내부 2중 for문을 추가하여 출구와 참가자를 포함하는 것이 확인되면 바로 회전에 돌입한다.</p>
<pre><code class="language-cpp">// 회전시킬 영역을 결정
void rotateFunc(){
    vector&lt;pair&lt;int, pair&lt;int, int&gt; &gt; &gt; info;
    for(int k=2; k&lt;=n; k++){
        for(int i=0; i&lt;n; i++){
            for(int j=0; j&lt;n; j++){
                if(i+k &gt; n || j+k &gt; n) continue;
                bool exit = false; // 출구 존재 여부
                bool user = false; // 참가자 존재 여부
                for(int a=i; a&lt;i+k; a++){
                    for(int b=j; b&lt;j+k; b++){
                        int sx = 0;
                        int sy = 0;
                        if(a &lt; 0 || b &lt; 0 || a &gt;= n || b &gt;= n) continue;
                        if(a == ex &amp;&amp; b == ey) exit = true;

                        for(auto &amp;it: v){
                            if(it.first &gt;= i &amp;&amp; it.first &lt; i+k &amp;&amp; it.second &gt;= j &amp;&amp; it.second &lt; j+k){
                                if(it.first == ex &amp;&amp; it.second == ey) continue;
                                sx = it.first;
                                sy = it.second;
                                user = true;
                                break;
                            }
                        }
                        if(exit == true &amp;&amp; user == true){ // 둘 다 존재함 -&gt; 현재 k, i, j 등록
                            rotate(i, j, k);
                            return;
                        }
                    }
                }
            }
        }
    }
}</code></pre>
<h2 id="rotate">rotate</h2>
<p>실제 회전은 다음과 같은 순서로 진행된다.</p>
<ol>
<li>정해진 정사각형 내부에 들어있는 참가자를 추적하고, 이를 기존 참가자 리스트 v에서 제거한다.</li>
<li>이후 임시 참가자 리스트에 저장한다.</li>
<li>2차원 배열을 시계방향으로 90도 회전한다. 이 때 출구가 회전되었을 경우 이를 반영해준다. </li>
<li>임시 참가자 리스트를 순회하며 참가자의 위치도 회전한고 다시 기존 참가자 리스트 v에 저장한다.</li>
<li>요구사항에 따라 회전 후 정사각형 내부 벽의 내구도를 1씩 낮춰준다.</li>
</ol>
<pre><code class="language-cpp">// 특정 영역 회전
void rotate(int r, int c, int size){
    int tmp[11][11]; // 임시 arr
    vector&lt;pair&lt;int, int&gt; &gt; tmpvec; // 임시 v

    // 영역에 들어있는 사람 추적
    for(int i=v.size()-1; i&gt;=0; i--){
        if(v[i].first &gt;= r &amp;&amp; v[i].first &lt; r+size &amp;&amp; v[i].second &gt;= c &amp;&amp; v[i].second &lt; c+size){
            if(!(v[i].first == ex &amp;&amp; v[i].second == ey)){
                tmpvec.push_back(v[i]);
            }
            v.erase(v.begin() + i);
        }
    }

    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            tmp[i][j] = arr[i][j];
        }
    }

    // 시계방향 90도 회전
    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            tmp[j - c + r][r + c + size - 1 - i] = arr[i][j];
        }
    }

    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            arr[i][j] = tmp[i][j];
            if(arr[i][j] == -1){ // 출구 조정
                ex = i;
                ey = j;
            }
        }
    }

    // 사람 좌표 갱신
    for(auto&amp; it: tmpvec){
        int x = it.first;
        int y = it.second;
        int nx = y - c + r; // 회전된 x 좌표
        int ny = r + c + size - 1 - x; // 회전된 y 좌표
        v.push_back(make_pair(nx, ny));
    }
    sort(v.begin(), v.end(), cmp2);

    // 회전 후 내구도 감소
    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            if(arr[i][j] &gt; 0) arr[i][j]--; 
        }
    }
}</code></pre>
<h1 id="소스-코드">소스 코드</h1>
<p>개인적으로 초기에 방향을 잘못 잡아 트러블 슈팅에 꽤나 고생했던 문제였다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;map&gt;
#include &lt;algorithm&gt;

using namespace std;
int n, m, t, ex, ey;
vector&lt;pair&lt;int, int&gt; &gt; v;
int arr[11][11]; // 좌표별 벽 정보
int answer = 0;
map&lt;int, pair&lt;int, int&gt; &gt; dict;
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

// 좌표 오름차순 정렬
bool cmp(pair&lt;int, int&gt; p1, pair&lt;int, int&gt; p2){
    if(p1.second == p2.second){
        return p1.second &lt; p2.second;
    }
    return p1.first &lt; p2.first;
}

// 디버깅용
void print(){
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            cout &lt;&lt; arr[i][j] &lt;&lt; &quot; &quot;;
        }
        cout &lt;&lt; &#39;\n&#39;;
    }
    cout &lt;&lt; &#39;\n&#39;;
}

// 참가자들이 이동
void move(){
    for(int i=v.size()-1; i&gt;=0; i--){
        if(v[i].first == ex &amp;&amp; v[i].second == ey) {
            v.erase(v.begin() + i);
            continue;
        }
        pair&lt;int, int&gt; now = v[i];
        int nowdis = abs(ex - now.first) + abs(ey - now.second); // 현재 출구까지의 거리

        for(int a=0; a&lt;4; a++){
            int nx = now.first + dx[a];
            int ny = now.second + dy[a];

            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue; // 이탈
            if(arr[nx][ny] &gt; 0) {
                continue; // 벽
            }
            int nextdis = abs(ex - nx) + abs(ey - ny); // 다음 좌표 기준 출구까지의 거리
            if(nowdis &gt; nextdis){ // 이동 가능
                answer++;
                v[i] = make_pair(nx, ny);
                break;
            }
        }
    }
    if(!v.empty())
        sort(v.begin(), v.end(), cmp);
}

// 특정 영역 회전
void rotate(int r, int c, int size){
    int tmp[11][11]; // 임시 arr
    vector&lt;pair&lt;int, int&gt; &gt; tmpvec; // 임시 v

    // 영역에 들어있는 사람 추적
    for(int i=v.size()-1; i&gt;=0; i--){
        if(v[i].first &gt;= r &amp;&amp; v[i].first &lt; r+size &amp;&amp; v[i].second &gt;= c &amp;&amp; v[i].second &lt; c+size){
            if(!(v[i].first == ex &amp;&amp; v[i].second == ey)){
                tmpvec.push_back(v[i]);
            }
            v.erase(v.begin() + i);
        }
    }

    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            tmp[i][j] = arr[i][j];
        }
    }

    // 시계방향 90도 회전
    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            tmp[j - c + r][r + c + size - 1 - i] = arr[i][j];
        }
    }

    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            arr[i][j] = tmp[i][j];
            if(arr[i][j] == -1){ // 출구 조정
                ex = i;
                ey = j;
            }
        }
    }

    // 사람 좌표 갱신
    for(auto&amp; it: tmpvec){
        int x = it.first;
        int y = it.second;
        int nx = y - c + r; // 회전된 x 좌표
        int ny = r + c + size - 1 - x; // 회전된 y 좌표
        v.push_back(make_pair(nx, ny));
    }
    sort(v.begin(), v.end(), cmp);

    // 회전 후 내구도 감소
    for(int i=r; i&lt;r+size; i++){
        for(int j=c; j&lt;c+size; j++){
            if(arr[i][j] &gt; 0) arr[i][j]--; 
        }
    }
}

// 회전시킬 영역을 결정
void rotateFunc(){
    vector&lt;pair&lt;int, pair&lt;int, int&gt; &gt; &gt; info;
    for(int k=2; k&lt;=n; k++){
        for(int i=0; i&lt;n; i++){
            for(int j=0; j&lt;n; j++){
                if(i+k &gt; n || j+k &gt; n) continue;
                bool exit = false; // 출구 존재 여부
                bool user = false; // 참가자 존재 여부
                for(int a=i; a&lt;i+k; a++){
                    for(int b=j; b&lt;j+k; b++){
                        int sx = 0;
                        int sy = 0;
                        if(a &lt; 0 || b &lt; 0 || a &gt;= n || b &gt;= n) continue;
                        if(a == ex &amp;&amp; b == ey) exit = true;

                        for(auto &amp;it: v){
                            if(it.first &gt;= i &amp;&amp; it.first &lt; i+k &amp;&amp; it.second &gt;= j &amp;&amp; it.second &lt; j+k){
                                if(it.first == ex &amp;&amp; it.second == ey) continue;
                                sx = it.first;
                                sy = it.second;
                                user = true;
                                break;
                            }
                        }
                        if(exit == true &amp;&amp; user == true){ // 둘 다 존재함 -&gt; 현재 k, i, j 등록
                            rotate(i, j, k);
                            return;
                        }
                    }
                }
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    cin &gt;&gt; n &gt;&gt; m &gt;&gt; t;
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            cin &gt;&gt; arr[i][j];
        }
    }
    for(int i=1; i&lt;=m; i++){
        int a, b;
        cin &gt;&gt; a &gt;&gt; b;
        v.push_back(make_pair(a-1, b-1));
    }
    cin &gt;&gt; ex &gt;&gt; ey;
    arr[--ex][--ey] = -1;

    for(int i=1; i&lt;=t; i++){
        move();
        rotateFunc();
    }
    cout &lt;&lt; answer &lt;&lt; &#39;\n&#39;;
    cout &lt;&lt; ex+1 &lt;&lt; &quot; &quot; &lt;&lt; ey+1;
    return 0;
}
</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>코드 먼저 앞서나가지 말고 요구사항에 맞는 최적의 구조는 무엇인지 일정 시간 고민하고 시작하도록 하자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring 요청 데이터의 예외처리 (@Valid, AOP)]]></title>
            <link>https://velog.io/@win-luck/Spring-%EC%9A%94%EC%B2%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-Valid-AOP</link>
            <guid>https://velog.io/@win-luck/Spring-%EC%9A%94%EC%B2%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC-Valid-AOP</guid>
            <pubDate>Sat, 07 Sep 2024 09:12:41 GMT</pubDate>
            <description><![CDATA[<h1 id="1-controller에서의-예외처리-valid-기반">1. Controller에서의 예외처리 (@Valid 기반)</h1>
<ul>
<li>RequestDto에 여러 어노테이션 기반으로 제약을 걸고, 이를 Controller 단의 RequestBody에 @Valid 어노테이션을 추가하는 방안입니다.</li>
<li>Controller 진입 전 <strong>Dto 내부 필드의 유효성을 간단하게 검사할 때 주로 사용</strong>하며, 유효성 검사 실패 시 GlobalExceptionHandler와 연계하여 <strong>관련 예외를 손쉽게 처리할 수 있다</strong>는 특징이 있습니다.</li>
<li>다만 필드에 대한 더 복잡한 유효성 검사의 경우 온전하게 수행하기 어려우며, 이를 위해 Service 내부에서 Validator 객체를 활용하거나 AOP 기반으로 추가적인 유효성 검사를 추가하기도 합니다.</li>
<li>또한 @Valid는 기본적으로 Controller 계층에서만 동작하며 다른 계층에서는 검증이 되지 않습니다. 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야 합니다.</li>
</ul>
<h2 id="buildgradle">build.gradle</h2>
<pre><code class="language-java">implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;</code></pre>
<h2 id="joinrequestdto">JoinRequestDto</h2>
<pre><code class="language-java">@Getter
@NoArgsConstructor
public class JoinRequestDto {

    @NotNull(message = &quot;이름은 필수 입력 값입니다.&quot;)
    @NotBlank(message = &quot;이름은 공백일 수 없습니다.&quot;)
    private String name;

    @Email(message = &quot;이메일 형식이 아닙니다.&quot;)
    private String email;

    @NotNull(message = &quot;비밀번호는 필수 입력 값입니다.&quot;)
    @NotBlank(message = &quot;비밀번호는 공백일 수 없습니다.&quot;)
    private String password;

    @Range(min = 1, max = 100, message = &quot;1~100 사이의 값을 입력해주세요.&quot;)
    private int size;
}</code></pre>
<ul>
<li>RequestDto 내부 필드에 대한 여러 제약을 어노테이션으로 추가합니다.<ul>
<li>@NotNull: <code>null</code>인 경우를 허용하지 않음</li>
<li>@NotEmpty:<code>null</code> 과 <code>&quot;&quot;</code> 둘 다 허용하지 않음</li>
<li>@NotBlank: <code>null</code> 과 <code>&quot;&quot;</code> 과 <code>&quot; &quot;</code> 모두 허용하지 않음</li>
<li>@DecimalMin(value = ??): value 미만인 경우를 허용하지 않음</li>
<li>@DecimalMax(value = ??): value 초과인 경우를 허용하지 않음</li>
<li>@Email: 이메일 형식에 부합해야 함</li>
<li>@Range(min = 1, max = 5): 값이 1 이상 5 이하여야 함</li>
</ul>
</li>
</ul>
<h2 id="authcontroller">AuthController</h2>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/user&quot;)
public class AuthController {

    private final AuthService authService;

    // 회원가입
    @PostMapping(&quot;/join&quot;)
    public ResponseEntity&lt;Void&gt; join(@RequestBody @Valid ****JoinRequestDto dto) {
        authService.join(dto);
        return ResponseEntity.ok().build();
    }</code></pre>
<ul>
<li>이렇게 @Valid를 통해 Dto 객체 내부 필드에 대한 유효성 검사를 진행할 수 있습니다.</li>
</ul>
<h2 id="globalexceptionhandler">GlobalExceptionHandler</h2>
<pre><code class="language-java">@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map&lt;String, String&gt; handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        Map&lt;String, String&gt; errors = new HashMap&lt;&gt;();
        exception.getBindingResult().getFieldErrors().forEach(error -&gt;
                errors.put(error.getField(), error.getDefaultMessage())
        );
        return errors;
    }
}</code></pre>
<ul>
<li>Valid로 인한 필드 유효성 검사가 실패할 경우 Spring에서는 MethodArgumentNotValidException을 발생시킵니다.</li>
<li>이는 GlobalExceptionHandler에서 관리하여, 어떤 필드가 왜 유효성 검사에 실패하는지에 대한 메시지를 반환하도록 설정할 수 있습니다.</li>
</ul>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/a1828210-adcc-4a4a-a9c9-208483e1debf/a1b4ffd3-78b5-447c-82de-0bea37e7c43f/image.png" alt="image.png"></p>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/a1828210-adcc-4a4a-a9c9-208483e1debf/f5a270db-02ec-43c4-b5ad-89eebebdb685/image.png" alt="image.png"></p>
<ul>
<li>실제로 @Valid 어노테이션이 없다면 500이 뜨지만, @Valid 어노테이션이 존재할 경우 의도한 대로 &lt;필드, 에러메시지&gt;의 형태로 응답이 반환되는 것을 확인할 수 있습니다.</li>
</ul>
<h1 id="2-service에서의-예외처리-aop-기반">2. Service에서의 예외처리 (AOP 기반)</h1>
<ul>
<li>비즈니스 로직에서 DB에 접근하는 것을 통해 유효성 검사가 진행되는 경우도 있습니다.<ul>
<li>예: 이메일 중복 여부 검증, 비밀번호 정/오 판정, PK로 Entity 존재 여부 판정 등</li>
</ul>
</li>
<li>이렇게 Service Layer에서 예외처리를 진행할 경우 AOP 기반으로 수행할 수 있습니다.</li>
</ul>
<h2 id="authservice">AuthService</h2>
<pre><code class="language-java">@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();

    if(memberRepository.existsByEmail(email)) {
       throw new IllegalArgumentException(&quot;이미 존재하는 회원입니다.&quot;);
    }

    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}</code></pre>
<ul>
<li>이 메서드는 DB에 접근하여 이메일로 유저의 중복 여부를 판정하는 <strong>유효성 검사가 존재</strong>합니다.<ul>
<li><strong>반환 객체가 없기에 별도로 분리해도 무방</strong>합니다.</li>
</ul>
</li>
<li>private 메서드로 분리하는 것이 가장 간단한 방법이지만, <strong>다른 Service에서도 유효성 검사 내용이 반복된다면 Spring AOP 기반으로 분리</strong>할 수 있습니다.</li>
</ul>
<h2 id="checkemailduplicate">CheckEmailDuplicate</h2>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckEmailDuplicate {
}</code></pre>
<h2 id="authvalidationaspect">AuthValidationAspect</h2>
<pre><code class="language-java">@Aspect
@Component
@RequiredArgsConstructor
public class AuthValidationAspect {

    private final MemberRepository memberRepository;

        // @CheckEmailDuplicate이 붙은 메서드 시작 전 실행하며 매개변수로 dto 사용
    @Before(&quot;@annotation(csw.practice.security.annotation.CheckEmailDuplicate) &amp;&amp; args(dto)&quot;)
    public void checkEmailDuplicate(JoinRequestDto dto) {
        if (memberRepository.existsByEmail(dto.getEmail())) {
            throw new IllegalArgumentException(&quot;이미 존재하는 회원입니다.&quot;);
        }
    }
}</code></pre>
<ul>
<li>특정 어노테이션을 조건으로 하여 메서드를 실행하여, @Before, @After, @Around 등으로 구체적인 실행 시점을 지정할 수 있습니다.</li>
</ul>
<h2 id="authservice-1">AuthService</h2>
<pre><code class="language-java">@CheckEmailDuplicate
@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();
    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}</code></pre>
<ul>
<li>유효성 검사 과정을 AOP 메서드로 분리하였습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/f03963dd-e405-42b2-bcfc-7be09bfd1599/image.png" alt=""></p>
<ul>
<li>이렇게 이메일 유효성 검사가 AOP 메서드에서 실행되며 Validation이 정상적으로 동작하는 것을 확인할 수 있습니다.</li>
</ul>
<h1 id="부록-aop를-활용한-로깅">부록: AOP를 활용한 로깅</h1>
<ul>
<li>AOP 기반으로 특정 메서드의 실행 시간을 측정할 수도 있습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/ab4ebe1f-8374-43d2-baa1-d7df69bea7cf/image.png" alt=""></p>
<h2 id="measuretime">MeasureTime</h2>
<pre><code class="language-java">@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MeasureTime {
}

@MeasureTime // 어노테이션을 특정 메서드에 붙인다.
@CheckEmailDuplicate
@Transactional
public void join(JoinRequestDto dto) {
    String name = dto.getName();
    String email = dto.getEmail();
    String password = dto.getPassword();
    memberRepository.save(Member.of(name, passwordEncoder.encode(password), email));
}</code></pre>
<h2 id="logaspect">LogAspect</h2>
<pre><code class="language-java">@Slf4j
@Aspect
@Component
public class LogAspect {

    // 조인포인트를 어노테이션으로 설정
    @Pointcut(&quot;@annotation(csw.practice.security.annotation.MeasureTime)&quot;)
    private void timer(){}

    // 메서드 실행 전,후로 시간을 공유
    @Around(&quot;timer()&quot;)
    public void loggingExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        StopWatch stopWatch = new StopWatch();

        stopWatch.start();
        joinPoint.proceed(); // 조인포인트의 메서드 실행
        stopWatch.stop();

        long totalTimeMillis = stopWatch.getTotalTimeMillis();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();

        log.info(&quot;실행 메서드: {}, 실행시간 = {}ms&quot;, methodName, totalTimeMillis);
    }
}</code></pre>
<ul>
<li>@Pointcut을 통해 어디에서 AOP가 적용될지를 지정합니다.</li>
<li>@Around를 통해 메서드 실행 전후에 특정 로직을 실행할 수 있습니다. 위 로직에서는 메서드가 실행되기 전과 후에 시간을 측정하고 그 결과를 로그로 남기는 역할을 합니다.</li>
<li>ProceedingJoinPoint는 현재 JointPoint, 즉 AOP가 적용된 메서드에 대한 정보를 담고 있는 객체입니다. 이 객체를 통해 대상 메서드를 실행하거나 메서드에 대한 다양한 정보를 가져을 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Springboot] Multi DataSource(readerDB, writerDB) 설정하고 라우팅하기]]></title>
            <link>https://velog.io/@win-luck/Springboot-Multi-DataSourcereaderDB-writerDB-%EC%84%A4%EC%A0%95%ED%95%98%EA%B3%A0-%EB%9D%BC%EC%9A%B0%ED%8C%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Springboot-Multi-DataSourcereaderDB-writerDB-%EC%84%A4%EC%A0%95%ED%95%98%EA%B3%A0-%EB%9D%BC%EC%9A%B0%ED%8C%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 05 Sep 2024 09:29:09 GMT</pubDate>
            <description><![CDATA[<ul>
<li>현재 사내 서버에서는 AWS AuroraDB를 활용한 ReaderDB/WriterDB를 설정하고 비즈니스 로직에서 DB에 대한 접근을 제어하고 있습니다.</li>
<li>Springboot으로 마이그레이션 도중 ReaderDB/WriterDB에 대한 접근 제어 방법을 기록하였습니다. </li>
</ul>
<h2 id="applicationyml">application.yml</h2>
<pre><code class="language-yaml">spring:
  datasource:
    writer:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3308/hello?useSSL=false&amp;allowPublicKeyRetrieval=true
        username: root
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver

    reader:
      hikari:
        jdbc-url: jdbc:mysql://localhost:3306/hello?useSSL=false&amp;allowPublicKeyRetrieval=true
        username: root
        password: 1234
        driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
<ul>
<li><code>Hikari</code>는 Java 기반의 데이터베이스 커넥션 풀 라이브러리로, Springboot에서 성능과 가벼운 메모리라는 이점을 얻기 위해 사용하였습니다.</li>
<li>기본적으로 writerDB, readerDB 2개의 데이터베이스를 운용한다고 가정하였습니다.</li>
</ul>
<h2 id="datasourceconfig">DataSourceConfig</h2>
<pre><code class="language-java">@Configuration
public class DataSourceConfig {

    /**
     * yml 파일에서 `spring.datasource.writer.hikari` prefix 설정을 바탕으로
     * `writerDataSource` 빈을 생성합니다. 이 데이터 소스는 &quot;writerDB&quot; 역할을 합니다.
     */
    @ConfigurationProperties(prefix = &quot;spring.datasource.writer.hikari&quot;)
    @Bean(name = &quot;writerDataSource&quot;)
    public DataSource writerDataSource() {
        // DataSourceBuilder를 사용해 HikariDataSource 타입의 데이터 소스를 생성합니다.
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * yml 파일에서 `spring.datasource.reader.hikari` prefix 설정을 바탕으로
     * `readerDataSource` 빈을 생성합니다. 이 데이터 소스는 &quot;readerDB&quot; 역할을 합니다.
     */
    @ConfigurationProperties(prefix = &quot;spring.datasource.reader.hikari&quot;)
    @Bean(name = &quot;readerDataSource&quot;)
    public DataSource readerDataSource() {
        // DataSourceBuilder를 사용해 HikariDataSource 타입의 데이터 소스를 생성합니다.
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * `writerDataSource`와 `readerDataSource`가 생성된 이후에 이 메서드를 실행하도록 지정
     * 주어진 &quot;writer&quot;와 &quot;reader&quot; 데이터 소스를 기준으로 동적으로 데이터 라우팅
     * `ReplicationRoutingDataSource` 빈을 생성합니다.
     * 
     * @param writer 쓰기 전용 데이터 소스
     * @param reader 읽기 전용 데이터 소스
     * @return 동적으로 읽기/쓰기 데이터 소스를 라우팅하는 데이터 소스
     */
    @DependsOn({&quot;writerDataSource&quot;, &quot;readerDataSource&quot;})
    @Bean
    public DataSource routingDataSource(
            @Qualifier(&quot;writerDataSource&quot;) DataSource writer,
            @Qualifier(&quot;readerDataSource&quot;) DataSource reader) {

        // `ReplicationRoutingDataSource`는 읽기/쓰기 작업에 따라 데이터 소스를 동적으로 라우팅
        ReplicationRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();

        Map&lt;Object, Object&gt; dataSourceMap = new HashMap&lt;&gt;();

        dataSourceMap.put(&quot;writer&quot;, writer);  // 쓰기 전용 데이터 소스
        dataSourceMap.put(&quot;reader&quot;, reader);  // 읽기 전용 데이터 소스

        routingDataSource.setTargetDataSources(dataSourceMap);
        // 기본 데이터 소스는 &quot;writer&quot;
        routingDataSource.setDefaultTargetDataSource(writer);
        return routingDataSource;
    }

    /**
     * `routingDataSource`가 생성된 이후에 이 메서드를 실행하도록 지정
     * `LazyConnectionDataSourceProxy`는 실제 연결을 사용하는 시점에 데이터베이스 연결을 지연 생성
     * 성능 최적화를 도모하고 트랜잭션 관리에서 데이터 소스의 사용을 효율적으로 처리 가능
     * 
     * @param routingDataSource 라우팅 가능한 데이터 소스
     * @return 지연된 연결 처리를 지원하는 데이터 소스 프록시
     */
    @DependsOn(&quot;routingDataSource&quot;)
    @Primary  // 이 빈을 기본 `DataSource`로 설정
    @Bean
    public DataSource dataSource(DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}</code></pre>
<h1 id="1-transactional-기반-접근-제어">1. @Transactional 기반 접근 제어</h1>
<ul>
<li>@Transactional 메서드 내부 생성/수정/삭제는 WriterDB로 라우팅되어 실행됩니다.</li>
<li>@Transactional(readOnly = true) 메서드 내부 조회는 ReaderDB로 라우팅되어 실행됩니다.</li>
<li><strong>장점:</strong> 핵심적인 논리적 단위인 트랜잭션 기반 간단한 라우팅 실현 가능</li>
<li><strong>단점:</strong> Express.js에 일부 존재하는 “readerDB에 대한 write 연산” 등의 세밀한 제어 불가능</li>
</ul>
<h2 id="replicationroutingdatasource">ReplicationRoutingDataSource</h2>
<pre><code class="language-java">public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    // 현재 트랜잭션이 읽기 전용인 경우 Reader DB, 그렇지 않으면 Writer DB로 라우팅
    @Override
    protected Object determineCurrentLookupKey() {
        return isCurrentTransactionReadOnly() ? &quot;reader&quot; : &quot;writer&quot;;
    }
}</code></pre>
<h2 id="memberservice">MemberService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public Long save(Member member) {
        return memberRepository.save(member).getId();
    }

    @Transactional(readOnly = true)
    public Member findById(Long id) {
        return memberRepository.findById(id).orElseThrow(() -&gt; new IllegalArgumentException(&quot;해당 회원이 없습니다. id=&quot; + id));
    }
}</code></pre>
<h2 id="memberservicetest">MemberServiceTest</h2>
<pre><code class="language-java">@SpringBootTest
class MemberServiceTest {

    @Autowired
    private MemberService memberService;

    @DisplayName(&quot;메서드의 @Transactional 옵션에 따라 DB 라우팅이 다르게 이루어져야 한다.&quot;)
    @Test
    void dbReplicationTest() {
        // @Transactional -&gt; writerDB로 라우팅하여 데이터 생성
        Member member = Member.of(&quot;name&quot;, &quot;email&quot;);
        Long id = memberService.save(member);

        // @Transactional(readOnly = true) -&gt; readerDB로 라우팅하여 데이터 조회
        assertThatThrownBy(() -&gt; {
            memberService.findById(id);
        }).isInstanceOf(IllegalArgumentException.class);
    }
}</code></pre>
<ul>
<li>save()는 writerDB에서 이루어지지만, findById는 (readOnly=true) 옵션으로 인해 readerDB에서 수행될 것이기에 Exception이 발생해야 합니다.</li>
<li>물론 실제 AuroraDB에선 동기화가 이루어질 것이기에 이러한 일은 발생하지 않습니다. 이 테스트는 라우팅 자체가 정상적으로 이루어지는지 확인할 목적입니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/15e4d3c4-ce6d-4ce4-8dd3-f8b1b269e2a8/image.png" alt=""></p>
<ul>
<li>실제 테스트 결과 Exception이 발생했기에 트랜잭션 옵션에 따라 올바르게 라우팅되는 것이 검증되었습니다.</li>
</ul>
<h1 id="2-aop-기반-접근-제어">2. AOP 기반 접근 제어</h1>
<ul>
<li>1번에서 일부 보완점을 추가하여, Spring AOP를 바탕으로 특정 어노테이션이 붙은 메서드는 DB 라우팅 대상을 readerDB로 변경하는 전략입니다.</li>
<li>이를 통해 readerDB에도 write 연산이 가능해집니다.</li>
<li>장점: AOP 기반으로 필요 시 메서드 단위로 라우팅 대상을 변경할 수 있기에 더욱 유연한 제어가 가능</li>
<li>단점: DB 엔드포인트가 늘어나면 관리가 어려워지며, AOP 도입으로 인해 난해한 코드 증가</li>
</ul>
<h2 id="datasourcecontextholder">DataSourceContextHolder</h2>
<pre><code class="language-java">@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DataSourceContextHolder {

    private static final ThreadLocal&lt;String&gt; CONTEXT = new ThreadLocal&lt;&gt;();

    public static void setDataSourceKey(String key) {
        log.info(&quot;Switch DataSource to {}&quot;, key);
        CONTEXT.set(key);
    }

    public static String getDataSourceKey() {
        return CONTEXT.get();
    }

    public static void clearDataSourceKey() {
        CONTEXT.remove();
    }
}</code></pre>
<h2 id="writetoreaderdb">WriteToReaderDB</h2>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteToReaderDB {
}</code></pre>
<h2 id="datasourceaspect">DataSourceAspect</h2>
<pre><code class="language-java">@Aspect
@Component
public class DataSourceAspect {

    // @WriteToReaderDB 애노테이션이 붙은 메서드가 실행되기 전에 reader 데이터 소스를 사용하도록 설정
    @Before(&quot;@annotation(com.example.demo.config.aop.WriteToReaderDB)&quot;)
    public void setReaderDataSource() {
        DataSourceContextHolder.setDataSourceKey(&quot;reader&quot;);
    }

    // @WriteToReaderDB 애노테이션이 붙은 메서드가 실행된 후에 데이터 소스를 초기화
    @After(&quot;@annotation(com.example.demo.config.aop.WriteToReaderDB)&quot;)
    public void clearDataSource() {
        DataSourceContextHolder.clearDataSourceKey();
    }
}</code></pre>
<p>위 코드에선 치명적인 문제점이 하나 있습니다.
이는 메서드 실행 도중 예외가 발생할 때 After가 실행되지 않아 초기화 작업이 누락될 수 있습니다.
아래와 같은 형태로 바꿀 수 있습니다.</p>
<pre><code class="language-java">@Aspect
@Component
public class DataSourceAspect {

    @Around(&quot;@annotation(com.example.demo.config.aop.WriteToReaderDB)&quot;)
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            DataSourceContextHolder.setDataSourceKey(&quot;reader&quot;);
            return joinPoint.proceed(); // 메서드 실행
        } catch (Exception ex) {
            throw ex;
        } finally {
            DataSourceContextHolder.clearDataSourceKey(); // 예외 및 정상 종료 후 데이터 소스 키 제거
        }
    }
}</code></pre>
<h2 id="replicationroutingdatasource-1">ReplicationRoutingDataSource</h2>
<pre><code class="language-java">public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 컨텍스트에 데이터 소스 키가 설정되어 있으면 그 값을 사용
        String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
        if (dataSourceKey != null) {
            return dataSourceKey;
        }

        // 그렇지 않으면 트랜잭션 읽기 전용 여부에 따라 기본적으로 라우팅
        return isCurrentTransactionReadOnly() ? &quot;reader&quot; : &quot;writer&quot;;
    }
}</code></pre>
<h2 id="memberservice-1">MemberService</h2>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional
    public Long save(Member member) {
        return memberRepository.save(member).getId();
    }

    @Transactional(readOnly = true)
    public Member findById(Long id) {
        return memberRepository.findById(id).orElseThrow(() -&gt; new IllegalArgumentException(&quot;해당 회원이 없습니다. id=&quot; + id));
    }

    @WriteToReaderDB
    @Transactional
    public Long saveToReaderDB(Member member) {
        return memberRepository.save(member).getId();
    }
}</code></pre>
<h2 id="memberservicetest-1">MemberServiceTest</h2>
<pre><code class="language-java">@SpringBootTest
class MemberServiceTest {

    @Autowired
    private MemberService memberService;

    @DisplayName(&quot;메서드의 @Transactional 옵션에 따라 DB 라우팅이 다르게 이루어져야 한다.&quot;)
    @Test
    void dbReplicationTest() {
        // @Transactional -&gt; writerDB로 라우팅하여 데이터 생성
        Member member = Member.of(&quot;name&quot;, &quot;email&quot;);
        Long id = memberService.save(member);

        // @Transactional(readOnly = true) -&gt; readerDB로 라우팅하여 데이터 조회
        assertThatThrownBy(() -&gt; {
            memberService.findById(id);
        }).isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName(&quot;메서드에 @WriteToReaderDB 어노테이션이 붙으면 readerDB로 라우팅되어야 한다.&quot;)
    @Test
    void writeToReaderDbTest() {
        // @WriteToReaderDB -&gt; readerDB로 라우팅하여 데이터 생성
        Member member = Member.of(&quot;name&quot;, &quot;email&quot;);
        Long id = memberService.saveToReaderDB(member);

        // @WriteToReaderDB -&gt; readerDB로 라우팅하여 데이터 조회
        Member foundMember = memberService.findById(id);

        assertThat(foundMember.getId()).isEqualTo(id);
        assertThat(foundMember.getName()).isEqualTo(&quot;name&quot;);
        assertThat(foundMember.getEmail()).isEqualTo(&quot;email&quot;);
    }
}
</code></pre>
<ul>
<li>@WriteToReaderDB가 붙은 saveToReaderDB() 메서드를 실행하면 AOP 설정으로 인해 메서드 실행 전 DataSourceContextHolder의 setDataSourceKey가 호출될 것입니다.</li>
<li>이후 readerDB에 write 연산이 실행되어야 합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/b6ada5f2-e1e5-4603-9101-56713a126311/image.png" alt=""></p>
<ul>
<li>로그를 통해 AOP로 해당 메서드 실행 시 Context가 reader로 변경되었음을 알 수 있습니다.</li>
<li>또한 1번과 달리 Exception이 발생하지 않았기에 생성/조회 연산이 readerDB에서 모두 실행된 것을 알 수 있습니다.</li>
</ul>
<h1 id="reference">Reference</h1>
<p><a href="https://minhye0k.github.io/spring-%EC%97%90%EC%84%9C-data-source-routing-%EC%9D%84-%ED%86%B5%ED%95%9C-db-read-write-%EC%9A%94%EC%B2%AD-%EB%B6%84%EC%82%B0%ED%95%98%EA%B8%B0">spring에서 data source routing을 통한 db read/write 요청 분산하기</a></p>
<p><a href="https://velog.io/@ekxk1234/Springboot-Application%EA%B3%BC-RDS-Aurora-DB">Springboot Application과 RDS, Aurora DB</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Springboot] 여러 프로필 관리하기]]></title>
            <link>https://velog.io/@win-luck/Springboot-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%ED%95%84-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Springboot-%EC%97%AC%EB%9F%AC-%ED%94%84%EB%A1%9C%ED%95%84-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 03 Sep 2024 07:47:45 GMT</pubDate>
            <description><![CDATA[<ul>
<li>프로젝트를 개발하다 보면 로컬 개발 환경과 실제 배포 환경을 분리해야 할 때가 있다.</li>
<li>이를 위해 Spring에서는 Profile을 통해 빌드 시 활용할 appplication.yml을 지정해줄 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/5c10b30c-6bd7-4a9e-b7a9-1caf342b23e8/image.png" alt=""></p>
<h1 id="applicationyml">application.yml</h1>
<pre><code class="language-yml">spring:
  profiles:
    default: local</code></pre>
<ul>
<li>spring.profiles.default: 별도의 Active Profile이 없는 경우 local 프로필이 활성화된다.<ul>
<li>즉 application-local.yml의 환경변수 활용</li>
</ul>
</li>
<li>spring.profiles.active: 특정 Profile을 명시적으로 활성화한다.</li>
</ul>
<h1 id="profile">@Profile</h1>
<ul>
<li>@Profile 어노테이션을 통해 프로필에 해당하는 환경별로 관련 리소스를 설정할 수 있다.</li>
</ul>
<pre><code class="language-java">@Configuration
public class AppConfig {

    @Bean
    @Profile(&quot;dev&quot;)
    public DataSource devDataSource() {
        return new DataSource(&quot;jdbc:h2:mem:devDb&quot;, &quot;devUser&quot;, &quot;devPassword&quot;);
    }

    @Bean
    @Profile(&quot;prod&quot;)
    public DataSource prodDataSource() {
        return new DataSource(&quot;jdbc:mysql://prodDb&quot;, &quot;prodUser&quot;, &quot;prodPassword&quot;);
    }
}</code></pre>
<h1 id="activeprofiles">@ActiveProfiles</h1>
<ul>
<li><p>테스트코드에서 클래스 혹은 메서드 단위에 @ActiveProfiles을 붙이면 실행 중 환경변수에 접근할 때 해당 프로필의 환경변수를 사용하게 된다.</p>
<pre><code class="language-java">  @DataJpaTest
  @ActiveProfiles(&quot;test&quot;)
  class CommentRepositoryTest {
      @Autowired
      CommentRepository commentRepository;
  }</code></pre>
</li>
<li><p>혹은 아래처럼 build.gradle 파일에 테스트코드 실행 시 프로필을 고정시키는 옵션을 추가할 수 있다.</p>
<pre><code class="language-java">  // build.gradle

  test {
      systemProperty &#39;spring.profiles.active&#39;, &#39;test&#39;
  }</code></pre>
</li>
</ul>
<h1 id="빌드">빌드</h1>
<ul>
<li>여러 application.yml이 존재할 때 특정 프로필로 빌드하기 위해서는 아래와 같은 형태로 실행할 수 있다.</li>
</ul>
<pre><code class="language-java">java -jar -Dspring.profiles.active=[프로필명] [jar파일명]</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 “인스턴스 프로파일에 연결된 역할 없음”(No roles attached to instance profile) 해결 방법]]></title>
            <link>https://velog.io/@win-luck/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%ED%94%84%EB%A1%9C%ED%8C%8C%EC%9D%BC%EC%97%90-%EC%97%B0%EA%B2%B0%EB%90%9C-%EC%97%AD%ED%95%A0-%EC%97%86%EC%9D%8CNo-roles-attached-to-instance-profile-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@win-luck/AWS-EC2-%EC%9D%B8%EC%8A%A4%ED%84%B4%EC%8A%A4-%ED%94%84%EB%A1%9C%ED%8C%8C%EC%9D%BC%EC%97%90-%EC%97%B0%EA%B2%B0%EB%90%9C-%EC%97%AD%ED%95%A0-%EC%97%86%EC%9D%8CNo-roles-attached-to-instance-profile-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 20 Aug 2024 12:45:54 GMT</pubDate>
            <description><![CDATA[<p>팀 프로젝트 중에 AWS EC2에 부여한 Role을 바꾼 적이 있었는데, 갑자기 “인스턴스 프로파일에 연결된 역할 없음” 이라는 문구가 뜨면서 관련 Role이 적용되지 않기 시작했다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/7ee5efb1-1cf3-4d43-988b-9eef6e0274d2/image.png" alt=""></p>
<p>심지어 &lt;IAM 역할 없음&gt;을 선택해도 에러가 발생하여 아예 수정조차 되지 않는 기가 막힌 상황이었다.</p>
<p>아래와 같은 명령어로, EC2 내부 CLI로 명시적으로 Role을 제거하고 기존의 다른 Role을 이식해주었다.</p>
<pre><code class="language-sql">aws ec2 describe-iam-instance-profile-associations --filters &quot;Name=instance-id,Values={인스턴스 ID}&quot; --region ap-northeast-2</code></pre>
<p>먼저 CLI로 assocation id를 추출한다.</p>
<pre><code class="language-sql">aws ec2 disassociate-iam-instance-profile --association-id &lt;AssociationId&gt; --region ap-northeast-2</code></pre>
<p>이후 EC2 상에 존재하는 IAM 인스턴스 프로필을 분리한다.</p>
<pre><code class="language-sql">aws ec2 associate-iam-instance-profile \
    --instance-id {인스턴스 ID} \
    --iam-instance-profile Name=TEAM_ORANGE_SERVER \
    --region ap-northeast-2</code></pre>
<p>이후 새로운 인스턴스 프로파일을 연결한다.</p>
<pre><code class="language-sql">aws ec2 describe-instances \
    --instance-ids {인스턴스 ID} \
    --query &quot;Reservations[].Instances[].IamInstanceProfile&quot; \
    --region ap-northeast-2</code></pre>
<p>마지막 위 명령어를 통해 현재 교체된 인스턴스 프로파일을 확인할 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/3c931ce5-0617-4ead-bf32-965ea13dc6f6/image.png" alt=""></p>
<p>EC2에 가보니 Role이 정상적으로 동작하는 것을 확인할 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Springboot] TestContainer로 격리된 테스트 환경 조성하기 + @DirtiesContext 없이 테스트 독립성 보장하기]]></title>
            <link>https://velog.io/@win-luck/Springboot-TestContainer%EB%A1%9C-%EA%B2%A9%EB%A6%AC%EB%90%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EC%A1%B0%EC%84%B1%ED%95%98%EA%B8%B0-DirtiesContext-%EC%97%86%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%85%EB%A6%BD%EC%84%B1-%EB%B3%B4%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Springboot-TestContainer%EB%A1%9C-%EA%B2%A9%EB%A6%AC%EB%90%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EC%A1%B0%EC%84%B1%ED%95%98%EA%B8%B0-DirtiesContext-%EC%97%86%EC%9D%B4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%85%EB%A6%BD%EC%84%B1-%EB%B3%B4%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 18 Aug 2024 16:08:23 GMT</pubDate>
            <description><![CDATA[<p>현 프로젝트에는 Mocktio 기반의 단위 테스트와 mySQL DB에 실제로 접근하는 통합 테스트가 섞여 있다.</p>
<p>CI 파이프라인에 테스트 성공 여부를 담아서 프로젝트의 신뢰성을 높이고자 하는데, DB에 의존하는 통합 테스트를 CI 파이프라인에 그대로 띄우기엔 추후 의존성(Redis 등)이 더해져 코드가 난잡해질까봐 우려스러웠다. </p>
<p><strong>테스트 컨테이너</strong>를 도입하여 개발 환경과 별개로 독립된 테스트 환경을 조성해보자!</p>
<h1 id="buildgradle">build.gradle</h1>
<pre><code>// test container
testImplementation &#39;org.testcontainers:testcontainers:1.19.3&#39;
testImplementation &#39;org.testcontainers:junit-jupiter:1.19.3&#39;
testImplementation &#39;org.testcontainers:mysql&#39;
testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;</code></pre><p>테스트 컨테이너 관련 의존성을 추가한다. </p>
<pre><code>test {
    systemProperty &#39;spring.profiles.active&#39;, &#39;test&#39;
    exclude &#39;**/load/**&#39;
}
</code></pre><p>우리 프로젝트는 테스트코드 실행 시 자동으로 profile을 &quot;test&quot;로 지정해두었기에, 관련된 설정을 추가하였다. 또한 일부 기능에 대한 부하 테스트를 load 패키지 내에 작성해두었는데, CI 파이프라인에서 큰 의미가 없기에 제외하였다.</p>
<h1 id="application-testyml">application-test.yml</h1>
<pre><code class="language-yml">spring:
  datasource:
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
    url: jdbc:tc:mysql:8.0:///
  jpa:
    hibernate:
      ddl-auto: create-drop</code></pre>
<p>test profile을 위한 의존성을 추가하여 커밋해두었다. </p>
<p>spring boot가 Testcontainers의 ContainerDatabaseDriver를 사용하게 함으로써 Docker 컨테이너에서 데이터베이스를 자동으로 실행할 수 있게 된다. (실제 로컬에서 통합테스트 코드를 실행하면 도커 컨테이너가 생성되었다가 소멸하는 것을 확인할 수 있다.)</p>
<p>jdbc:tc:mysql:8.0:/// 옵션을 통해 mySQL 8.0 기반의 컨테이너를 생성하여 애플리케이션의 테스트 코드가 이 DB에 접근할 수 있도록 한다. 참고로 3개의 슬래시는 기본 DB임을 의미한다. (이름을 따로 붙여도 된다.)</p>
<p>실제 Github Actions의 CI 파이프라인에서 ./gradlew test 를 실행하면 이 의존성을 바탕으로 테스트코드가 실행될 것이다.</p>
<p>이제 통합테스트들이 테스트 컨테이너에서 테스트를 실행할 수 있도록 전용 어노테이션을 만들자.</p>
<pre><code class="language-java">@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ActiveProfiles(&quot;test&quot;)
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public @interface TCDataJpaTest {
}</code></pre>
<p>이 어노테이션이 붙은 모든 통합 테스트는 테스트 컨테이너 + test 프로필 기반으로 DataJpaTest를 실행하게 될 것이며, @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 옵션을 통해 default인 h2가 아니라 테스트 컨테이너를 통해 테스트를 실행하도록 설정된다.</p>
<pre><code class="language-java">@TCDataJpaTest
class EventServiceSearchHintsTest  {
    @Autowired
    private EventMetadataRepository emRepo;
    @Autowired
    private EventFrameRepository efRepo;
}</code></pre>
<p>참고로 통합테스트 중 일부가 @Sql 어노테이션을 통해 데이터를 주입받은 후 테스트에 돌입하는 것 때문에 테스트 간 충돌로 인한 실패가 끊임없이 발생하였다. @Transcational 어노테이션을 붙여도 해결되지 않았다. 
<img src="https://velog.velcdn.com/images/win-luck/post/c25d3458-a438-4bea-a94f-3fa37d9412ac/image.png" alt=""></p>
<p>이를 막기 위해 @DirtiesContext를 도입하여, 각 테스트가 끝날 때마다 Springboot의 context 자체를 초기화하여 급한 불을 끄듯 문제를 해결하였다.</p>
<pre><code class="language-java">@Sql(value = &quot;classpath:sql/EventParticipationInfoRepositoryTest.sql&quot;, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
@TCDataJpaTest
class EventParticipationInfoRepositoryTest {
    @Autowired
    EventParticipationInfoRepository epiRepository;
}</code></pre>
<h1 id="ciyml">CI.yml</h1>
<pre><code class="language-yml">name: CI

on:
  push:
    branches: [ &quot;dev&quot; ]
  pull_request:
    branches: [ &quot;dev&quot; ]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Test with Gradle
        run: ./gradlew test

      - name: Build with Gradle
        run: ./gradlew build -x test

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Build Docker Image
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/orange .

      - name: Push Docker Image
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/orange
</code></pre>
<p>이제 dev 브랜치로 향하는 PR은 테스트코드 실행을 거치게 될 것이다.</p>
<p>Github Actions로 가보자!
<img src="https://velog.velcdn.com/images/win-luck/post/c51d66eb-6744-4912-801c-46bedbf29b76/image.png" alt=""></p>
<p>테스트 코드는 통과하지만 무려 2분 20초나 소요되고 있다. 빌드가 5초 전후 걸린다는 점을 감안했을 때 매우 성능이 미흡하다고 볼 수 있다.</p>
<p>그렇다고 @Sql 어노테이션을 없애고 애플리케이션 단에서 DB 테이블에 직접 Entity 객체를 생성하여 데이터를 삽입하는 것은 너무나도 번거로운 일이다.</p>
<p>*<em>테스트 코드 실패 로그를 뜯어보면 이 문제의 원인은 기본적으로 외래키 제약에 있다. *</em></p>
<pre><code>org.springframework.jdbc.datasource.init.ScriptStatementFailedException: Failed to execute SQL script statement #2 of class path resource [sql/CustomDrawEventWinningInfoRepositoryImplTest.sql]: INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (1, 1, &#39;HD_240808_001&#39;)

    at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:282)
    at org.springframework.jdbc.datasource.init.ResourceDatabasePopulator.populate(ResourceDatabasePopulator.java:254)
    at org.springframework.jdbc.datasource.init.DatabasePopulatorUtils.execute(DatabasePopulatorUtils.java:54)
    at org.springframework.jdbc.datasource.init.ResourceDatabasePopulator.execute(ResourceDatabasePopulator.java:269)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.lambda$executeSqlScripts$9(SqlScriptsTestExecutionListener.java:362)
    at org.springframework.transaction.support.TransactionOperations.lambda$executeWithoutResult$0(TransactionOperations.java:68)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
    at org.springframework.transaction.support.TransactionOperations.executeWithoutResult(TransactionOperations.java:67)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.executeSqlScripts(SqlScriptsTestExecutionListener.java:362)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.lambda$executeSqlScripts$4(SqlScriptsTestExecutionListener.java:275)
    at java.base/java.lang.Iterable.forEach(Iterable.java:75)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.executeSqlScripts(SqlScriptsTestExecutionListener.java:275)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.executeClassLevelSqlScripts(SqlScriptsTestExecutionListener.java:201)
    at org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener.beforeTestClass(SqlScriptsTestExecutionListener.java:145)
    at org.springframework.test.context.TestContextManager.beforeTestClass(TestContextManager.java:220)
    at org.springframework.test.context.junit.jupiter.SpringExtension.beforeAll(SpringExtension.java:133)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (test.event_metadata, CONSTRAINT FKm6p8gh3tahf7aepb6cc01ox81 FOREIGN KEY (event_frame_id) REFERENCES event_frame (id))
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
    at com.mysql.cj.jdbc.StatementImpl.executeInternal(StatementImpl.java:770)
    at com.mysql.cj.jdbc.StatementImpl.execute(StatementImpl.java:653)
    at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:94)
    at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
    at org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript(ScriptUtils.java:261)</code></pre><p>이는 각 테스트 클래스마다 @Sql을 통해 삽입되는 데이터끼리 서로 충돌하기 때문이다. </p>
<p>그렇다면 .sql 파일의 데이터끼리 서로 충돌하지 않도록 입력하는 데이터를 조절하면 해결될까?</p>
<p><strong>.sql 파일이 한두개일 때면 모를까 실제 서비스처럼 수십개까지 늘어날 때도 과연 그 전략이 유효할까?</strong></p>
<p>인터넷을 찾아보니 우아한테크코스와 외국 커뮤니티 등에서 비슷한 고민이 많았다.</p>
<p><strong>&quot;테스트 환경에서 요구되는 초기화&quot;는 테스트 클래스별 순서와 상관없이 DB의 상태를 매 클래스마다 최초 상태로 제공하여 결과를 일관적으로 보장하는 것이다. 다른 context의 초기화는 굳이 필요하지 않다.</strong></p>
<p>매 테스트 클래스가 종료될 때마다 DB의 상태만 초기화하는 로직을 삽입하면 어떨까?</p>
<p>먼저 추상 테스트 클래스 IntegrationDataJpaTest를 도입하였다.</p>
<pre><code class="language-java">@TCDataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class IntegrationDataJpaTest {

    @Autowired
    JdbcTemplate jdbcTemplate;

    // 이 클래스를 상속받는 모든 통합 테스트 클래스는 테스트가 끝나면 모든 테이블의 데이터를 삭제한다.
    @AfterAll
    void clearDatabase(){
        jdbcTemplate.execute(&quot;SET FOREIGN_KEY_CHECKS = 0&quot;);
        List&lt;String&gt; tableNameList = jdbcTemplate.queryForList(&quot;SHOW TABLES&quot;, String.class); // 모든 테이블 이름을 가져온다.
        for(String tableName : tableNameList) {
            jdbcTemplate.execute(&quot;TRUNCATE TABLE &quot; + tableName); // 모든 테이블의 데이터를 삭제한다.
            jdbcTemplate.execute(&quot;ALTER TABLE &quot; + tableName + &quot; AUTO_INCREMENT = 1&quot;); // AUTO_INCREMENT 초기화
        }
        jdbcTemplate.execute(&quot;SET FOREIGN_KEY_CHECKS = 1&quot;);
    }
}</code></pre>
<p>모든 테이블의 데이터를 제거하고 AUTO_INCREMENT를 1로 초기화하였다. 
이제 이 클래스를 상속받은 통합 테스트 클래스는 자신이 가진 모든 테스트 메서드가 종료되면 clearDatabase()를 통해 DB를 초기화하게 될 것이다.</p>
<p>다음 클래스는 초기화된 DB를 마주하므로 각 테스트 클래스별로 격리된 테스트 환경을 제공할 수 있게 된다!</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/1b0b6563-919d-4576-9c0e-048199864a7e/image.png" alt=""></p>
<p>로컬에서 테스트가 통과되었으니, Github Actions로 가보자.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/38f480d7-a28a-4e5d-9cc0-3c86fdb977dc/image.png" alt=""></p>
<p><strong>@DirtiesContext를 뜯어냄으로써 테스트 속도를 2분 중반대에서 1분 초반대까지 약 50% 가량 개선하는 데 성공했다. 통합 테스트 수가 많아질수록 이 차이는 더 벌어질 것이다.</strong></p>
<p>다만 현재의 clearDatabase() 메서드가 mySQL만을 기반으로 하기에 추후 Redis가 끼어들거나 아예 다른 데이터베이스로 교체될 경우 필연적으로 초기화 코드의 변경이 요구될 것이다.</p>
<h1 id="reference">Reference</h1>
<p><a href="https://stackoverflow.com/questions/48714118/reset-database-after-each-test-on-spring-without-using-dirtiescontext">https://stackoverflow.com/questions/48714118/reset-database-after-each-test-on-spring-without-using-dirtiescontext</a>
<a href="https://blog.gtiwari333.com/2021/07/making-integration-tests-faster-without.html#google_vignette">https://blog.gtiwari333.com/2021/07/making-integration-tests-faster-without.html#google_vignette</a>
<a href="https://newwisdom.tistory.com/95">https://newwisdom.tistory.com/95</a>
<a href="https://devlopsquare.tistory.com/227">https://devlopsquare.tistory.com/227</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] docker-compose로 Springboot + Promtail 함께 빌드하기]]></title>
            <link>https://velog.io/@win-luck/Docker-docker-compose%EB%A1%9C-Springboot-Promtail-%ED%95%A8%EA%BB%98-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Docker-docker-compose%EB%A1%9C-Springboot-Promtail-%ED%95%A8%EA%BB%98-%EB%B9%8C%EB%93%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 18 Aug 2024 15:27:21 GMT</pubDate>
            <description><![CDATA[<p>로그 수집 및 모니터링은 애플리케이션의 상태를 모니터링하고, 문제를 신속하게 해결하는 데 필수적인 요소이다. </p>
<p>Docker-compose 환경으로 Promtail과 Spring Boot 애플리케이션을 함께 빌드하여 로그를 수집하고, 별도의 모니터링 서버에 있는 Grafana Loki로 전송하는 방법에 대해 알아보자.</p>
<p>먼저 Promtail은 로그를 수집하고 이를 Loki에 전송하는 역할을 하는 도구이다. 다양한 로그 파일을 읽고, 이를 특정 형식으로 정제하여 Loki에 보낼 수 있다. </p>
<p>Loki는 Grafana에서 로그를 쿼리하고 시각화할 수 있도록 지원하는 로그 관리 시스템이다.</p>
<h1 id="resourceslogback-springxml">resources/logback-spring.xml</h1>
<pre><code>&lt;configuration&gt;
    &lt;!-- prod에서만 로그 수집하도록 설정 --&gt;
    &lt;springProfile name=&quot;prod&quot;&gt;
        &lt;property name=&quot;LOG_DIR&quot; value=&quot;/var/log/spring-boot&quot; /&gt;

        &lt;!-- 콘솔 출력 설정 --&gt;
        &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
            &lt;encoder&gt;
                &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n&lt;/pattern&gt;
            &lt;/encoder&gt;
        &lt;/appender&gt;

        &lt;!-- 일반 파일 출력 설정 --&gt;
        &lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
            &lt;file&gt;${LOG_DIR}/general.log&lt;/file&gt;
            &lt;encoder&gt;
                &lt;pattern&gt;%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n&lt;/pattern&gt;
            &lt;/encoder&gt;
            &lt;!-- 지난 로그는 loghistory로 저장 --&gt;
            &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;
                &lt;!-- 로그 파일 이름 설정: 날짜별 --&gt;
                &lt;fileNamePattern&gt;${LOG_DIR}/general-%d{yyyy-MM-dd}.loghistory&lt;/fileNamePattern&gt;
                &lt;maxHistory&gt;7&lt;/maxHistory&gt; &lt;!-- 로그 파일을 최대 7일 동안 유지 --&gt;
            &lt;/rollingPolicy&gt;
        &lt;/appender&gt;

        &lt;!-- 루트 로거 설정: 콘솔 및 일반 파일에 로그 저장 --&gt;
        &lt;root level=&quot;info&quot;&gt;
            &lt;appender-ref ref=&quot;CONSOLE&quot;/&gt;
            &lt;appender-ref ref=&quot;FILE&quot;/&gt;
        &lt;/root&gt;
    &lt;/springProfile&gt;

    &lt;!-- test 환경 설정 --&gt;
    &lt;springProfile name=&quot;test&quot;&gt;
        &lt;property name=&quot;LOG_DIR&quot; value=&quot;logs&quot; /&gt;

        &lt;!-- 콘솔 출력 설정 --&gt;
        &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
            &lt;encoder&gt;
                &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n&lt;/pattern&gt;
            &lt;/encoder&gt;
        &lt;/appender&gt;

        &lt;!-- test 환경에서는 로그를 콘솔에만 출력 --&gt;
        &lt;root level=&quot;info&quot;&gt;
            &lt;appender-ref ref=&quot;CONSOLE&quot;/&gt;
        &lt;/root&gt;
    &lt;/springProfile&gt;
&lt;/configuration&gt;
</code></pre><p>참고로 테스트 환경을 둔 이유는 통합 테스트 실행 시 logback이 함께 동작하며 /var/log/spring-boot 디렉토리를 찾지 못해 자동으로 테스트가 실패하고 있어, 이를 콘솔에만 출력하도록 명시적으로 설정해주어야 했다.</p>
<h1 id="infrapromtail-configyml">/infra/promtail-config.yml</h1>
<pre><code class="language-yml">server:
  http_listen_port: ${PROMTAIL_PORT}

positions:
  filename: /tmp/positions.yaml

clients:
  - url: ${LOKI_URL}

scrape_configs:
  - job_name: spring-boot
    static_configs:
      - targets:
        - localhost
        labels:
          job: springboot
          __path__: /var/log/spring-boot/*.log</code></pre>
<ol>
<li><p>promtail 서버가 http 요청을 수신할 포트를 정의한다.</p>
</li>
<li><p>positions를 통해 로그 수집을 진행하면서 읽은 위치를 기록하여 재시작 시에도 이전 위치부터 로그 수집을 재개할 수 있게 한다.</p>
</li>
<li><p>로그 데이터를 전송할 Loki URL을 지정한다.</p>
</li>
<li><p>/var/log/spring-boot 디렉토리 내 모든 log 파일을 수집 대상으로 지정한다.</p>
</li>
</ol>
<p>이제 docker-compose에 이 설정을 바탕으로 promtail을 이식하자.</p>
<h1 id="infradocker-composeyml">infra/docker-compose.yml</h1>
<pre><code class="language-yml">version: &#39;3.8&#39;

services:
  spring_server:
    image: ${DOCKERHUB_USERNAME}/orange
    container_name: orange
    env_file:
      - ../${ENV_NAME}
    ports:
      - &quot;${SERVER_PORT}:${SERVER_PORT}&quot;
    volumes:
      - /var/log/spring-boot:/var/log/spring-boot
    networks:
      - my_custom_network

  promtail:
    image: grafana/promtail
    container_name: promtail
    env_file:
      - ../${ENV_NAME}
    ports:
      - &quot;${PROMTAIL_PORT}:${PROMTAIL_PORT}&quot;
    volumes:
      - ./promtail-config.yml:/promtail-config.yml
      - /var/log/spring-boot:/var/log/spring-boot
    command:
      - -config.file=/promtail-config.yml
      - -config.expand-env=true  # 환경 변수 확장 활성화
    networks:
      - my_custom_network

networks:
  my_custom_network:
    driver: bridge
</code></pre>
<p>먼저 docker compose에서 관리할 2개의 서비스(spring_server, promtail)를 정의한다.</p>
<p>Springboot 서버는 기존에 내가 Docker Image로 관리하고 있었기에 포트 번호와 환경변수를 활용해 빌드한다.</p>
<p>이 때 /var/log/spring-boot 디렉토리에 로그를 저장할 수 있도록 컨테이너와 호스트 간 볼륨을 마운트해주어야 한다. </p>
<pre><code>level=info ts=2024-08-18T07:27:24.764641526Z caller=promtail.go:133 msg=&quot;Reloading configuration file&quot; md5sum=a589992849a397d12d685f680c4e418c
level=info ts=2024-08-18T07:27:24.767594712Z caller=server.go:322 http=[::]:???? grpc=[::]:???? msg=&quot;server listening on addresses&quot;
level=info ts=2024-08-18T07:27:24.767944002Z caller=main.go:174 msg=&quot;Starting Promtail&quot; version=&quot;(version=2.9.10, branch=HEAD, revision=7664eda07b)&quot;
level=warn ts=2024-08-18T07:27:24.768096667Z caller=promtail.go:263 msg=&quot;enable watchConfig&quot;
level=info ts=2024-08-18T07:27:29.767879827Z caller=filetargetmanager.go:361 msg=&quot;Adding target&quot; key=&quot;/var/log/spring-boot/*.log:{job=\&quot;springboot\&quot;}&quot;</code></pre><p>이 옵션이 없으면 <strong>promtail이 켜져 있으나 위처럼 로그를 수집하지 않고 멍때리고 있는 진풍경을 볼 수 있다. (무려 3시간이나 봤다...)</strong></p>
<p>로그를 정상적으로 수집하는 상황이라면 아래 로그를 확인할 수 있을 것이다.</p>
<pre><code class="language-yml">ts=2024-08-18T14:24:51.935022178Z caller=log.go:168 level=info msg=&quot;Seeked /var/log/spring-boot/general.log - &amp;{Offset:0 Whence:0}&quot;
level=info ts=2024-08-18T14:24:51.935064479Z caller=tailer.go:145 component=tailer msg=&quot;tail routine: started&quot; path=/var/log/spring-boot/general.log</code></pre>
<p>다음으로 promtail이 사용할 포트 번호와 환경 변수를 지정하고, expand-env=true 옵션을 통해 환경 변수 확장을 활성화한다. 참고로 <strong>환경변수 파일이 docker-compose 빌드 직전에 반드시 있어야 한다.</strong></p>
<p>참고로 이런 일을 겪을 수 있다.</p>
<pre><code>Unable to parse config: read /promtail-config.yml: is a directory.</code></pre><ul>
<li>마운트를 제대로 못할 때 아예 디렉토리로 간주해버려서 발생하는 에러이다. 권한이 없거나 경로를 제대로 설정하지 못하는 경우가 대부분이니 주의하자. </li>
</ul>
<pre><code>Unable to parse config: read /promtail-config.yml: is a directory. Use -config.expand-env=true flag if you want to expand environment variables in your config file</code></pre><ul>
<li>나처럼 외부 환경변수를 확장할 경우 -config.expand-env=true 옵션이 반드시 존재해야 한다.</li>
</ul>
<p>마지막으로 두 서비스 모두 사용자 정의 네트워크인 my_custom_network에 편입시킨다.</p>
<p>나는 Github Actions를 활용한 CI/CD 파이프라인을 구축해 두었기에 변동 사항을 CD에 아래와 같은 형태로 반영해주었다. --env-file 옵션을 통해 환경변수 파일을 지정할 수 있다.
(현재 docker-compose가 infra 디렉토리 안에 존재하고 env 파일이 루트 디렉토리에 있는 상태이기에 상대경로로 접근할 수 있도록 해주었다.)</p>
<h1 id="cdyml">CD.yml</h1>
<pre><code class="language-yml">      - name: Deploy with Docker Compose
        run: |
          cd infra
          sudo docker-compose --env-file ../${{ secrets.ENV_NAME }} down
          sudo docker-compose --env-file ../${{ secrets.ENV_NAME }} up -d</code></pre>
<p><img src="https://velog.velcdn.com/images/win-luck/post/9bcec8a7-472e-4222-a5b0-e77d4e34b476/image.png" alt=""></p>
<p>정상적으로 잘 동작하여 Loki에서 로그를 수집할 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Springboot] Springboot에서 네이버 텍스트 감정분석 API(Naver CLOVA Sentiment) 사용하기]]></title>
            <link>https://velog.io/@win-luck/Springboot-Springboot%EC%97%90%EC%84%9C-%EB%84%A4%EC%9D%B4%EB%B2%84-%EA%B0%90%EC%A0%95%EB%B6%84%EC%84%9D-APICLOVA-Sentiment-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Springboot-Springboot%EC%97%90%EC%84%9C-%EB%84%A4%EC%9D%B4%EB%B2%84-%EA%B0%90%EC%A0%95%EB%B6%84%EC%84%9D-APICLOVA-Sentiment-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 02 Aug 2024 11:34:25 GMT</pubDate>
            <description><![CDATA[<p>현대자동차 소프티어 부트캠프의 프로젝트 요구사항에 욕설 및 부정적인 문장을 필터링하라는 정책이 있어, 네이버 클라우드 플랫폼에서 제공하는 감정분석 API를 활용하여 간단하게 이를 구현해보았다.</p>
<h1 id="환경변수-설정">환경변수 설정</h1>
<p>먼저 네이버 클라우드 플랫폼의 client-id와 client-secret를 발급받고 이를 application-yml과 같은 환경변수에 등록해둔다.</p>
<pre><code class="language-yml">naver:
  client-id: {발급받은 client-id}
  client-secret: {발급받은 clinet-secret}
  url: https://naveropenapi.apigw.ntruss.com/sentiment-analysis/v1/analyze</code></pre>
<p>환경변수가 많으면, @Value가 아닌 @Configuration을 통해 변수를 등록할 수 있다.</p>
<pre><code class="language-java">@Data
@Configuration
@ConfigurationProperties(prefix = &quot;naver&quot;)
public class NaverApiConfig {
    private String clientId;
    private String clientSecret;
    private String url;
}


@RequiredArgsConstructor
@Service
public class ApiService {

    private final NaverApiConfig naverApiConfig;

    // 비즈니스 로직..
}</code></pre>
<h1 id="api-요청-준비">API 요청 준비</h1>
<p>참고로 API 요청의 응답은 아래와 같은 형태로 나타난다.
<strong>sentiment는 감정의 결과 (positive/negative/neutral)이며, confidence는 구체적인 판정 확률</strong>이다.</p>
<pre><code class="language-json">{
    &quot;document&quot;: {
        &quot;sentiment&quot;: &quot;positive&quot;,
        &quot;confidence&quot;: {
            &quot;negative&quot;: 0.037957642,
            &quot;positive&quot;: 99.957375,
            &quot;neutral&quot;: 0.004668334
        }
    },
    &quot;sentences&quot;: [
        {
            &quot;content&quot;: &quot;현대자동차 만세!&quot;,
            &quot;offset&quot;: 0,
            &quot;length&quot;: 9,
            &quot;sentiment&quot;: &quot;positive&quot;,
            &quot;confidence&quot;: {
                &quot;negative&quot;: 0.0022774586,
                &quot;positive&quot;: 0.9974425,
                &quot;neutral&quot;: 2.8010004E-4
            },
            &quot;highlights&quot;: [
                {
                    &quot;offset&quot;: 0,
                    &quot;length&quot;: 8
                }
            ]
        }
    ]
}</code></pre>
<p>감정분석 API를 활용하는 현재 프로젝트에서는 &quot;부정&quot; 평가의 경우 &quot;부정&quot; 비율이 99.5% 이상이라면 욕설 및 비속어가 포함된 상태로 간주하여 예외를 발생시켜야 한다.</p>
<p>이를 반영하여 ApiService에 코드를 작성하였고, 실행 결과 문제가 없음을 확인하였다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class ApiService {

    private final NaverApiConfig naverApiConfig;

    public boolean analyzeComment(String content) {
        // 요청 Header 및 Body 설정
        HttpHeaders headers = new HttpHeaders();
        headers.set(ConstantUtil.CLIENT_ID, naverApiConfig.getClientId());
        headers.set(ConstantUtil.CLIENT_SECRET, naverApiConfig.getClientSecret());
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity&lt;String&gt; requestEntity = new HttpEntity&lt;&gt;(content, headers);

        // RestTemplate을 이용하여 POST로 요청 후 응답 받기
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity&lt;String&gt; responseEntity = restTemplate.postForEntity(naverApiConfig.getUrl(), requestEntity, String.class);
        String responseBody = responseEntity.getBody();
        boolean isPositive = true;

        // JSON 형식의 API 응답을 분석하여 긍정/부정 여부 확인
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            JsonNode rootNode = objectMapper.readTree(responseBody);

            String sentiment = rootNode.path(&quot;document&quot;).path(&quot;sentiment&quot;).asText();
            if (sentiment.equals(&quot;negative&quot;)) {
                isPositive = false;
                double documentNegativeConfidence = rootNode.path(&quot;document&quot;).path(&quot;confidence&quot;).path(&quot;negative&quot;).asDouble();
                if (documentNegativeConfidence &gt;= ConstantUtil.LIMIT_NEGATIVE_CONFIDENCE) { // 부정이며 확률이 99.5% 이상일 경우 재작성 요청
                    throw new CommentException(ErrorCode.INVALID_COMMENT);
                }
            }
        } catch (JsonProcessingException e) {
            throw new CommentException(ErrorCode.INVALID_JSON);
        }
        return isPositive;
    }
}</code></pre>
<p>크게 <strong>요청 Header 및 Body 설정, POST로 요청 후 응답 수신, 응답 분석</strong>으로 단계를 나눌 수 있다.</p>
<p>Naver Cloud API 명세서에서 요구하는 요청 헤더 및 content를 설정한 뒤 RestTemplate를 활용해 전송하고 JSON 분석하고 예외처리해주었다.</p>
<h2 id="resttemplate-vs-webclient">RestTemplate vs WebClient</h2>
<p>Springboot에서 외부로 API를 요청할 때 대표적으로 이 두 가지를 사용한다.</p>
<h3 id="resttemplate">RestTemplate</h3>
<ul>
<li>Spring 3부터 지원하는 <strong>Blocking, Multi-Thread</strong> 기반 HTTP 통신용 템플릿이다.</li>
<li>사용법이 간단하다.</li>
<li>동기식이기 때문에 동시 사용자가 수천 명 이상 늘어날 경우 매우 느려진다.</li>
</ul>
<h3 id="webclient">WebClient</h3>
<ul>
<li>Spring 5에서 추가된 <strong>Non-Blocking, Single Thread</strong> 기반 인터페이스이다.</li>
<li>Spring webflux를 기반으로 하는 비동기 처리가 가능하다.</li>
<li>많은 양의 요청을 병렬적으로 처리할 수 있고, 현재 Spring에선 WebClient의 사용을 권고하고 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/3b867d63-5b1d-4bd9-9b47-9f6415ecb5a1/image.png" alt="">
실제로 Webflux의 경우 1000명 이상의 동시 사용자가 존재할 때 높은 효율을 보여준다.</p>
<p>다만 현재 요구사항 상 1000명이 동시에 Comment를 작성하는 케이스가 발생할 확률은 매우 낮고, 요청 대상이 안정적인 네이버 클라우드의 API이다.</p>
<p>이를 감안하여 나는 전통적인 동기식인 RestTemplate를 활용하여 신속하게 요구사항을 구현하고자 했다. </p>
<p>물론 feign client과 같은 다른 방법도 존재한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AWS] VPC로 Public/Private Subnet 설정]]></title>
            <link>https://velog.io/@win-luck/AWS-VPC%EB%A1%9C-PublicPrivate-Subnet-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/AWS-VPC%EB%A1%9C-PublicPrivate-Subnet-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 27 Jul 2024 06:16:56 GMT</pubDate>
            <description><![CDATA[<p>프로젝트 환경을 위해 AWS VPC로 Public Subnet에는 Springboot를, Private Subnet에는 DB 서버를 배치할 것이다.</p>
<h1 id="vpc-생성">VPC 생성</h1>
<p>먼저 [VPC] - [VPC 생성]을 눌러 CIDR 블록을 설정하고 VPC를 생성하자.
<img src="https://velog.velcdn.com/images/win-luck/post/8fe18f3a-db56-485d-8630-b7da183cf1ca/image.png" alt=""></p>
<h1 id="subnet-설정">Subnet 설정</h1>
<ul>
<li>Springboot 서버는 외부에서 접근할 수 있도록 Public Subnet으로 설정할 것이다.</li>
<li>DB 서버는 Springboot 서버를 제외하고는 외부에서 접근할 수 없도록 막기 위해 Private Subnet으로 설정할 것이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/win-luck/post/4db86a96-460b-481a-90c6-e2b81ddafdce/image.png" alt=""></p>
<p>[서브넷] - [서브넷 생성]을 눌러 서브넷을 생성하고, VPC ID로 방금 만든 VPC를 선택한다.
이후 서브넷 CIDR 블록을 채워 서브넷을 생성한다.</p>
<p>[그룹]-[범위]-[이름]-[가용영역]의 형태로 작명하면 편리하다.
예) 팀이름-public-app-a</p>
<h1 id="인터넷-게이트웨이">인터넷 게이트웨이</h1>
<p>[인터넷 게이트웨이]로 이동하여 인터넷 게이트웨이를 생성하고, 앞에서 생성한 VPC를 붙인다.
<img src="https://velog.velcdn.com/images/win-luck/post/18e13cab-9c49-472e-b0b4-d2575a28ae27/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/39d97214-54c6-448b-bb25-8c9f63c5afec/image.png" alt=""></p>
<h1 id="라우팅-테이블">라우팅 테이블</h1>
<p>이제 생성한 VPC 인스턴스를 클릭하고, [기본 라우팅 테이블]로 이동하여 수정한다.
라우팅에서 [라우팅 편집]을 누른다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/58584205-c8e9-43fd-8777-c41761e48b49/image.png" alt=""></p>
<p>0.0.0.0/0 외부 IP 대역의 경우 외부로 나가도록 설정해야 한다. 
igw를 눌러 아까 만든 인터넷 게이트웨이를 선택하면, 이제 이 서브넷은 Public Subnet이 된다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/65dca6d0-bb6c-440f-b789-38ba7d685ef3/image.png" alt=""></p>
<p>Private subnet은 외부에서 접근하면 안 되기 때문에, 별도의 라우팅 테이블을 생성하여 아까 만든 VPC를 연결해주고 위 과정 없이 Private Subnet과 명시적으로 연결해준다.</p>
<p>이제 Public Subnet은 Public 라우팅 테이블을 갖고, Private Subnet은 Private 라우팅 테이블을 갖게 된다.</p>
<h1 id="nat-게이트웨이">NAT 게이트웨이</h1>
<p>private subnet에 있는 EC2가 업데이트 등을 위해 인터넷에 접근하려면 NAT 게이트웨이를 생성해주어야 한다.
이 때의 서브넷은 public subnet으로 지정해준다.
<img src="https://velog.velcdn.com/images/win-luck/post/89d61955-6fcf-4182-b4ae-167974b3ad28/image.png" alt=""></p>
<p>이후 private subnet의 라우팅 테이블을 수정해준다.
<img src="https://velog.velcdn.com/images/win-luck/post/2a14e597-4cfd-440d-857d-aad77c1efc46/image.png" alt=""></p>
<h1 id="ec2-생성">EC2 생성</h1>
<p>이제 만들어둔 네트워크 환경에 EC2를 배치하자.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/fdd4209c-ea05-4034-aaa8-3b27dce2b1e6/image.png" alt=""></p>
<ul>
<li>Public Subnet에는 클라이언트의 요청을 처리하기 위한 Springboot Application EC2를 생성하며, 이를 위해 [네트워크 설정] 에서 VPC와 Public Subnet을 선택한다.</li>
<li>Private Subnet의 경우 Redis 및 mySQL을 위한 EC2를 생성하며, 마찬가지로 동일 VPC 및 Private Subnet을 선택하면 된다.</li>
</ul>
<p>public subnet으로 들어간 후 ssh 명령어를 통해 private subnet으로 들어갈 수 있다.</p>
<p>이제 mySQL과 Redis를 설치하고 public subnet에 서 있을 Springboot 서버가 접근할 수 있게 포트를 열어두어야 한다.
<img src="https://velog.velcdn.com/images/win-luck/post/c115c3ff-5bf1-4981-ad53-89fe17715152/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 Lv.2] (C++) 순위 검색]]></title>
            <link>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EC%88%9C%EC%9C%84-%EA%B2%80%EC%83%89</link>
            <guid>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EC%88%9C%EC%9C%84-%EA%B2%80%EC%83%89</guid>
            <pubDate>Tue, 21 May 2024 05:38:05 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/72412">https://school.programmers.co.kr/learn/courses/30/lessons/72412</a>
효율성을 떠올리게 만들었던 문제.</p>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/4e933659-13ed-4cf9-8223-91f5b43859d2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/a793a147-4466-407d-9a2b-6f483fe6a7c2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/20e18ac9-dbb9-48ea-8925-81be39405c10/image.png" alt=""></p>
<p>문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>Info에서 4개의 범주와 점수가 담긴 문자열이 입력되며, 이를 파싱하여 저장해야 한다.</li>
<li>Query에서 찾고 싶은 범주와 점수를 담은 질의가 들어오며, 해당 질의를 만족하는 사람 수를 계산해야 한다.</li>
<li><strong>질의에 포함된 &quot;-&quot;는 와일드카드이며, 이 조건을 고려하지 않는다(= 조건 따지지 말고 무조건 포함한다)는 의미이다.</strong></li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>크게 문자열 파싱 및 정보 저장, 쿼리 계산 파트로 나눌 수 있다.</p>
<h2 id="문자열-파싱">문자열 파싱</h2>
<p>*<em>C++은 split 함수가 없는 대신 stringstream을 통해 공백이 포함된 문자열을 공백 기준으로 파싱할 수 있다.
*</em></p>
<p>간단한 사용법은 아래와 같다.
&quot;java backend junior pizza 150&quot;가 입력된다고 가정하자.</p>
<pre><code class="language-cpp">    for(int i=0; i&lt;info.size(); i++){
        stringstream ss(info[i]);
        string tmp;
        string now = &quot;&quot;;
        int idx = 0;
        while(ss &gt;&gt; tmp){ // 공백 기준으로 잘라 순차적으로 tmp에 담음
            word[idx++] = tmp;
        }

        now = word[0] + word[1] + word[2] + word[3]; // javabackendjuniorpizza
        int num = stoi(word[4]); // 150
        target[now].push_back(num);

        // 추가 코드..

     }</code></pre>
<h2 id="쿼리-계산">쿼리 계산</h2>
<p>*<em>각 query마다 4중 for문으로 완전 탐색 시 시간초과가 발생한다. 
따라서 query 진입 전 이미 모든 계산이 끝나있어야 한다.
*</em>
Info에서 하나씩 정보를 추출할 때마다 와일드카드(&quot;-&quot;)까지 고려하여 한 번에 계산해야 한다.</p>
<p>예를 들어 입력이 &quot;java backend junior pizza 150&quot; 일 경우 &quot;javabackendjuniorpizza&quot; 그룹에 150이 추가되는 것은 당연하고,
와일드카드가 포함된 &quot;-backendjuniorpizza&quot;, &quot;--juniorpizza&quot;, &quot;java---&quot;, &quot;----&quot; 등의 그룹에도 150을 추가해야 한다.</p>
<p>즉 Info[i]에서 추출되는 4개의 카테고리들이 1~4개의 와일드카드로 대체되는 케이스를 모두 찾아야 한다.</p>
<p><strong>백트래킹으로 조합을 구현</strong>하면 누락 없이 기존 범주들 중 일부가 와일드카드로 대체된 모든 그룹에 대해서도 동일한 점수를 추가해줄 수 있다.</p>
<pre><code class="language-cpp">void dfs(int totalcnt, int nowcnt, int idx, int num){
    if(nowcnt == totalcnt){
        string now = word[0] + word[1] + word[2] + word[3];
        target[now].push_back(num);
    }

    for(int i=idx; i&lt;4; i++){
        if(visited[i]) continue;
        visited[i] = true;
        string tmp = word[i];
        word[i] = &quot;-&quot;;
        dfs(totalcnt, nowcnt+1, i, num);
        word[i] = tmp;
        visited[i] = false;
    }
}</code></pre>
<p>이를 통해 <strong>와일드카드가 포함된 질의가 들어왔을 때 추가적인 계산 없이 바로 정답을 추출할 수 있다.</strong></p>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;iostream&gt;
#include &lt;sstream&gt;
#include &lt;map&gt;

using namespace std;
map&lt;string, vector&lt;int&gt;&gt; target;
map&lt;string, int&gt; m;
string word[5];
bool visited[4];

void dfs(int totalcnt, int nowcnt, int idx, int num){
    if(nowcnt == totalcnt){
        string now = word[0] + word[1] + word[2] + word[3];
        target[now].push_back(num);
    }

    for(int i=idx; i&lt;4; i++){
        if(visited[i]) continue;
        visited[i] = true;
        string tmp = word[i];
        word[i] = &quot;-&quot;;
        dfs(totalcnt, nowcnt+1, i, num);
        word[i] = tmp;
        visited[i] = false;
    }
}

vector&lt;int&gt; solution(vector&lt;string&gt; info, vector&lt;string&gt; query) {
    vector&lt;int&gt; answer;

    for(int i=0; i&lt;info.size(); i++){
        stringstream ss(info[i]);
        string tmp;
        string now = &quot;&quot;;
        int idx = 0;
        while(ss &gt;&gt; tmp){
            word[idx++] = tmp;
        }

        now = word[0] + word[1] + word[2] + word[3];
        int num = stoi(word[4]);
        target[now].push_back(num);
        // 와일드카드 -를 반영해야함
        dfs(1, 0, 0, num);
        dfs(2, 0, 0, num);
        dfs(3, 0, 0, num);
        target[&quot;----&quot;].push_back(num);
    }
    for(int i=0; i&lt;query.size(); i++){
        stringstream ss(query[i]);
        string tmp;
        string now = &quot;&quot;;
        int idx = 0;
        string arr[5];
        while(ss &gt;&gt; tmp){
            if(tmp == &quot;and&quot;) continue;
            arr[idx++] = tmp;
        }

        now = arr[0] + arr[1] + arr[2] + arr[3];
        int num = stoi(arr[4]);
        int cnt = 0;
        for(auto it: target[now]){
            if(it &gt;= num) cnt++;
        }
        answer.push_back(cnt);
    }
    return answer;
}
</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>C++에 없는 Split() 함수의 대용으로 사용할 수 있는 stringstream의 사용법을 익혀두어 시간을 절약하자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 Lv.2] (C++) 과제 진행하기]]></title>
            <link>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EA%B3%BC%EC%A0%9C-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EA%B3%BC%EC%A0%9C-%EC%A7%84%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 18 May 2024 11:45:44 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/176962#">https://school.programmers.co.kr/learn/courses/30/lessons/176962#</a></p>
<p>스택에 대한 이해가 필요했던 문제.</p>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/e0074588-eadf-4ed0-aade-b9943df8cd17/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/8770af9a-32b1-4ab8-bca2-a1fbbb18bb03/image.png" alt=""></p>
<p>문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>입력되는 과제 배열은 <strong>시간 오름차순으로 정렬해야 한다.</strong></li>
<li>현재 과제 종료시각과 다음 과제 시작시각이 동일하면 <strong>진행중이던 과제는 끝난 것으로 간주한다.</strong></li>
<li>현재 과제는 다음 과제의 진행 시각이 될 때까지 <strong>완료되지 못하면 중단된다.</strong></li>
<li>중단된 과제들은 스택 형태로 보관되어야 한다. </li>
<li><em>(&quot;멈춰둔 과제가 여러 개일 경우 최근에 멈춘 과제부터 시작한다&quot;)*</em></li>
<li>현재 과제의 종료시각부터 다음 과제의 시작시각까지 <strong>시간이 남았을 때 스택에서 중단된 과제를 꺼내어 다시 수행</strong>할 수 있다.</li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>먼저 <strong>시간 문자열을 편리하게 다루기 위해 시간을 분 단위의 정수로 변환</strong>했으며, 본 로직에 들어가기 전 <strong>시간 오름차순 정렬</strong>을 수행한다.</p>
<pre><code class="language-cpp">int getTime(string time1){ // 시간 문자열을 정수값으로 변환
    int hour1 = stoi(time1.substr(0, 2));
    int minute1 = stoi(time1.substr(3, 2));
    return hour1*60 + minute1;
}

bool cmp(vector&lt;string&gt; v1, vector&lt;string&gt; v2){ // 시간 오름차순 정렬
    return getTime(v1[1]) &lt; getTime(v2[1]);
}</code></pre>
<p>또한 스택에 들어가야 할 정보는 **&lt;중단된 과제의 잔여 시간, 중단된 과제의 이름&gt; **이다.</p>
<p>이제 본 로직을 설계해보자.</p>
<p><strong>(1) 시작시각이 정해진 &quot;다음 과제&quot;가 있을 때 (index &lt; 과제수-1)</strong></p>
<ul>
<li>(a) 현재 과제의 종료시각 &gt; 다음 과제의 시작시각
  -&gt; 다음 과제의 시작시각까지는 진행한 뒤, <strong>잔여시간+이름을 스택에 담고 다음 과제를 수행</strong></li>
</ul>
<ul>
<li>(b) 현재 과제의 종료시각 = 다음 과제의 시작시각
  -&gt; 중단된 과제보다 <strong>다음 과제가 우선이기 때문에 다음 과제를 바로 수행</strong></li>
</ul>
<ul>
<li>(c) 현재 과제의 종료시각 &lt; 다음 과제의 시작시각
  -&gt; 현재 과제의 종료시각에서 <strong>스택에서 하나씩 중단된 과제의 잔여 시간을 더해나가며 다음 과제의 시작시각 이전까지 수행</strong>
  참고로 <strong>if문이 아니라 while문</strong>임에 주의한다. (16분이 비어있고 중단된 과제가 5분/10분이라면 둘 다 수행할 수 있기 때문이다.)**</li>
</ul>
<p><strong>(2) 다음 과제가 더 이상 없을 때</strong>
-&gt; 스택에 담긴 중단된 과제들을 모두 빼내고 로직을 종료한다.</p>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;stack&gt;
#include &lt;algorithm&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

using namespace std;
stack&lt;pair&lt;int, string&gt;&gt; s;

int getTime(string time1){ // 시간 문자열을 정수값으로 변환
    int hour1 = stoi(time1.substr(0, 2));
    int minute1 = stoi(time1.substr(3, 2));
    return hour1*60 + minute1;
}

bool cmp(vector&lt;string&gt; v1, vector&lt;string&gt; v2){ // 시간 오름차순 정렬
    return getTime(v1[1]) &lt; getTime(v2[1]);
}

vector&lt;string&gt; solution(vector&lt;vector&lt;string&gt;&gt; plans) {
    vector&lt;string&gt; answer;
    sort(plans.begin(), plans.end(), cmp);
    int i = 0;
    while(1){
        if(i == plans.size()-1) {
            answer.push_back(plans[i][0]);
            while(!s.empty()){
                answer.push_back(s.top().second);
                s.pop();
            }
            break;
        }

        if(i &lt; plans.size()-1){
            string name = plans[i][0];
            string time = plans[i][1];
            string len = plans[i][2];
            int endTime = getTime(time) + stoi(len);
            int nextTime = getTime(plans[i+1][1]);
            int gap = endTime - nextTime;
            if(gap &gt; 0){ // 중간에 중단해야 하는 과제
                s.push({gap, name}); // (완료까지 남은 시간, 이름)
                i++;
            } else { // 중단하지 않고 완료됨 -&gt; 스택 확인해야함
                answer.push_back(name);
                if(gap == 0) { // 바로 다음 과제가 붙어있는 경우 스택 여부와 상관없이 다음 과제로 이동
                    i++;
                } else { // 중단된 과제가 있을 때
                    while(!s.empty()){
                        pair&lt;int, string&gt; now = s.top();
                        s.pop();
                        if(endTime + now.first &lt;= nextTime){
                            answer.push_back(now.second);
                            endTime += now.first;
                            if(endTime == nextTime){
                                break;
                            }
                        } else {
                            s.push({endTime+now.first-nextTime, now.second});
                            break;
                        }
                    }
                    i++;
                } 
            }
        } 
    }    
    return answer;
}
</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>스택과 같은 변칙적인 자료구조를 활용할 때는 while문을 바탕으로 한 흐름 제어가 편리할 때가 있으니, 항상 for문과 while문 중 유불리에 따라 정하도록 하자.</li>
<li>복잡한 흐름제어가 필요한 문제는 침착하게 예외상황을 판단하고 꼼꼼하게 처리하자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 Lv.2] (C++) 방금그곡]]></title>
            <link>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EB%B0%A9%EA%B8%88%EA%B7%B8%EA%B3%A1</link>
            <guid>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EB%B0%A9%EA%B8%88%EA%B7%B8%EA%B3%A1</guid>
            <pubDate>Wed, 15 May 2024 07:43:22 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/17683#">https://school.programmers.co.kr/learn/courses/30/lessons/17683#</a></p>
<p>카카오는 시간계산을 참 좋아한다.</p>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/b591b517-a8dd-407a-a573-8dfee066d1ae/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/d91e3ba6-bbc6-423d-888e-31e4d6020b3f/image.png" alt=""></p>
<p>문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>&lt;시작시각,종료시각,음악제목,악보&gt;의 문자열이 차례대로 주어진다.</li>
<li>음악은 재생시각이 <strong>악보의 길이를 넘어서면 처음부터 다시 반복</strong>된다.</li>
<li>특정 멜로디 m을 포함하고 있는 음악을 찾아야 하며, 여러 개 존재하면 재생시간이 가장 길고, 재생시간이 같다면 음악 입력 순서가 가장 빠른 <strong>음악의 제목을 반환</strong>해야 한다.</li>
<li><strong>C와 C#은 다른 음이다.</strong></li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>먼저 시작시각과 종료시각을 통해 총 재생시간을 계산한다.</p>
<pre><code class="language-cpp">int getTime(string s1, string s2){ // 시간 계산
    int hour1 = stoi(s1.substr(0, 2));
    int minute1 = stoi(s1.substr(3, 2));
    int hour2 = stoi(s2.substr(0, 2));
    int minute2 = stoi(s2.substr(3, 2));

    int gap = minute2 - minute1;
    if(gap &lt; 0){
        hour2--;
        gap += 60;
    }
    gap += (hour2-hour1) * 60;
    return gap;
}</code></pre>
<p>다음으로, 계산한 재생시간에 따른 음악 문자열을 산출한다.
<strong>이때 음악 문자열의 알파벳이 #이 붙어있다면 이를 반영해주어야 한다. (A != A#)</strong></p>
<pre><code class="language-cpp">string getMusic(int time, string s){ // 재생 시간에 따른 음악 산출
    string str = &quot;&quot;;
    vector&lt;string&gt; v; // #으로 끝나는 경우 #도 포함해야 함
    for(int i=0; i&lt;s.size(); i++){
        if(i &lt; s.size()-1 &amp;&amp; s[i+1] == &#39;#&#39;){
            v.push_back(s.substr(i, 2));
            i++;
        } else {
            string a = &quot;&quot;;
            a += s[i];
            v.push_back(a);
        }
    }

    int len = v.size();
    for(int i=0; i&lt;time; i++){
        str += v[i % len];
    }
    return str;
}</code></pre>
<p>음악 문자열 산출이 끝났다면, 찾으려는 문자열 m이 존재하는지 판정한다.
<strong>이때 m을 찾았을 때 마지막 알파벳 끝에 #가 붙는지 추가로 확인해야 한다.
(ABC != ABC#)</strong></p>
<pre><code class="language-cpp">int nowtime = getTime(started, finished); 
string now = getMusic(nowtime, origin);

if(now.size() &lt; m.size()) continue;
for(int j=0; j&lt;=now.size()-m.size(); j++){
    if(now.substr(j, m.size()) == m){
        if(now[j+m.size()] != &#39;#&#39;){
            candi.push_back(music(i, nowtime, name));
        }
    }
}</code></pre>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;

using namespace std;
struct music { // 정렬 및 정답 반환을 위한 음악 구조체
    int idx, time;
    string name;

    music(int a, int b, string c){
        idx = a;
        time = b;
        name = c;
    }
};

bool cmp(music m1, music m2){ // 총 시간 내림차순 + 등장 오름차순
    if(m1.time == m2.time){
        return m1.idx &lt; m2.idx;
    }
    return m1.time &gt; m2.time;
}

int getTime(string s1, string s2){ // 시간 계산
    int hour1 = stoi(s1.substr(0, 2));
    int minute1 = stoi(s1.substr(3, 2));
    int hour2 = stoi(s2.substr(0, 2));
    int minute2 = stoi(s2.substr(3, 2));

    int gap = minute2 - minute1;
    if(gap &lt; 0){
        hour2--;
        gap += 60;
    }
    gap += (hour2-hour1) * 60;
    return gap;
}

string getMusic(int time, string s){ // 재생 시간에 따른 음악 산출
    string str = &quot;&quot;;
    vector&lt;string&gt; v; // #으로 끝나는 경우 #도 포함해야 함
    for(int i=0; i&lt;s.size(); i++){
        if(i &lt; s.size()-1 &amp;&amp; s[i+1] == &#39;#&#39;){
            v.push_back(s.substr(i, 2));
            i++;
        } else {
            string a = &quot;&quot;;
            a += s[i];
            v.push_back(a);
        }
    }

    int len = v.size();
    for(int i=0; i&lt;time; i++){
        str += v[i % len];
    }
    return str;
}

string solution(string m, vector&lt;string&gt; musicinfos) {
    string answer = &quot;&quot;;
    vector&lt;music&gt; candi;

    for(int i=0; i&lt;musicinfos.size(); i++){
        string started = musicinfos[i].substr(0, 5); // 시작시각
        string finished = musicinfos[i].substr(6, 5); // 종료시각
        string other = musicinfos[i].substr(12);
        string name = &quot;&quot;; // 음악 이름
        string origin = &quot;&quot;; // 음악 문자열
        for(int j=0; j&lt;other.size(); j++){
            if(other[j] == &#39;,&#39;){
                name = other.substr(0, j);
                origin = other.substr(j+1);
                break;
            }
        }
        int nowtime = getTime(started, finished); 
        string now = getMusic(nowtime, origin);

        if(now.size() &lt; m.size()) continue;
        for(int j=0; j&lt;=now.size()-m.size(); j++){
            if(now.substr(j, m.size()) == m){
                if(now[j+m.size()] != &#39;#&#39;){
                    candi.push_back(music(i, nowtime, name));
                }
            }
        }
    }

    if(candi.empty()) return &quot;(None)&quot;; // 없는 경우 None
    sort(candi.begin(), candi.end(), cmp); // 조건에 따라 정렬
    return candi[0].name;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 Lv.2] (C++) 문자열 압축]]></title>
            <link>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%95%95%EC%B6%95</link>
            <guid>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-Lv.2-C-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%95%95%EC%B6%95</guid>
            <pubDate>Wed, 15 May 2024 05:44:48 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/60057">https://school.programmers.co.kr/learn/courses/30/lessons/60057</a></p>
<p>침착하게 규칙을 찾으면 해결되는 문제.</p>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/1ac140e9-c364-4ae6-a94a-403c46958b9f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/366b7433-6d58-41d2-975f-2c340fbcb855/image.png" alt=""></p>
<p>문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>같은 값이 연속해서 나타나는 부분을 (숫자)(값)으로 처리해야 한다.</li>
<li>1개 이상의 단위로 문자열을 잘라서 압축을 진행할 경우 <strong>만들 수 있는 가장 짧은 문자열의 길이를 구해야 한다.</strong></li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<ol>
<li>압축할 문자열의 길이를 <strong>1부터 문자열의 절반 길이까지 점점 늘려가며 이웃한 문자열끼리 동일한지 판정한다.</strong></li>
<li>동일한 경우 해당 길이만큼 빼주고, 동일하지 않으면 해당 문자열이 존재했던 수만큼 앞에 숫자를 붙여줘야 하는데, 직접 문자열에 숫자를 붙이지 말고 숫자의 문자열 상의 길이(10이면 2, 100이면 3)만큼 더해준다.</li>
<li>최솟값을 갱신한다.</li>
</ol>
<p>이때 동일한 경우가 <strong>2번 감지되는 &quot;aaa&quot;의 경우 앞에 붙는 숫자가 &quot;3&quot;임에 주의한다. 이 케이스를 생각해주지 않으면 만약 aaaaaaaaaa의 경우 9번 감지되지만 10a이기 때문에 오답이 발생하게 된다.</strong></p>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;iostream&gt;
#include &lt;vector&gt;

using namespace std;

int solution(string s) {
    int answer = s.size();
    int maxcutsize = s.size()/2;

    for(int a=1; a&lt;=maxcutsize; a++){
        int nowlen = s.size();
        int cnt = 0;
        for(int i=0; i&lt;s.size()-a; i+=a){
            string target = s.substr(i, a);
            string next = s.substr(i+a, a);
            if(target == next){
                cnt++;
                nowlen -= a;
            } else {
                if(cnt &gt; 0) nowlen += to_string(cnt+1).size();
                cnt = 0;
            }
        }
        if(cnt &gt; 0) nowlen += to_string(cnt+1).size();
        cnt = 0;
        answer = min(answer, nowlen);
    }
    return answer;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스 Lv.2] (C++) 후보키  ]]></title>
            <link>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-C-%ED%9B%84%EB%B3%B4%ED%82%A4-2019-KAKAO-BLIND-RECRUITMENT</link>
            <guid>https://velog.io/@win-luck/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-C-%ED%9B%84%EB%B3%B4%ED%82%A4-2019-KAKAO-BLIND-RECRUITMENT</guid>
            <pubDate>Tue, 14 May 2024 07:53:20 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42890">https://school.programmers.co.kr/learn/courses/30/lessons/42890</a></p>
<p>조합을 떠올리지 못해서 굉장히 시간을 많이 잡아먹었던 문제.</p>
<h1 id="문제-설명">문제 설명</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/570750e5-64e7-45e6-9183-a281ed048228/image.png" alt=""><img src="https://velog.velcdn.com/images/win-luck/post/b2aaf5d9-f8dd-4cb0-8200-1565d596a18e/image.png" alt=""><img src="https://velog.velcdn.com/images/win-luck/post/5a141a92-337f-404a-b1ce-23becbf00b3e/image.png" alt=""></p>
<p>문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>릴레이션의 컬럼 중 후보키가 될 수 있는 조합을 찾아야 한다.</li>
<li>여러 열을 합친 것이 후보키가 될 수도 있다.</li>
<li>후보키는 &quot;유일성&quot;과 &quot;최소성&quot;을 만족해야 한다.</li>
<li>유일성은 해당 후보키를 통해 입력된 모든 행이 식별되어야 함을 의미한다.</li>
<li>최소성은 최소한의 열 개수로 모든 행이 식별되어야 함을 의미한다.</li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>크게 2가지 과정으로 나눌 수 있다.
<strong>1. n개의 열 중 중복 없이 1~n개의 열을 선택하는 경우의 수 찾기</strong>
<strong>2. 선택을 마쳤다면 해당 선택 case가 &quot;유일성&quot;과 &quot;최소성&quot;을 만족하는지 파악</strong></p>
<p>1번째 과정은 dfs를 통한 조합으로 간단하게 구현할 수 있다. (이걸 떠올리지 못해서 크게 헤맸다.)</p>
<pre><code class="language-cpp">void dfs(int idx, int cnt, int size){
    if(cnt == size){
        if(unique() &amp;&amp; minimal()) answer++;
        return;
    }

    for(int i=idx; i&lt;m; i++){
        if(visited[i]) continue;
        visited[i] = true;
        v.push_back(i);
        dfs(i, cnt+1, size);
        v.pop_back();
        visited[i] = false;
    }
}

int solution(vector&lt;vector&lt;string&gt;&gt; relation) {
    map = relation;
    n = map.size();
    m = map[0].size();
    for(int i=1; i&lt;=n; i++){
        dfs(0, 0, i);
    }
    return answer;
}
</code></pre>
<p>2번째는 유일성과 최소성을 판정하는 과정이다.</p>
<p>먼저 유일성은 선택된 열들의 필드값을 하나의 String에 모두 이어붙인 뒤, 해당 값이 이전에 존재했는지 Set을 통해 판정했다. 이후 유일성을 갖는다는 것이 확인되면 추후 최소성 판정을 위해 해당 조합을 따로 저장해두었다.</p>
<pre><code class="language-cpp">bool unique(){
    set&lt;string&gt; s;
    for(int i=0; i&lt;map.size(); i++){
        string now = &quot;&quot;;
        for(int j=0; j&lt;v.size(); j++){
            now += map[i][v[j]];
        }
        if(s.find(now) == s.end()) s.insert(now);
        else return false; // 이전에 존재함 -&gt; false
    }
    c.insert(v); // 최소성 판정을 위해 해당 조합을 저장
    return true;
}</code></pre>
<p>다음으로 최소성이다. 최소성을 만족한다는 것은 최소한의 열만 골라서 전체 행의 식별이 가능하단 의미이다. n개 중 1개만 골라도 식별이 가능하다고 해도, 전체 조합에선 해당 열을 포함한 복수 개의 결과를 만들어내는 케이스가 발생한다. </p>
<p>즉 과하게 열을 선택한 경우를 모두 배제해야 한다. <strong>현재 선택된 조합을 구성하는 열의 개수보다 적으면서 유일성을 만족하는 조합이 이전에 존재했는지 판정하면 된다.</strong></p>
<pre><code class="language-cpp">bool minimal(){
    for(auto it: c){
        vector&lt;int&gt; tmp = it;
        int len = 0;
        if(tmp.size() &lt; v.size()){
            for(int j=0; j&lt;tmp.size(); j++){
                if(find(v.begin(), v.end(), tmp[j]) != v.end()){
                    len++;
                }
            }
            // 더 적은 열 수로 유일성을 만족하는 케이스가 이전에 존재함
            if(len == tmp.size()) return false; 
        }
    }
    return true;
}</code></pre>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;
#include &lt;set&gt;
#include &lt;vector&gt;

using namespace std;
int answer = 0;
int n, m;
vector&lt;int&gt; v;
bool visited[21];
vector&lt;vector&lt;string&gt;&gt; map;
set&lt;vector&lt;int&gt;&gt; c;

bool unique(){
    bool selected[21];
    set&lt;string&gt; s;
    for(int i=0; i&lt;map.size(); i++){
        string now = &quot;&quot;;
        for(int j=0; j&lt;v.size(); j++){
            now += map[i][v[j]];
        }
        if(s.find(now) == s.end()) s.insert(now);
        else return false;
    }
    c.insert(v);
    return true;
}

bool minimal(){
    for(auto it: c){
        vector&lt;int&gt; tmp = it;
        int len = 0;
        if(tmp.size() &lt; v.size()){
            for(int j=0; j&lt;tmp.size(); j++){
                if(find(v.begin(), v.end(), tmp[j]) != v.end()){
                    len++;
                }
            }
            if(len == tmp.size()) return false;
        }
    }
    return true;
}

void dfs(int idx, int cnt, int size){
    if(cnt == size){
        if(unique() &amp;&amp; minimal()) answer++;
        return;
    }

    for(int i=idx; i&lt;m; i++){
        if(visited[i]) continue;
        visited[i] = true;
        v.push_back(i);
        dfs(i, cnt+1, size);
        v.pop_back();
        visited[i] = false;
    }
}

int solution(vector&lt;vector&lt;string&gt;&gt; relation) {
    map = relation;
    n = map.size();
    m = map[0].size();
    for(int i=1; i&lt;=n; i++){
        dfs(0, 0, i);
    }
    return answer;
}
</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>문제에서 <strong>백트래킹을 비롯한 순열과 조합 개념을 활용할 수 있는지 항상 체크한 뒤</strong>에 완전탐색이든 DP든 다른 개념을 떠올리자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ] (C++) 21609번: 상어 중학교 <Gold 2>]]></title>
            <link>https://velog.io/@win-luck/BOJ-C-21609%EB%B2%88-%EC%83%81%EC%96%B4-%EC%A4%91%ED%95%99%EA%B5%90-Gold-2</link>
            <guid>https://velog.io/@win-luck/BOJ-C-21609%EB%B2%88-%EC%83%81%EC%96%B4-%EC%A4%91%ED%95%99%EA%B5%90-Gold-2</guid>
            <pubDate>Wed, 21 Feb 2024 14:23:57 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/21609">https://www.acmicpc.net/problem/21609</a></p>
<p>2021년 삼성 코딩테스트 2번 문제였다.
정말 호흡이 길었던 빡센 구현 문제여서 풀었을 때 도파민이 엄청났다..</p>
<h1 id="문제-및-입출력">문제 및 입출력</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/b3d98c43-1849-49ec-875b-8846b77698c2/image.png" alt=""><img src="https://velog.velcdn.com/images/win-luck/post/07f464b7-878c-4626-be18-2c08854a8022/image.png" alt=""></p>
<p>중간 그림은 생략하였다.</p>
<p>정보량이 정말 많아서 세심하게 정리하지 않으면 뻘짓으로 가득해지는 문제다.
문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li>특정 블록의 <strong>상하좌우</strong> 블록을 인접한 블록이라고 한다.</li>
<li>블록 그룹은 적어도 하나의 일반 블록(1~M번)이 존재해야 하고, <strong>적어도 2개 이상</strong>의 블록으로 구성되어야 하며, 그룹 내부 일반 블록의 색은 모두 같아야 한다.</li>
<li><strong>그룹 내에선 어떤 그룹 구성 블록으로 이동할 수 있어야 한다.</strong></li>
<li>-1은 검은 블록, 0은 무지개 블록이다.</li>
<li><strong>비어있는 블록임을 표현할 번호가 필요하다.</strong> (-2로 처리)</li>
<li><strong>무지개 블록은 어떤 그룹 블록이든 포함될 수 있다.</strong></li>
<li>블록 그룹은 기준 블록을 가지며, <strong>기준 블록은 무지개색이 아니다.</strong></li>
<li>우선순위가 가장 높은 블록 그룹을 찾아 제거하고 그룹의 크기^2를 계산한다.</li>
<li><strong>우선순위는 크기 내림차순, 무지개 개수 내림차순, 기준블록 행-열 오름차순이다.</strong></li>
<li>제거 후 <strong>중력으로 인해 블록들이 바닥(마지막 행 방향)으로 떨어진다.</strong></li>
<li><strong>검은 블록은 중력이 적용되지 않으며, 일종의 받침대 역할을 수행</strong>한다.</li>
<li>회전을 통해 <strong>전체 블록이 90도 반시계로 회전한다.</strong></li>
<li>회전 후 다시 중력이 적용된다.</li>
<li>이 사이클이 종료 전까지 반복된다.</li>
<li><strong>종료조건: 블록 그룹이 아예 없거나, 현재 사이클에 크기가 2 이상인 블록 그룹이 존재하지 않는 경우</strong></li>
</ul>
<p>이 모든 정보를 쥐고 해결 과정으로 들어가야 한다.
거의 비문학 수준이다,,</p>
<h1 id="해결-과정">해결 과정</h1>
<p>문제에서 제시한 대로 오토플레이를 구성하는 하나의 Cycle는 다음과 같다.</p>
<ul>
<li>각 그룹 블록별 BFS</li>
<li>BFS의 결과로 생성된 그룹 블록 중 가장 우선순위가 높은 블록 선택</li>
<li>해당 블록을 제거하고 블록의 크기^2 계산</li>
<li>중력 적용</li>
<li>90도 반시계 회전</li>
<li>중력 적용</li>
</ul>
<p><strong>BFS, 우선순위 산출, 블록 그룹 제거, 중력, 회전</strong>
크게 6가지로 과정을 분리해서 하나씩 구현하였다.</p>
<h2 id="bfs">BFS</h2>
<p>우선순위 산출에 활용하기 위해 그룹 크기와 무지개블록 개수를 반환하도록 했다.</p>
<pre><code class="language-cpp">pair&lt;int, int&gt; bfs(int sx, int sy){
    queue&lt;pair&lt;int, int&gt; &gt; q;
    visited[sx][sy] = true;
    q.push(make_pair(sx, sy));
    int cnt = 1;
    int rainbow = 0;
    int color = map[sx][sy];

    while(!q.empty()){
        int x = q.front().first;
        int y = q.front().second;
        q.pop();

        for(int i=0; i&lt;4; i++){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
            if(visited[nx][ny] || map[nx][ny] == -1) continue;
            if(map[nx][ny] == 0 || map[nx][ny] == color){
                if(map[nx][ny] == 0) rainbow++;
                q.push(make_pair(nx, ny));
                visited[nx][ny] = true;
                cnt++;
            }
        }
    }
    return make_pair(cnt, rainbow); // 그룹 크기, 무지개 개수
}</code></pre>
<p>참고로 매 BFS 전 모든 무지개 블록의 방문 여부를 다시 False로 초기화주어야 한다.
<strong>무지개 블록은 모든 그룹이 사용할 수 있는 공공재이기에, 앞 그룹이 사용했다는 이유로 바로 뒷 그룹이 사용하지 못하는 불상사가 발생할 수 있기 때문이다.</strong></p>
<p>이 문제를 파악하는 게 중력 구현과 더불어 가장 오랜 시간이 걸렸다.</p>
<h2 id="우선순위-산출">우선순위 산출</h2>
<pre><code class="language-cpp">bool cmp(pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p1, pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p2){ // 문제 요구에 따른 정렬
    if(p1.first.first == p2.first.first){
        if(p1.first.second == p2.first.second){
            if(p1.second.first == p2.second.first){
                return p1.second.second &gt; p2.second.second;
            }
            return p1.second.first &gt; p2.second.first;
        }
        return p1.first.second &gt; p2.first.second;
    }
    return p1.first.first &gt; p2.first.first;
}</code></pre>
<p>2쌍의 pair로 구성된 벡터를 통해 (그룹크기, 그룹 내 무지개 개수, 기준블록 행번호, 기준블록 열번호)를 바탕으로 정렬을 수행하여 가장 우선순위가 높은 블록 그룹을 선택 및 제거하였다.</p>
<h2 id="블록-그룹-제거">블록 그룹 제거</h2>
<p>이 그룹에 속한 <strong>모든 블록을 다 치우고 빈 블록으로 채워야 하기 때문에 이 역시 BFS가 필요했다. 빈 블록은 -2로 표현하였다. (0이 무지개블록이므로)</strong></p>
<pre><code class="language-cpp">int deleteblock(int sx, int sy){
    memset(visited, false, sizeof(visited));
    queue&lt;pair&lt;int, int&gt; &gt; q;
    visited[sx][sy] = true;
    q.push(make_pair(sx, sy));
    int cnt = 1;
    int color = map[sx][sy];
    copys[sx][sy] = -2; // 빈 블록

    while(!q.empty()){
        int x = q.front().first;
        int y = q.front().second;
        q.pop();

        for(int i=0; i&lt;4; i++){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
            if(visited[nx][ny] || map[nx][ny] == -1) continue;
            if(map[nx][ny] == 0 || map[nx][ny] == color){
                q.push(make_pair(nx, ny));
                visited[nx][ny] = true;
                copys[nx][ny] = -2;
                cnt++;
            }
        }
    }

    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            map[i][j] = copys[i][j]; // 빈 블록 업데이트
        }
    }
    return cnt*cnt;
}</code></pre>
<p>참고로 <strong>복사본 배열을 사용하지 않고 바로바로 좌표를 빈 블록 -2로 수정하면 다음 연산이 이 영향을 받아 잘못된 결과가 나타난다.</strong></p>
<p>이해하기 어려우면 <a href="https://www.acmicpc.net/problem/2573">이 문제를 먼저 풀고 오는 것을 권한다.</a></p>
<h2 id="중력">중력</h2>
<p><strong>무지개 블록 이슈와 더불어 사실상 이 문제의 승부처였다.</strong>
회전도 아닌 중력..이라는 낯선 연산을 -1이라는 받침대(?)와 함께 구현해야 했다.</p>
<p>바닥으로부터 위로 올라오며 받침대인지, 빈 공간인지, 블록인지를 판정한 후,</p>
<ul>
<li>빈 공간(-2) -&gt; 아무 일 없음</li>
<li>받침대(-1) -&gt; 빈 공간이 나오기 전까지 계속 바닥 상승,
  만약 인덱스가 지붕(0)을 뚫으면 해당 열은 중력 연산 종료</li>
<li>블록(0~M) -&gt; 블록과 현재 바닥을 의미하는 빈 블록을 Swap하고 바닥 한칸 상승<pre><code class="language-cpp">void gravity(){ // 중력
  for(int j=0; j&lt;n; j++){
      int nextf = n-1; // 바닥 설정
      for(int i=n-1; i&gt;=0; i--){
          if(map[i][j] == -2) { // 빈공간 -&gt; 아무일도 없음
              continue;
          } else if(map[i][j] == -1){ // 받침대
              nextf = i;
              while(map[nextf][j] != -2) { // 빈공간 나오기 전까지 쭉 올라감
                  nextf--;
                  if(nextf &lt; 0){ // 지붕 뚫을 시 종료
                      i = -1;
                      break;
                  }
              }
              i = nextf; // 받침대 위 어딘가의 새로운 바닥 정의
          } else {
              swap(map[nextf][j], map[i][j]); // 블록이 바닥으로 이동
              nextf--; // 바닥 1칸 상승
          }
      }
  }
}</code></pre>
</li>
</ul>
<p><strong>Swap을 해도 되는가? 라는 고민이 있었지만, 받침대를 감지할 시 바닥을 재정의하는 부분 덕분에 문제 없이 처리가 가능하다.</strong></p>
<h2 id="회전">회전</h2>
<pre><code class="language-cpp">void rotate(){ // 90도 반시계 회전
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            copys[i][j] = map[j][n-1-i];
        }
    }
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            map[i][j] = copys[i][j];
        }
    }
}</code></pre>
<p>복사본 배열을 통해 회전을 구현한 후 원본 배열에 반영하였다.</p>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstring&gt;
#include &lt;vector&gt;
#include &lt;queue&gt;
#include &lt;algorithm&gt;

using namespace std;
int n, m;
int copys[401][401]; // 복사본
int map[401][401]; // -2는 공백, -1은 검정, 0은 무지개
bool visited[401][401];
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, -1, 0, 1};

void input(){ // 입력
    cin &gt;&gt; n &gt;&gt; m;
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            cin &gt;&gt; map[i][j];
        }
    }
}

void makecopy(){ // 복사본 배열 초기화
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            copys[i][j] = map[i][j];
        }
    }
}

bool cmp(pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p1, pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p2){ // 문제 요구에 따른 정렬
    if(p1.first.first == p2.first.first){
        if(p1.first.second == p2.first.second){
            if(p1.second.first == p2.second.first){
                return p1.second.second &gt; p2.second.second;
            }
            return p1.second.first &gt; p2.second.first;
        }
        return p1.first.second &gt; p2.first.second;
    }
    return p1.first.first &gt; p2.first.first;
}

pair&lt;int, int&gt; bfs(int sx, int sy){
    queue&lt;pair&lt;int, int&gt; &gt; q;
    visited[sx][sy] = true;
    q.push(make_pair(sx, sy));
    int cnt = 1;
    int rainbow = 0;
    int color = map[sx][sy];

    while(!q.empty()){
        int x = q.front().first;
        int y = q.front().second;
        q.pop();

        for(int i=0; i&lt;4; i++){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
            if(visited[nx][ny] || map[nx][ny] == -1) continue;
            if(map[nx][ny] == 0 || map[nx][ny] == color){
                if(map[nx][ny] == 0) rainbow++;
                q.push(make_pair(nx, ny));
                visited[nx][ny] = true;
                cnt++;
            }
        }
    }
    return make_pair(cnt, rainbow); // 그룹 크기, 무지개 개수
}

int deleteblock(int sx, int sy){
    memset(visited, false, sizeof(visited));
    queue&lt;pair&lt;int, int&gt; &gt; q;
    visited[sx][sy] = true;
    q.push(make_pair(sx, sy));
    int cnt = 1;
    int color = map[sx][sy];
    copys[sx][sy] = -2; // 빈 블록

    while(!q.empty()){
        int x = q.front().first;
        int y = q.front().second;
        q.pop();

        for(int i=0; i&lt;4; i++){
            int nx = x + dx[i];
            int ny = y + dy[i];
            if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
            if(visited[nx][ny] || map[nx][ny] == -1) continue;
            if(map[nx][ny] == 0 || map[nx][ny] == color){
                q.push(make_pair(nx, ny));
                visited[nx][ny] = true;
                copys[nx][ny] = -2;
                cnt++;
            }
        }
    }

    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            map[i][j] = copys[i][j]; // 빈 블록 업데이트
        }
    }
    return cnt*cnt;
}

void gravity(){ // 중력
    for(int j=0; j&lt;n; j++){
        int nextf = n-1; // 바닥
        for(int i=n-1; i&gt;=0; i--){
            if(map[i][j] == -2) {
                continue;
            } else if(map[i][j] == -1){
                nextf = i;
                while(map[nextf][j] != -2) {
                    nextf--;
                    if(nextf &lt; 0){
                        i = -1;
                        break;
                    }
                }
                i = nextf;
            } else {
                swap(map[nextf][j], map[i][j]);
                nextf--;
            }
        }
    }
}

void rotate(){ // 90도 반시계 회전
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            copys[i][j] = map[j][n-1-i];
        }
    }
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            map[i][j] = copys[i][j];
        }
    }
}

void zeroreset(){ // 무지개 블록은 모든 그룹들이 쓸 수 있어야 하므로 매 탐색마다 초기화해줘야함
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            if(visited[i][j] &amp;&amp; map[i][j] == 0) visited[i][j] = false;
        }
    }
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    input();
    int answer = 0;
    while(1){
        memset(visited, false, sizeof(visited));
        makecopy();
        vector&lt;pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; &gt; v;

        for(int i=0; i&lt;n; i++){
            for(int j=0; j&lt;n; j++){
                if(!visited[i][j] &amp;&amp; map[i][j] &gt;= 1){
                    pair&lt;int, int&gt; blocks = bfs(i, j);
                    v.push_back(make_pair(blocks, make_pair(i, j)));
                    zeroreset();
                }
            }
        }
        if(v.empty()) break; // 비었으면 종료
        sort(v.begin(), v.end(), cmp);
        if(v[0].first.first &lt; 2) break; // 젤 큰 그룹 크기가 2 미만이면 종료
        answer += deleteblock(v[0].second.first, v[0].second.second);
        gravity(); // 중력
        rotate(); // 90도 반시계 회전
        gravity(); // 중력
    }
    cout &lt;&lt; answer;
    return 0;
}</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>코드를 치기 전에 <strong>내가 놓치면 안 되는 정보를 미리 정리해두고 시작</strong>하자. 숨겨진 정보로 인해 시간을 너무 낭비했다.</li>
<li>2차원 배열을 활용한 회전 등의 변칙적인 연산은 여러 문제로 익숙해져야 실전에서 빠르게 처리할 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ] (C++) 21608번: 상어 초등학교 <Gold 5>]]></title>
            <link>https://velog.io/@win-luck/BOJ-C-21608%EB%B2%88-%EC%83%81%EC%96%B4-%EC%B4%88%EB%93%B1%ED%95%99%EA%B5%90-Gold-5</link>
            <guid>https://velog.io/@win-luck/BOJ-C-21608%EB%B2%88-%EC%83%81%EC%96%B4-%EC%B4%88%EB%93%B1%ED%95%99%EA%B5%90-Gold-5</guid>
            <pubDate>Wed, 21 Feb 2024 12:17:14 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/21608">https://www.acmicpc.net/problem/21608</a>
2021년 삼성 코딩테스트 1번 문제였다.</p>
<h1 id="문제-및-입출력">문제 및 입출력</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/0d8e5d20-d153-4e15-affb-4a8d19a86937/image.png" alt=""><img src="https://velog.velcdn.com/images/win-luck/post/add557f2-5572-4467-abfa-700b101f96e0/image.png" alt=""><img src="https://velog.velcdn.com/images/win-luck/post/0c29d9bf-2b15-4655-88f5-5c1e2c652dbc/image.png" alt="">
다행히 N이 작아서 큰 부담은 없다.
문제에서 추출할 수 있는 정보는 다음과 같다.</p>
<ul>
<li><strong>우선순위가 가장 높은</strong> 좌표로 <strong>순서대로</strong> 학생이 배치된다.</li>
<li>우선순위를 위한 정렬: 상하좌우 4개 좌표 기준으로, 
  <strong>1. 좋아하는 학생 칸 수 내림차순</strong>
  <strong>2. 비어있는 칸 수 내림차순</strong>
  <strong>3. 행 번호 오름차순</strong>
  <strong>4. 열 번호 오름차순</strong></li>
<li>자기 자신을 좋아하는 경우는 없으며 학생수 N은 최대 400명이다.</li>
</ul>
<h1 id="해결-과정">해결 과정</h1>
<p>크게 규칙에 따라 <strong>학생을 배치하는 부분</strong>과 <strong>총점을 계산하는 부분</strong>으로 나눌 수 있다.
<strong>첫 번째로 배치되는 학생은 무조건 인덱스 기준 2차원 배열의 (1,1) 지점이다.</strong></p>
<p>좌표를 순회하며 특정 좌표에 학생을 배치하고 모두 배치가 끝났다면 작업을 종료하는 재귀함수를 작성하고, 다음 좌표를 구하기 위한 vector와 정렬 과정을 구현해야 한다.</p>
<p>2차원 배열을 순회하며 <strong>어떤 좌표에 다음 학생이 담겨야 할지</strong> 판정하기 위한 정렬 기준이 되는 4가지 값(좋아하는 친구 수, 빈칸 수, 행 번호, 열 번호)을 2쌍의 pair로 담아서 정렬한다.</p>
<pre><code class="language-cpp">bool cmp(pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p1, pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p2){ // 문제 요구에 따른 정렬
    if(p1.first.first == p2.first.first){
        if(p1.first.second == p2.first.second){
            if(p1.second.first == p2.second.first){
                return p1.second.second &lt; p2.second.second;
            }
            return p1.second.first &lt; p2.second.first;
        }
        return p1.first.second &gt; p2.first.second;
    }
    return p1.first.first &gt; p2.first.first;
}

void sit(int x, int y, int idx){
    pos[x][y] = v[idx];
    if(idx == n*n-1) return; // 종료조건

    vector&lt;pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; &gt; tmp;
    int next = v[idx+1]; // 다음 학생
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            if(!pos[i][j]){ // 비어있는 칸일 때
                int likecnt = 0; // 좋아하는 학생 수
                int blankcnt = 0; // 빈칸 수
                for(int a=0; a&lt;4; a++){
                    int nx = i + dx[a];
                    int ny = j + dy[a];
                    if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
                    if(!pos[nx][ny]){
                        blankcnt++;
                        continue;
                    }
                    for(int b=0; b&lt;4; b++){
                        if(pos[nx][ny] == like[next][b]){
                            likecnt++;
                            break;
                        }
                    }
                }
                tmp.push_back(make_pair(make_pair(likecnt, blankcnt), make_pair(i, j))); // (좋아하는 학생 수, 빈칸 수, 행, 열)
            }
        }
    }
    sort(tmp.begin(), tmp.end(), cmp);
    sit(tmp.front().second.first, tmp.front().second.second, idx+1); // 다음 학생 진행
}</code></pre>
<p><strong>총점 계산</strong> 역시 유사한 로직으로 구현할 수 있다.</p>
<pre><code class="language-cpp">int satisfy(int cnt){ // 학생별 만족도 계산
    if(cnt == 1) return 1;
    else if(cnt == 2) return 10;
    else if(cnt == 3) return 100;
    else if(cnt == 4) return 1000;
    else return 0;
}

void getscore(){ // 총점 계산
    int answer = 0;
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            int cnt = 0;
            for(int a=0; a&lt;4; a++){
                int nx = i + dx[a];
                int ny = j + dy[a];
                int num = pos[i][j]; // 특정 학생
                if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
                for(int b=0; b&lt;4; b++){ // 이 좋아하는 학생인지 판정
                    if(pos[nx][ny] == like[num][b]){
                        cnt++;
                        break;
                    }
                }
            }
            answer += satisfy(cnt);
        }
    }
    cout &lt;&lt; answer;
}</code></pre>
<h1 id="소스-코드">소스 코드</h1>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstring&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
int n;
int like[401][4]; // 학생이 좋아하는 사람
int pos[401][401]; // 학생 번호
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, -1, 0, 1};
vector&lt;int&gt; v;

void input(){ // 입력
    cin &gt;&gt; n;
    for(int i=1; i&lt;=n*n; i++){
        int a;
        cin &gt;&gt; a;
        for(int j=0; j&lt;4; j++){
            cin &gt;&gt; like[a][j];
        }
        v.push_back(a);
    }
    memset(pos, 0, sizeof(pos));
}

int satisfy(int cnt){ // 학생별 만족도 계산
    if(cnt == 1) return 1;
    else if(cnt == 2) return 10;
    else if(cnt == 3) return 100;
    else if(cnt == 4) return 1000;
    else return 0;
}

void getscore(){ // 총점수 계산
    int answer = 0;
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            int cnt = 0;
            for(int a=0; a&lt;4; a++){
                int nx = i + dx[a];
                int ny = j + dy[a];
                int num = pos[i][j]; // 특정 학생
                if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
                for(int b=0; b&lt;4; b++){ // 이 좋아하는 학생인지 판정
                    if(pos[nx][ny] == like[num][b]){
                        cnt++;
                        break;
                    }
                }
            }
            answer += satisfy(cnt);
        }
    }
    cout &lt;&lt; answer;
}

bool cmp(pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p1, pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; p2){ // 문제 요구에 따른 정렬
    if(p1.first.first == p2.first.first){
        if(p1.first.second == p2.first.second){
            if(p1.second.first == p2.second.first){
                return p1.second.second &lt; p2.second.second;
            }
            return p1.second.first &lt; p2.second.first;
        }
        return p1.first.second &gt; p2.first.second;
    }
    return p1.first.first &gt; p2.first.first;
}

void sit(int x, int y, int idx){
    pos[x][y] = v[idx];
    if(idx == n*n-1) return; // 종료조건

    vector&lt;pair&lt;pair&lt;int, int&gt;, pair&lt;int, int&gt; &gt; &gt; tmp;
    int next = v[idx+1]; // 다음 학생
    for(int i=0; i&lt;n; i++){
        for(int j=0; j&lt;n; j++){
            if(!pos[i][j]){ // 비어있는 칸일 때
                int likecnt = 0; // 좋아하는 학생 수
                int blankcnt = 0; // 빈칸 수
                for(int a=0; a&lt;4; a++){
                    int nx = i + dx[a];
                    int ny = j + dy[a];
                    if(nx &lt; 0 || ny &lt; 0 || nx &gt;= n || ny &gt;= n) continue;
                    if(!pos[nx][ny]){
                        blankcnt++;
                        continue;
                    }
                    for(int b=0; b&lt;4; b++){
                        if(pos[nx][ny] == like[next][b]){
                            likecnt++;
                            break;
                        }
                    }
                }
                tmp.push_back(make_pair(make_pair(likecnt, blankcnt), make_pair(i, j))); // (좋아하는 학생 수, 빈칸 수, 행, 열)
            }
        }
    }
    sort(tmp.begin(), tmp.end(), cmp);
    sit(tmp.front().second.first, tmp.front().second.second, idx+1); // 다음 학생 진행
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    input();
    sit(1, 1, 0);  
    getscore();
    return 0;
}
</code></pre>
<h1 id="교훈">교훈</h1>
<ul>
<li>재귀함수는 종료조건을 신중하게 잘 정해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Algorithm] 스위핑(Sweeping) by C++]]></title>
            <link>https://velog.io/@win-luck/Algorithm-%EC%8A%A4%EC%9C%84%ED%95%91Sweeping-by-C</link>
            <guid>https://velog.io/@win-luck/Algorithm-%EC%8A%A4%EC%9C%84%ED%95%91Sweeping-by-C</guid>
            <pubDate>Tue, 20 Feb 2024 12:14:20 GMT</pubDate>
            <description><![CDATA[<p>오늘은 이분탐색에 이어 스위핑을 알아보겠다.</p>
<h1 id="스위핑이란">스위핑이란?</h1>
<p><img src="https://velog.velcdn.com/images/win-luck/post/bea1cac8-aada-4b96-b33e-dbb583024ef4/image.png" alt=""></p>
<p>스위핑 (Sweeping)은 영어로 &quot;쓸다&quot;라는 뜻이며, 보통 한 쪽 방향부터 시작해서 다른 방향으로 진행하며 탐색하는 과정을 구현하는 상황을 의미한다.</p>
<p>자료형이 1차원인 경우 라인 스위핑, 2차원인 경우 평면, 3차원인 경우 공간 스위핑이라고 이야기한다.</p>
<p>일반적으로 정렬 및 그리디 알고리즘과 관련된 테마이며,  조건에 따라 난이도가 천차만별인 주제이기도 하다.</p>
<p>말로만 하면 너무 간단해 보이기에 대표적인 스위핑 문제를 예제 기준으로 다뤄보자.</p>
<h2 id="선분의-길이">선분의 길이</h2>
<p>가장 대표적인 유형이다.
(시작점, 끝점) 형태로 주어지는 Line을 오름차순으로 정렬한 후, 조건에 따라 병합하며 가장 긴 선분의 길이 혹은 총 선분의 합을 계산하는 유형의 문제이다.</p>
<p>이러한 유형은 크게 3가지 조건을 생각하면 쉽게 풀 수 있을 것이다.</p>
<ul>
<li><strong>현재 선분의 시작점이 직전 선분의 끝점보다 크다.</strong>
  (= 두 선분이 겹치지 않는다.)</li>
<li><strong>현재 선분의 시작점이 직전 선분의 끝점보다 작다.</strong>
  (= 두 선분이 일부가 겹친다.)</li>
<li><strong>현재 선분의 끝점이 직전 선분의 끝점보다 작다.</strong>
  (= 현재 선분이 직전 선분에 포함된다.)</li>
</ul>
<h3 id="예제">예제</h3>
<p><a href="https://www.acmicpc.net/problem/2170">2170번: 선 긋기</a>
<img src="https://velog.velcdn.com/images/win-luck/post/e7a6baa8-7caa-4404-95f1-5445633ee424/image.png" alt=""></p>
<p>정답 코드는 아래와 같다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;algorithm&gt;
#include &lt;vector&gt;

using namespace std;
int n;
long long x, y;
vector&lt;pair&lt;long long, long long&gt; &gt; v;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    cin &gt;&gt; n;
    for(int i=0; i&lt;n; i++){
        long long a, b;
        cin &gt;&gt; a &gt;&gt; b;
        v.push_back(make_pair(a, b));
    }
    sort(v.begin(), v.end());

    long long sum = 0;
    long long l = v[0].first;
    long long r = v[0].second;
    if(n == 1) {
        cout &lt;&lt; r-l;
        return 0;
    }
    for(int i=1; i&lt;n; i++){
        if(r &lt; v[i].first) { // 현재 시작점이 직전 끝점보다 큼 (겹치지 않음)
            sum += r-l;
            l = v[i].first;
            r = v[i].second;
        } else { // 현재 시작점이 직전 끝점보다 작음
            if(r &lt; v[i].second) // 현재 끝점이 직전 끝점보다 큼 (선분 연장됨)
                r = v[i].second;
        }
    }
    sum += r-l; // 마지막 Case
    cout &lt;&lt; sum;
    return 0;
}</code></pre>
<p>유사한 문제가 많으니 풀어보면 금방 감을 잡을 수 있을 것이다.</p>
<p>[15922번: 아우으 우아으이야!! ]
(<a href="https://www.acmicpc.net/problem/15922">https://www.acmicpc.net/problem/15922</a>)
<a href="https://www.acmicpc.net/problem/23740">23740번: 버스 노선 개편하기</a>
[1911번: 흙길 보수하기]
(<a href="https://www.acmicpc.net/problem/1911">https://www.acmicpc.net/problem/1911</a>)</p>
<h2 id="선분의-개수">선분의 개수</h2>
<p>길이와 비교했을 때 풀이법이 약간 다르다.</p>
<p>보통 (시작점, 1), (끝점, 0) 형태의 pair 배열을 담아 오름차순으로 정렬하고,
시작점을 감지하면 +1, 끝점을 감지하면 -1로 연산해나가며 최대 개수를 갱신한다.</p>
<p><strong>어차피 시작점은 오름차순으로 등장하기에 끝점이 나오기 전에는 선분들이 점점 포개어지는 것으로 간주할 수 있기 때문이다.</strong></p>
<p><a href="!%5B%5D(https://velog.velcdn.com/images/win-luck/post/82081535-203f-448b-b56b-992e69aee1e5/image.png)">1689번: 겹치는 선분</a>
<img src="https://velog.velcdn.com/images/win-luck/post/3d1b3884-bb2c-4f3c-a7eb-c8782a8f3611/image.png" alt=""></p>
<p>정답 코드는 아래와 같다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

using namespace std;
int n;
vector&lt;pair&lt;int, int&gt; &gt; v;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    cin &gt;&gt; n;
    for(int i=1; i&lt;=n; i++){
        int s, e;
        cin &gt;&gt; s &gt;&gt; e;
        v.push_back(make_pair(s, 1));
        v.push_back(make_pair(e, 0));
    }
    sort(v.begin(), v.end());

    int nowcnt = 0;
    int answer = 1;
    for(int i=0; i&lt;v.size(); i++){
        if(v[i].second){ // 시작점
            nowcnt++;
        } else { // 끝점
            answer = max(answer, nowcnt);
            nowcnt--;
        }
    }
    cout &lt;&lt; answer;
    return 0;
}</code></pre>
<p><a href="https://www.acmicpc.net/problem/19598">19598번: 최소 회의실 개수</a>
[28070번: 유니의 편지 쓰기]
(<a href="https://www.acmicpc.net/problem/28070">https://www.acmicpc.net/problem/28070</a>)</p>
<p>이 외에도 누적 합 등과 연계된 <a href="https://www.acmicpc.net/problem/17611">직각다각형</a> 등 다양한 유형이 있으므로, 1차원 Line Sweeping에 익숙해진 후엔 다양한 스위핑 유형을 직접 다루어보는 것이 큰 도움이 된다.</p>
<p>스위핑(특히 Line Sweeping)의 결론을 요약하자면 아래와 같다.</p>
<ul>
<li>두 선분이 겹치는가?</li>
<li>일부가 겹치는가? 특정 선분이 다른 선분에 아예 포함되는가?</li>
</ul>
<p>겹침과 관련된 정보를 빠르게 파악하면 충분히 풀 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Docker와 Github Actions로 CI/CD 적용하기 (Springboot/EC2)]]></title>
            <link>https://velog.io/@win-luck/Docker-Docker%EC%99%80-Github-Actions%EB%A1%9C-CICD-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-SpringbootEC2</link>
            <guid>https://velog.io/@win-luck/Docker-Docker%EC%99%80-Github-Actions%EB%A1%9C-CICD-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-SpringbootEC2</guid>
            <pubDate>Sat, 17 Feb 2024 02:37:43 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/win-luck/post/3d18ca4b-f5b8-4cca-979c-f53da710f49a/image.png" alt="">
<a href="https://velog.io/@win-luck/Docker-Springboot-postgreSQL-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Docker%EB%A1%9C-AWS-EC2%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0">Docker를 이용한 EC2 프로젝트 배포</a>에 이어 Github Actions를 기반으로 한 CI/CD를 적용해보았다.</p>
<p>뻘짓을 하도 많이 해서 반복하기 싫어 깔끔하게 정리해두려고 한다.</p>
<h1 id="cicd란">CI/CD란?</h1>
<p>지속적 통합, 지속적 배포의 줄임말이며, 협업 프로젝트에서 점점 필수적인 요소로 떠오르고 있는 옵션이다.</p>
<p>지속적 통합이란, 구성원들이 구현한 여러 기능과 관련된 코드를 빌드 및 테스트 과정을 거친 후 문제가 없다면 자동으로 통합하는 과정이다.</p>
<p>지속적 배포란, 이렇게 통합되어 새로 반영된 프로젝트 내 변동사항을 적용한 버전의 프로젝트의 배포를 자동화하는 과정이다.</p>
<p>Github Actions, Jenkins 등의 관련 Tool이 존재하지만, 일단 Github Actions를 기반으로 하여 적용을 시작해보자.</p>
<h1 id="환경">환경</h1>
<ul>
<li>프로젝트 개발 환경: Macbook M1 Pro</li>
<li>서버 배포 환경: AWS EC2 Ubuntu Server 20.04 LTS (HVM) x86</li>
<li>Springboot 환경: Gradle + Java 17 + Springboot 3.2.2</li>
<li>데이터베이스: postgreSQL</li>
</ul>
<h1 id="dockerfile">Dockerfile</h1>
<pre><code class="language-python"># Docker file for building the image

# jdk 17
FROM openjdk:17

# Copy the jar file to the container
ARG JAR_FILE=build/libs/*.jar
# jar file Copy
COPY ${JAR_FILE} app.jar

ENTRYPOINT [&quot;java&quot;,&quot;-Dspring.profiles.active=docker&quot;, &quot;-jar&quot;,&quot;app.jar&quot;]
</code></pre>
<p>이전 게시물과 동일하다.</p>
<h1 id="github-actions">Github Actions</h1>
<p>Github Actions에 들어가 [New workflow]를 누른다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/de91a5e2-74de-42d5-b20a-85dddc33cb2f/image.png" alt=""></p>
<p>내 프로젝트는 Java/Gradle이기에 오른쪽 위 Configure를 클릭한다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/851a2691-56db-462c-9ecf-ecb547e4c656/image.png" alt="">gradle.yml이 작성되기 시작한다. 
*<em>원하는 Action을 등록하면 특정 상황마다 우리가 원하는 동작을 자동화할 수 있다.
*</em></p>
<p>그러나 특정 동작 처리를 위해서는 환경변수나 계정 정보들을 알려주어야 하는데, 이를 yml 코드 상에서 직접적으로 노출시키는 것은 위험한 일이다.</p>
<p>Github에서 다행히 환경변수를 은닉하는 기능을 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/98f38d82-a5c1-4a9c-827e-1017bb2c47f9/image.png" alt="">현재 Github Repository의 Setting에서 위 메뉴를 클릭한다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/6b3b7818-919b-4173-9c01-2c3a6d7d82e4/image.png" alt=""></p>
<p>[New repository secret] 버튼을 눌러 환경변수를 새롭게 추가할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/c33f3dcc-3e88-4d80-a73a-f06a5e5ecfaa/image.png" alt="">내 프로젝트를 예시로 들면, 은닉한 변수는 다음과 같다.</p>
<ul>
<li>DB 정보(url/username/password 등)</li>
<li>jwt token 관련 정보</li>
<li>dockerhub 계정 정보</li>
<li>firebaseAuth 기능 사용을 위한 serviceAccountKey.json 관련 정보</li>
</ul>
<p>특정 파일이나 키 등을 Docker 컨테이너화 시 포함할 수 있는 볼륨/마운트 등의 기능이 존재하는 것으로 확인하였으나, 일단 기초적인 현재 방식으로 처리해보자.</p>
<p>참고로 <strong>secret 변수는 수정 및 삭제만 가능하고 조회는 불가능하다.</strong></p>
<p>Github Actions workflow를 처리하는 yml 파일에서 이러한 secret 변수는 
<strong>${{ secrets.변수명 }}</strong> 형식으로 접근할 수 있다.</p>
<p>참고로 환경변수 FIREBASE_JSON_KEY의 경우 firebase 사용 시 내려받는 serviceAccountKey.json 파일을 base64로 인코딩한 값을 Github Secret에 추가하였고, 실제 코드에선 아래와 같이 디코딩 과정을 거쳐 JSON으로 복원하여 사용하도록 했다.</p>
<h2 id="firebaseconfig">FirebaseConfig</h2>
<pre><code class="language-java">@Configuration
public class FirebaseConfig {

    @Bean
    public FirebaseAuth firebaseAuth() throws IOException {
        // Base64 인코딩된 JSON 키 파일 읽기
        String base64EncodedKey = System.getenv(&quot;FIREBASE_JSON_KEY&quot;);

        // Base64 디코딩
        byte[] decodedKey = Base64.getDecoder().decode(base64EncodedKey);

        // 바이트 배열에서 InputStream 생성
        ByteArrayInputStream serviceAccountStream = new ByteArrayInputStream(decodedKey);

        FirebaseOptions options = FirebaseOptions.builder()
                .setCredentials(GoogleCredentials.fromStream(serviceAccountStream))
                .build();
        FirebaseApp.initializeApp(options);
        return FirebaseAuth.getInstance();
    }
}</code></pre>
<h1 id="ci">CI</h1>
<pre><code class="language-yml">name: CI/CD # yml 파일 이름

on: # develop 브랜치 push/pr 시 가동
  push:
    branches: [ &quot;develop&quot; ]
  pull_request:
    branches: [ &quot;develop&quot; ]

permissions: # 이 workflow에 Repository 읽기 권한 부여
  contents: read

jobs:
  CI: # CI 과정이므로 CI라고 명명하며, ubuntu 최신환경에서 실행
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3 # 체크아웃

    # jdk 17 설치
    - name: Setup JDK 17 
      uses: actions/setup-java@v3
      with:
        java-version: &#39;17&#39;
        distribution: &#39;temurin&#39;

    # Gradle 설치
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0

    # Gradle로 프로젝트 빌드 (-x test 유닛테스트 배제)
    - name: Build with Gradle
      run: ./gradlew build -x test

    # 은닉한 환경변수로 Dockerhub 로그인
    - name: Docker Login
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_PASSWORD }}

    # Docker 이미지 빌드
    - name: Docker Image Build
      run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/alert .

    # Dockerhub에 통합된 내용을 Push
    - name: DockerHub Push
      run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/alert</code></pre>
<p>실제 develop 브랜치에 해당 내용을 푸시 및 PR 요청하면 workflow가 가동되어 빌드 및 테스트 및 자동화 과정을 진행하고,
문제 발생 시 fail 처리되며 해당 workflow는 롤백된다.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/68c305db-b974-4858-98cd-faa5e0b229db/image.png" alt=""></p>
<p>workflow yml 파일을 다루는데 익숙하지 않으면 위와 같은 온갖 뻘짓이 실시간으로 드러나니 꼭 관련 형식을 숙지하고 시작하도록 하자..</p>
<h1 id="cd">CD</h1>
<pre><code class="language-yml">CD:
    runs-on: self-hosted # self-hosted 방식
    needs: CI # CI가 성공한 후 진행할 수 있음
    steps:

    - name: Docker Container Remove # 현재 돌고 있는 도커 컨테이너 삭제
      run: sudo docker rm -f alert 2&gt;/dev/null || true

    - name: Docker Old Image Remove # 기존 도커 이미지 삭제
      run: sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/alert

    - name: Docker Run New
      run: sudo docker run -d --name alert
        -e DB_URL=${{secrets.DB_URL}}
        -e DB_USERNAME=${{secrets.DB_USERNAME}}
        -e DB_PASSWORD=${{secrets.DB_PASSWORD}}
        -e JWT_SECRET=${{secrets.JWT_SECRET}}
        -e FIREBASE_JSON_KEY=${{secrets.FIREBASE_ACCOUNT_KEY}}
        -p 8080:8080 ${{secrets.DOCKERHUB_USERNAME}}/alert</code></pre>
<p>명령어는 runs-on에 self-hosted를 제외하면 이전 게시물과 큰 차이는 없다.</p>
<p>secret에 등록해둔 주요 환경변수를 Docker run 명령어 시 포함하도록 하여 해당 환경변수에 프로젝트에 적절하게 삽입되도록 구성하였다.</p>
<h2 id="self-hosted">self-hosted</h2>
<p>self-hosted runner란, 유저가 직접 hosting한 서버(EC2)에서 Github Action Application을 띄우는 과정을 의미한다. <a href="https://bumday.tistory.com/90">자세한 설명은 이 게시물을 참고하면 좋다.</a></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/1cce0855-cc1e-4aba-ad80-35a8aab955fe/image.png" alt="">Actions에 [Management] - [Runners] - [Selt-hosted runner] 에서 New runner 버튼을 누르자.</p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/3058e646-6ded-43a9-a9f6-441a88107d51/image.png" alt=""></p>
<p>Download 아래에 있는 잡다한 명령어를 배포한 서버 상에서 실행하고, yml 파일에서 CD 부분의 runs-on을 self-hosted라고 설정하자. 내가 배포한 EC2 서버는 Linux x64이므로 해당 환경에 맞춰서 명령어를 실행해야 한다.</p>
<p>명령어를 모두 실행했다면 해당 배포 환경에서 
ls
cd actions/runner
./run.sh</p>
<p>명령어를 실행하여 Github Action의 job 요청을 배포한 서버에서 처리할 수 있도록 준비하자.
배포한 서버는 yml 파일에서 알 수 있듯이 크게 3가지 작업을 처리해주어야 한다.</p>
<p><strong>1. 현재 실행중인 Springboot Docker Container 삭제 (docker rm)
2. 기존 Docker 이미지 삭제 (docker rmi)
3. 최신화된 Docker 이미지 다운로드 및 실행 (docker run)</strong></p>
<p><img src="https://velog.velcdn.com/images/win-luck/post/35195b02-c733-4fea-ae44-3a3ae9f5df8d/image.png" alt=""></p>
<p>docker logs 명령어를 통해 해당 Container의 상황을 확인한 결과 새로운 내용이 잘 반영되어 배포까지 모두 완료된 것을 확인할 수 있다.</p>
<p>이렇게 간단한 Docker 기반 CI/CD를 알아보았다.</p>
<h1 id="ci와-cd-분리하기">CI와 CD 분리하기</h1>
<p>때때로 Pull request가 Merge되었을 때만 배포되는 등 제약을 걸어야 하는 순간이 있다.</p>
<p>그런 경우엔 CI와 CD 파일을 별도로 분리하는 것이 유용할 수 있다.</p>
<pre><code class="language-yml">name: CI

on:
  push:
    branches: [ &quot;dev&quot; ]
  pull_request:
    branches: [ &quot;dev&quot; ]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#39;17&#39;
          distribution: &#39;temurin&#39;

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@ec92e829475ac0c2315ea8f9eced72db85bb337a # v3.0.0

      - name: Build with Gradle
        run: ./gradlew build -x test # 테스트코드 검사 없이 빌드

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Build Docker Image
        run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명 .

      - name: Push Docker Image
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명</code></pre>
<pre><code class="language-yml">name: CD

on:
  push:
    branches: [ &quot;dev&quot; ]
  pull_request:
    branches: [ &quot;dev&quot; ]
    types: [closed]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  deploy:
    if: github.event.pull_request.merged == true # dev 브랜치로 향하는 PR이 Merge되었을 때만 실행
    runs-on: self-hosted

    steps:
      - uses: actions/checkout@v4

      - name: Docker Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      - name: Pull Docker Image
        run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명

      - name: Remove Old Docker Container
        run: sudo docker rm -f 프로젝트명 || true

      - name: Run Updated Docker Container
        run: sudo docker run -t --env-file ~/.env -d --name 프로젝트명 -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/프로젝트명
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] Springboot + postgreSQL 프로젝트 Docker로 AWS EC2에 배포하기]]></title>
            <link>https://velog.io/@win-luck/Docker-Springboot-postgreSQL-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Docker%EB%A1%9C-AWS-EC2%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@win-luck/Docker-Springboot-postgreSQL-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Docker%EB%A1%9C-AWS-EC2%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 12 Feb 2024 08:51:36 GMT</pubDate>
            <description><![CDATA[<p>처음으로 Docker를 활용해 토이 프로젝트를 원격 서버에 배포해보았고, 쉽지 않았기에 따로 정리해두려고 한다.</p>
<p>Docker-Compose 방식도 있었는데, 일단 초입부인 만큼 기본적인 것만 다루어보자.</p>
<p>2월 동안 Docker 환경에 대한 기본적인 적응 및 CI/CD를 적용해보는 것이 내 목표이다.</p>
<h2 id="docker란">Docker란?</h2>
<ul>
<li>소프트웨어 개발과 배포를 간소화하고 자동화하는 데 사용되는 오픈 소스 플랫폼</li>
<li>애플리케이션 및 관련 의존성을 &lt;컨테이너&gt;라고 하는 격리된 환경에 패키징하여 신속하게 애플리케이션을 배포 및 확장할 수 있다.</li>
<li>컨테이너는 가벼우며, 어느 환경에서나 동일하게 실행될 수 있도록 설계되었기에 서버가 돌아가는 환경에 크게 영향을 받지 않는다.</li>
</ul>
<h2 id="환경">환경</h2>
<ul>
<li>프로젝트 개발 환경: Macbook M1 Pro</li>
<li>서버 배포 환경: AWS EC2 Ubuntu Server 20.04 LTS (HVM) x86</li>
<li>Springboot 환경: Gradle + Java 17 + Springboot 3.2.2</li>
<li>데이터베이스: postgreSQL</li>
<li><strong>편의상 M1을 &lt;내 컴퓨터&gt;로, EC2를 &lt;원격 컴퓨터&gt;로 표현하겠다.</strong></li>
</ul>
<h2 id="dockerhub-로그인">Dockerhub 로그인</h2>
<p>Dockerhub 아이디가 없으면 새로 만들어야 한다.
<img src="https://velog.velcdn.com/images/win-luck/post/ede116c9-7390-4484-83b4-e2b04b8bef3a/image.png" alt=""></p>
<p>위와 같이 Dockerhub 개인 화면에 접속했다면,** 닉네임 및 비밀번호를 기억**해두어야 한다.</p>
<p>이제 배포하려는 <strong>프로젝트의 루트 파일에 Dockerfile를 작성</strong>해야 한다.</p>
<h2 id="dockerfile-작성-및-빌드">Dockerfile 작성 및 빌드</h2>
<pre><code class="language-python">
# 기본 이미지는 jdk 17
FROM openjdk:17 

#빌드 시점에 사용될 변수를 정의 
#JAR_FILE은 빌드 컨텍스트 내의 jar 파일 경로를 지정하는 데 사용
ARG JAR_FILE=build/libs/*.jar 

# ARG에서 정의한 JAR_FILE 경로의 jar 파일을 컨테이너 내 app.jar로 복사
COPY ${JAR_FILE} app.jar

# 컨테이너가 시작될 때 실행될 명령어 정의
ENTRYPOINT [&quot;java&quot;,&quot;-Dspring.profiles.active=docker&quot;, &quot;-jar&quot;,&quot;app.jar&quot;]
</code></pre>
<p>Dockerfile을 다 작성했다면, Springboot 프로젝트의 Terminal을 켜고 아래와 같은 명령어를 입력한다.</p>
<blockquote>
<p>** ./gradlew clean build -x test     **
Springboot 프로젝트를 Gradle 기반으로 빌드한다. 단 테스트 코드는 배제한다.</p>
</blockquote>
<blockquote>
<p>** docker login **
dockerhub에 username 및 password로 로그인한다.</p>
</blockquote>
<blockquote>
<p><strong>docker build --platform linux/amd64 -t [사용자ID]/[파일명] .</strong>
docker 파일을 특정 파일명으로 빌드하되, 플랫폼은 linux/amd64이다.
참고로 M1에서 EC2 Ubuntu x86로 Docker 프로젝트를 배포할 경우 반드시 플랫폼 관련 명령어가 존재해야 한다. <a href="https://velog.io/@msung99/Docker-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%ED%94%8C%EB%9E%AB%ED%8F%BC-%ED%98%B8%ED%99%98%EC%84%B1-%EA%B4%80%EB%A0%A8-%EC%97%90%EB%9F%AC-linuxamd64">그렇지 않으면 에러가 발생한다.</a></p>
</blockquote>
<blockquote>
<p><strong>docker push [사용자ID]/[파일명]</strong>
빌드한 Dockerfile을 Dockerhub에 Push한다.</p>
</blockquote>
<h2 id="applicationproperties">application.properties</h2>
<pre><code class="language-yaml">spring.datasource.url=jdbc:postgresql://[Docker host IP주소]:5432/[DB명]
spring.datasource.username=postgres
spring.datasource.password=[비밀번호]
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA
spring.jpa.hibernate.ddl-auto=create # 기존에 존재하던 데이터가 있다면 create X
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.database=postgresql</code></pre>
<p>우리는 EC2 Docker에 진입하여 Docker를 활성화한 후 PostgreSQL에게 할당되는 IP Address를 알아야 한다.
기본적인 상황이라 username이 postgres지만, 필요에 따라 다양한 권한을 가진 DB 유저를 구축해야 할 수도 있다.</p>
<h2 id="원격-컴퓨터에서-docker-빌드">원격 컴퓨터에서 Docker 빌드</h2>
<p>Dockerfile이 성공적으로 빌드 및 Push되었다면 EC2 Ubuntu로 이동한다.</p>
<blockquote>
<p><strong>ssh -i [키 페어 파일명] ubuntu@[할당받은 탄력적 IP]</strong>
EC2 인스턴스 생성 시 내려받은 키페어 파일명 및 탄력적 IP를 활용해 
원격 컴퓨터에 접속한다. 인바운드 설정 등은 생략한다. (8080 포트)</p>
</blockquote>
<blockquote>
<p><strong>sudo apt update</strong>
<strong>sudo apt install docker.io</strong>
원격 컴퓨터 Ubuntu를 업데이트하고 docker를 설치한다.</p>
</blockquote>
<blockquote>
<p><strong>docker run --name [postgresql 컨테이너명] <br>-p 5432:5432 <br>-e TZ=Asia/Seoul <br>-e POSTGRES_PASSWORD=[비밀번호] <br>-d postgres</strong>
postgreSQL 이미지를 설정대로 다운로드받은 후 컨테이너화한다.
비밀번호는 application.properties(yaml)에 존재하는 spring.datasource.password과 동일해야 한다.</p>
</blockquote>
<p>postgreSQL 컨테이너화가 성공했다면 추가적인 정보를 얻어야 한다.
나는 ddl-auto create이기 때문에 postgreSQL 컨테이너에 진입하여 데이터베이스를 새로 생성해주었다.</p>
<blockquote>
<p><strong>docker exec -it [컨테이너 ID] bash</strong>
<strong>psql -U postgres</strong>
exec 명령어로 외부에서 컨테이너로 진입하고, 위에서 설정한 비밀번호를 입력한 뒤 create database [데이터베이스명]으로 DB를 새로 생성한다.</p>
</blockquote>
<p>postgreSQL 컨테이너에게 할당된 Docker Host IP주소를 확보하자.</p>
<blockquote>
<p><strong>docker inspect [postgresql 컨테이너명] | grep IPAddress</strong>
inspect 명령어를 통해 컨테이너의 상세 정보 중 컨테이너에게 도커가 할당해준 Host IP주소를 확인하고, 해당 값을 application.properties(yaml)의 spring.datasource.url에 채워넣는다.</p>
</blockquote>
<blockquote>
<p>** docker run -d --name [Springboot 컨테이너명] -p 8080:8080 [사용자ID]/[파일명] **
Docker Repository에서 Docker Image를 pull하여 내려받고 이를 통해 Springboot 프로젝트를 실행(컨테이너화)한다.
-d를 통해 백그라운드에서 실행하게 하여 터미널이 다른 작업을 수행할 수 있게 하고,
--name을 통해 자체적인 별명을 부여한다.
-p를 통해 포트를 부여한다. (8080)</p>
</blockquote>
<p><strong>참고로 최초 run을 실행했다면 그 다음부터는 run이 아니라 &lt;docker start [컨테이너명]&gt; 으로 컨테이너를 실행해야 한다. <a href="https://lucas-owner.tistory.com/48">그렇지 않으면 Docker 내부에 같은 내용을 담은 컨테이너가 창궐(?)하게 된다.</a></strong></p>
<p>다음은 관련 작업 시 필요한 기본적인 Docker 명령어이다.
<a href="https://brunch.co.kr/@hopeless/10">여기</a>를 참고하여 작성하였다.</p>
<blockquote>
<p><strong>docker attach [컨테이너명]</strong>
컨테이너 실행 시 사용한다. (직접 컨테이너 내부를 실시간으로 들여다본다.)
Ctrl + P + Q로 쉘을 빠져나올 수 있다.</p>
</blockquote>
<blockquote>
<p>** docker start(stop) [컨테이너명]**
컨테이너를 실행하거나, 실행되던 컨테이너를 중단시킨다.</p>
</blockquote>
<blockquote>
<p><strong>docker logs [컨테이너명]</strong>
해당 컨테이너가 생성하는 로그를 조회할 수 있다.</p>
</blockquote>
<blockquote>
<p><strong>docker ps (-a)</strong>
현재 실행중인 컨테이너의 목록을 조회할 수 있다. 
-a를 통해 모든 컨테이너를 조회할 수도 있다.</p>
</blockquote>
<blockquote>
<p><strong>docker rmi [이미지id]</strong>
pull이나 run으로 내려받은 docker 이미지를 삭제한다.</p>
</blockquote>
<blockquote>
<p>*<em>docker rm -f [컨테이너id] *</em>
해당 docker 컨테이너를 (강제로) 삭제한다.</p>
</blockquote>
<p>참고로, <strong>Got permission denied while trying to connect to the Docker daemon~</strong>으로 시작하는 에러는 <a href="https://newbedev.com/shell-got-permission-denied-while-trying-to-connect-to-the-docker-daemon-socket-at-unix-var-run-docker-sock-get-http-2fvar-2frun-2fdocker-sock-v1-24-images-json-all-1-dial-unix-var-run-docker-sock-connect-permission-denied-code-example">아래와 같이 해결했다.</a></p>
<blockquote>
<p>sudo chmod 666 /var/run/docker.sock
sudo usermod -aG docker ${USER}</p>
</blockquote>
<p>다음에는 CI/CD도 도전해볼 계획이다.
여담으로, 에러가 정말 많이 나서(특히 postgreSQL 연동 관련) 굉장히 당혹스러웠다.</p>
]]></description>
        </item>
    </channel>
</rss>