<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chae_ag.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 09 Mar 2025 14:21:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>chae_ag.log</title>
            <url>https://velog.velcdn.com/images/chae_ag/profile/6b82a2f3-aa94-4ae7-aea2-07441d7f38c2/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. chae_ag.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chae_ag" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[인턴 2주차 다닌 후기 겸 일기]]></title>
            <link>https://velog.io/@chae_ag/internship-retrospective-1</link>
            <guid>https://velog.io/@chae_ag/internship-retrospective-1</guid>
            <pubDate>Sun, 09 Mar 2025 14:21:57 GMT</pubDate>
            <description><![CDATA[<p>벌써 인턴에 붙어서 출근한 지 2주가 지났다.
합격 전화를 받은게 엊그제 같고 그냥 눈 감았다 뜬 것 같은데 벌써 2주가 지났다니?… 
배울 것이 넘쳐나는데 하루하루가 너무 아깝고 그래서 아직까지는 출근이 재밌다.</p>
<p>나는 현재 3개월 체험형 인턴으로, 백엔드 엔지니어 직무를 수행하고 있다.</p>
<h2 id="🌱-인턴-합격부터-첫-출근까지">🌱 인턴 합격부터 첫 출근까지</h2>
<p>인턴에 지원할 때부터의 기억을 더듬어 본다.</p>
<p>처음 이 인턴 공고를 발견했을 당시에 대부분의 들어봤다 싶은 기업의 공고에는 모두 지원을 하고 있었던 상태였기도 하고 여기는 평소 애용하는 서비스인데다가 여러 경험으로 이 기업 개발자에게도 좋은 인상을 가지고 있었기에 당연히 지원했다.
그치만 기대는 전혀 안했다.
소위 말하는 네카라쿠배당토 계열의 기업이니까. 대단한 사람들이 많이 지원할 테니까..</p>
<p>그런데 서류가 붙었다. 웬걸 실수로 합격시킨거 아니야? 라고 생각했지만 그래도 좋았다.
그런데 면접 일자가 다가오면 다가올 수록 점점 무서워졌다.
진짜 실수로 합격 시킨 것 아닐까? 면접에서 너무 한심하게 쉬운 질문에 대답을 못하면 어떡하지? 라이브코테에서 너무 쉬운 문제인데 못 풀면 어떡하지?
설날도 껴있어서 면접 준비를 할 시간이 많았지만 오히려 불안감에 제대로 준비를 하지 못했다.
그래서 면접 전날까지 고민하다가 그냥 면접을 취소할까 계획도 짰다. 그러다 개발자 커뮤니티 같은 곳에 진심을 담아 면접을 취소할지 고민이라고 털어놓았는데 모든 사람들이 무조건 갔다 오라고 했다. 경험은 돈 주고도 못 산다고.
이 말이 정말 큰 원동력이 됐다. 경험은 돈 주고 못 사고 나는 그 경험을 할 수 있는 기회를 얻은 사람인데 왜 이 기회를 발로 차려고 하지?
그때부터는 마음을 다잡았다. 모든 걸 보여주고 오자고.</p>
<p>결국 다음날 면접을 보러갔다.
긴장돼서 오후 면접인데도 아침 10시부터 회사 근처 카페에 있었다.
카페에 있는 동안 정말 많은 생각이 들었는데 뭔가 부정적이고 자신감이 떨어지는 생각이 들 때마다 난 그냥 경험을 하러 온거고 면접 떨어지면 다신 볼 일도 없는 사람들이다 그냥 나를 보여주고 오자. 라고 계속 되뇌었다. (실제 효과 좋음)</p>
<p>그렇게 불안했고 걱정됐던 면접이 사실 너무 좋았다.
면접관은 인사팀이 아니라 내가 1순위로 적은 팀의 백엔드 개발자 두 분이었고,
딱딱한 면접 분위기가 아니라 마치 처음 보는 개발자들과 대화를 나누는 느낌이었다.
뭔가 중간중간 두 분이 티키타카도 하면서 면접을 진행했던게 이 회사의 분위기를 말해주는 듯했다.
역경 없이(?) 순탄하게 면접을 마치고 마지막에 질문이 있다면 편하게 물어보라고 하셔서 물어봤다. 두 분께서 생각하는 오래 일할 수 있는 개발자의 자세나 장점이 뭐라고 생각하냐고. 왜냐하면 평소에 많이 고민했던 것이기 때문에 언제 이런 유능한 개발자들에게 물어볼 수 있겠나 싶어서 물어봤다.
한 분은 개발을 하는 과정에 하나라도 재미 요소가 있는 사람. 뭐 문제 해결하는 과정이 재밌다던지 이런게 하나라도 있어야 오래 할 수 있는 것 같다고.
한 분은 개발자도 결국 누군가와 함께 무언가를 만들어내는 사람이기 때문에 소통을 힘들어하지 않고 소통을 하려고 하는 사람이라고 했다.
이것도 정말 물어보기 잘했다고 생각한다. 지금까지도 계속 되뇌이고 있으니.
주저리주저리 말이 길어졌지만 암튼 정말 좋게 면접을 마쳤다.
(근데 면접이 정말 좋았지만 붙을 거라는 기대를 정말 .. 안했다. 못한 대답도 많고 내 실력이 턱없이 부족하다고 생각했기 때문에. 그래서 실제 붙으면 입사할 첫째 주에 여행 일정까지 잡아버렸다..)</p>
<p>근데 진짜 웬걸이다. 합격했다. 당장 다음주부터 출근해야 했다. 진짜 얼떨떨한 상태로 일주일을 지냈고 그렇게 나는 인턴으로 출근하게 됐다.</p>
<h2 id="첫-실무-그리고-고민들">첫 실무, 그리고 고민들</h2>
<p>이 글을 쓰는 지금은 이렇게 입사를 해서 출근한지 딱 2주가 지났다.
첫 직장이고 첫 실무다. 모든 것이 신기하고, 모든 것이 새롭고, 모든 것이 낯설다.
사실 아직도 적응 중이다. 분위기, 커뮤니케이션, 업무툴 등 새로운게 너무 많다!
일단 인턴에게 주어지는 아주 가벼운 업무 프로세스가 있는데 그걸 수행하고 있다.
정말 쉬운 작업인 것 같은데 이것저것 모르는게 너무 많고 해서 속도가 안나니까 조금 답답하기도 하다.
(백엔드 엔지니어지만 우리 팀에는 프론트 엔지니어가 따로 없어서 프론트 작업을 하고 있다는 것도 이유라면 이유겠지!)</p>
<p>사실 1주차 출근을 하고 맞이한 주말에는 정<del>~</del>말 생각이 많았다 ㅋㅋㅋ 고민도 많고 너무 내가 팀원들에 비해 부족한 것 같았기 때문이다. 질문을 편하게 하라시는데 너무 유능하신 분들에게 나의 하찮은 질문을 하기란 쉽지 않더라. 또, 우리 팀에는 나 빼고 같이 입사한 인턴 두 분이 더 계시다. 두 분 모두 너무 친절하고 좋으시다. 그런데 두 분 모두 여기 들어오기 전에 인턴 경험이나 회사 경험이 있으시다고 하고 일주일 동안 너무 잘 적응하시는 것 같았다. 나만 부족한 것 같았다.
(그런데 사실 팀원 두 분도 주말에 나와 비슷한 고민을 하셨다고 했다 ㅋㅋㅋ)
동기 두 분과 저녁 자리를 가지면서 많은 이야기를 했다. 모두 같은 상황이고 낯선 환경에서 동지(?)애 덕에 공감이 많이 가는 시간이었다.
이때 나는 이런 낯선 환경에 심지어 인턴으로 들어왔는데 부족한 건 당연하잖아! 라는 걸 깨닫게 되는 귀중한 시간이었달까.</p>
<h2 id="2주-차를-마치며-앞으로의-방향">2주 차를 마치며, 앞으로의 방향</h2>
<p>2주차 출근을 마친 지금은 1주차 주말과 다른 고민들과 생각들이 꽃피워 있다.
솔직히 여기서 3개월을 그냥 생각 없이 일하면 내 능력이 폭발적으로 늘지는 않을 것 같다는 생각을 했다.
이 3개월이라는 시간, 기회에서 내 성장을 뽑아낼 대로 뽑아내야(?) 될 것 같았고 그러기 위한 방법을 고민하고 있다..
구체적인 솔루션이라기 보다 뭔가 새로운 마음가짐을 가지면 그로 인해 행동은 따라 올 것이기 때문에 적절한 마음가짐을 찾고 있다.</p>
<p>그래서 이번 주말동안은 내가 가진 생각을 정리해 보는 시간을 가졌다.
내가 이 기업에서 일하면서 이루고 싶은 것, 나의 개발자로서의 강점이 될 수 있는 것과 단점을 고민해 봤다.
고민하는 과정에서 나에 대해 더 잘 알게 되긴 했지만 새로운 마음가짐은 찾지 못했다. 3주차때는 감을 잡으려나.</p>
<p>암튼 이번주는 고민 끝에 나온 결정은 이거다.</p>
<ol>
<li>미팅, 논의 등에서 나의 주장을 말하는 연습을 하자.</li>
<li>한 가지 생각에 너무 매몰되지 말고 필요하면 세수라도 하며 환기하자.</li>
<li>궁금한 건 적극적으로 물어보자. <code>cc.뻔뻔해지자</code></li>
<li>나태해지지 않게 조심하자. 그렇지만 너무 긴장해 있지도 말자.</li>
<li>내 일을 열심히 하면 인간관계는 자연스럽게 형성될 것.</li>
</ol>
<br>

<hr>
<h3 id="tmi">TMI</h3>
<blockquote>
<p>질문을 편하게 해주세요. 저희는 답할 준비가 되어 있으니
팀 리더 분과 1on1 중 해주셨던 말인데 뭐랄까... 인상 깊고 감동이었기에 계속 생각난다.</p>
</blockquote>
<blockquote>
<p>내 면접을 봤던 개발자 두 분이 우리 팀, 파트의 개발자 두 분이시다. 면접을 봐주신 분들과 함께 일을 하고 있다는게 뭔가 신기하다.</p>
</blockquote>
<blockquote>
<p>고작 인턴 가지고 뭘 이렇게 장황하게 글을 쓰나 싶을 수도 있겠다. 그치만 나에겐 이 인턴 일이 이 정도로 크고 새롭고 소중한 경험이다 😁</p>
</blockquote>
<blockquote>
<p>식대 제한이 없어서 맛있는 걸 너무 많이 먹는다. 살이 찔 듯.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[크루스칼 알고리즘]]></title>
            <link>https://velog.io/@chae_ag/kruskal-algorithm</link>
            <guid>https://velog.io/@chae_ag/kruskal-algorithm</guid>
            <pubDate>Sun, 16 Feb 2025 18:54:42 GMT</pubDate>
            <description><![CDATA[<h2 id="크루스칼-알고리즘이란">크루스칼 알고리즘이란?</h2>
<p>크루스칼 알고리즘은 최소 신장 트리(MST, Minimum Spanning Tree)를 찾는 대표적인 알고리즘 중 하나로, 다음과 같은 특징이 있다.</p>
<ul>
<li>간선을 하나씩 추가하면서 MST를 만들어 간다.</li>
<li>탐욕적 기법(Greedy Algorithm)을 사용한다.</li>
<li>시간 복잡도는 O(E log V)</li>
</ul>
<br>

<h2 id="크루스칼-알고리즘-과정">크루스칼 알고리즘 과정</h2>
<p>초기 상태로 정점는 서로 연결되어 있지 않다.
간선을 하나씩 추가하면서 최소 신장 트리(MST)를 만든다.
크루스칼 알고리즘을 수행하고 완성된 그래프는 최소 신장 트리이다.</p>
<p>간선을 추가하는 방식은 다음과 같다.</p>
<ol>
<li>모든 간선을 가중치 기준으로 오름차순 정렬한다.</li>
<li>가중치가 작은 간선부터 하나씩 선택하여 MST에 추가한다.</li>
<li>사이클이 발생하는지 확인한다.<ul>
<li>사이클이 생기지 않으면 해당 간선을 MST에 포함한다.</li>
<li>사이클이 발생하면 해당 간선을 제외한다.</li>
</ul>
</li>
<li>모든 정점이 연결될 때까지 위 과정을 반복한다.</li>
</ol>
<p>👉 여기서, 사이클 판별을 위해 <strong>유니온 파인드(Union-Find) 알고리즘</strong>을 사용한다.</p>
<p><code>정점 V</code>와 <code>정점 W</code>를 연결하는 <code>간선 E</code>를 선택할 때,
<code>V</code>와 <code>W</code>의 루트 노드가 같다면(이미 같은 집합에 속해 있다면) 사이클 발생 → 간선 추가 X
루트 노드가 다르면 서로 다른 집합 → 간선 추가 O</p>
<blockquote>
<p>유니온 파인드 알고리즘 알아보기
🔗 <a href="https://wikidocs.net/206632">https://wikidocs.net/206632</a></p>
</blockquote>
<br>

<h2 id="크루스칼-알고리즘-예제-문제">크루스칼 알고리즘 예제 문제</h2>
<h3 id="📌-프로그래머스-섬-연결하기-level-3">📌 <a href="https://school.programmers.co.kr/learn/courses/30/lessons/42861">프로그래머스 섬 연결하기 [level 3]</a></h3>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/c573393b-6146-411d-8af9-97f4ba3a75bb/image.png" alt=""></p>
<blockquote>
<p>문제 설명
여러 개의 섬이 주어지고, 각 섬을 연결하는 다리를 건설하는 비용이 주어진다.
모든 섬을 연결하는 데 필요한 최소 비용을 구하는 문제이다.</p>
</blockquote>
<br>

<h3 id="✨-크루스칼-알고리즘-적용-과정">✨ 크루스칼 알고리즘 적용 과정</h3>
<ol>
<li>다리 건설 비용을 기준으로 간선들을 오름차순 정렬한다.</li>
<li>사이클이 발생하지 않도록 하나씩 간선을 추가한다.</li>
<li>(정점 개수 - 1)개의 간선을 선택하면 종료한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/05114df8-7b68-4c9d-b1ec-2f55eee9e900/image.jpg" alt=""></p>
<table>
<thead>
<tr>
<th>단계</th>
<th>선택된 간선</th>
<th>총 비용</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>(0-1) 비용 1</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>(1-3) 비용 1</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>(0-2) 비용 2 → <strong>n-1개 선택 완료(종료)</strong></td>
<td>4</td>
</tr>
<tr>
<td>4</td>
<td>(1-2) 비용 5 → <strong>사이클 발생, 선택 X</strong></td>
<td></td>
</tr>
<tr>
<td>5</td>
<td>(2-3) 비용 8</td>
<td></td>
</tr>
</tbody></table>
<br>

<h3 id="풀이-코드-java">풀이 코드 (JAVA)</h3>
<pre><code class="language-java">import java.util.*;

class Solution {
    public void union(int[] parent, int x, int y) {
        x = find(parent, x);
        y = find(parent, y);

        if (x &lt; y) {
            parent[y] = x;
        } else {
            parent[x] = y;
        }
    }

    public int find(int[] parent, int x) {
        if (parent[x] == x) {
            return x;
        } else {
            return find(parent, parent[x]);
        }
    }

    public int solution(int n, int[][] costs) {
        int answer = 0;
        Arrays.sort(costs, (o1, o2) -&gt; o1[2] - o2[2]);

        // 부모노드 초기화
        int[] parent = new int[n];
        for (int i = 0; i &lt; n; i++) {
            parent[i] = i;
        }

        // 크루스칼 알고리즘
        for (int i = 0; i &lt; costs.length; i++) {
            // 부모가 같지 않으면. 즉, 사이클이 생기지 않으면 간선 선택
            if (find(parent, costs[i][0]) != find(parent, costs[i][1])) {
                answer += costs[i][2];
                union(parent, costs[i][0], costs[i][1]);
            }
        }

        return answer;
    }
}</code></pre>
<h3 id="✅-코드-설명">✅ 코드 설명</h3>
<ul>
<li><code>find()</code> : 특정 노드의 부모(루트 노드)를 찾는 함수</li>
<li><code>union()</code> : 두 노드를 같은 집합으로 합치는 함수</li>
<li>costs 배열을 가중치 기준으로 정렬하고, 가중치가 작은 간선부터 추가</li>
<li>사이클이 발생하는지 체크 후, MST에 포함 여부 결정</li>
</ul>
<br>

<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향 캡슐화에 대해 알아보기]]></title>
            <link>https://velog.io/@chae_ag/java-encapsulation</link>
            <guid>https://velog.io/@chae_ag/java-encapsulation</guid>
            <pubDate>Sun, 09 Feb 2025 06:28:50 GMT</pubDate>
            <description><![CDATA[<h2 id="🤔-먼저-캡슐화가-뭔가요">🤔 먼저, 캡슐화가 뭔가요?</h2>
<p><strong>캡슐화</strong>(encapsulation)는 객체 지향 프로그래밍에서 다음 2가지 측면이 있다.</p>
<ul>
<li>객체의 <strong>속성</strong>(data fields)과 <strong>행위</strong>(메서드, methods)를 <strong>하나로 묶고</strong>,</li>
<li>실제 구현 내용 일부를 <strong>외부에 감추어 은닉한다.</strong></li>
</ul>
<blockquote>
<p><em>우리는 <strong>캡슐</strong> 약 내부에 무엇이 들어있는지 알 수 없다.</em></p>
<p>그런데 분명 캡슐 약 안에는 <strong>여러 가지 성분들</strong>이 들어있는 것을 알고,
캡슐의 <strong>용도</strong> 또한 알고 있다. 약 먹으면 몸이 나아지는 것!!</p>
<p>그런데 알약 껍질에 의해 <strong>물리적으로 접근할 수 없기 때문</strong>에 
캡슐에 무엇이 들어있는지 우리는 모른다.</p>
</blockquote>
<br>

<h2 id="자바의-캡슐화">자바의 캡슐화</h2>
<p>자바의 캡슐화는 알약 껍질처럼 외부 접근에 대한 차단이고, 차단의 방법은 <strong>접근 제어자</strong>를 사용한다.</p>
<table>
<thead>
<tr>
<th>접근 제어자</th>
<th>접근 제한 범위</th>
</tr>
</thead>
<tbody><tr>
<td><strong>public</strong></td>
<td>접근 제한 x</td>
</tr>
<tr>
<td><strong>protected</strong></td>
<td>동일 패키지 + 상속관계에 있는 클래스만 접근 가능</td>
</tr>
<tr>
<td><strong>default</strong></td>
<td>동일 패키지 내에서만 접근 가능</td>
</tr>
<tr>
<td><strong>private</strong></td>
<td>동일 클래스 내에서만 접근 가능</td>
</tr>
</tbody></table>
<br>

<blockquote>
<p>간단한 소스코드로 알아보자.</p>
</blockquote>
<pre><code class="language-java">class Capsule {
    private int number;

    public Capsule(int number) {
        this.number = number;
    }

    public double getHalf() {
        return number / 2;
    }
}</code></pre>
<pre><code class="language-java">class Main {
    public static void main(String[] args) {
        Capsule capsule = new Capsule(10);
        System.out.println(capsule.getHalf());
    }
}</code></pre>
<p>👉 <code>int</code>값을 초기값으로 갖는 객체가 있고, 그 값의 절반을 반환하는 <strong><code>getHalf()</code></strong> 라는 메소드가 존재한다.</p>
<p>❓ 여기서 <strong>캡슐화</strong>는 바로 <strong>Capsule 클래스</strong> 자체를 의미한다.</p>
<p>위 코드가 캡슐화의 정의를 만족하는지 보자.</p>
<p>객체 지향 캡슐화 : <strong>데이터와, 데이터를 처리하는 행위를 묶고, 외부에는 그 행위를 보여주지 않는 것.</strong></p>
<p><strong>Capsule 클래스는</strong></p>
<ul>
<li><code>int</code> 값의 데이터를 가지고 있다.</li>
<li>그리고 <code>getHalf</code> 라는 데이터를 처리하는 행위 또한 가지고 있다.</li>
<li>마지막으로 Main메소드의 입장에서 Capsule 클래스의 <code>getHalf()</code> 를 사용할 수는 있지만, 구현이 어떻게 되어 있는지는 알 수 없다.</li>
</ul>
<p><strong>즉, 캡슐화되어 있다고 말할 수 있다!</strong></p>
<br>

<h3 id="여기서-드는-의문은">여기서 드는 의문은?</h3>
<h4 id="데이터와-행위를-하나로-묶고-그걸-외부에-노출시키지-않는-캡슐화를-왜-하는가"><em>데이터와, 행위를 하나로 묶고, 그걸 외부에 노출시키지 않는 캡슐화를 왜 하는가?</em></h4>
<p><strong>잘 된</strong> 캡슐화를 통해 얻을 수 있는 가장 큰 이점은</p>
<ul>
<li><strong>코드의 중복을 피할 수 있다는 점</strong></li>
<li><strong>데이터를 처리하는 동작 방식을 외부에서 알 필요가 없다는 점</strong></li>
</ul>
<blockquote>
<p>이 이점들이 왜 필요한지 코드로 알아보자!</p>
</blockquote>
<br>
아래와 같은 제품 클래스가 있다.

<pre><code class="language-java">class Product {
    int price = 10000;

    public Product(int price) {
        this.price = price;
    }
}</code></pre>
<ul>
<li>만약 어떤 제품의 10% 할인된 금액을 구해서 다른 로직으로 넘겨야 한다면 다음과 같은 코드를 추가하게 될 것이다.</li>
</ul>
<pre><code class="language-java">class Product {
    private int price = 10000;

    public Product(int price) {
        this.price = price;
    }

    public int getPrice() { // getter 추가
        return price;
    }
}</code></pre>
<pre><code class="language-java">public void first(Product product) {
    double discountedPrice = product.getPrice() * 0.9;
    show(discountedPrice);
}</code></pre>
<p>상품의 가격을 가지고 와서 10프로 할인된 가격을 구하고, 다른 로직으로 넘겼다.</p>
<ul>
<li>그렇다면, 만약 10프로 할인된 금액을 다른 로직에서도 사용하게 된다면 어떻게 될까?</li>
</ul>
<pre><code class="language-java">public void first(Product product) {
    double discountedPrice = product.getPrice() * 0.9; // 중복
    show(discountedPrice);
}

public void second(Product product) {
    double discountedPrice = product.getPrice() * 0.9;  // 중복
    secondShow(discountedPrice); // 할인된 금액을 show와 다르게 사용하는 메서드
}</code></pre>
<p>코드에서 중복이 일어났다.</p>
<p>한 줄짜리 코드 중복 좀 되면 어때??? 할 수 있지만, 이러한 로직이 수십 개,, 수백 개 더 필요하면 일일이 타이핑하는 것은 정말 힘들 것이다.</p>
<blockquote>
<p>🤔 코드의 중복은 좀 별로군?</p>
</blockquote>
<blockquote>
<p>다음으로는 <strong>데이터를 처리하는 방식이 외부에 드러나지 않는 것</strong>은 
어떤 이점이 있는지 알아보자.</p>
</blockquote>
<ul>
<li><p>요구사항이 변경되어 10프로 할인된 금액이 아니라 20프로 할인된 금액으로 로직을 바꿔야 하는 상황이다.</p>
</li>
<li><p>위와 같이 코드가 작성되었다면, 엄청난 양의 코드를 아래와 같이 고쳐야 한다.</p>
<pre><code class="language-java">  public void first(Product product) {
      double discountedPrice = product.getPrice() * 0.8; // 20프로 할인
      show(discountedPrice);
  }</code></pre>
</li>
<li><p>그러다가 10프로 할인 로직을 20프로 할인 로직으로 변경하지 못한 코드가 하나라도 존재한다면, 서비스에 큰 타격을 입게 될 것이다.</p>
</li>
</ul>
<blockquote>
<p>🤔 <strong>캡슐화</strong>를 지키기 위한 규칙 중에는 <em>Tell, Don&#39;t Ask</em> 라는 원칙이 있다.
객체 내부의 데이터를 꺼내와서 처리하는 게 아닌, 객체에게 처리할 <strong>행위를</strong> <strong>요청</strong>하라는 행위이다. 
이러한 행위를 우리는 <strong>&quot;객체에 메세지를 보낸다&quot;</strong> 라고 말한다.</p>
</blockquote>
<p>데이터를 객체로부터 받아오는 것이 아닌, 객체에게 처리를 요청하는 방식의 코드를 작성해 보자.</p>
<pre><code class="language-java">class Product {
    private int price = 10000;

    public Product(int price) {
        this.price = price;
    }

    public int getDiscountedPrice() { // 할인된 금액을 반환하도록
        return price * 0.9;
    }
}</code></pre>
<pre><code class="language-java">public void first(Product product) {
    double discountedPrice = getDiscountedPrice(); // 할인된 금액을 알려줘!
    show(discountedPrice);
}</code></pre>
<p>이렇게 하면 위에서 생겼던 문제들이 다 해결된다.</p>
<ul>
<li>할인된 금액을 사용하는 로직이 수백 개가 있고, 요구사항이 20프로를 할인하도록 변경됐다면?</li>
</ul>
<pre><code class="language-java">public int getDiscountedPrice() {
    return price * 0.8; // 20프로 할인
}</code></pre>
<p>Product 클래스의 <code>getDiscountedPrice()</code> 로직만 수정하면 끝이다.</p>
<br>

<h3 id="캡슐화가-왜-중요한가">캡슐화가 왜 중요한가?</h3>
<p>객체지향 프로그래밍에서 캡슐화는 단순히 데이터를 숨기는 개념을 넘어서, 코드를 더 안정적이고 유지보수하기 쉽게 만들어주는 핵심 원칙이다.</p>
<p>캡슐화를 잘 활용하면</p>
<ul>
<li>유지보수성이 높고</li>
<li>변경에 유연하며</li>
<li>객체가 스스로 책임을 가지는</li>
</ul>
<p>탄탄한 코드 구조를 만들 수 있다.</p>
<br>

<hr>
<blockquote>
<p>앞으로 코드를 작성할 때 
<code>&quot;이 객체가 스스로 처리하도록 만들 수 있을까?&quot;</code> <code>&quot;외부에서 직접 데이터를 조작하지 않도록 보호할 수 있을까?&quot;</code>
이런 질문을 스스로에게 계속해서 던지면서 캡슐화를 적용할 수 있도록 노력해 봐야겠다! 🚀</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Clean Code 읽으며 배운 것들 (1부)]]></title>
            <link>https://velog.io/@chae_ag/dev-books-clean-code</link>
            <guid>https://velog.io/@chae_ag/dev-books-clean-code</guid>
            <pubDate>Sun, 26 Jan 2025 04:22:30 GMT</pubDate>
            <description><![CDATA[<p>로버트 C. 마틴의 『Clean Code』라는 개발 관련 서적을 읽고 있다.</p>
<p>이 책은 프로그래머뿐만 아니라 소프트웨어 공학도, 프로젝트 관리자, 팀 리더, 시스템 분석가 등 소프트웨어 개발과 관련된 다양한 역할의 사람들에게 도움이 되는 책이라고 한다.</p>
<p>교보문고에서는 이 책을 다음과 같이 소개하고 있다.</p>
<blockquote>
<p>『Clean Code(클린 코드)』는 오브젝트 멘토(Object Mentor)의 동료들과 함께 클린 코드를 만드는 최상의 애자일 기법을 소개하는 책이다. 소프트웨어 장인 정신의 가치를 심어 주며 프로그래밍 실력을 높여준다. 여러분이 노력만 한다면.
어떤 노력이 필요하냐고? 코드를 읽어야 한다. 아주 많은 코드를. 그리고 그 코드의 무엇이 옳은지, 그른지 생각해야 한다.
더 나아가, 전문가로서 자신이 지닌 가치와 장인으로서 자신의 작품에 대한 헌신을 돌아보게 된다.</p>
</blockquote>
<p>아직 책을 완독하지는 못했지만, 절반 정도 읽으면서 깨달았던 것들이 많다. 그런 깨달음을 잊지 않고 기록으로 남기기 위해 블로그에 정리해본다. 이 기록은 단순히 내용을 정리하는 것뿐만 아니라, 내가 느낀 점을 바탕으로 실천하고 싶은 부분을 되새기기 위함이다.</p>
<br>

<h2 id="clean-code에서-배운-것">Clean Code에서 배운 것</h2>
<h3 id="보이스카우트-규칙ㅤㅤ">보이스카우트 규칙ㅤㅤ</h3>
<p><em>#지속적인 개선</em></p>
<ul>
<li><p>잘 짠 코드가 전부가 아니라, 시간이 지나도 언제나 깨끗하게 유지해야 한다.</p>
</li>
<li><p>*미국 보이스카우트가 따르는 규칙 : 캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라</p>
</li>
<li><p>체크아웃할 때보다 좀 더 깨끗한 코드를 체크인한다면 코드는 절대 나빠지지 않는다.</p>
</li>
<li><p>한꺼번에 많은 시간과 노력을투자해 코드를 정리할 필요는 없다.</p>
</li>
<li><p>변수 이름 하나를 개선하고, 조금 긴 함수 하나를 분할하고, 약간의 중복을 제거하고, 복잡한 if문 하나를 정리하면 충분하다.</p>
</li>
</ul>
<blockquote>
<p>이 부분을 보고 지난 내 개발 과정을 떠올려보니, 한 번에 코드를 개선하려다가 점점 복잡해지고 원래 형태를 알아보기 어려워져서 테스트도 힘들어졌던 적이 많았던 것 같다. 결국 깃에서 로컬 변경사항을 버렸던 일도 종종 있었다. 어차피 하루 개발하고 끝낼 코드도 아닌데 왜 그렇게 한 번에 하려고 했는지 모르겠다. 이 단락을 읽고 나서야, 전보다 조금 더 깨끗하게 만드는 게 결국 지속적인 개선으로 이어진다는 것을 깨달았다.</p>
</blockquote>
<br>

<h3 id="이름을-의미있게-구분하라">이름을 의미있게 구분하라</h3>
<p><em>#읽는 사람이 차이를 알도록 이름을 지어라</em></p>
<ul>
<li><p>불용어를 추가한 이름 역시 아무런 정보도 제공하지 못한다.</p>
</li>
<li><p>Product 클래스가 있을 때 다른 클래스를 ProductInfo, ProductData라 부른다면 개념을 구분하지 않은 채 이름만 달리하는 경우다.</p>
</li>
</ul>
<blockquote>
<p>실제로 이렇게 사용했던 적이 있어서 찔렸다. 그러다 보니 매번 &quot;이게 무슨 클래스였더라?&quot; 하고 헷갈리곤 했었다. 시간이 조금 더 걸리더라도, 클래스 이름을 다른 사람이 봤을 때도 바로 이해할 수 있을지 가정해보고 신중하게 지어야겠다는 생각이 들었다.</p>
</blockquote>
<br>

<h3 id="의미-있는-맥락을-추가하라-불필요한-맥락을-없애라">의미 있는 맥락을 추가하라, 불필요한 맥락을 없애라</h3>
<ul>
<li><p>일반적으로 짧은 이름이 긴 이름보다 좋다. (단, 의미가 분명한 경우에 한해서)</p>
</li>
<li><p>아니라면 길더라도 서술적인 이름을 사용하라! 그게 낫다.</p>
</li>
<li><p>이름에 불필요한 맥락을 추가하지 않도록 주의한다.</p>
</li>
<li><p>accountAddress, customerAddress는 Address 클래스 인스턴스로는 좋은 이름이나, 클래스 이름으로는 적합하지 못하다.</p>
<ul>
<li>차라리 포트 주소, Mac주소, 웹 주소를 구분해야 한다면 PostalAddress, Mac, URI라는 이름이 더 괜찮다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>나도 클래스명을 accountAddress, customerAddress처럼 사용하고 있었다. (이게 맞는 줄 알았다.) 
하지만 의미를 명확하게 하고, 불필요한 맥락은 추가하지 않는 게 더 낫다는 것을 이번에 깨달았다.</p>
</blockquote>
<br>

<h3 id="함수-인수는-적을-수록-좋다">함수 인수는 적을 수록 좋다.</h3>
<ul>
<li><p>함수 인수는 0개가 이상적인 개수이며, 그 다음은 1개, 2개이고 3개는 가능한 피하는 편이 좋다.</p>
</li>
<li><p>4개부터는 특별한 이유가 필요하다. (특별한 이유가 있어도 사용하지마라)</p>
</li>
<li><p>StringBuffer를 인자로 넘겨줬다고 생각하면 코드를 읽는 사람은 매 함수에서 StringBuffer를 발견할 때마다 이 StringBuffer의 의미를 해석해야 한다.</p>
</li>
<li><p>플래그(Flag) 인수는 추하다.</p>
<ul>
<li>함수는 하나의 작업만 해야한다고 했는데, 플래그 인수를 사용하면 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까.</li>
<li>플래그가 참이면 이걸 하고거짓이면 저걸 한다는 말이니까.</li>
</ul>
</li>
<li><p>이항 함수도 되도록이면 단항 함수로 바꾸려 노력해라.</p>
</li>
</ul>
<br>

<h3 id="디미터-법칙">디미터 법칙</h3>
<ul>
<li><p>잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙</p>
</li>
<li><p>즉, 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안된다.
예) </p>
<pre><code class="language-java">// 여러 객체를 한 번에 호출하며 의존성을 노출함
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();</code></pre>
</li>
<li><p>위 같은 코드를 기차 충돌이라고 부른다.</p>
<ul>
<li>여러 객차가 한 줄로 이어진 기차처럼 보이기 때문 (조잡함)</li>
<li>아래처럼 나누는 편이 좋다.
ctxt 객체가 Options을 포함하며, options가 ScratchDir을 포함하고 ScratchDir이 AbsolutePath를 포함한다는 사실을 안다.
(그치만 이 예제도 디미터 법칙을 위반하는지 여부는 저것들이 객체인지 자료구조인지에 달렸다)<pre><code class="language-java">// 각 객체가 자신만의 책임을 가지며 유지보수가 용이해짐
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();</code></pre>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>코드의 길이를 줄이고 &quot;이렇게 해도 직관적이고 충분히 괜찮지 않나?&quot; 싶어서 어쩔 때는 줄줄 호출하곤 했다. 
하지만 각자의 책임을 분리하는 방식이 훨씬 더 좋은 접근이라는 것을 위 내용을 읽고 알게 됐다.</p>
</blockquote>
<br>

<h3 id="클래스는-작게">클래스는 작게</h3>
<ul>
<li><p>함수는 작게, 매개변수 목록을 짧게 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 많아진다. 이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.</p>
</li>
<li><p>응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.</p>
</li>
<li><p>클래스가 응집력을 잃으면 쪼개라!</p>
</li>
<li><p>클래스에 있는 큰 함수를 작은 함수 여러 개로 쪼개다 보면 몇몇 함수가 몇몇 변수만 사용한다? 
→ 독자적인 클래스로 분리해도 되지 않는가! 
⇒ 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.</p>
</li>
</ul>
<hr>
<blockquote>
<p>항상 코드를 짜면서 &quot;이럴 땐 어떻게 해야 옳은 걸까? 더 나은 방향은 뭘까?&quot;라는 고민을 많이 했지만, 답을 잘 찾지 못할 때가 많았다. 인터넷에서 검색해도 뭔가 명쾌한 답을 얻기가 어려웠다. 그런데 이런 개발 서적을 읽으니 가려운 곳을 시원하게 긁어주는 기분이다. 왜 진작에 책을 읽지 않았는지 후회가 된다. 물론, 여기 적힌 내용이 항상 정답은 아니겠지만, 내 사고를 확장하는 데에는 충분히 큰 도움이 된다. 이제 다음에 읽을 책은 또 어떤 깨달음을 줄지 기대가 된다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 그리디 문제: 13164 행복 유치원 풀이]]></title>
            <link>https://velog.io/@chae_ag/coding-test-baekjoon-13164</link>
            <guid>https://velog.io/@chae_ag/coding-test-baekjoon-13164</guid>
            <pubDate>Sat, 18 Jan 2025 08:59:34 GMT</pubDate>
            <description><![CDATA[<h3 id="백준-13164-행복-유치원-골드-5">백준 13164: 행복 유치원 (골드 5)</h3>
<p>문제 링크: <a href="https://www.acmicpc.net/problem/13164">https://www.acmicpc.net/problem/13164</a></p>
<p>이 문제는 그리디 알고리즘을 사용하여 풀 수 있는 문제다.
처음부터 그리디 문제라는 것을 알고 접근했지만, 생각보다 최적의 선택을 정하는 과정이 쉽지 않았다.</p>
<h4 id="문제-요약">문제 요약</h4>
<blockquote>
<ul>
<li>총 N명의 유치원생을 키 기준 오름차순으로 세워논 상태에서 원생들을 K개의 조로 나눈다.
➡️ 이때, 각 조마다 가장 키가 작은 원생과 가장 키가 큰 원생의 키 차이를 구하고 조마다의 키 차이를 합산했을 때 최소가 되도록 조를 나눠야 한다.</li>
</ul>
</blockquote>
<ul>
<li>위처럼 조를 나눈 뒤, 최소 키차이 합을 출력</li>
</ul>
<br>

<h4 id="문제-접근">문제 접근</h4>
<p>처음엔 그리디 알고리즘의 원리인 단계마다 최적의 선택을 한다는 큰 틀로 접근했다.</p>
<p><code>어떤 단계에서 최적의 선택을 해야 할까?</code> 라는 고민에 
유치원생 키가 오름차순 정렬된 상태이니, 
<code>맨 처음부터 차례대로 최적의 선택을 하면 되지 않을까?</code> 라고 생각했다.</p>
<p>하지만 이렇게 접근하니 문제가 해결되지 않았다. 
조의 개수인 K가 정해져 있기도 하고, 단순히 앞에서부터 차례대로 조를 나누는 방식으로는 최적의 결과에 도달할 수가 없었다.</p>
<p>며칠째 시도해보다가 도저히 안되겠어서 푸신 분이 올려준 힌트를 봤다.
힌트를 보고 조금 충격을 받았는데, 그리디에서 중요한 점은 최적의 선택을 <strong>어느 순간</strong>에서 할지를 알아내는 것임을 깨달았다.</p>
<br>

<h4 id="풀이-아이디어">풀이 아이디어</h4>
<p>힌트를 바탕으로 내가 이 문제를 풀어낸 방법은 아래와 같다.</p>
<blockquote>
<p><strong>K개의 조를 나눌 위치를 찾는 방식에서 최적의 선택을 하고자 했다.</strong></p>
<ol>
<li>원생들이 나열되어 있을 때 뒤 원생과 <strong>키 차이</strong>와 <strong>해당 원생의 위치</strong>를 함께 기록한다.</li>
<li>우선순위 큐를 사용해서 키 차이가 가장 큰 위치가 어딘지 <code>K-1</code>개만큼 알아낸다.</li>
</ol>
<p><strong>(키 차이가 가장 큰 원생끼리 다른 조로 배치해버리면 최소합을 이룰 수 있기 때문!!)</strong>
(<code>K-1</code>개인 이유는, 파티션 <code>K-1</code>개를 놓아야 K개의 조가 완성되기 때문)
3. 해당 파티션 위치를 기준으로 조를 이루고 가장 작은 원생과 큰 원생의 키 차이를 계산함.
(예를 들어, 파티션 위치가 <code>{3,5}</code>면, → <code>{0,1,2,3}</code>, <code>{4, 5}</code>, <code>{6, 7, ...}</code> 이렇게 3개의 조로 나눔)</p>
</blockquote>
<br>

<h4 id="최종-풀이-코드-java">최종 풀이 코드 (Java)</h4>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int K = Integer.parseInt(st.nextToken());

        int[] array = Arrays.stream(br.readLine().split(&quot; &quot;))
                .mapToInt(Integer::parseInt)
                .toArray();

        PriorityQueue&lt;int[]&gt; q = new PriorityQueue&lt;&gt;(((o1, o2) -&gt; o2[0] - o1[0]));

        for (int i = 0; i &lt; N - 1; i++) {
            q.add(new int[]{array[i + 1] - array[i], i}); // 키 차이가 큰 순으로 내림차순
        }

        int[] partitions = new int[K - 1]; // 차이가 제일 큰 곳부터 다른 조로 쪼개기 위한 파티션 인덱스를 구함.

        for (int i = 0; i &lt; K - 1; i++) {
            partitions[i] = q.poll()[1];
        }

        Arrays.sort(partitions);

        int start = array[0];
        int result = 0;
        for (int i = 0; i &lt; K; i++) {
            if (i == K - 1) { // 마지막일 경우 분기처리
                result += array[N - 1] - start;
                break;
            }
            result += (array[partitions[i]] - start);
            start = array[partitions[i] + 1];
        }

        System.out.print(result);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/831f3fbd-11e8-4b9c-975f-62ead449a234/image.png" alt=""></p>
<br>

<hr>
<blockquote>
<p>그리디는 쉬운 알고리즘이라고 생각했는데, 이번 문제를 풀면서 생각이 바뀌었다.
그리디 알고리즘의 핵심은 &#39;최적의 선택을 <strong>어디에서 할지</strong> 정하는 것&#39; 임을 알게 되었고, 그리디 문제를 잘 풀기 위해서는 문제 상황에 따라 최적의 선택 기준을 새롭게 세우는 사고가 중요함을 깨달았다.
더 다양한 유형의 그리디 문제를 풀어보면서 감을 익혀봐야겠다. 😅</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[java] 자바 우선순위 큐 PriorityQueue]]></title>
            <link>https://velog.io/@chae_ag/java-%EC%9E%90%EB%B0%94-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90-PriorityQueue</link>
            <guid>https://velog.io/@chae_ag/java-%EC%9E%90%EB%B0%94-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90-PriorityQueue</guid>
            <pubDate>Sun, 12 Jan 2025 10:40:55 GMT</pubDate>
            <description><![CDATA[<p>꾸준히 진행하고 있는 민문리 스터디의 이번주 알고리즘 주제는 <strong>힙</strong> 이었다.
코딩 테스트에서 힙 문제는 우선순위 큐를 활용하는 문제로 자주 등장하는 주제다.</p>
<p>우선순위 큐를 사용하는 코딩테스트 문제 여러개를 풀면서 공부하게 된 내용을 적어본다~! (｡◝‿◜｡)</p>
<br>

<h2 id="1-우선순위-큐priorityqueue란">1. 우선순위 큐(PriorityQueue)란?</h2>
<ul>
<li>일반적인 큐 : 먼저 들어간 데이터가 먼저 나가는 구조인 <strong>FIFO(First-In, First-Out) 형식</strong>의 자료구조 </li>
<li>우선순위 큐 : 들어가는 순서와는 상관없이 <strong>우선순위가 높은</strong> 데이터가 먼저 나가는 자료구조</li>
</ul>
<p>자바의 우선순위 큐는 내부적으로 <strong>힙(Heap) 자료구조</strong>를 사용해 구현되어 있다.</p>
<br>

<h2 id="2-priorityqueue-선언-방법">2. PriorityQueue 선언 방법</h2>
<pre><code class="language-java">// 우선순위가 낮은 수가 먼저 나옴, 즉 오름차순
PriorityQueue&lt;Integer&gt; queue = new PriorityQueue&lt;&gt;();

// 우선순위가 높은 수가 먼저 나옴, 즉 내림차순 
// Collections.reverseOrder()는 우선순위를 반대로 뒤집어주는 역할
PriorityQueue&lt;Integer&gt; queue = new PriorityQueue&lt;&gt;(Collections.reverseOrder());</code></pre>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-java">import java.util.PriorityQueue; // 우선순위 큐 사용 시
import java.util.Collections;  // 우선순위 반대로 원할 시

public class A {
    public static void main(String[] args) {
        // 큐1 : 낮은 수부터 나옴 (오름차순)
        PriorityQueue&lt;Integer&gt; queue1 = new PriorityQueue&lt;&gt;();

        // 큐2 : 큰 수부터 나옴 (내림차순)
        PriorityQueue&lt;Integer&gt; queue2 = new PriorityQueue&lt;&gt;(Collections.reverseOrder());

        queue1.offer(1);    // 큐1에 원소 1 추가
        queue1.offer(10);   // 큐1에 원소 10 추가
        queue1.offer(100);  // 큐1에 원소 100 추가

        queue2.offer(1);    // 큐2에 원소 1 추가
        queue2.offer(10);   // 큐2에 원소 10 추가
        queue2.offer(100);  // 큐2에 원소 100 추가

         System.out.println(&quot;&lt;큐1 예상결과 : 낮은 수부터 오름차순으로 나옴&gt;&quot;);

        while(!queue1.isEmpty()) { // 큐1이 빌 때까지 반복
            // queue1의 첫 번째 값을 꺼냄
            System.out.println(&quot;queue1.poll() = &quot; + queue1.poll());
        }

        System.out.println(&quot;&lt;큐2 예상결과 : 높은 수부터 내림차순으로 나옴&gt;&quot;);

        while(!queue2.isEmpty()) { // 큐2가 빌 때까지 반복
            // queue2의 첫 번째 값을 꺼냄
            System.out.println(&quot;queue2.poll() = &quot; + queue2.poll());
        }
    }
}</code></pre>
<h4 id="결과">결과</h4>
<pre><code class="language-bash">&lt;큐1 예상결과 : 낮은 수부터 오름차순으로 나옴&gt;
queue1.poll() = 1
queue1.poll() = 10
queue1.poll() = 100
&lt;큐2 예상결과 : 높은 수부터 내림차순으로 나옴&gt;
queue1.poll() = 100
queue1.poll() = 10
queue1.poll() = 1</code></pre>
<br>

<h2 id="3-priorityqueue-기본적인-메서드">3. PriorityQueue 기본적인 메서드</h2>
<ul>
<li><code>add()</code> : 우선순위 큐에 원소를 추가. <strong>큐가 꽉 찬 경우 에러 발생</strong></li>
<li><code>offer()</code> : 우선순위 큐에 원소를 추가. <strong>값 추가 실패 시 false 반환</strong></li>
<li><code>remove()</code> : 우선순위 큐에서 첫 번째 값을 반환하고 제거, <strong>비어있으면 에러 발생</strong></li>
<li><code>poll()</code> : 우선순위 큐에서 첫 번째 값을 반환하고 제거, <strong>비어있으면 null 반환</strong></li>
<li><code>isEmpty()</code> : 우선순위 큐가 비어있는지 확인. 비어있다면 true, 값이 하나라도 있으면 false 반환</li>
<li><code>clear()</code> : 우선순위 큐를 초기화</li>
<li><code>size()</code> : 우선순위 큐에 포함되어 있는 원소의 수를 반환</li>
</ul>
<br>

<h2 id="4-우선순위-커스텀하기">4. 우선순위 커스텀하기</h2>
<p>코딩테스트를 풀다 보면 이 우선순위를 커스텀하는 부분을 잘 사용할 줄 알아야 한다!</p>
<p>위처럼 일반 정수나 문자열같은 건 기본적으로 정의된 우선순위에 따라서 값이 잘 나온다.</p>
<p>그치만 우선순위를 내 멋대로 커스텀해서 사용할 줄 알아야 한다.</p>
<blockquote>
<p>🤔 why? 이차원 배열이나 사용자 정의 객체와 같은건 비교 기준이 없기 때문에 우선순위 큐에 넣으면 우선순위에 따라 정렬하려고 할 때 <strong>ClassCastException 에러가 발생한다.</strong> 
따라서 이러한 데이터를 사용할 때는 정렬 기준을 직접 정의해야 한다.</p>
</blockquote>
<h3 id="커스텀-예시">커스텀 예시</h3>
<pre><code class="language-java">import java.util.PriorityQueue;
import java.util.Comparator;

class Student {
    private int id;       // 학생 ID
    private double grade; // 성적 평균

    public Student(int id, double grade) {
        this.id = id;
        this.grade = grade;
    }

    public double getGrade() {
        return this.grade;
    }

    @Override
    public String toString() {
        return &quot;ID : &quot; + this.id + &quot;, grade : &quot; + this.grade;
    }
}

// 클래스 객체의 우선순위를 위한 클래스
class StudentComparator implements Comparator&lt;Student&gt; {
    @Override
    public int compare(Student s1, Student s2) {
        return Double.compare(s2.getGrade(), s1.getGrade()); // 내림차순
    }
}

public class Main {
    public static void main(String[] args) {
        // 방법 1 : 클래스 정의
        // StudentComparator 클래스에서 비교 기준을 정의하여 사용
        PriorityQueue&lt;Student&gt; queue = new PriorityQueue&lt;&gt;(new StudentComparator());

        // 방법 2 : 익명 클래스 활용
        // 바로 Comparator의 compare 메서드를 오버라이드하여 기준 작성
        PriorityQueue&lt;Student&gt; queue = new PriorityQueue&lt;&gt;(new Comparator&lt;Student&gt;() {
            @Override
            public int compare(Student s1, Student s2) {
                return Double.compare(s2.getGrade(), s1.getGrade()); // 내림차순
            }
        });

        // 방법 3 : 람다식 사용 (가장 간단한 방식)
        // 람다식을 사용해 compare 메서드를 간결히 작성
        PriorityQueue&lt;Student&gt; queue = new PriorityQueue&lt;&gt;((s1, s2) -&gt; Double.compare(s2.getGrade(), s1.getGrade())); // 내림차순

        // 학생 객체 추가
        queue.offer(new Student(1, 88.5));
        queue.offer(new Student(2, 95.2));
        queue.offer(new Student(3, 91.3));

        System.out.println(&quot;&lt;예상 결과: 성적 평균이 높은 순으로 출력&gt;&quot;);
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}</code></pre>
<h4 id="결과-1">결과</h4>
<pre><code class="language-bash">&lt;예상 결과: 성적 평균이 높은 순으로 출력&gt;
ID : 2, grade : 95.2
ID : 3, grade : 91.3
ID : 1, grade : 88.5</code></pre>
<h4 id="예시-설명">예시 설명</h4>
<ul>
<li><p><code>Double.compre(d1, d2)</code> 메서드는 d1과 d2를 비교하여 다음 값을 반환한다.
  <code>0</code> : 두 값이 같음.
  <code>1</code> : d1이 d2보다 큼.
  <code>-1</code> : d1이 d2보다 작음.</p>
</li>
<li><p>우선순위 큐는 내부적으로 두 요소를 비교할 때 Comparator.compare() 메서드를 사용한다.</p>
</li>
<li><p><code>compare()</code> 메서드는 반환값에 따라 요소의 순서를 결정한다.
<code>1</code> : 순서를 바꿈 → 뒤 요소가 앞에 온다.
<code>-1</code> : 순서를 유지함 → 앞 요소가 그대로 남는다.
<code>0</code> : 순서를 바꾸지 않음.</p>
<pre><code class="language-java">public int compare(Student s1, Student s2) {
    return Double.compare(s2.getGrade(), s1.getGrade()); // 내림차순
 }</code></pre>
<ul>
<li>위 코드에서 <code>Double.compare(s2.getGrade(), s1.getGrade())</code>는 s2의 성적이 s1보다 클 경우 1을 반환하여 순서를 바꾼다.
결과적으로 성적이 높은 학생이 먼저 나오도록 내림차순 정렬을 정의한 것이다.
👉 즉, 큰 값이 먼저 오도록 우선순위를 설정한 것!</li>
</ul>
</li>
<li><p>람다식 사용</p>
<ul>
<li>람다식을 사용하게 되면 코드를 간결하게 줄일 수 있다.<pre><code class="language-java">PriorityQueue&lt;Student&gt; queue = new PriorityQueue&lt;&gt;((s1, s2) -&gt; Double.compare(s2.getGrade(), s1.getGrade())); // 내림차순</code></pre>
</li>
</ul>
</li>
</ul>
<br>

<hr>
<blockquote>
<p>우선순위 큐가 없었으면 직접 힙을 구현해야 했을 텐데, 생각만 해도 너무 복잡하고 아찔하다. 😅
이렇게 좋은 우선순위 큐를 제공하고 있으니, 제대로 익혀두고 코딩 테스트나 프로젝트에서 잘 써먹어보아야지~
이번에 제대로 공부한 덕분에 우선순위 큐를 내 마음대로 자유롭게 정의해서 잘 활용할 수 있을 것 같다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[서비스 릴리즈 3주 만에 터진 첫 이슈!]]></title>
            <link>https://velog.io/@chae_ag/service-first-issue-resolution</link>
            <guid>https://velog.io/@chae_ag/service-first-issue-resolution</guid>
            <pubDate>Fri, 03 Jan 2025 05:11:18 GMT</pubDate>
            <description><![CDATA[<p>현재 나와 팀원들은 약 1년간 꾸준히 진행해오던 프로젝트를 드디어 3주 전에 성공적으로 출시했다! (이렇게 보니 정말 오래 걸렸다 ㅎ.ㅎ) 출시 후 서비스 런칭 이벤트도 진행하며, 누적 유저 수가 300명을 넘기는 등 순조로운 운영을 이어가고 있었다.</p>
<p>그런데… 새해가 밝은 1월 1일, 릴리즈 이후 처음으로 꽤 심각한 이슈가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/90e74f1e-87de-4142-b591-02b07b5aa7dd/image.jpeg" alt=""></p>
<hr>
<h2 id="🛠️-이슈-발생-상황">🛠️ 이슈 발생 상황</h2>
<p>운영 서버와 별개로 테스트용으로 분리해둔 개발 서버에 새로운 글들이 올라오고 있는 것을 발견했다!
확인해보니 안드로이드 앱 업데이트 시 개발 서버의 URL로 API 요청이 가도록 설정되어 있었던 것이 원인이었다. 😱</p>
<p>이로 인해 12월 29일부터 1월 1일까지 총 23명의 유저가 개발 서버에서 회원가입 및 활동을 진행한 사실을 로그를 통해 확인했다.
게다가 문제를 더 심각해진건, 현재 서비스 런칭 기념으로 글 작성자 대상으로 경품 이벤트를 진행하고 있었다는 점이었다..</p>
<hr>
<h2 id="🚨-긴급-대응-과정">🚨 긴급 대응 과정</h2>
<ol>
<li><p><strong>문제 해결</strong>
즉시 클라이언트 API URL을 운영 서버로 복구하고 문제를 해결했다.</p>
</li>
<li><p><strong>데이터 정리</strong>
개발 서버의 데이터베이스를 확인해 유저들의 이메일을 확보한 후, 이메일로 사과와 재가입 요청을 드렸다.
새로 가입한 유저를 식별하며, 그들이 개발 서버에 등록했던 글과 작품 등 활동 데이터를 운영 서버로 이전하고 있다.</p>
</li>
</ol>
<p>어쨌든 큰 탈 없이 (?) 문제가 해결되는 듯 하고 있는데,
확실히 사람은 큰 일 나봐야 정신 차린다고 ....
백엔드쪽에 개선할 것이 많다고 느껴졌다.</p>
<hr>
<h2 id="💡-이번-이슈에서-느낀-개선점">💡 이번 이슈에서 느낀 개선점</h2>
<h3 id="1️⃣-유저-가입-일시-미저장-문제">1️⃣ 유저 가입 일시 미저장 문제</h3>
<p>현재 서비스의 <code>User</code> 테이블에는 유저의 가입 일시를 저장하지 않고 있었다.
초기 설계 당시, &quot;유저 가입 일시는 쓸 일이 없을 것 같다&quot;는 판단 하에 제외했던 것인데, 이번 사건으로 그 판단이 얼마나 잘못되었는지를 뼈저리게 깨달았다.</p>
<p>왜냐하면 개발 서버 DB만으로는 12월 29일부터 가입한 유저를 정확히 식별할 방법이 없었기 때문이다.
결국 이를 계기로 유저 가입 일시를 저장하도록 데이터베이스 구조를 변경했다.</p>
<h3 id="2️⃣-신규-유저-모니터링-부족">2️⃣ 신규 유저 모니터링 부족</h3>
<p>이번 이슈에서 이 문제가 4일 동안 (12/29~1/1) 발견되지 않은 점이 특히 아쉬웠다. 신규 유저 가입 이벤트를 실시간으로 확인할 방법이 없었던 것이 문제였다.
그래서 신규 유저 가입 알림 기능을 디스코드 알람으로 구축하기로 했다.</p>
<p><strong>🛠️ 알람 시스템 개편</strong>
기존의 디스코드 알람은 글 신고와 회원 탈퇴 관련 알람만 존재했고, 개발 서버와 운영 서버의 알람이 혼재되어 있었다.
이번 기회에 알람 시스템을 전면 개편했다!</p>
<ol>
<li><p><strong>서버별 알람 공간 분리</strong>
운영 서버와 개발 서버 각각의 디스코드 채널을 별도로 설정.</p>
</li>
<li><p><strong>이벤트별 알람 채널 분리</strong>
신규 유저 가입, 글 신고, 회원 탈퇴 등 이벤트별 채널을 나눠 알람 발생.</p>
</li>
<li><p><strong>웹훅 URL 자동화 설정</strong>
CI/CD 과정에서 배포 서버에 따라 적합한 디스코드 웹훅 URL을 설정하도록 yml 파일을 활용해 자동화.</p>
</li>
</ol>
<p>이로써 유저 가입 알람을 통해 신규 사용자 가입 추이를 실시간으로 확인할 수 있게 되었고, 이번과 같은 이슈가 발생하더라도 빠르게 대처할 수 있는 환경을 만들었다.</p>
<hr>
<h2 id="✨-이번-경험의-교훈">✨ 이번 경험의 교훈</h2>
<p>처음 개발 서버에서 유저 데이터가 쌓이고 있는 걸 확인했을 때는 정말 머리가 하얘졌다.
하지만 결국 문제는 해결할 방법이 있다는 사실을 다시금 깨달았다.
이번 일을 계기로 서비스의 안정성과 관리 효율성을 한 단계 끌어올릴 수 있었고, 앞으로 더 나은 서비스를 위해 고민해야 할 점들도 명확히 알게 되었다.</p>
<p>서비스 운영에서 실수는 피할 수 없지만, 그 실수를 통해 배우고 성장하는 것이 진정한 발전이 아닐까? 💪</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/16af6ef1-8991-41fe-8b27-c9d0a963b267/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java + Spring 애플 소셜 로그인 도입 시행착오]]></title>
            <link>https://velog.io/@chae_ag/Java-Spring-%EC%95%A0%ED%94%8C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%8F%84%EC%9E%85-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4</link>
            <guid>https://velog.io/@chae_ag/Java-Spring-%EC%95%A0%ED%94%8C-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%8F%84%EC%9E%85-%EC%8B%9C%ED%96%89%EC%B0%A9%EC%98%A4</guid>
            <pubDate>Mon, 18 Nov 2024 06:59:59 GMT</pubDate>
            <description><![CDATA[<p>현재 출시를 앞둔 서비스인 <em>웹소설 유저를 위한 기록 &amp; 커뮤니티 서비스 웹소소</em> 는 안드로이드와 iOS 두 OS를 모두 지원하는데, 사용자들이 간편하게 서비스에 가입하고 이용할 수 있도록 소셜 로그인 기능을 도입하고자 했다.</p>
<p>이번 글에서는 애플 소셜 로그인 백엔드 부분을 구현하면서 겪었던 시행착오와 해결 과정을 공유하려고 한다!</p>
<blockquote>
<p>2024년 3월 이후, iOS 앱이 타사 소셜 로그인 서비스를 이용하는 경우 필수였던 <strong>애플 소셜 로그인 요건이 해제</strong>되었지만, 여전히 데이터 수집 범위는 이메일 주소로 제한되어야 한다는 새로운 조건이 추가됨. <a href="https://developer.apple.com/kr/app-store/review/guidelines/#4.8">- Apple 앱 심사지침 공식문서</a></p>
</blockquote>
<p>애플의 정책 변화로 인해 기존에는 필수였던 애플 소셜 로그인 요건이 완화되었지만, 사용자 경험 향상을 위해 서비스에 애플 소셜 로그인 기능을 도입하기로 결정했다.</p>
<br>
애플 소셜 로그인 구현 방법은 많은 기술 블로그에서 찾아볼 수 있었는데, 그럼에도 타 소셜 로그인보다 과정이 복잡해서 조금 어려웠다 🥲

<br>
<br>

<h3 id="겪은-시행착오">겪은 시행착오</h3>
<ol>
<li><p>로컬 환경에서 테스트가 불가능하여, ngrok을 활용한 애플 소셜 로그인 작동 테스트</p>
</li>
<li><p>애플 소셜 유저가 탈퇴 시 해당 토큰을 어떻게 취소할지에 대한 문제
(탈퇴 직전 재로그인 처리 vs 유효기간이 없는 리프레시 토큰을 DB에 저장하여 관리)</p>
</li>
<li><p>자꾸 발생하는 client_id 오류</p>
</li>
</ol>
<p><br><br></p>
<h2 id="1-로컬-환경에서-테스트가-불가한-애플-소셜-로그인">1. 로컬 환경에서 테스트가 불가한 애플 소셜 로그인</h2>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/75c8f8b4-9fde-44b5-808b-078bd96fdc5e/image.png" alt="">
<a href="https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens">애플 공식문서</a></p>
<p>애플이 제공하는 소셜 로그인 REST API는 로컬 환경에서 테스트가 불가능하도록 설계되어 있다.</p>
<p>코드 구현은 완료했지만 로컬 환경에서 테스트할 수 없었기 때문에, 방법을 모색하던 중 외부 네트워크에서 로컬 환경으로 접근할 수 있는 터널링 프로그램인 <strong>ngrok</strong>을 알게 되었다. ngrok은 HTTPS로 연결할 수 있는 기능도 제공하기 때문에 이 문제를 해결하는 데 적합하다고 판단했다.
<br></p>
<h3 id="ngrok을-활용한-애플-소셜-로그인-작동-테스트">ngrok을 활용한 애플 소셜 로그인 작동 테스트</h3>
<pre><code class="language-bash">% brew install ngrok</code></pre>
<p>위 명령어를 통해 ngrok을 설치한다.</p>
<p>로컬 환경에서 ngrok을 실행시키면 세션 유지 시간이 기본적으로 2시간으로 제한되기 때문에, 세션이 만료되면 명령어를 통해 포워딩을 다시 시작해야 한다. </p>
<p>이때, 다시 시작된 포워딩 URI가 변경되어 기존에 설정해 둔 경로를 사용할 수 없게 되므로, Redirect URI 값이 고정되어야 하는 애플 소셜 로그인에는 사용하기 어렵다.</p>
<p>다행히도, ngrok에서 회원가입을 하면 URI를 고정시킬 수 있는 기능을 제공한다.
회원가입 후 아래 명령어를 터미널에 입력하여 고정된 URI를 사용할 수 있다.<br><br></p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/1c6c1a1b-d747-43c6-af71-5c4b9b113643/image.png" alt=""></p>
<p>이제 아래 명령어로 ngrok을 실행시켜주면 끝이다!</p>
<pre><code class="language-bash">% ngrok http localhost:8080</code></pre>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/168194f3-ecd1-4055-a83d-502599d9792f/image.png" alt=""></p>
<p>ngrok을 실행하면 위와 같은 화면이 나타나고, 애플 개발자 계정에 이 포워딩 주소를 이용하여 Redirect URI를 설정한 후 테스트를 진행하면 된다.</p>
<p>하단에는 어떤 요청이 들어왔고, 어떤 상태 코드를 반환했는지 실시간으로 기록된다. 
(에러가 난게 찍혔다 하하)</p>
<p>ngrok의 자세한 사용법은 <a href="https://ngrok.com/docs/getting-started/">ngrok 공식문서</a>에서 확인할 수 있다.
<br></p>
<p>이 방법으로 애플 소셜 로그인 테스트를 마치고 1차 배포를 무사히 완료했다. 😃
<br><br></p>
<h2 id="2-애플-탈퇴-토큰-관리">2. 애플 탈퇴 토큰 관리</h2>
<p>두 번째로 문제가 되었던 부분이다.</p>
<p>애플 소셜 로그인을 통해 서비스에 가입한 사용자가 탈퇴를 원할 경우, 애플 서버에서 사용자의 연결을 끊는 과정이 필요하다. 이는 애플에서 제공하는 (<a href="https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens">공식문서</a>)에 설명되어 있다.
<br></p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/3227f736-a304-4473-a389-538496eccd60/image.png" alt=""></p>
<p>이 과정에서 보이는 것처럼, <code>token</code> 값이 필요하다.</p>
<p>이 <code>token</code> 값은 사용자가 로그인할 때 발급받을 수 있는 정보이며, 토큰의 종류는 총 두 가지로 나뉜다.</p>
<ul>
<li>만료 기간이 1시간인 <code>access_token</code></li>
<li>만료 기간이 없는 <code>refresh_token</code>이다.</li>
</ul>
<p>처음 구현 방식에서는 사용자가 탈퇴 직전에 클라이언트 측에서 재로그인을 통해 <code>access_token</code> 과 <code>refresh_token</code>을 다시 발급받고, 서버가 이 정보를 받아 애플 서버에 토큰 취소 요청을 보내도록 했다.</p>
<p>이 방식 자체에는 문제가 없었으나, 클라이언트 측에서 플로우가 다소 불편한 부분이 있었고, 사용자가 탈퇴하려면 재로그인하는 부분이 어색하게 느껴질 수 있었다.</p>
<p>이에 따라, 사용자가 탈퇴 직전에 재로그인하지 않고, 최초 로그인 시 발급받은 정보를 저장해 두었다가 탈퇴 시 사용하는 것이 더 나을 것이라는 제안을 받았다.</p>
<p>하지만, 아래와 같은 우려가 따라왔다.
&#39;탈퇴할지 모르는 사용자의 <strong>탈퇴를 위한 정보</strong>를 데이터베이스에 저장해 둔다는 것이 과연 적절한가? 이는 성능 저하를 불러오지 않을까?&#39;</p>
<p>그러나 Apple 토큰의 값은 JWT 형식의 일반 문자열이다. 이러한 토큰 정보를 사용자마다 하나씩 저장한다고 해서 성능에 미치는 영향은 매우 미미할 것이라고 판단했고, 서비스 자체가 아주 많은 고객을 거느리고 있지 않기 때문에 성능 우려는 크지 않았다.</p>
<p>또한, 새로운 방식의 경우 처음 로그인 시 발급받은 <code>refresh_token</code>은 만료 기간이 없고, 이 토큰을 사용해 탈퇴를 처리할 수 있어 재로그인 및 추가적인 토큰 발급 과정이 필요 없다는 점에서 훨씬 간편하다는 이점이 있었다. 이러한 이유로 새로운 방식을 채택하기로 결정했다.</p>
<p>따라서 애플 사용자 탈퇴 로직은 아래와 같이 최종적으로 선정했다.</p>
<blockquote>
<ol>
<li>탈퇴 API 호출</li>
<li>DB에서 해당 사용자의 Apple <code>refresh_token</code> 값 조회</li>
<li>Apple 서버에 <code>refresh_token</code>으로 해당 사용자 토큰 취소 요청</li>
<li>DB에서 사용자 삭제</li>
</ol>
</blockquote>
<br>
그림으로 표현하면 아래와 같다!

<p><img src="https://velog.velcdn.com/images/chae_ag/post/9e18b10f-dbf9-4ce6-a7c1-57097ed0b117/image.png" alt="">
<br></p>
<h2 id="3-자꾸-발생하는-client_id-오류">3. 자꾸 발생하는 client_id 오류</h2>
<p>애플 서버에 소셜 로그인 요청을 보낼 때, 500 에러가 발생하여 로그를 확인해본 결과, 아래와 같은 에러 메시지가 나타났다.</p>
<pre><code class="language-json">400 Bad Request: {&quot;error&quot;:&quot;invalid_grant&quot;,&quot;error_description&quot;:&quot;client_id mismatch. The code was not issued to {client_id값}.&quot;}</code></pre>
<p>해당 에러는 토큰 발급 요청 시 전달된 client_id 값이 애플 서버에서 기대하는 값과 일치하지 않는 경우 발생한다. </p>
<p>문제를 확인해본 결과, 애플 로그인에서 client_id 값은 특정 상황에 따라 서로 다른 두 가지 ID를 사용해야 하며, 이를 잘못 지정한 것이 원인이었다.</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/39bb108b-70a0-49d4-8f26-e117ba97e68a/image.png" alt=""></p>
<p>애플 로그인을 구현할 때, 아래와 같이 App IDs와 Service IDs라는 두 가지 ID를 생성하게 된다. 
이 두 ID는 로그인 방식에 따라 다르게 사용해야 하고, 잘못된 ID를 사용하면 인증 요청이 실패하게 된다.</p>
<ol>
<li><p><strong>App IDs</strong>
사용 시점: iOS 앱 내에서 로그인 연동을 진행하는 경우.
설명: iOS 앱의 Bundle ID를 기반으로 생성되며, 애플 앱 생태계에서 인증 요청을 처리할 때 사용된다.</p>
</li>
<li><p><strong>Service IDs</strong>
사용 시점: 웹사이트 또는 웹뷰 환경에서 로그인 연동을 진행하는 경우.
설명: 웹 기반 서비스에서 애플 로그인을 처리하기 위해 별도로 생성한 ID로, 애플 Developer Console에서 설정한 Service ID와 일치해야 한다.</p>
</li>
</ol>
<br>

<p>우리 서비스에서는 앱 내부에서 로그인을 진행하고 있었지만, 잘못된 ID(Service IDs)를 client_id로 전달하고 있었다. 이를 앱 환경에 맞는 <strong>App IDs</strong>로 변경한 후, 애플 서버와의 인증이 정상적으로 이루어졌다!</p>
<br>

<hr>
<blockquote>
<p>애플 소셜 로그인은 다른 소셜 로그인보다 더 까다로운 점이 많아 처음 도입할 때 시행착오를 많이 겪었다. 특히 테스트 환경의 제약이나 탈퇴 토큰 관리 같은 부분은 예상치 못했던 도전 과제였다.
이번 글을 통해 내가 겪은 문제와 해결 과정을 공유하면서, 비슷한 어려움을 겪고 있는 사람들에게 조금이라도 도움이 되었으면 한다. 😊</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot와 Discord 웹훅으로 실시간 신고 알림 시스템 구현하기
]]></title>
            <link>https://velog.io/@chae_ag/%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%9B%B9-%ED%9B%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%8B%A0%EA%B3%A0-%EC%95%8C%EB%A6%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chae_ag/%EB%94%94%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%9B%B9-%ED%9B%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%8B%A0%EA%B3%A0-%EC%95%8C%EB%A6%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 12 Nov 2024 17:03:55 GMT</pubDate>
            <description><![CDATA[<p>현재 개발 중인 &quot;웹소설 유저를 위한 기록 &amp; 커뮤니티 서비스 웹소소&quot;에는 신고 서비스가 존재한다.
다른 사용자가 작성한 피드 글 또는 댓글이 부적절한 표현 또는 스포일러가 포함되어 있다고 생각했을 때 두 가지 유형의 신고가 가능하다.
우리 팀은 신고가 발생했을 때 이유와 글의 본문 내용 등을 바로바로 확인할 수 있어야 할 것 같다고 판단하여, 현재 사용 중인 협업 툴인 디스코드로 신고 알림을 받기로 결정했다.
<br></p>
<h2 id="디스코드-설정">디스코드 설정</h2>
<blockquote>
<p>웹훅(webhook)은 Discord 채널에 메시지를 게시하는 간편한 방법입니다.
<a href="https://discord.com/developers/docs/resources/webhook">- Discord webhook 공식 문서</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/a288b297-6fa9-4c17-98a1-a247613f3ccc/image.png" alt=""></p>
<p>먼저 사용 중인 팀 디스코드 서버에 웹훅을 생성해 줘야 한다.
<br></p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/47d02b60-41bd-49bb-8873-6051f8473684/image.png" alt=""></p>
<p>별다른 과정 없이 바로 생성이 가능하다.
생성 후 사진처럼 이름과 알림을 받을 채널을 등록해 줘야 한다.
그리고 저 웹후크 URL을 복사해두면 된다!
<br><br></p>
<h2 id="spring-프로젝트에-웹훅-연동하기">Spring 프로젝트에 웹훅 연동하기</h2>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/b0cb2131-aaff-43dd-81af-3c70e5b6a3c9/image.png" alt=""></p>
<p>Discord가 제공하는 웹훅 공식 문서에 따르면 다양한 옵션들을 사용할 수 있는데, 우리는 간단한 알림만 받아보면 됐었기에 내용(content) 파라미터만 사용했다.
<br><br></p>
<pre><code class="language-java">public record DiscordWebhookMessage(
        String content
) {
    public static DiscordWebhookMessage of(String content) {
        if (content.length() &gt;= 2000) {
            content = content.substring(0, 1993) + &quot;\n...```&quot;;
        }
        return new DiscordWebhookMessage(content);
    }
}
</code></pre>
<p>메세지를 담을 클래스를 만들어줬다.
신고 내용이 너무 길면 가독성이 떨어질 수 있어서, 메시지 길이를 2000자 이하로 제한하는 코드를 추가해주었다.
<br><br></p>
<pre><code class="language-java">import static org.websoso.WSSServer.domain.common.ReportedType.IMPERTINENCE;
import static org.websoso.WSSServer.domain.common.ReportedType.SPOILER;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.websoso.WSSServer.domain.Comment;
import org.websoso.WSSServer.domain.Feed;
import org.websoso.WSSServer.domain.User;
import org.websoso.WSSServer.domain.common.ReportedType;

public class MessageFormatter {

    private static final String DATE_TIME_FORMAT = &quot;yyyy-MM-dd HH:mm&quot;;

    private static final String FEED_REPORT_MESSAGE =
            &quot;```[%s] 피드 %s 신고가 접수되었습니다.\n\n&quot; +
                    &quot;[신고된 피드 작성자]\n&quot; +
                    &quot;유저 아이디 : %d\n&quot; +
                    &quot;유저 닉네임 : %s\n\n&quot; +
                    &quot;[신고된 피드 내용]\n%s\n\n&quot; +
                    &quot;[신고 횟수]\n총 신고 횟수 %d회.\n&quot; +
                    &quot;%s\n```&quot;;

    private static final String COMMENT_REPORT_MESSAGE =
            &quot;```[%s] 피드 댓글 %s 신고가 접수되었습니다.\n\n&quot; +
                    &quot;[신고된 댓글 작성자]\n&quot; +
                    &quot;유저 아이디 : %d\n&quot; +
                    &quot;유저 닉네임 : %s\n\n&quot; +
                    &quot;[신고된 댓글 내용]\n%s\n\n&quot; +
                    &quot;[신고 횟수]\n총 신고 횟수 %d회.\n&quot; +
                    &quot;%s\n```&quot;;

    public static String formatFeedReportMessage(Feed feed, ReportedType reportedType, int reportedCount,
                                                 boolean isHidden) {
        String hiddenMessage = isHidden ? &quot;해당 피드는 숨김 처리되었습니다.&quot; : &quot;해당 피드는 숨김 처리되지 않았습니다.&quot;;

        return String.format(
                FEED_REPORT_MESSAGE,
                LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)),
                reportedType.getDescription(),
                feed.getUser().getUserId(),
                feed.getUser().getNickname(),
                feed.getFeedContent(),
                reportedCount,
                hiddenMessage
        );
    }

    public static String formatCommentReportMessage(Comment comment, ReportedType reportedType, User user,
                                                    int reportedCount, boolean isHidden) {
        String hiddenMessage = &quot;해당 댓글은 현재 숨김 처리되지 않은 상태입니다.&quot;;
        if (isHidden) {
            if (reportedType.equals(SPOILER)) {
                hiddenMessage = &quot;해당 댓글은 스포일러 댓글로 지정되었습니다.&quot;;
            } else if (reportedType.equals(IMPERTINENCE)) {
                hiddenMessage = &quot;해당 댓글은 부적절한 내용으로 인해 숨김 처리되었습니다.&quot;;
            }
        }

        return String.format(
                COMMENT_REPORT_MESSAGE,
                LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)),
                reportedType.getDescription(),
                user.getUserId(),
                user.getNickname(),
                comment.getCommentContent(),
                reportedCount,
                hiddenMessage
        );
    }
}</code></pre>
<p>그리고 알림의 내용 포맷을 정의할 클래스를 만들어줬다.
피드와 댓글 신고 알림 메시지는 서로 다른 양식을 사용하기 때문에 신고된 콘텐츠의 종류와 상태에 따라 자동으로 알맞은 메시지를 포맷팅하도록 했다.
<br><br></p>
<pre><code class="language-java">import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpStatus.NO_CONTENT;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.websoso.WSSServer.domain.common.DiscordWebhookMessage;

@Service
@Slf4j
public class MessageService {

    @Value(&quot;${logging.discord.webhook-url}&quot;)
    String discordWebhookUrl;

    public void sendDiscordWebhookMessage(DiscordWebhookMessage message) {
        try {
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.add(&quot;Content-Type&quot;, &quot;application/json; utf-8&quot;);
            HttpEntity&lt;DiscordWebhookMessage&gt; messageEntity = new HttpEntity&lt;&gt;(message, httpHeaders);

            RestTemplate template = new RestTemplate();
            ResponseEntity&lt;String&gt; response = template.exchange(
                    discordWebhookUrl,
                    POST,
                    messageEntity,
                    String.class
            );

            if (response.getStatusCode().value() != NO_CONTENT.value()) {
                log.error(&quot;메시지 전송 이후 에러 발생&quot;);
            }

        } catch (Exception e) {
            log.error(&quot;에러 발생 :: &quot; + e);
        }
    }
}</code></pre>
<p>이제 마지막으로 실제로 디스코드와 통신해 메세지를 전송할 서비스 클래스를 만들어준다.
디스코드 웹훅 URL은 복사해두었던 것을 그대로 사용하되, 주소가 공개되면 안되기 때문에 yml 파일에 등록해주었다.
RestTemplate를 이용하여 POST로 디스코드 웹훅에게 알림을 요청하고 있는 것을 확인할 수 있다.
<br><br></p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/38816f2a-f013-4cdb-8603-7bd3464a14c6/image.png" alt=""></p>
<p>실제로 신고가 발생하면 알림이 잘 발송되었다 🙌
<br><br></p>
<hr>
<blockquote>
<p>이렇게 디스코드 웹훅을 사용해서 신고 발생 시 디스코드 채널에 실시간으로 알림을 전송해, 팀원들이 바로 신고 내용을 확인할 수 있도록 구현했다.
비교적 간편하게 구현이 가능해서 간단한 로그를 확인하는 데 유용하게 사용이 가능할 것 같다!</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 코드 주도 개발 시작하기 5장 정리]]></title>
            <link>https://velog.io/@chae_ag/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-5%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@chae_ag/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-5%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 13 Dec 2023 11:31:01 GMT</pubDate>
            <description><![CDATA[<h2 id="junit-5-모듈-구성">JUnit 5 모듈 구성</h2>
<p>크게 3개의 요소로 구성되어 있다.</p>
<ul>
<li>JUnit 플랫폼<ul>
<li>테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공한다.</li>
</ul>
</li>
<li>JUnit 주피터<ul>
<li>JUnit 5를 위한 테스트 API와 실행 엔진을 제공한다.</li>
</ul>
</li>
<li>JUnit 빈티지<ul>
<li>JUnit 3과 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈을 제공한다.</li>
</ul>
</li>
</ul>
<p>JUnit 5 <code>build.gradle</code> 설정</p>
<pre><code class="language-java">dependencies {
    testImplementation(&#39;org.junit.jupiter:junit-jupiter:5.5.0&#39;)
}
test {
    useJUnitPlatform()
}</code></pre>
<ul>
<li>junit-jupiter 의존 추가</li>
<li>test 태스크 JUnit 5 플랫폼 사용하도록 설정</li>
</ul>
<br>

<h2 id="test-어노테이션과-테스트-메서드">Test 어노테이션과 테스트 메서드</h2>
<p>JUnit을 이용해서 테스트 코드 만드는 법</p>
<ul>
<li>테스트로 사용할 클래스를 만들고 <code>@Test</code> 어노테이션을 메서드에 붙이기만 하면 된다.</li>
</ul>
<pre><code class="language-java">import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class SumTest {

    @Test
    void sum() {
        int result = 2 + 3;
        assertEquals(5, result);
    }
}</code></pre>
<ul>
<li>테스트 클래스의 이름은 보통 다른 클래스와 구분을 쉽게 하기 위해 ‘Test’를 접미사로 붙인다.</li>
<li><code>@Test</code> 어노테이션을 붙인 메서드는 private이면 안 된다.</li>
</ul>
<br>

<h2 id="주요-단언-메서드">주요 단언 메서드</h2>
<p>Assertions 클래스는 여러 단언 메서드를 제공한다.</p>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>assertEquals(expected, actual)</td>
<td>실제 값(actual)이 기대하는 값(expected)과 같은지 검사한다.</td>
</tr>
<tr>
<td>assertNotEquals(unexpected, actual)</td>
<td>실제 값(actual)이 특정 값(unexpected)과 같지 않은지 검사한다.</td>
</tr>
<tr>
<td>assertSame (Object expected, Object actual)</td>
<td>두 객체가 동일한 객체인지 검사한다.</td>
</tr>
<tr>
<td>assertNotSame (Object unexpected, Object actual)</td>
<td>두 객체가 동일하지 않은 객체인지 검사한다.</td>
</tr>
<tr>
<td>assertTrue (boolean condition)</td>
<td>값이 true인지 검사한다.</td>
</tr>
<tr>
<td>assertFalse (boolean condition)</td>
<td>값이 false인지 검사한다.</td>
</tr>
<tr>
<td>assertNull (Object actual)</td>
<td>값이 null인지 검사한다.</td>
</tr>
<tr>
<td>assertNotNull (Object actual)</td>
<td>값이 null이 아닌지 검사한다.</td>
</tr>
<tr>
<td>fail()</td>
<td>테스트를 실패 처리한다.</td>
</tr>
</tbody></table>
<h3 id="assertequals-메서드">assertEquals() 메서드</h3>
<ul>
<li>주요 타입별로 존재한다.<ul>
<li>int 타입을 위한 assertEquals() 메서드, Long 타입을 위한 assertEquals() 메서드 등</li>
</ul>
</li>
<li>assertEquals(expected, actual)<ul>
<li>첫 번째 인자가 기대하는 값이고 두 번째 인자가 검사하려는 값이다.</li>
</ul>
</li>
</ul>
<h3 id="fail-메서드">fail() 메서드</h3>
<ul>
<li>테스트에 실패했음을 알리고 싶을 때 사용한다.</li>
<li>e.g) 파라미터 값이 null이면 IllegalArgumentException이 발생하도록 구현한 상태에서 null을 전달했는데, 익셉션이 발생하지 않으면 테스트에 실패했다고 볼 수 있다.</li>
</ul>
<pre><code class="language-java">try {
    AuthService authService = new AuthService();
    authService.authenticate(null, null);
    fail(); // 이 지점에 다다르면 fail()메서드는 테스트 실패 에러를 발생
} catch(IllegalArgumentException e) {
}</code></pre>
<p><strong>but, 익셉션 발생 유무가 검증 대상이라면 fail() 메서드를 사용하는 것보다 따로 제공하는 익셉션 발생 유무 검사 메서드를 사용하는 것이 더욱 명시적이다.</strong></p>
<h3 id="assertthrows">assertThrows()</h3>
<p>assertThrows() 을 이용해서 지정한 익셉션이 발생하는지 검사하는 코드</p>
<pre><code class="language-java">assertThrows(IllegalArgumentException.class,
    () -&gt; {
        AuthService authService = new AuthService();
        authService.authenticate(null, null);
    });</code></pre>
<ul>
<li>assertThrows() 메서드는 발생한 익셉션 객체를 리턴한다.</li>
<li>발생한 익셉션을 이용해서 추가로 검증이 필요하다면 assertThrows() 메서드가 리턴한 익셉션 객체를 사용하면 된다.</li>
</ul>
<pre><code class="language-java">assertThrows(IllegalArgumentException.class,
    () -&gt; {
        AuthService authService = new AuthService();
        authService.authenticate(null, null);
    });
assertTure(thrown.getMessage().contains(&quot;id&quot;));</code></pre>
<h3 id="assertall">assertAll()</h3>
<ul>
<li>assert 메서드는 실패하면 다음 코드를 실행하지 않고 바로 익셉션을 발생한다.</li>
<li>하지만, 경우에 따라 일단 모든 검증을 실행하고 그중에 실패한 것이 있는지 확인하고 싶을 때, assertAll() 메서드를 사용할 수 있다.</li>
</ul>
<pre><code class="language-java">assertAll(
        () -&gt; assertEquals(3, 5/2),
        () -&gt; assertEquals(4, 2*2),
        () -&gt; assertEquals(6, 11/2)
);</code></pre>
<ul>
<li>assertAll() 메서드는 실행 결과로 검증에 실패한 코드가 있으면 그 목록을 모아서 에러 메세지로 보여준다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/9f0b7a5a-d433-45d4-8e3c-1e8a31de965f/image.png" alt=""></p>
<br>

<h2 id="테스트-라이프사이클">테스트 라이프사이클</h2>
<aside>
💡 JUnit의 테스트 메서드 코드 실행 순서

<ol>
<li>테스트 메서드를 포함한 객체 생성</li>
<li>(존재하면) @BeforeEach 어노테이션이 붙은 메서드 실행</li>
<li>@Test 어노테이션이 붙은 메서드 실행</li>
<li>(존재하면) @AfterEach 어노테이션이 붙은 메서드 실행</aside>
</li>
</ol>
<ul>
<li>간단한 테스트 코드 예</li>
</ul>
<pre><code class="language-java">import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class LifecycleTest {
    public LifecycleTest() {
        System.out.println(&quot;new LifecycleTest&quot;);
    }

    @BeforeEach
    void setUp() {
        System.out.println(&quot;setUp&quot;);
    }

    @Test
    void a() {
        System.out.println(&quot;A&quot;);
    }

    @Test
    void b() {
        System.out.println(&quot;B&quot;);
    }

    @AfterEach
    void tearDown() {
        System.out.println(&quot;tearDown&quot;);
    }
}</code></pre>
<ul>
<li>실행 결과</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/6013faaa-1f32-45d2-8440-ed66b141ce3e/image.png" alt=""></p>
<p>~ 결과를 보고 알 수 있는 사실 ~</p>
<ul>
<li>@Test 메서드를 실행할 때마다 객체를 새로 생성한다.</li>
<li>테스트 메서드를 실행 하기 전과 후에 @BeforeEach 어노테이션과 @AfterEach 어노테이션을 붙인 메서드를 실행한다.</li>
</ul>
<h3 id="beforeeach-어노테이션">@BeforeEach 어노테이션</h3>
<ul>
<li>테스트를 실행하는데 필요한 준비 작업을 할 때 사용한다.<ul>
<li>테스트에서 사용할 임시 파일, 객체 등을 생성</li>
</ul>
</li>
</ul>
<h3 id="aftereach-어노테이션">@AfterEach 어노테이션</h3>
<ul>
<li>테스트를 실행한 후에 정리할 것이 있을 때 사용한다.<ul>
<li>테스트에서 사용한 임시 파일을 삭제해야 할 때 등</li>
</ul>
</li>
</ul>
<p>💡 <em>@BeforeEach, @AfterEach 어노테이션을 붙인 메서드는 @Test 어노테이션과 마찬가지로 private면 안 된다.</em></p>
<h3 id="beforeall-어노테이션">@BeforeAll 어노테이션</h3>
<ul>
<li>한 클래스의 모든 테스트 메서드가 실행되기 전에 특정 작업을 수행해야 할 때 사용한다.</li>
<li>정적 메서드에 붙여서 사용하는데, 이 메서드는 클래스의 모든 테스트 메서드를 실행하기 전에 한 번 실행된다.</li>
</ul>
<h3 id="afterall-어노테이션">@AfterAll 어노테이션</h3>
<ul>
<li>클래스의 모든 테스트 메서드를 실행한 뒤에 실행된다.</li>
<li>정적 메서드에 적용한다.</li>
</ul>
<br>

<h2 id="테스트-메서드-간-실행-순서-의존과-필드-공유하지-않기">테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기</h2>
<ul>
<li>테스트 메서드가 특정 순서대로 실행된다는 가정하에 테스트 메서드를 작성하면 안 된다.</li>
<li>테스트 메서드를 작성한 순서에 상관 없이 테스트 순서가 결정된다.<ul>
<li>메서드간의 순서가 의존되어 있다면 순서가 달라졌을 때 테스트는 실패한다.</li>
</ul>
</li>
</ul>
<p>즉, 각 테스트 메서드는 서로 독립적으로 동작해야 하고, 한 테스트 메서드의 결과에 따라 다른 테스트 메서드의 실행 결과가 달라지면 안 된다.</p>
<p><strong><em>→ 테스트 메서드가 서로 필드를 공유하거나 실행 순서를 가정하고 테스트를 작성하지 말아야 한다!</em></strong></p>
<h3 id="displayname-어노테이션">@DisplayName 어노테이션</h3>
<p>자바는 메서드 이름에 공백이나 특수 문자를 사용할 수 없기 때문에 메서드 이름만으로 테스트 내용을 설명하기가 부족할 수 있기에 @DisplayName 어노테이션을 사용해서 테스트에 표시 이름을 붙인다.</p>
<pre><code class="language-java">@DisplayName(&quot;라이프사이클 테스트&quot;)
public class LifecycleTest {
    @DisplayName(&quot;에이!&quot;)
    @Test
    void a() {
        System.out.println(&quot;A&quot;);
    }

    @Test
    void b() {
        System.out.println(&quot;B&quot;);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/2bd18eb2-1033-4abd-986f-a0988a1657a2/image.png" alt=""></p>
<h3 id="disabled-어노테이션">@Disabled 어노테이션</h3>
<ul>
<li>특정 테스트를 실행하지 않고 싶을 때 사용한다.</li>
<li>@Disabled 어노테이션이 붙은 클래스나 메서드는 테스트 실행 대상에서 제외한다.</li>
<li>아직 테스트 코드가 완성되지 않았거나 잠시 동안 테스트를 실행하지 말아야 할 때 이 어노테이션을 사용한다.</li>
</ul>
<br>

<h2 id="모든-테스트-실행하기">모든 테스트 실행하기</h2>
<p><code>src/test/java</code> 폴더에서 테스트를 실행하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/ad8661f5-3269-4acc-a816-cc173232fe77/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[테스트 코드 작성 순서, TDD · 기능 명세 · 설계]]></title>
            <link>https://velog.io/@chae_ag/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-%EC%88%9C%EC%84%9C-TDD-%EA%B8%B0%EB%8A%A5-%EB%AA%85%EC%84%B8-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@chae_ag/%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-%EC%88%9C%EC%84%9C-TDD-%EA%B8%B0%EB%8A%A5-%EB%AA%85%EC%84%B8-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 20 Nov 2023 06:27:10 GMT</pubDate>
            <description><![CDATA[<h1 id="테스트-코드-작성-순서">테스트 코드 작성 순서</h1>
<h3 id="초반에-복잡한-테스트부터-시작하면-안되는-이유">초반에 복잡한 테스트부터 시작하면 안되는 이유</h3>
<ul>
<li>초반부터 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가하면 해당 테스트를 통과시키기 위해 한 번에 구현해야 할 코드가 많아진다.</li>
</ul>
<h2 id="eg-비밀번호-검사-테스트">e.g. 비밀번호 검사 테스트</h2>
<p>👉 <strong>다음과 같은 순서로 테스트를 만들어보자.</strong></p>
<ol>
<li>대문자 포함 규칙만 충족하는 경우</li>
<li>모든 규칙을 충족하는 경우</li>
<li>숫자를 포함하지 않고 나머지 규칙은 충족하는 경우<br>

</li>
</ol>
<h3 id="1-대문자-포함-규칙만-충족하는-경우-테스트">1. 대문자 포함 규칙만 충족하는 경우 테스트</h3>
<ul>
<li>테스트 코드 작성</li>
</ul>
<pre><code class="language-java">@Test
void 대문자_포함_규칙만_충족() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(&quot;abcDef&quot;);
    assertEquals(PasswordStrength.WEAK, result);
}</code></pre>
<ul>
<li>구현</li>
</ul>
<pre><code class="language-java">public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return PasswordStrength.WEAK;
    }
}</code></pre>
<h3 id="2-모든-규칙을-충족하는-경우-테스트">2. 모든 규칙을 충족하는 경우 테스트</h3>
<ul>
<li>테스트 코드</li>
</ul>
<pre><code class="language-java">@Test
void 모든_규칙_충족() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(&quot;abcDef12&quot;);
    assertEquals(PasswordStrength.STRONG, result);
}</code></pre>
<ul>
<li>구현 - 가장 간편한 구현을 함.</li>
</ul>
<pre><code class="language-java">public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        if(&quot;abcDef12&quot;.equals(s)) return PasswordStrength.STRONG;
        return PasswordStrength.WEAK;
    }
}</code></pre>
<p>🤔 <strong>이 때, 새로운 테스트 케이스를 추가한다면?</strong></p>
<ul>
<li>테스트 케이스 추가</li>
</ul>
<pre><code class="language-java">@Test
void 모든_규칙_충족() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(&quot;abcDef12&quot;);
    assertEquals(PasswordStrength.STRONG, result);
    PasswordStrength result2 = meter.meter(&quot;aZcDef12&quot;);
    assertEquals(PasswordStrength.STRONG, result2);**
}</code></pre>
<blockquote>
<p><em>어떻게 해야 테스트를 통과할 수 있을까 ? …</em></p>
</blockquote>
<p>👉 문자열이 &quot;abcDef12&quot; 나 &quot;aZcDef12&quot; 면 STRONG을 리턴하고 나머지는 WEAK를 리턴하게 구현할까?</p>
<p>그러면 케이스를 추가할 때마다 if 절이 늘어나겠네..</p>
<p>😬 <strong>모든 케이스를 이렇게 구현할 수는 없다!!</strong></p>
<p>두번째 테스트를 통과시키려면 모든 규칙을 확인하는 코드를 벌써 구현해야 할 것 같다. </p>
<p>→ <em>막막하다.</em></p>
<ul>
<li>이런 상황이 오지 않으려면?</li>
</ul>
<h2 id="구현하기-쉬운-테스트부터-시작하기">구현하기 쉬운 테스트부터 시작하기</h2>
<h3 id="첫-번째-선택">첫 번째 선택</h3>
<ul>
<li>모든 조건을 충족하는 경우</li>
<li>모든 조건을 충족하지 않는 경우</li>
</ul>
<p>이 두 가지 경우는 모두 그냥 해당 값인 STRONG 또는 WEAK를 리턴하면 된다.</p>
<p>아무거나 선택한다.</p>
<blockquote>
</blockquote>
<p><strong>&lt;테스트 순서&gt;</strong></p>
<ol>
<li>모든 조건을 충족하는 경우</li>
</ol>
<br>

<h3 id="두-번째-선택">두 번째 선택</h3>
<ul>
<li>모든 규칙을 충족하지 않는 경우</li>
<li>한 규칙만 충족하는 경우</li>
<li>두 규칙을 충족하는 경우</li>
</ul>
<p>모든 규칙을 충족하지 않는 경우는 난감하다.</p>
<p>첫 번째 테스트를 모든 조건을 충족하는 경우를 선택했기 때문이다.</p>
<p>나머지 두 경우는 난이도가 비슷할 것 같다.</p>
<blockquote>
</blockquote>
<p><strong>&lt;테스트 순서&gt;</strong></p>
<ol>
<li>모든 조건을 충족하는 경우</li>
<li>한 규칙만 충족하는 경우</li>
<li>두 규칙을 충족하는 경우</li>
</ol>
<p>그리고 마지막에 모든 조건을 충족하지 않는 경우를 테스트 해주면 된다.
<br></p>
<h3 id="최종-테스트-순서">최종 테스트 순서</h3>
<blockquote>
</blockquote>
<p><strong>&lt;테스트 순서&gt;</strong></p>
<ol>
<li>모든 조건을 충족하는 경우</li>
<li>한 규칙만 충족하는 경우</li>
<li>두 규칙을 충족하는 경우</li>
<li>모든 조건을 충족하지 않는 경우</li>
</ol>
<br>

<h2 id="예외-상황을-먼저-테스트-해라">예외 상황을 먼저 테스트 해라</h2>
<ul>
<li><p>예외 상황을 전혀 고려하지 않은 코드에 예외 상황을 반영하려면?</p>
<ul>
<li><p>코드의 구조를 뒤집거나,</p>
</li>
<li><p>코드 중간에 예외 상황을 처리하기 위해 조건문을 중복해서 추가해야 함.</p>
<p>⇒ 코드를 복잡하게 만들어 버그 발생 가능성 ⬆</p>
</li>
</ul>
</li>
</ul>
<h3 id="초반에-예외-상황을-테스트하면-얻는-이점">초반에 예외 상황을 테스트하면 얻는 이점</h3>
<ul>
<li>예외 상황에 따른 코드의 구조가 미리 만들어짐.<ul>
<li>코드 구조의 변경이 줄어듬.</li>
</ul>
</li>
<li>개발을 하는 동안의 예외 상황을 처리하지 않아 발생하는 버그를 줄여줌.<ul>
<li>사전에 예외 상황을 찾고 테스트하면 예외 상황으로 인한 곤란을 겪지 않을 수 있음!</li>
</ul>
</li>
</ul>
<p><strong>~ TDD 시 얻는 이점 ~</strong></p>
<h2 id="완급-조절">완급 조절</h2>
<ul>
<li>TDD가 익숙해지면 상황에 따라 구현 속도를 조절할 수 있게 된다.</li>
</ul>
<p>한 번에 기능 구현을 시도했는데 잘 안된다면 한발 물러서서 천천히 아래 단계를 밟아가면 된다.</p>
<ol>
<li>정해진 값을 리턴</li>
<li>값 비교를 이용해서 정해진 값을 리턴</li>
<li>다양한 테스트를 추가하면서 구현을 일반화</li>
</ol>
<h2 id="지속적인-리팩토링">지속적인 리팩토링</h2>
<ul>
<li><p>TDD 에서는 테스트를 통과한 뒤에는 리팩토링을 진행한다.</p>
<ul>
<li>매번 해야 하는 것은 아님.</li>
<li>적당한 후보가 보이면 진행</li>
</ul>
</li>
<li><p>TDD를 진행하는 과정에서 지속적으로 리팩토링을 진행하면 코드 가독성이 높아진다.</p>
<ul>
<li><p>코드 가독성이 높아지면 개발자는 더욱 빠르게 코드 분석 가능</p>
</li>
<li><p>수정 요청이 있을 때 변경할 코드 빠르게 찾을 수 있음.</p>
</li>
<li><p><em>⇒ 코드 변경의 어려움을 줄여주어 향후 유지보수에 도움이 된다.*</em></p>
<br>


</li>
</ul>
</li>
</ul>
<h1 id="tdd-·-기능-명세-·-설계">TDD · 기능 명세 · 설계</h1>
<h2 id="기능-명세">기능 명세</h2>
<ul>
<li>크게 <strong>입력</strong>과 <strong>결과</strong>로 나눌 수 있다.<ul>
<li><strong>입력</strong>은 기능을 실행하는데 필요한 값</li>
<li><strong>결과</strong>는 기능을 실행했을 때 얻는 값</li>
</ul>
</li>
</ul>
<h3 id="eg-로그인-기능">e.g) 로그인 기능</h3>
<ul>
<li>입력<ul>
<li>아이디와 암호</li>
</ul>
</li>
<li>결과<ul>
<li>아이디와 암호가 일치하면 성공</li>
<li>아이디와 암호가 일치하지 않으면 실패</li>
</ul>
</li>
</ul>
<pre><code class="language-java">public String login(String id, String pw) {
    User user = getUser(id);
    if (!user.matchPassword(pw)) {
        throw new IdPwNotMatchException();
    }
    return &quot;로그인 되었습니다.&quot;;
}</code></pre>
<ul>
<li><p>입력</p>
<ul>
<li>입력은 보통 메서드의 파라미터로 전달한다.<ul>
<li><code>id</code></li>
<li><code>pw</code></li>
</ul>
</li>
</ul>
</li>
<li><p>결과</p>
<ul>
<li>결과 형식은 여러 형식으로 정의할 수 있다.</li>
<li>리턴값과 익셉션을 결과로 사용했다.<ul>
<li><code>&quot;로그인 되었습니다.&quot;</code></li>
<li><code>IdPwNotMatchException</code></li>
</ul>
</li>
</ul>
</li>
<li><p>‘결과’ 에는 변경도 포함한다.</p>
<ul>
<li>e.g) 회원 가입 기능<ul>
<li>실행 결과 : DB에 회원 정보 추가 ← 리턴값이 없는 ‘변경’
<img src="https://velog.velcdn.com/images/chae_ag/post/97f60493-c3bc-43fa-9237-1c6628fe5bcb/image.png" alt=""></li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="기능-명세로부터-설계가-시작되는-과정">기능 명세로부터 설계가 시작되는 과정</h3>
<p>스토리보드를 포함한 다양한 형태의 요구사항 문서를 이용해, 기능 명세 구체화</p>
<p>➡ 기능 명세를 구체화하는 동안 입력과 결과를 도출하고, 도출한 기능 명세를 코드에 반영</p>
<p>➡ 기능 명세의 입력과 결과를 코드에 반영하는 과정에서 기능의 이름, 파라미터, 리턴 타입 등이 결정됨.</p>
<p>➡ 이는 곧 기능에 대한 설계 과정과 연결됨.</p>
<h2 id="설계-과정을-지원하는-tdd">설계 과정을 지원하는 TDD</h2>
<ul>
<li>TDD는 테스트를 만드는 것부터 시작.</li>
<li>테스트 코드를 먼저 만들기 위해 필요한 것<ul>
<li>테스트할 기능을 실행</li>
<li>실행 결과를 검증</li>
</ul>
</li>
</ul>
<h3 id="테스트할-기능을-실행">테스트할 기능을 실행</h3>
<ul>
<li>기능을 실행할 수 없으면 테스트를 할 수 없다.<ul>
<li>즉, 테스트에서 실행할 수 있는 객체나 함수가 존재해야 한다.<ul>
<li>e.g) 테스트 대상이 되는 클래스와 메서드 이름 결정</li>
<li>e.g) 메서드를 실행할 때 사용할 인자의 타입, 개수 결정</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="실행-결과를-검증">실행 결과를 검증</h3>
<ul>
<li>실행 결과를 어떻게 검증할지 고민한다.<ul>
<li>e.g) 리턴 값을 이용해서 검증하는 테스트<ul>
<li>리턴 타입을 결정</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>⇒ 이렇게 TDD에서 테스트를 작성할 때 고민하는 부분은 곧 설계 과정과 유사하다.</strong></p>
<blockquote>
<p>💡 <em>즉, TDD 자체가 설계는 아니지만, TDD를 하다 보면 테스트 코드를 작성하는 과정에서 일부 설계를 진행하게 된다!</em></p>
</blockquote>
<h3 id="필요한-만큼-설계하기">필요한 만큼 설계하기</h3>
<ul>
<li>TDD는 테스트를 통과할 만큼만 코드를 작성한다.<ul>
<li>즉, TDD로 개발을 진행하면 현시점에서 테스트를 통과시키는데 필요한 만큼의 코드만 만들게 된다.</li>
</ul>
</li>
<li>필요할 것으로 예측해서 미리 설계를 유연하게 만들지 않는다.</li>
</ul>
<blockquote>
<p>👉 이를 통해 설계가 불필요하게 복잡해지는 것을 방지할 수 있으며, 요구사항이 바뀌더라도 TDD는 미리 앞서서 코드를 만들지 않으므로 불필요한 구성 요소를 덜 만들게 된다.</p>
</blockquote>
<br>

<h2 id="기능-명세-구체화">기능 명세 구체화</h2>
<ul>
<li>개발자는 보통 기획자가 작성한 스토리보드나 와이어프레임과 같은 형태로 <strong>요구사항 명세</strong>를 전달 받는다.<ul>
<li>이런 문서는 사용자나 기획자가 보기에는 적당할지 모르나 개발자가 기능을 구현하기에는 생략된 내용이 많다.</li>
</ul>
</li>
<li><strong>테스트 코드</strong>를 작성하려면 파라미터와 결과 값을 정해야 하므로 개발자는 <strong>요구사항 문서</strong>에서 <strong>기능의 입력과 결과를 도출</strong>해야 한다.</li>
</ul>
<p><strong>⇒ 테스트 코드를 통해 개발자는 기능 명세를 구체화할 수 있다.</strong></p>
<ul>
<li><p>모호한 상황을 만나면 이를 구체적인 예로 바꾸어 테스트 코드에 반영한다.</p>
</li>
<li><p><strong>예</strong></p>
<p>  <strong>요구사항 : 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.</strong></p>
<ul>
<li><p><em>4월 1일에 만원을 납부하면 만료일은 4월 30일? 5월 1일?</em></p>
</li>
<li><p><em>1월 31일에 만원을 납부하면 만료일은 2월 28일? 3월 2일?</em></p>
<p>위와 같이 모호한 상황이 생긴다면 기획자와의 소통으로 답을 얻은 뒤, 이를 구체적인 예로 바꾸어 테스트에 구현한다.</p>
</li>
<li><p><strong>이는 예를 이용한 구체적인 명세가 된다.</strong></p>
<ul>
<li><p>구체적인 예는 개발자가 요구사항을 더 잘 이해할 수 있게 만들고,</p>
<p>  모호함을 없애 주어 개발자가 올바르게 동작하는 기능을 만들 수 있게 한다.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<blockquote>
<p>👉 TDD는 <strong>처음 접하는 보드게임을 익히는 과정</strong>과 유사하다.</p>
<blockquote>
<p>처음 접하는 보드게임을 하려면 게임 규칙을 배운다.
하지만 규칙을 들었다고 해서 바로 완벽하게 이해를 하는 것은 어렵고 실제 게임을 진행하면서 다양한 상황에 규칙을 적용하다 보면 규칙을 점차 이해하게 된다.
TDD도 이와 유사하게 구체적인 예를 이용해서 테스트 코드를 추가하다 보면 기능 명세를 보다 잘 이해하고 모호함을 없앨 수 있다!</p>
</blockquote>
</blockquote>
<br>

<hr>
<blockquote>
<p>이 글을 읽는 모두, 좋은 하루 되세요. 😸</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[java] 자바 스트림 Stream]]></title>
            <link>https://velog.io/@chae_ag/i01pb15k</link>
            <guid>https://velog.io/@chae_ag/i01pb15k</guid>
            <pubDate>Sat, 11 Nov 2023 05:06:16 GMT</pubDate>
            <description><![CDATA[<h3 id="💡-스트림의-흐름">💡 스트림의 흐름</h3>
<ol>
<li><strong>생성하기</strong> : 스트림 인스턴스 생성</li>
<li><strong>가공하기</strong> : 필터링 및 맵핑 등 원하는 결과를 만들어가는 중간 작업</li>
<li><strong>결과 만들기</strong> : 최종적으로 결과를 만들어내는 작업</li>
</ol>
<p><code>전체 -&gt; 맵핑 -&gt; 필터링 1 -&gt; 필터링 2 -&gt; 결과 만들기 -&gt; 결과물</code></p>
<h3 id="스트림-특징">스트림 특징</h3>
<ul>
<li>데이터를 변경하지 않는다.<ul>
<li>원본 데이터로부터 데이터를 읽기만 할 뿐, 원본 데이터 자체를 변경하지 않는다.</li>
</ul>
</li>
<li>일회용이다.<ul>
<li>한 번 사용하면 닫혀서 재사용이 불가능하다.</li>
</ul>
</li>
<li>작업을 내부 반복으로 처리한다.<ul>
<li>반복문을 메서드의 내부에 숨길 수 있다는 것 (반복문이 코드 상에 노출되지 않는다.)<br>

</li>
</ul>
</li>
</ul>
<h2 id="일반-스트림과-특화-스트림">일반 스트림과 특화 스트림</h2>
<h3 id="일반스트림">일반스트림?</h3>
<p>Stream<Integer>, Stream<String> 과 같은 것들</p>
<h3 id="특화-스트림">특화 스트림?</h3>
<p>자바 스트림 API는 숫자 스트림을 효율적으로 처리할 수 있도록 세 가지 기본형 특화 스트림을 제공한다.</p>
<p>IntStream, DoubleStream, LongStream</p>
<ul>
<li>기본형 종류</li>
</ul>
<table>
<thead>
<tr>
<th>종류</th>
<th>데이터형</th>
<th>크기(byte / bit)</th>
</tr>
</thead>
<tbody><tr>
<td>논리형</td>
<td>boolean</td>
<td>1 / 8</td>
</tr>
<tr>
<td>문자형</td>
<td>char</td>
<td>2 / 16</td>
</tr>
<tr>
<td>정수형</td>
<td>byte</td>
<td>1 / 8</td>
</tr>
<tr>
<td>정수형</td>
<td>short</td>
<td>2 / 16</td>
</tr>
<tr>
<td>정수형</td>
<td>int</td>
<td>4 / 32</td>
</tr>
<tr>
<td>정수형</td>
<td>long</td>
<td>8 / 64</td>
</tr>
<tr>
<td>실수형</td>
<td>float</td>
<td>4 / 32</td>
</tr>
<tr>
<td>실수형</td>
<td>double</td>
<td>8 / 64</td>
</tr>
</tbody></table>
<p>이러한 특화 스트림은 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다!</p>
<h3 id="특화-스트림으로-변환">특화 스트림으로 변환</h3>
<p><code>mapToInt</code>, <code>mapToDouble</code>, <code>mapToLong</code> 메서드를 이용하면 일반 스트림을 특화 스트림으로 변환할 수 있다.</p>
<pre><code class="language-java">int totalPayPrice = pays.stream()
    .mapToInt(Pay::getPayPrice)
    .sum();</code></pre>
<p>💡 <strong>저 <code>::</code> 은 도대체 뭘까?</strong>
자바의 <strong>메소드 레퍼런스</strong>라는 것으로, 람다식을 다른 방식으로 표현한 것이다.
예
람다식  <code>(text)-&gt; System.out.println(text)</code>
메소드 레퍼런스  <code>System.out :: println</code></p>
<p>메소드 레퍼런스는 <code>[ClassName] :: [MethodName]</code> 형식으로 입력하고, 
메소드를 호출하는 것이지만 괄호()는 써주지 않고 생략한다!</p>
<p><code>.mapToInt(Pay::getPayPrice)</code>
람다식로 변환하면?
<code>.mapToInt(onePay → onePay.getPayPrice)</code></p>
<p><strong>List<Integer>를 int[]로 변환하고 싶다면?</strong></p>
<p><code>mapToInt</code>이용!</p>
<pre><code class="language-java">int[] arr = list.stream()
                .mapToInt(i -&gt; i) // == mapToInt(Integer::intValue)
                .toArray();</code></pre>
<h3 id="boxed-메서드">boxed 메서드</h3>
<p>boxed() 메서드는 IntStream 같이 원시 타입에 대한 스트림 지원을 클래스 타입(예: <code>IntStream</code> -&gt; <code>Stream&lt;Integer&gt;</code>) 으로 전환해준다.</p>
<p>사용 예: int 자체로는 Collection에 못 담기 때문에 Integer 클래스로 변환하여 <code>List&lt;Integer&gt;</code> 로 담기 위해 사용한다.</p>
<pre><code class="language-java">List&lt;Integer&gt; list1 = Arrays.stream(arr) // == IntStream.of(arr)
                                                        .boxed()
                                                        .collect(Collectors.toList());</code></pre>
<br>

<h2 id="유용하고-중요한-메서드-주관적">유용하고 중요한 메서드! (주관적)</h2>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/a6f05f40-ff28-4c16-8a13-2694a2934d18/image.png" alt=""></p>
<h3 id="filter">filter</h3>
<p><strong>if문</strong>이라고 생각하면 편하다.</p>
<p><strong>일치하는 모든 요소를 포함하는 스트림</strong>으로 <strong>반환</strong>한다.</p>
<p>메뉴 List에서 베지테리안 메뉴만 찾아서 리스트로 저장하고 싶다면?</p>
<pre><code class="language-java">List&lt;Dish&gt; vegetarianMenu = menu.stream()
  .filter(Dish::isVegetarian) // 리턴값이 true면 다음 단계 진행, false면 버려짐!
  .collect(Collectors.toList());</code></pre>
<h3 id="collect">collect</h3>
<p>스트림의 요소들을 우리가 원하는 자료형으로 변환할 수 있다.
<code>toList()</code>, <code>toSet()</code>, <code>toMap()</code>, <code>toCollection()</code></p>
<pre><code class="language-java">stream.collect(Collectors.toSet()); //set으로 변환
stream.collect(Collectors.toList()); //list 변환
stream.collect(Collectors.joining()); //요소를 다 이어붙여서 하나의 string으로 변환
stream.collect(Collectors.joining(&quot;, &quot;)); //요소들 사이에 &quot;,&quot;을 넣어서 한개의 string 반환</code></pre>
<p>List나 Set이 아닌 특정 컬렉션을 지정하려면 <code>toCollection()</code> 에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 된다.</p>
<pre><code class="language-java">ArrayList&lt;String&gt; list = names.stream()
  .collect(Collectors.toCollection(ArrayList::new));</code></pre>
<ul>
<li>collect()는 아니지만, 배열로 변환하고 싶다면 <code>stream.toArray()</code> 사용!</li>
</ul>
<pre><code class="language-java">Person[] personArray1 = personStream1.toArray(Person[]::new);</code></pre>
<h3 id="map">map</h3>
<p>스트림을 우리가 원하는 모양의 새로운 스트림으로 변환할 수 있다.</p>
<p>스트림 내 문자열을 모두 대문자로 변경한 스트림 만들기?</p>
<pre><code class="language-java">Stream&lt;String&gt; stream = names.stream()
  .map(String::toUpperCase);</code></pre>
<h3 id="match">~~Match</h3>
<ul>
<li>하나라도 조건을 만족하는 요소가 있는지 <em><code>anyMatch</code></em></li>
<li>모두 조건을 만족하는지 <em><code>allMatch</code></em></li>
<li>모두 조건을 만족하지 않는지 <em><code>noneMatch</code></em></li>
</ul>
<pre><code class="language-java">List&lt;String&gt; names = Arrays.asList(&quot;Eric&quot;, &quot;Elena&quot;, &quot;Java&quot;);

boolean anyMatch = names.stream()
  .anyMatch(name -&gt; name.contains(&quot;a&quot;));
boolean allMatch = names.stream()
  .allMatch(name -&gt; name.length() &gt; 3);
boolean noneMatch = names.stream()
  .noneMatch(name -&gt; name.endsWith(&quot;s&quot;));</code></pre>
<p>결과는 모두 <strong>true!</strong></p>
<h3 id="스트림-활용-예제">스트림 활용 예제</h3>
<p>1.int[] numbers = {1, 2, 3, 4, 5};
위와 같은 배열이 있을 때,
스트림을 활용해서 아래와 같은 동작을 수행하자!</p>
<p><strong>1. 홀수만 골라서, → filter()
2. 2를 곱한 뒤, → map()
3. 배열에 저장한다. → toArray</strong></p>
<pre><code class="language-java">int[] result = Arrays.stream(numbers)
                                   .filter(num -&gt; num % 2 == 1)
                                   .map(num-&gt; num * 2) -&gt; 스트림
                                   .toArray(); </code></pre>
<p>2.int[] data = {5, 6, 4, 2, 3, 1, 1, 2, 2, 4, 8};
위와 같은 배열이 있을 때,
스트림을 활용해서 아래와 같은 동작을 수행하자!</p>
<p><strong>1. 짝수만 뽑은 뒤, → filter
2. 중복을 제거하고, → 
3. 역순으로 정렬해서 → 
4. 출력한다. → foreach</strong></p>
<pre><code class="language-java">Arrays.stream(data)
      .boxed() // IntStream에서 -&gt; Stream&lt;Integer&gt;로 변환
      .filter(n -&gt; n % 2 == 0)
      .distinct() // 중복 제거
      .sorted(Comparator.reverseOrder())
      .forEach(System.out :: println); //  == .forEach((num) -&gt; System.out.println(num))</code></pre>
<ul>
<li><code>boxed</code> 하는 이유?<ul>
<li><code>sorted(Comparator.reverseOrder())</code>를 하기 위함.
IntStream이 제공하는 <code>sorted()</code>는 내림차순이 불가능!!</li>
</ul>
</li>
</ul>
<p>3.프로그래머스 음양 더하기</p>
<pre><code class="language-java">class Solution {
    public int solution(int[] absolutes, boolean[] signs) {
        int sum = 0;
        for(int i=0; i&lt;absolutes.length; i++) {
            if(!signs[i]) {
                absolutes[i] *= -1;
            }
            sum +=  absolutes[i];
        }
        return sum;
    }
}</code></pre>
<p>스트림 활용 리팩토링</p>
<pre><code class="language-java">import java.util.stream.IntStream;
class Solution {
    public int solution(int[] absolutes, boolean[] signs) {
        int sum = IntStream.range(0, absolutes.length)
                            .map(i -&gt; !signs[i] ? -absolutes[i] : absolutes[i])
                            .sum();
        return sum;
    }
}                                         </code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[java] 자바 enum이란?]]></title>
            <link>https://velog.io/@chae_ag/java-%EC%9E%90%EB%B0%94-enum%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@chae_ag/java-%EC%9E%90%EB%B0%94-enum%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Wed, 08 Nov 2023 04:13:33 GMT</pubDate>
            <description><![CDATA[<h2 id="enum이란">enum이란?</h2>
<p>서로 연관된 상수들의 집합으로, 서로 관련된 상수를 편하게 선언하기 위해 사용하며, 상수를 여러개 정의해야할 때 사용한다.</p>
<p>수 많은 언어에서 이처럼 새로운 열거형을 정의할 수 있게 하고 있지만, 이번에는 자바의 enum 클래스에 대해서 알아보겠다.</p>
<blockquote>
<h3 id="상수란">상수란?</h3>
<p>자바에서는 <strong>final</strong>로 문자열이나 숫자들을 나타내는 기본 자료형의 값을 고정할 수 있다. 이렇게 <strong>고정된 값</strong>을 상수라고 한다.</p>
</blockquote>
<br>

<h3 id="enum을-사용하지-않고-상수를-선언하는-법">enum을 사용하지 않고 상수를 선언하는 법</h3>
<pre><code class="language-java">pubilc class EnumExample {

    private final static int JANUARY = 1;
    private final static int FEBRUARY = 2;
    private final static int MARCH = 3;
    private final static int APRIL = 4;
    private final static int MAY = 5;
    private final static int JUNE = 6;

    public static void main(String[] args) {

    }
}</code></pre>
<p>위 코드처럼 final을 사용하여 한 번 지정하면 바뀌지 않게 설정하면서 동시에 static을 사용하여 메모리에 한 번만 할당 되게 설정하여 변수를 상수로 만들었다.
이렇게 선언하는 방법도 좋지만, 사용해야 하는 상수가 많아지게 된다면 코드 상위에 계속해서 상수를 선언해줘야 하고, 그렇게 되면 상수가 너무 많아지고 상수들이 어떤 것에 관련된 것인지 한 눈에 알기 힘들다.</p>
<br>

<h3 id="enum을-사용하여-상수를-선언하는-법">enum을 사용하여 상수를 선언하는 법</h3>
<pre><code class="language-java">enum Month {
    JANUARY,
    FEBRUARY,
    MARCH,
    APRIL,
    MAY,
    JUNE
}</code></pre>
<p>위에서 선언했던 상수를 그대로 enum클래스를 사용해서 선언한 것이다.
enum을 사용해 상수를 정의하게 되면, 여러 장점을 얻을 수 있다.</p>
<br>

<h3 id="enum-사용-장점">enum 사용 장점</h3>
<ul>
<li><p><strong>코드가 단순해지며, 가독성이 좋다.</strong></p>
</li>
<li><p><strong>상수와 값이 논리적인 연관이 없는 문제를 해결할 수 있다.</strong></p>
<pre><code class="language-java">private static final int CAT = 1;
private static final int DOG = 2;
private static final int PANDA = 3;</code></pre>
<p>CAT이라는 상수와 값인 1은 전혀 논리적인 연관이 없다.</p>
</li>
<li><p><strong>서로 다른 개념끼리 이름이 충돌할 수 있는 문제를 해결할 수 있다.</strong>
예를 들어, 중학교의 1학년과 고등학교 1학년은 다른 의미를 가진다. middleSchool.FIRST_GRADE  highSchool.FIRST_GRADE과 같이 enum으로 정의할 경우 완전히 구분이 가능하다.</p>
</li>
<li><p><strong>리팩토링 변경 범위가 최소화된다.</strong>
내용의 추가나 수정이 필요하더라도, enum 코드만 건들면 된다.</p>
</li>
<li><p><strong>여러 메서드를 제공한다.</strong></p>
<ul>
<li>values() : 해당 enum 타입에 정의된 상수 배열을 반환하는 메서드</li>
<li>valueOf() : 지정된 열거형에서 name과 일치하는 열거형 상수를 반환하는 메서드</li>
<li>ordinal() : 열거형 상수가 정의된 순서를 반환하는 메서드</li>
<li>name() : 열거형 상수의 이름을 문자열로 반환하는 메서드</li>
<li>getDeclaringClass() : 열거형의 클래스 객체를 반환하는 메서드</li>
</ul>
</li>
</ul>
<br>

<h3 id="enum의-추가-속성-부여">enum의 추가 속성 부여</h3>
<p>enum클래스는 위에서 말한 장점 외에도 유용한 옵션을 제공하는데, 바로 열거형 상수에 <strong>추가 속성을 부여</strong>할 수 있다.</p>
<pre><code class="language-java">enum Month {
    JANUARY(&quot;1월&quot;),
    FEBRUARY(&quot;2월&quot;),
    MARCH(&quot;3월&quot;),
    APRIL(&quot;4월&quot;),
    MAY(&quot;5월&quot;),
    JUNE(&quot;6월&quot;)

    private String monthName;
}</code></pre>
<p><strong>여러개</strong>의 속성을 부여할 수도 있다.</p>
<pre><code class="language-java">enum Month {
    JANUARY(&quot;1월&quot;, &quot;겨울&quot;),
    FEBRUARY(&quot;2월&quot;, &quot;겨울&quot;),
    MARCH(&quot;3월&quot;, &quot;봄&quot;),
    APRIL(&quot;4월&quot;, &quot;봄&quot;),
    MAY(&quot;5월&quot;, &quot;봄&quot;),
    JUNE(&quot;6월&quot;, &quot;여름&quot;)

    private String month;
    private String seasons;
}</code></pre>
<br>

<p><em>앞으로는 연관된 상수를 선언할 때 enum을 활용해보자!</em></p>
<br>

<hr>
<blockquote>
<p>이 글을 읽는 모두, 좋은 하루 되세요. 💗</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Array와 ArrayList를 알아보자!]]></title>
            <link>https://velog.io/@chae_ag/Array%EC%99%80-ArrayList%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@chae_ag/Array%EC%99%80-ArrayList%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 20 Oct 2023 10:40:43 GMT</pubDate>
            <description><![CDATA[<h2 id="🚀-1-array-배열">🚀 1. Array (배열)</h2>
<hr>
<h3 id="배열이란">배열이란?</h3>
<ul>
<li><strong>같은 데이터 타입의 변수</strong>들로 이루어진 자료구조</li>
<li>자바에서 기본적으로 지원하는 자료구조<br>

</li>
</ul>
<h3 id="배열의-장점">배열의 장점</h3>
<ul>
<li><strong>index</strong>는 값에 대한 유일무이한 식별자 <strong>→</strong> 인덱스를 통한 <strong>검색이 용이하다</strong>.</li>
<li>연속된 메모리의 공간으로 이루어져 있다. <strong>→</strong> 연속적이므로 <strong>메모리 관리가 편하다.</strong><br>

</li>
</ul>
<h3 id="배열의-단점">배열의 단점</h3>
<ul>
<li><p>정적이므로 배열의 크기를 컴파일 이전에 정해주어야 한다.</p>
<p>  <strong>→</strong> 컴파일 이후 배열의 크기를 변동 할 수 없다. → <strong>메모리가 낭비됨.</strong></p>
</li>
</ul>
<br>

<h2 id="🪐-2-list-리스트">🪐 2. List (리스트)</h2>
<hr>
<h3 id="리스트란">리스트란?</h3>
<ul>
<li>배열과 유사하게, <strong>같은 데이터 타입의 변수</strong>들로 이루어진 자료구조</li>
<li>순서가 있는 <strong>데이터의 집합</strong>이며, 데이터의 <strong>중복을 허용</strong>한다.</li>
<li>자바에서 기본적으로 지원하는 자료구조</li>
<li>배열과 달리 <strong>크기가 가변적이다.</strong></li>
<li>중간의 빈 공간을 허용하지 않는다.<br>

</li>
</ul>
<h3 id="리스트의-장점">리스트의 장점</h3>
<ul>
<li><p>포인터를 통하여 다음 데이터의 위치를 가리키고 있어 삽입 삭제의 용이.</p>
<p>  <strong>→ 추가/삭제 성능이 좋다.</strong></p>
</li>
<li><p>불연속적으로 메모리 공간을 차지. <strong>→</strong> 불연속적이므로 <strong>메모리 관리가 편리하다.</strong></p>
<br>

</li>
</ul>
<h3 id="리스트의-단점">리스트의 단점</h3>
<ul>
<li>검색 성능이 좋지 않다.</li>
<li>포인터를 통해 다음 데이터를 가리키므로 추가적인 메모리 공간이 발생한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/c27100ed-c395-40f2-b538-076110075744/image.png" alt=""><br></p>
<h2 id="🌟-3-arraylist-배열리스트">🌟 3. ArrayList (배열리스트)</h2>
<hr>
<h3 id="arraylist란">ArrayList란?</h3>
<ul>
<li><strong>List 인터페이스를 상속받은</strong> 여러 클래스 중 하나이다.<br>

</li>
</ul>
<h3 id="arraylist-특징">ArrayList 특징</h3>
<ul>
<li>배열과 유사하게 <strong>index 식별자</strong> 활용이 가능하다.</li>
<li>리스트의 특성인 <strong>동적 할당</strong> 가능하다. → <em>크기가 가변적이다.</em></li>
<li>타입 안정성을 보장해주는 <strong>제네릭</strong> 사용이 가능하다.<br>

</li>
</ul>
<h2 id="🌌-array와-arraylist는-어떤-경우에-사용하면-좋을까">🌌 Array와 ArrayList는 어떤 경우에 사용하면 좋을까?</h2>
<hr>
<h3 id="array">Array</h3>
<ul>
<li>데이터 개수가 고정적이고 삽입, 삭제가 빈번하지 않은 경우</li>
<li>데이터 접근이 빈번한 경우</li>
<li>기본 자료형(int, char, double..) 사용하고 싶은 경우<br>

</li>
</ul>
<h3 id="arraylist">ArrayList</h3>
<ul>
<li>데이터 개수가 고정적이지 않고, 삽입, 삭제가 빈번할 경우</li>
<li>데이터 접근이 드물 경우<br>

</li>
</ul>
<hr>
<blockquote>
<p>이 글을 읽는 모두, 좋은 하루 되세요. 😄</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD. 테스트 주도 개발이 무엇인지 알자]]></title>
            <link>https://velog.io/@chae_ag/TDD.-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%95%8C%EC%9E%90</link>
            <guid>https://velog.io/@chae_ag/TDD.-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%95%8C%EC%9E%90</guid>
            <pubDate>Wed, 04 Oct 2023 18:22:44 GMT</pubDate>
            <description><![CDATA[<h2 id="tdd란">TDD란?</h2>
<ul>
<li><p>Test Driven Development의 약자. 우리말로 <strong>테스트 주도 개발</strong></p>
</li>
<li><p>반복 테스트를 이용한 소프트웨어 방법론으로 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.</p>
</li>
<li><p>이 기법을 개발했거나 ‘재발견’한 것으로 인정되는 켄트 벡(Kent Beck)은 TDD가 단순한 설계를 장려하고 자신감을 불어넣어 준다고 말하였다.</p>
</li>
<li><p>최근에는 테스트 코드가 동작을 테스트하기 위해 사용될 뿐 아니라, 
jenkins 등의 ci 도구를 사용할 때 테스트 코드의 성공 여부를 확인해 전부 성공한 경우에만 pr을 통과시키거나 운영브랜치로 merge시키는 등의 추가 동작을 하는데도 많이 사용된다고 한다.</p>
<br>

</li>
</ul>
<h2 id="tdd-순서">TDD 순서</h2>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/67372637-40e0-467c-b20f-0f64aa645dfb/image.png" alt=""></p>
<p>1단계 🔴) 기능을 검증하는 테스트 코드 작성
2단계 🟢) 테스트를 통과하는 기능 개발
3단계 🔵) 작성한 코드 리팩토링</p>
<p>TDD는 기본적으로 위 3단계의 반복으로 진행하며 점진적으로 코드를 개선해나가며 개발이 진행된다.</p>
<h4 id="왜-1단계가-write-a-failing-test-실패한-테스트-작성일까">왜 1단계가 Write a failing test (실패한 테스트 작성)일까?</h4>
<p>기능을 개발하기 전, 테스트 코드를 우선으로 작성하기 때문에 당연히 그 테스트는 <strong>실패하는 테스트</strong>일 것이다. 
2단계에서 이 실패하는 테스트를 성공시키기 위한 기능 개발을 하면 된다.
<br></p>
<hr>
<h2 id="간단한-tdd-예제">간단한 TDD 예제</h2>
<p>숫자로 구성된 문자열을 입력받아 기본적인 더하기와 빼기 연산을 수행하는 계산기 프로그램을 만들려고 한다.
기능을 개발하기 전, 기능을 검증할 수 있는 테스트 코드를 작성해 본다.</p>
<pre><code class="language-java">import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class StringCalculatorTest {

    @Test
    public void testAdd() {
        StringCalculator calculator = new StringCalculator();
        assertThat(calculator.add(&quot;&quot;)).isEqualTo(0);
        assertThat(calculator.add(&quot;1,2&quot;)).isEqualTo(3);
        assertThat(calculator.add(&quot;2,3&quot;)).isEqualTo(5);
        assertThat(calculator.add(&quot;4,5&quot;)).isEqualTo(9);
    }

    @Test
    public void testSubtract() {
        StringCalculator calculator = new StringCalculator();
        assertThat(calculator.subtract(&quot;&quot;)).isEqualTo(0);
        assertThat(calculator.subtract(&quot;3,2&quot;)).isEqualTo(1);
        assertThat(calculator.subtract(&quot;4,3&quot;)).isEqualTo(1);
        assertThat(calculator.subtract(&quot;5,3&quot;)).isEqualTo(2);
    }
}</code></pre>
<p>테스트 코드를 작성하면 아래와 같이 코드가 빨갛게 물들 것이다.
<img src="https://velog.velcdn.com/images/chae_ag/post/f11a27c1-9a4c-4c5a-840e-e09a37ae8be9/image.png" alt="">
<strong>Why?</strong> 기능 개발을 하기 전이기 때문이다. 
이 상태로 테스트를 실행시키면
<img src="https://velog.velcdn.com/images/chae_ag/post/6f42162f-0e4f-431c-aea4-5db70517dd72/image.png" alt="">
아주 친절하게 <code>StringCalculator</code>가 없다고 알려준다.
이게 바로 위에서 설명했던 1단계 &#39;실패한 테스트 작성&#39;이다.
그럼 이제 2단계로 넘어가서 &#39;테스트를 통과하는 기능 개발&#39;을 하면 된다.
우리는 <code>StringCalculator</code> 클래스를 만들어주면 되는 것이다.</p>
<pre><code class="language-java">public class StringCalculator {

    public int add(String input) {
        // 아무것도 입력하지 않았을 때
        if (input.isEmpty()) {
            return 0;
        }

        String[] numbers = input.split(&quot;,&quot;);
        int sum = 0;
        for (String number : numbers) {
            sum += Integer.parseInt(number);
        }
        return sum;
    }

    public int subtract(String input) {
        // 아무것도 입력하지 않았을 때
        if (input.isEmpty()) {
            return 0;
        }

        String[] numbers = input.split(&quot;,&quot;);
        int result = Integer.parseInt(numbers[0]);
        for (int i = 1; i &lt; numbers.length; i++) {
            result -= Integer.parseInt(numbers[i]);
        }
        return result;
    }
}</code></pre>
<p><code>StringCalculator</code>를 만들고 작성했던 테스트 코드에 다시 돌아와보자.
<img src="https://velog.velcdn.com/images/chae_ag/post/99633a59-a181-4f7d-a945-1631e52df1f9/image.png" alt="">
무진장 붉었던 테스트 코드들이 정상으로 돌아왔고 테스트를 실행시켜보면
<img src="https://velog.velcdn.com/images/chae_ag/post/fc65b2d9-a731-4256-8593-1e77fae9f91c/image.png" alt="">
초록초록하게 테스트가 성공하는 것을 볼 수 있다.
아, 여기서 <code>DIsplayName()</code>은 위처럼 테스트명을 지정해 줄 수 있는 어노테이션이다.</p>
<p>테스트가 성공했으니 이제 더 좋게 코드를 리팩토링해주면 된다.
입력값으로 null이 입력된 경우를 추가하거나 for문을 없애거나 하면 더 효율 높은 코드가 될 것이다.
이렇게 코드를 리팩토링하고 테스트하는 과정을 반복하여 보다 <strong>효율적이고 깔끔한 코드</strong>를 작성하는 것이 TDD의 목적이다.
<br></p>
<h1 id="tdd-예제--암호-검사기">TDD 예제 : 암호 검사기</h1>
<ul>
<li><strong>규칙 세 가지</strong><ul>
<li>길이가 8글자 이상</li>
<li>0부터 9 사이의 숫자를 포함</li>
<li>대문자 포함</li>
</ul>
</li>
<li><strong>세 규칙을 모두 충족하면 암호는 강함이다.</strong></li>
<li><strong>2개의 규칙을 충족하면 암호는 보통이다.</strong></li>
<li><strong>1개 이하의 규칙을 충족하면 암호는 약함이다.</strong></li>
</ul>
<blockquote>
<p>책에서는 junit의 <code>assertEquals</code>를 사용했지만 나는 assertj의 <code>assertThat</code>을 사용해서 만들어보겠다.</p>
</blockquote>
<br>

<h2 id="첫-번째-테스트--모든-규칙을-충족하는-경우">첫 번째 테스트 : 모든 규칙을 충족하는 경우</h2>
<p>#가장 쉽거나 가장 예외적인 상황을 선택</p>
<p>→ <strong>모든 조건을 충족하지 않는 경우 ←</strong> 는 한번에 만들어야 할 코드가 많기 때문에 모든 규칙을 충족하는 경우를 택함.</p>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">import org.junit.jupiter.api.Test;

// assertj의 Assertions 임포트
import static org.assertj.core.api.Assertions.*;

public class PasswordStrengthMeterTest {
@Test
    void meetsAllCriteria_Then_Strong() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();

        PasswordStrength result1 = meter.meter(&quot;ab12!@AB&quot;);
                // result1의 값이 PasswordStrength.STRONG와 동일한지 검증하는 코드
        **assertThat(PasswordStrength.STRONG).isEqualTo(result1);**

        PasswordStrength result2 = meter.meter(&quot;abc1!Add&quot;);
       **/assertThat(PasswordStrength.STRONG).isEqualTo(result2);**
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">public calss PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return PasswordStrength.STRONG;
    }
}</code></pre>
<p>💡 기능을 작성할 때는 단순히 테스트가 통과하도록 기능을 최소한으로 구현한다!</p>
<br>

<h2 id="두-번째-테스트--길이만-8글자-미만이고-나머지-조건은-충족하는-경우">두 번째 테스트 : 길이만 8글자 미만이고 나머지 조건은 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void meetsAllCriteria_Then_Strong() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(&quot;ab12!@A&quot;);
    **assertThat(PasswordStrength.NOMAL).isEqualTo(result);**
  PasswordStrength result2 = meter.meter(&quot;abc1!Add&quot;);
  **assertThat(PasswordStrength.STRONG).isEqualTo(result2);**
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">**if(s.length() &lt; 8) {
    return PasswordStrength.NORMAL;
}**</code></pre>
<br>

<h2 id="세-번째-테스트--숫자를-포함하지-않고-나머지-조건은-충족하는-경우">세 번째 테스트 : 숫자를 포함하지 않고 나머지 조건은 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void meetsOtherCriteria_except_for_Length_Then_Normal() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(&quot;ab12!@A&quot;);
    assertThat(PasswordStrength.NOMAL).isEqualTo(result);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">// 암호가 숫자를 포함했는지 판단해서 포함하지 않은 경우 NORMAL을 리턴한다.
**boolean containsNum = false;
    for(char c : s.toCharArray()) {
      if(c &gt;= &#39;0&#39; &amp;&amp; c &lt;= &#39;9&#39;) {
        containsNum = true;
            break;
    }
  }
if(!containsNum) return PasswordStrength.NORMAL;**</code></pre>
<br>

<ul>
<li><p><strong><em>코드의 가독성을 개선하기 위해 리팩토링 진행!</em></strong></p>
<pre><code class="language-java">  public calss PasswordStrengthMeter {
      public PasswordStrength meter(String s) {
          if(s.length() &lt; 8) {
              return PasswordStrength.NORMAL;
          }
          boolean containsNum = **meetsContainingNumberCriteria(s);**
          if(!containsNum) return PasswordStrength.NORMAL;
  ****        return PasswordStrength.STRONG;
      }

      // 숫자 포함 여부 확인 코드를 메서드로 분리한다.
      **private boolean meetsContainingNumberCriteria(String s) {
      for(char c : s.toCharArray()) {
          if(c &gt;= &#39;0&#39; &amp;&amp; c &lt;= &#39;9&#39;) {
              return true;
          }
      }
      return false;
    }**
  }</code></pre>
</li>
</ul>
<br>   

<h2 id="코드-정리--테스트-코드-정리">코드 정리 : 테스트 코드 정리</h2>
<p>#코드의 중복 제거</p>
<p><em>→ PasswordStrengthMeter 객체 생성을 필드에서!</em></p>
<pre><code class="language-java">public class PasswordStrengthMeterTest {
    private PasswordStrengthMeter meter = new PasswordStrengthMeter();

    private void assertStrength(String password, PasswordStrength expStr) {
      PasswordStrength result = meter.meter(password);
      assertThat(expStr).isEqualTo(result);
  }

    @Test
  void meetsAllCriteria_Then_Strong() {
      assertStrength(&quot;ab12!@AB&quot;, PasswordStrength.STRONG);
      assertStrength(&quot;abc1!Add&quot;, PasswordStrength.STRONG);
    }

  @Test
  void meetsOtherCriteria_except_for_Length_Then_Normal() {
      assertStrength(&quot;ab12!@A&quot;, PasswordStrength.NOMAL);
  }

  @Test
  void meetsOtherCriteria_except_for_number_Then_Normal() {
      assertStrength(&quot;abc!@ABC&quot;, PasswordStrength.NOMAL);
  }
}</code></pre>
<p>✅ 테스트 코드를 수정 후 실패하는 곳이 없는지 확인 후 다음 테스트 진행</p>
<br>

<h2 id="네-번째-테스트--값이-없는-경우">네 번째 테스트 : 값이 없는 경우</h2>
<p>#값이 없을 때는 NullPointerException 이 발생한다. </p>
<p><em>→ 유효하지 않는 암호가 입력되면 PasswordStrength.INVALID를 리턴하도록 설계하자.</em></p>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void nullInput_Then_Invalid() {
    assertStrength(null, PasswordStrength.INVALID);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">// 암호가 null이라면 INVALID를 리턴한다.
**if(s == null) return PasswordStrength.INVALID;**</code></pre>
<br>

<p>*<em>→ 암호가 빈 문자열일 때의 예외상황도 고려해보자!
*</em></p>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void emptyInput_Then_Invalid() {
  assertStrength(&quot;&quot;, PasswordStrength.INVALID);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">// 암호가 빈 문자열이라면 INVALID를 리턴한다.
if(s == null **|| s.isEmpty()**) return PasswordStrength.INVALID;</code></pre>
<br>

<h2 id="다섯-번째-테스트--대문자를-포함하지-않고-나머지-조건을-충족하는-경우">다섯 번째 테스트 : 대문자를 포함하지 않고 나머지 조건을 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void emptyInput_Then_Invalid() {
  assertStrength(&quot;&quot;, PasswordStrength.INVALID);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">boolean containsNum = meetsContainingNumberCriteria(s);
****if(!containsNum) return PasswordStrength.NORMAL;

// 암호가 대문자를 포함하지 않은 경우를 판별한다.
**boolean containsUpp = false;
for(char ch : s.toCharArray()) {
    if(Character.isUpperCase(ch)) {
      containsUpp = true;
        break;
  }
}
if (!containsUpp) PasswordStrength.NORMAL;**</code></pre>
<br>

<p><strong>→ 코드의 가독성을 개선하기 위해 리팩토링 진행!</strong></p>
<pre><code class="language-java">// 대문자 포함 여부 확인 코드를 메서드로 분리한다.
**private boolean meetsContainingUppercaseCriteria(String s) {
  for(char c : s.toCharArray()) {
    if(Character.isUpperCase(c)) {
        return true;
    }
  }
  return false;
}**</code></pre>
<br>

<h2 id="여섯-번째-테스트--길이가-8글자-이상인-조건만-충족하는-경우">여섯 번째 테스트 : 길이가 8글자 이상인 조건만 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void 길이가_8글자_이상인_조건만_충족() {
  assertStrength(&quot;abcdefghi&quot;, PasswordStrength.WEAK);
}</code></pre>
<p><em>→ 테스트 이름은 한글로 작성해도 된다!</em></p>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">// 이전 코드 삭제
**~~if(s.length() &lt; 8) {
    return PasswordStrength.NORMAL;
}~~**

// 암호의 길이가 8 이상인지 검증한다.
**boolean lengthEnough = s.length() &gt;= 8;**
boolean containsNum = meetsContainingNumberCriteria(s);
boolean containsUpp = meetsContainingUppercaseCriteria(s);

**if(lengthEnough &amp;&amp; containsNum &amp;&amp; containsUpp) {
    return PasswordStrength.WEAK;
}**

**if(!lengthEnough) {
    return PasswordStrength.NORMAL;
}**
if(!containsNum) return PasswordStrength.NORMAL;
if(!containsUpp) return PasswordStrength.NORMAL;
return PasswordStrength.STRONG;

****</code></pre>
<br>

<h2 id="일곱-번째-테스트--숫자-포함-조건만-충족하는-경우">일곱 번째 테스트 : 숫자 포함 조건만 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void 숫자_포함_조건만_충족() {
  assertStrength(&quot;12345&quot;, PasswordStrength.WEAK);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">**if(!lengthEnough &amp;&amp; containsNum &amp;&amp; !containsUpp) {
    return PasswordStrength.WEAK;
}**</code></pre>
<br>

<h2 id="여덟-번째-테스트--대문자-포함-조건만-충족하는-경우">여덟 번째 테스트 : 대문자 포함 조건만 충족하는 경우</h2>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void 대문자_포함_조건만_충족() {
  assertStrength(&quot;ABCEF&quot;, PasswordStrength.WEAK);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">**if(!lengthEnough &amp;&amp; !containsNum &amp;&amp; containsUpp) {
    return PasswordStrength.WEAK;
}**</code></pre>
<br>

<ul>
<li><p><strong><em>지금까지 작성한 코드 리팩토링 진행</em></strong></p>
<pre><code class="language-java">  public class PasswordStrengthMeter {
      public PasswordStrength meter (String s) {
          if(s == null || s.isEmpty()) return PasswordStrength.INVALID;
          int metCounts = 0;
          if(s.length() &gt;= 8) metCounts++;
          if(meetsContainingNumberCriteria(s)) metCounts++;
          if(meetsContainingUppercaseCriteria(s)) metCounts++;

          if(metCounts == 1) return PasswordStrength.WEAK;
          if(metCounts == 2) return PasswordStrength.NOMAL;

          return PasswordStrength.STRONG;
      }

          // 숫자 포함 여부 검증 메서드
      private boolean meetsContainingNumberCriteria(String s) {
          for(char c : s.toCharArray()) {
              if(c &gt;= &#39;0&#39; &amp;&amp; c &lt;= &#39;9&#39;) {
                  return true;
              }
          }
          return false;
      }
          // 대문자 포함 여부 검증 메서드
      private boolean meetsContainingUppercaseCriteria(String s) {
          for(char c : s.toCharArray()) {
              if(Character.isUpperCase(c)) {
                  return true;
              }
          }
          return false;
      }
  }</code></pre>
</li>
</ul>
<br>    

<h2 id="아홉-번째-테스트--아무-조건도-충족하지-않은-경우">아홉 번째 테스트 : 아무 조건도 충족하지 않은 경우</h2>
<p>#모든 상황을 다 테스트 했으니, 마지막으로 <em>아무 조건도 충족하지 않은 경우</em>를 검증한다.</p>
<ul>
<li>테스트 작성</li>
</ul>
<pre><code class="language-java">@Test
void 아무조건도_충족하지_않음() {
  assertStrength(&quot;abc&quot;, PasswordStrength.WEAK);
}</code></pre>
<ul>
<li>기능 작성</li>
</ul>
<pre><code class="language-java">// 비교 조건 수정
~~if(metCounts == 1) return PasswordStrength.WEAK;~~
if(metCounts &lt;= 1) return PasswordStrength.WEAK;</code></pre>
<br>

<blockquote>
<p>✅ 암호 검사기 프로그램의 모든 기능이 완성되었다~! 얏호</p>
</blockquote>
<br>

<ul>
<li><p><strong><em>마지막으로 코드의 가독성을 조금 더 높여보자 (리팩토링)</em></strong></p>
<pre><code class="language-java">  public class PasswordStrengthMeter {
      public PasswordStrength meter (String s) {
          if(s == null || s.isEmpty()) return PasswordStrength.INVALID;
                  **int metCounts = getMetCriteriaCounts(s);**
          if(metCounts == 1) return PasswordStrength.WEAK;
          if(metCounts == 2) return PasswordStrength.NOMAL;

          return PasswordStrength.STRONG;
      }

          // metCounts를 계산하는 메서드
          **private int getMetCriteriaCounts(String s) {
          int metCounts = 0;
          if(s.length() &gt;= 8) metCounts++;
          if(meetsContainingNumberCriteria(s)) metCounts++;
          if(meetsContainingUppercaseCriteria(s)) metCounts++;
          return metCounts;
      }**

          // 숫자 포함 여부 검증 메서드
      private boolean meetsContainingNumberCriteria(String s) {
          for(char c : s.toCharArray()) {
              if(c &gt;= &#39;0&#39; &amp;&amp; c &lt;= &#39;9&#39;) {
                  return true;
              }
          }
          return false;
      }
          // 대문자 포함 여부 검증 메서드
      private boolean meetsContainingUppercaseCriteria(String s) {
          for(char c : s.toCharArray()) {
              if(Character.isUpperCase(c)) {
                  return true;
              }
          }
          return false;
      }
  }</code></pre>
</li>
</ul>
<br>

<hr>
<h2 id="tdd의-장점">TDD의 장점</h2>
<p><strong>1. 기능 단위로 테스트하기 때문에 문제의 원인을 알기 쉽다.</strong></p>
<p>보통 개발을 다 해놓고 나서 테스트를 하는 ATDD(인수테스트 주도 개발)를 하는데, 테스트 중 혹여나 문제를 발견한다면 정확하게 문제의 원인이 무엇인지 진단하기 힘들다.</p>
<p>하지만 TDD를 사용하면 <strong>기능 단위로 테스트</strong>하기 때문에 문제의 원인을 알기 쉽다.</p>
<p><strong>2. 변화에 대한 두려움 해소</strong></p>
<p>테스트 코드를 먼저 작성하고 기능 개발을 하기 때문에 개발한 기능이 잘 동작할지에 대한 불안감을 없앨 수 있다.</p>
<p><strong>3. 프로그래머의 오버 엔지니어링을 방지한다.</strong></p>
<p>개발을 하다 보면 간혹 계획하지 않았던 코드를 추가하여 오버 엔지니어링하는 경우가 있다. 하지만 TDD의 원칙 중 하나는, 테스트를 통과하기 위한 최소한의 코드만 작성 및 개선해야 한다는 것이다. 기능 단위로 테스트를 진행하기 때문에, 문제가 발견되지 않은 코드에 영향을 줄 수 있는 오버 코딩은 하지 않는다.</p>
<h2 id="tdd의-단점">TDD의 단점</h2>
<p><strong>TDD를 익히는 데 많은 시간이 걸린다.</strong></p>
<pre><code>TDD는 마치 운동과 같다. 
운동을 꾸준히 하면 건강해지고 체력이 좋아지는 것처럼 
TDD도 꾸준히 연습하고 적용해야 실력이 늘고 효과를 볼 수 있다.
-저자 최범균 [테스트 주도 개발 시작하기] 255p</code></pre><p>TDD를 효과적으로 사용하기까지는 많은 시간이 필요하다고 한다.</p>
<p><strong>생산성 저하</strong></p>
<p>TDD에 대한 프로그래머들의 의견은 늘 엇갈리는데, 그 이유 중 가장 큰 것이 이 생산성 저하 문제 때문일 것이다. 
TDD를 지키면서 개발하려면 처음부터 2개의 코드를 짜야 하고, 중간중간 테스트를 하면서 고쳐나가야 한다. -&gt; 개발 속도가 느려지기 때문에 생산성이 저하된다.
<br></p>
<hr>
<p>이 글은 <code>최범균 저 &#39;테스트 주도 개발 시작하기&#39;</code> 를 읽고 작성한 글입니다.
<br></p>
<p>참고
<a href="https://media.fastcampus.co.kr/knowledge/dev/tdd/">TDD란? 테스트주도개발에 대한 편견과 실상, 방법론</a>
<a href="http://www.incodom.kr/%ED%85%8C%EC%8A%A4%ED%8A%B8_%EC%A3%BC%EB%8F%84_%EA%B0%9C%EB%B0%9C">테스트 주도 개발</a>
<a href="https://frozenpond.tistory.com/149">[개발상식] tdd란 (tdd 예제, tdd하는법)</a>
<br></p>
<blockquote>
<p>이 글을 읽는 모두, 좋은 하루 되세요. 🙂</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GDG Campus Korea X Whatever 6주 프로덕트 메이커 챌린지] 4주차 회고]]></title>
            <link>https://velog.io/@chae_ag/series%ED%9A%8C%EA%B3%A0%EB%A1%9DGDG-Campus-Korea-X-Whatever-6%EC%A3%BC-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EB%A9%94%EC%9D%B4%EC%BB%A4-%EC%B1%8C%EB%A6%B0%EC%A7%80-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@chae_ag/series%ED%9A%8C%EA%B3%A0%EB%A1%9DGDG-Campus-Korea-X-Whatever-6%EC%A3%BC-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EB%A9%94%EC%9D%B4%EC%BB%A4-%EC%B1%8C%EB%A6%B0%EC%A7%80-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 22 Sep 2023 14:15:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="4주차-회고">4주차 회고</h2>
<p><strong>마지막,, 회고? 😯</strong></p>
</blockquote>
<br>

<h3 id="❔-이번주는-좋은-것과-나쁜-것이-무엇이-있었나요">❔ 이번주는 좋은 것과 나쁜 것이 무엇이 있었나요?</h3>
<h4 id="👍-good">👍 Good</h4>
<p><strong><code>토요일 오프라인 행사에서 처음으로 팀원 전체가 모여 작업을 했는데, 너무너무 좋았고 행복하고 재미있었다,, 😊</code></strong></p>
<h4 id="👎-bad">👎 Bad</h4>
<p>** <code>나쁜게 없었던 일주일!</code> **</p>
<br>

<h3 id="📖-이번주-진행했던-학습개발-내용은-무엇이었나요">📖 이번주 진행했던 학습/개발 내용은 무엇이었나요?</h3>
<p><strong>👩‍💻 아래와 같은 개발을 진행했습니다.</strong></p>
<pre><code>   1) 24시간 서버를 돌리면서 프론트분들의 요구사항이나, 오류가 발견되거나 하면 그때그때 필요한 기능들을 제작하여 추가했다!
   2) 배포 자동화 !!!!!!🙉</code></pre><p><strong>✍ 아래와 같은 내용을 학습했습니다.</strong></p>
<pre><code>   1) Github Actions를 활용한 CI/CD 구축법</code></pre><p>실제 구축한 과정을 담은 글 ,, (보러 와주십사🤤)
<a href="https://velog.io/@chae_ag/series%EB%B0%B0%ED%8F%ACSpring-Boot-GitHub-Actions-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0">Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기</a></p>
<br>

<h3 id="💭-가장-고민을-했던-부분은-무엇이었나요">💭 가장 고민을 했던 부분은 무엇이었나요?</h3>
<h4 id="cicd를-구축하려-했을-때-혹시나-정체모를-오류라도-나서-서버를-켤-수-없는-상황이-올까봐-고민을-많이-했다">CI/CD를 구축하려 했을 때, 혹시나 정체모를 오류라도 나서 서버를 켤 수 없는 상황이 올까봐 고민을 많이 했다..</h4>
<p>▶ 그래도 혼자 작업하는게 아닌, 다른 백엔드 팀원분도 계시기 때문에 서로 편하게 빌드하고 배포하기 위해서는 꼭 필요한 작업이라고 생각했다. 
그래서 결국 아무도 서버를 사용하지 않을 것 같은 새벽에 작업을 진행..!
결과는 수많은 에러들을 해결하고 성공적으로 구축 완료했다! 😁
<br></p>
<h3 id="🤔-아쉬운-부분을-개선하기-위해서-필요한-것은-무엇인가요">🤔 아쉬운 부분을 개선하기 위해서 필요한 것은 무엇인가요?</h3>
<ul>
<li>개발을 진행하면서 기능을 계속 수정해야 하는 상황이 발생하고, 때로는 팀원들 간에 요구사항을 다르게 이해하는 경우도 있었다. 
괜히 제품을 만들기 전 문서 작성이 중요하다고 하는 것이 아니구나 하며 깨달았다. 
이를 계기로 문서를 작성하는 습관을 길러야 할 것 같다.<br>

</li>
</ul>
<h3 id="🕒-다음주는-어떻게-보낼-예정인가요">🕒 다음주는 어떻게 보낼 예정인가요?</h3>
<ul>
<li>마지막까지 수정이 필요한 부분을 계속해서 손보고, 후회없는 결과물을 만들어 내기!</li>
</ul>
<br>

<hr>
<blockquote>
<p>4주차 회고를 마칩니다. 💚</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot + GitHub Actions + AWS CodeDeploy를 사용하여 CI/CD 구축하기]]></title>
            <link>https://velog.io/@chae_ag/series%EB%B0%B0%ED%8F%ACSpring-Boot-GitHub-Actions-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@chae_ag/series%EB%B0%B0%ED%8F%ACSpring-Boot-GitHub-Actions-AWS-CodeDeploy%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-CICD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 20 Sep 2023 18:21:44 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="0-개발환경">0. 개발환경</h2>
<ul>
<li><strong>Spring Boot</strong></li>
<li><strong>GitHub Actions</strong></li>
<li><strong>AWS CodeDeploy</strong></li>
<li><strong>AWS EC2</strong></li>
<li><strong>AWS S3</strong><br>

</li>
</ul>
<h3 id="전체적인-흐름">전체적인 흐름</h3>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/3238d05a-ff63-4a44-bb55-6a20beebee20/image.JPG" alt=""></p>
<br>

<hr>
<h2 id="1-s3-생성">1. S3 생성</h2>
<br>

<h3 id="11-s3-버킷-생성">1.1 S3 버킷 생성</h3>
<p>aws S3 ➡ 버킷 ➡ 버킷 만들기
나는 이미 버킷을 생성해둔 상태다.
<img src="https://velog.velcdn.com/images/chae_ag/post/17a40e89-3a2f-465a-990d-16715156d4f0/image.png" alt=""></p>
<p>버킷 생성 시 리전은 꼭 EC2 인스턴스와 같은 것으로 지정해야 한다.
만약 리전이 다르다면 S3 리전에서 EC2 리전으로 데이터를 옮기는 데에 💸비용이 청구된다.
(그게 나야,, 둠빠둠빠두비두밥,,)
<img src="https://velog.velcdn.com/images/chae_ag/post/9b63592f-1de9-420f-a7e9-34513202b8b6/image.png" alt=""></p>
<p>나머지는 모두 디폴트 값으로 놔두고 버킷 생성!</p>
<br>
<br>

<hr>
<h2 id="2-ec2-설정-추가">2. EC2 설정 추가</h2>
<p><strong>1. 태그 추가
2. IAM 역할 추가
3. EC2 서버에 CodeDeploy 에이전트 설치</strong></p>
<br>


<h3 id="21-인스턴스-태그-추가">2.1 인스턴스 태그 추가</h3>
<p>CodeDeploy를 생성할 때 수행할 인스턴스를 지정해줘야하기 때문에 인스턴스들을 구분할 태그를 추가해줘야 한다.</p>
<p>인스턴스 선택 ➡ 작업 ➡ 인스턴스 설정 ➡ 태그 관리
<img src="https://velog.velcdn.com/images/chae_ag/post/ad7290a2-c9a6-4dec-8825-9d112cf0e050/image.png" alt=""></p>
<ul>
<li>태그 확인
<img src="https://velog.velcdn.com/images/chae_ag/post/0b5dd1a4-eb4b-41dd-813b-c302af06333e/image.png" alt=""></li>
</ul>
<br>

<h3 id="22-iam-역할-추가">2.2 IAM 역할 추가</h3>
<p>EC2 인스턴스에서 S3에 접근할 수 있도록 <strong>AmazonS3FullAccess</strong> 권한을 추가한 뒤, 인스턴스에 연결한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/3a0966ba-715e-4ee5-a9e0-32ce0b6ea9b8/image.png" alt=""></p>
<br>

<h3 id="23-codedeploy-에이전트-설치">2.3 CodeDeploy 에이전트 설치</h3>
<p>EC2 서버에 접속한 뒤, 똑같이 입력하여 설치해준다.</p>
<pre><code class="language-bash">$ sudo apt update
$ sudo apt install ruby-full
$ sudo apt install wget
$ cd /home/ubuntu
$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install
$ sudo ./install auto &gt; /tmp/logfile
$ sudo service codedeploy-agent status</code></pre>
<br>
<br>

<hr>
<h2 id="3-codedeploy-생성">3. CodeDeploy 생성</h2>
<p><strong>1. IAM 역할 추가
2. CodeDeploy 애플리케이션 생성
3. CodeDeploy 배포 그룹 생성</strong></p>
<br>

<h3 id="31-iam-역할-추가">3.1 IAM 역할 추가</h3>
<p><strong>AWSCodeDeployRole</strong> 권한을 추가하여 역할을 생성한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/84689ec0-ffb5-4740-8a1e-052b9f1b0d28/image.png" alt=""></p>
<br>

<h3 id="32-codedeploy-애플리케이션-생성">3.2 CodeDeploy 애플리케이션 생성</h3>
<p>컴퓨팅 플랫폼은 <strong>EC2/온프레미스</strong>를 선택하여 생성한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/ce570cfb-819c-4513-9d0a-f52e83697168/image.png" alt=""></p>
<br>

<h3 id="33-codedeploy-배포-그룹-생성">3.3 CodeDeploy 배포 그룹 생성</h3>
<p>위에서 생성한 CodeDeploy 애플리케이션 이름을 눌러 들어가면 배포 그룹을 생성할 수 있다.
<img src="https://velog.velcdn.com/images/chae_ag/post/21997b2d-ae0f-4aa9-8c84-9d57c7f6920d/image.png" alt=""></p>
<p>서비스 역할에서는 <strong>3.1</strong>에서 생성한 AWSCodeDeployRole 권한을 가진 IAM을 선택한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/c973abee-de61-4a86-979d-fb8d2805b18d/image.png" alt=""></p>
<p>다음 환경 구성에서는 Amazon EC2 인스턴스를 선택하고 2.1에서 태그를 추가했다면 아래와 같이 해당 태그의 인스턴스를 선택할 수 있다.
<img src="https://velog.velcdn.com/images/chae_ag/post/58910506-cd77-4e1e-96d4-7e825b318cde/image.png" alt=""></p>
<p>나머지 설정은 아래와 같이 해주고 배포 그룹을 생성한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/b2466f29-ee86-4c36-933d-cee8de268e11/image.png" alt=""></p>
<br>
<br>

<hr>
<h2 id="4-github-actions에서-사용할-iam-사용자-추가">4. Github Actions에서 사용할 IAM 사용자 추가</h2>
<p><strong>1. IAM 사용자 생성
2. Access Key, Secret Key 생성 및 확인</strong></p>
<br>

<h3 id="41-iam-사용자-생성">4.1 IAM 사용자 생성</h3>
<p>IAM ➡ 액세스 관리 ➡ 사용자 ➡ 사용자 생성
<img src="https://velog.velcdn.com/images/chae_ag/post/00002961-e6ea-4309-a72a-9fee8ce4798f/image.png" alt="">
직접 정책 연결을 선택하고
<strong>AmazonS3FullAccess, AWSCodeDeployRole</strong> 권한을 추가하여 사용자를 생성한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/cb7ab86d-372d-47a7-b903-65c229492a4a/image.png" alt=""></p>
<br>

<h3 id="42-access-key-secret-key-생성-및-확인">4.2 Access Key, Secret Key 생성 및 확인</h3>
<p>생성한 사용자 이름을 클릭하여 들어가서 아래와 같이 액세스 키 만들기를 클릭한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/2d01beda-3ca4-4139-b881-566009ebae13/image.png" alt=""></p>
<p>그러면 아래와 같은 화면이 뜨는데,
이 화면에서는 아무거나 선택해도 무방하다.
<img src="https://velog.velcdn.com/images/chae_ag/post/ce77c420-1c64-4ca5-b292-777db546f885/image.png" alt=""></p>
<p>액세스 키를 생성하게 되면
Access Key, Secret Key가 발급이 되는데</p>
<p>❗ 이때, Secret Key는 이 화면을 나가게 되면 더이상 알 수 있는 방법이 없기 때문에 꼭 csv파일로 가지고 있는 것을 추천한다.
<img src="https://velog.velcdn.com/images/chae_ag/post/65daaf25-cd59-4f6c-9f6e-28056658aee5/image.png" alt=""></p>
<br>
<br>

<hr>
<h2 id="5-github-repository-의-secrets-추가">5. Github Repository 의 Secrets 추가</h2>
<p>Github Repository ➡ Settings ➡ Secrets and variables ➡ Actions ➡ New repository secret</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/1051a49c-5951-49f3-b19f-79532691c785/image.png" alt=""></p>
<p>Access Key, Secret Key를 등록해준다.
<img src="https://velog.velcdn.com/images/chae_ag/post/7d80fef9-ab95-4c9f-9d44-0634a7c0bf3c/image.png" alt=""></p>
<h3 id="❗-여기서-application-키는-뭘까">❗ 여기서 APPLICATION 키는 뭘까?</h3>
<p>APPLICATION 키는 바로 Spring 프로젝트 내의 <strong><code>application.properties</code></strong> 파일의 내용을 가지고 있는 키다.</p>
<p>Spring 프로젝트 내에 있는 <code>application.properties</code> 파일은 각종 민감정보를 담고있는 파일이기 때문에 깃허브에 올릴 때는 <code>.gitignore</code>에 작성해서 깃허브에 올라가지 않도록 설정하는데, github action을 사용해서 깃허브에 푸쉬한 코드로 빌드를 해야하기 때문에 <code>application.properties</code> 파일이 필요하다!</p>
<p>그렇기 때문에 우리는 Screts에 <code>application.properties</code> 파일의 내용을 저장해두고 빌드할 때마다 파일을 생성하여 빌드해주려고 한다. </p>
<ul>
<li>깃허브에 올라가는 것은 아니다!</li>
</ul>
<p><code>application.properties</code> 파일의 내용을 모두 복붙하여 키를 생성해두면 된다.</p>
<br>
<br>

<hr>
<h2 id="6-appspecyml-파일-작성">6. appspec.yml 파일 작성</h2>
<br>

<p>Spring 프로젝트 루트 디렉토리에 appspec.yml 파일을 생성하고 아래와 같이 작성한다.</p>
<pre><code class="language-java"># appspec.yml

version: 0.0
os: linux

files:
  - source:  /
    destination: /home/ubuntu/app
    overwrite: yes

permissions:
  - object: /
    pattern: &quot;**&quot;
    owner: ubuntu
    group: ubuntu

hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ubuntu
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas: ubuntu</code></pre>
<br>
<br>

<hr>
<h2 id="7-배포-스크립트-작성">7. 배포 스크립트 작성</h2>
<p><strong>1. start.sh 작성
2. stop.sh 작성
3. build.gradle 파일 수정</strong></p>
<br>

<p>아래와 같이 프로젝트 루트 디렉토리 아래 scripts 디렉토리를 만들고 그 안에 <code>start.sh</code>와 <code>stop.sh</code>를 작성했다.
<img src="https://velog.velcdn.com/images/chae_ag/post/4a6c1eb1-c2ea-4ce4-be46-9d9b8dc5b1b1/image.png" alt=""></p>
<h3 id="71-startsh-작성">7.1 start.sh 작성</h3>
<pre><code class="language-bash">#!/usr/bin/env bash

PROJECT_ROOT=&quot;/home/ubuntu/app&quot;
JAR_FILE=&quot;$PROJECT_ROOT/spring-webapp.jar&quot;

APP_LOG=&quot;$PROJECT_ROOT/application.log&quot;
ERROR_LOG=&quot;$PROJECT_ROOT/error.log&quot;
DEPLOY_LOG=&quot;$PROJECT_ROOT/deploy.log&quot;

TIME_NOW=$(date +%c)

# build 파일 복사
echo &quot;$TIME_NOW &gt; $JAR_FILE 파일 복사&quot; &gt;&gt; $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE

# jar 파일 실행
echo &quot;$TIME_NOW &gt; $JAR_FILE 파일 실행&quot; &gt;&gt; $DEPLOY_LOG
nohup java -jar $JAR_FILE &gt; $APP_LOG 2&gt; $ERROR_LOG &amp;

CURRENT_PID=$(pgrep -f $JAR_FILE)
echo &quot;$TIME_NOW &gt; 실행된 프로세스 아이디 $CURRENT_PID 입니다.&quot; &gt;&gt; $DEPLOY_LOG</code></pre>
<br>

<h3 id="72-stopsh-작성">7.2 stop.sh 작성</h3>
<pre><code class="language-bash">#!/usr/bin/env bash

PROJECT_ROOT=&quot;/home/ubuntu/app&quot;
JAR_FILE=&quot;$PROJECT_ROOT/spring-webapp.jar&quot;

DEPLOY_LOG=&quot;$PROJECT_ROOT/deploy.log&quot;

TIME_NOW=$(date +%c)

# 현재 구동 중인 애플리케이션 pid 확인
CURRENT_PID=$(pgrep -f $JAR_FILE)

# 프로세스가 켜져 있으면 종료
if [ -z $CURRENT_PID ]; then
  echo &quot;$TIME_NOW &gt; 현재 실행중인 애플리케이션이 없습니다&quot; &gt;&gt; $DEPLOY_LOG
else
  echo &quot;$TIME_NOW &gt; 실행중인 $CURRENT_PID 애플리케이션 종료 &quot; &gt;&gt; $DEPLOY_LOG
  kill -15 $CURRENT_PID
fi</code></pre>
<br>

<h3 id="73-buildgradle-파일-수정">7.3 build.gradle 파일 수정</h3>
<p>build.gradle에 아래 코드를 추가해준다.</p>
<pre><code class="language-java">jar {
    enabled = false
}</code></pre>
<br>
<br>

<hr>
<h2 id="8-github-actions-workflow-작성">8. Github Actions Workflow 작성</h2>
<p><strong>1. Github Actions Workflow 생성
2. deploy.yml 파일 작성</strong></p>
<br>

<h3 id="81-github-actions-workflow-생성">8.1 Github Actions Workflow 생성</h3>
<p>Github Repository ➡ Actions ➡ Simple workflow 선택</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/8c4fe555-af40-4f5f-8884-05de53bf5be3/image.png" alt="">
<br></p>
<h3 id="82-deployyml-파일-작성">8.2 deploy.yml 파일 작성</h3>
<pre><code class="language-ruby">name: Deploy to Amazon EC2

on:
  push:
    branches:
      - main #브랜치명

# 본인이 설정한 값을 여기서 채워넣는다.
env:
  AWS_REGION: us-east-1 #리전
  S3_BUCKET_NAME: test-bucket #버킷 이름
  CODE_DEPLOY_APPLICATION_NAME: test-app #CodeDeploy 애플리케이션 이름
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: test-deployment-group #CodeDeploy 배포 그룹 이름

permissions:
  contents: read

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production

    steps:
    # (1) 기본 체크아웃
    - name: Checkout
      uses: actions/checkout@v3

    # (2) application.properties 설정
    - uses : actions/checkout@v3
    - run: touch ./src/main/resources/application.properties
    - run: echo &quot;${{ secrets.APPLICATION }}&quot; &gt; ./src/main/resources/application.properties
    - run: cat ./src/main/resources/application.properties

    # (3) gradlew 권한 추가
    - name: Run chmod to make gradlew executable
      run: chmod +x ./gradlew

    # (4) JDK 11 세팅
    - name: Set up JDK 11
      uses: actions/setup-java@v3
      with:
        distribution: &#39;temurin&#39;
        java-version: &#39;11&#39;

    # (5) Gradle build (Test 제외)
    - name: Build with Gradle
      uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee
      with:
        arguments: clean build -x test

    # (6) AWS 인증 (IAM 사용자 Access Key, Secret Key 활용)
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    # (7) 빌드 결과물을 S3 버킷에 업로드
    - name: Upload to AWS S3
      run: |
        aws deploy push \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --ignore-hidden-files \
          --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
          --source .

    # (8) S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행
    - name: Deploy to AWS EC2 from S3
      run: |
        aws deploy create-deployment \
          --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
          --s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip</code></pre>
<br>
<br>

<hr>
<h2 id="9-세팅-끝-배포-테스트">9. 세팅 끝. 배포 테스트</h2>
<br>

<p>action이 일어나면 워크 플로우가 동작하도록 설정한 브랜치에 push를 한 뒤,
워크 플로우가 정상적으로 수행이 끝나면 아래와 같이 ✔표시가 뜬다.
<img src="https://velog.velcdn.com/images/chae_ag/post/4e82e07f-ccab-4e5e-a766-de9e8ff4dde0/image.png" alt=""></p>
<p>Actions 탭에서 보고 싶은 워크 플로우를 클릭하면 아래와 같이 수행 과정을 볼 수 있다.
만약 에러가 난다면 어떤 부분에서 에러가 났는지도 확인이 가능하다.
<img src="https://velog.velcdn.com/images/chae_ag/post/b360ee98-1d73-4d9e-a8f1-8f7e5d85a641/image.png" alt=""></p>
<p>배포가 완료된 후 AWS S3에는 zip형식의 빌드파일이 저장되어 있고
<img src="https://velog.velcdn.com/images/chae_ag/post/5bebd77e-6df9-41ee-b360-d004a7e6f9ab/image.png" alt="">
AWS CodeDeploy에서 배포 내역을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/chae_ag/post/327fb258-4a9f-44ee-b405-3e85ef7d1ecf/image.png" alt=""></p>
<p>배포 자동화는 참 힘든거구나 싶었다..
다음엔 도커를 사용해보고 싶다! 😊
<br></p>
<p>참고한 블로그
<a href="https://bcp0109.tistory.com/363">https://bcp0109.tistory.com/363</a>
<a href="https://velog.io/@sun1203/Github-Action-Spring-Application.properties-%EB%AF%BC%EA%B0%90%EC%A0%95%EB%B3%B4-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0">https://velog.io/@sun1203/Github-Action-Spring-Application.properties</a>
<br></p>
<hr>
<blockquote>
<p>이 글을 읽는 모두, 좋은 하루 되세요. 🤗</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GDG Campus Korea X Whatever 6주 프로덕트 메이커 챌린지] 3주차 회고]]></title>
            <link>https://velog.io/@chae_ag/seriesGDG-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@chae_ag/seriesGDG-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 15 Sep 2023 07:00:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="3주차-회고">3주차 회고</h2>
<p><strong>시간이 너무 빠르다 !! 😱</strong></p>
</blockquote>
<br>

<h3 id="❔-이번주는-좋은-것과-나쁜-것이-무엇이-있었나요">❔ 이번주는 좋은 것과 나쁜 것이 무엇이 있었나요?</h3>
<h4 id="👍-good">👍 Good</h4>
<p><strong><code>지원했던 연합동아리에 서류 합격을 했다. 🥳</code></strong></p>
<p><strong><code>aws에 배포를 해서 24시간 서버를 돌릴 수 있게 되었다!</code></strong></p>
<h4 id="👎-bad">👎 Bad</h4>
<p>** <code>GDG 개발 기간이 별로 남지 않았다는 사실을 알아버린 것..</code> **</p>
<p>** <code>학교에서 제공하는 aws 계정으로는 S3를 사용하지 못한다는 사실을 늦게 알아버린 것. 삽질 엄청 했음 💢</code> **
<br></p>
<h3 id="📖-이번주-진행했던-학습개발-내용은-무엇이었나요">📖 이번주 진행했던 학습/개발 내용은 무엇이었나요?</h3>
<p><strong>👩‍💻 아래와 같은 개발을 진행했습니다.</strong></p>
<pre><code>   1) aws EC2 인스턴스에 스프링 프로젝트를 빌드하여 24시간 사용이 가능한 서버를 구축했다.

   2) aws S3 버킷을 생성한 뒤, 스프링 프로젝트에 연동하여 파일 업로드 기능을 구현했다.

   3) 현재까지 개발된 API의 목록을 테스트해볼 수 있도록 프로젝트에 Swagger를 연동했다.

   4) 백엔드 팀원분과 짜놓은 erd를 다시 한번 수정했다 ^^ (멈춰,,,)

   5) 포즈를 인원수로 필터링 하는 기능과 랜덤으로 하나의 포즈를 제공하는 기능을 구현했다.</code></pre><p><strong>✍ 아래와 같은 내용을 학습했습니다.</strong></p>
<pre><code>   1) aws EC2 서버 구축과 백그라운드에서 구동시키는 법

   2) aws S3 Spring으로 사용하는 법</code></pre><ul>
<li>aws EC2 서버 구축과 백그라운드에서 구동시키는 법
참고한 사이트
<a href="https://bcp0109.tistory.com/356">https://bcp0109.tistory.com/356</a></li>
</ul>
<pre><code>   # 백그라운드 구동시키기
   nohup java -jar {프로젝트명}-0.0.1-SNAPSHOT.jar &amp;

   # 백그라운드에서 구동중인 프로그램 종료하기
   # 실행 중인 서버 PID 확인
   ps -ef
   # 종료
   kill -9 [PID]</code></pre><br>

<ul>
<li>aws S3를 활용한 Spring 이미지 업로드
참고한 사이트
<a href="https://studyandwrite.tistory.com/498">https://studyandwrite.tistory.com/498</a>
<a href="https://jforj.tistory.com/261">https://jforj.tistory.com/261</a>
<a href="https://velog.io/@chaeri93/SpringBoot-AWS-S3%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0">https://velog.io/@chaeri93/SpringBoot-AWS-S3</a></li>
</ul>
<br>

<h3 id="💭-가장-고민을-했던-부분은-무엇이었나요">💭 가장 고민을 했던 부분은 무엇이었나요?</h3>
<h4 id="유저의-사진과-관리자의-사진을-어떤식으로-객체를-구분하여-관리할지">유저의 사진과 관리자의 사진을 어떤식으로 객체를 구분하여 관리할지?</h4>
<p>▶ 백엔드 팀원분과 이야기하여 관리자용 사진첩을 만들고 그 안에 사진을 저장하는 식으로 결정했다.
<br></p>
<h3 id="🤔-아쉬운-부분을-개선하기-위해서-필요한-것은-무엇인가요">🤔 아쉬운 부분을 개선하기 위해서 필요한 것은 무엇인가요?</h3>
<ul>
<li>API가 많아지다 보니 코드를 보면 너무 정신없다.
앞으로 개발하는 내용에는 주석을 필히 달고
전체적인 코드 리팩토링이 필요하다.<br>

</li>
</ul>
<h3 id="🕒-다음주는-어떻게-보낼-예정인가요">🕒 다음주는 어떻게 보낼 예정인가요?</h3>
<ul>
<li>09.18(월) 팀원들과 오프라인 모각작</li>
<li>09.20(수)까지 기능 구현을 끝내기</li>
<li>기능 구현을 마치고 서버 배포 후 에러 잡기</li>
</ul>
<br>

<hr>
<blockquote>
<p>3주차 회고를 마칩니다. 💚</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[GDG Campus Korea X Whatever 6주 프로덕트 메이커 챌린지] 2주차 회고]]></title>
            <link>https://velog.io/@chae_ag/series%ED%9A%8C%EA%B3%A0%EB%A1%9DGDG-X-Whatever-6%EC%A3%BC-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EB%A9%94%EC%9D%B4%EC%BB%A4-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@chae_ag/series%ED%9A%8C%EA%B3%A0%EB%A1%9DGDG-X-Whatever-6%EC%A3%BC-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EB%A9%94%EC%9D%B4%EC%BB%A4-%EC%B1%8C%EB%A6%B0%EC%A7%80-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 08 Sep 2023 12:01:52 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="2주차-회고">2주차 회고</h2>
<p>힘들지만 즐겁다 즐겁지만 힘들다~..🤤</p>
</blockquote>
<h3 id="❔-이번주는-좋은-것과-나쁜-것이-무엇이-있었나요">❔ 이번주는 좋은 것과 나쁜 것이 무엇이 있었나요?</h3>
<p><strong><code>가족끼리 캠핑을 가서 힐링하고 왔다</code></strong></p>
<p><strong><code>연합동아리 지원 자소서를 쓰느라 정신이 없었다..😨</code></strong>
<br></p>
<h3 id="📖-이번주-진행했던-학습개발-내용은-무엇이었나요">📖 이번주 진행했던 학습/개발 내용은 무엇이었나요?</h3>
<p>👩‍💻 아래와 같은 기능을 구현했습니다.</p>
<pre><code> - 아카이브 기능
     1) 앨범 생성 시 원하는 유저 초대
    2) 앨범에 포함된 사진 조회</code></pre><p>✍ 아래와 같은 내용을 학습했습니다.</p>
<pre><code>- Spring 외래키 연관관계 매핑 방법</code></pre><blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/7076941e-5d8a-48da-b438-60882a90b923/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/c4b6bcb2-75c7-449c-b818-c6709be40c3e/image.png" alt=""></p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/84cc6001-a5f5-4604-ac28-b5f779824255/image.png" alt=""></p>
<br>

<h3 id="💭-가장-고민을-했던-부분은-무엇이었나요">💭 가장 고민을 했던 부분은 무엇이었나요?</h3>
<p>객체끼리 연관관계 매핑 시 외래키로 특정 개체를 조회하는 것(ex. 특정 사진첩에 들어있는 모든 사진 조회)이 아무리 해도 안돼서 지인한테도 물어보고 GDG 질문방에도 올렸다가 결국 내 힘으로 해결해냈다! 😚
<br></p>
<h3 id="🤔-아쉬운-부분을-개선하기-위해서-필요한-것은-무엇인가요">🤔 아쉬운 부분을 개선하기 위해서 필요한 것은 무엇인가요?</h3>
<ul>
<li>이미지 업로드와 배포를 위해 aws 서버 구동이 필요하다.<br>

</li>
</ul>
<h3 id="🕒-다음주는-어떻게-보낼-예정인가요">🕒 다음주는 어떻게 보낼 예정인가요?</h3>
<ul>
<li><p>09.13(수) 팀원들과 오프라인 모각작</p>
</li>
<li><p>09.16(토) 리뷰데이 in 구글 스타트업 캠퍼스 (강남)</p>
</li>
<li><p>aws S3로 파일 업로드, EC2로 배포까지 해내기</p>
<br>

</li>
</ul>
<hr>
<blockquote>
<p>2주차 회고를 마칩니다. 😉</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Model, DTO, VO, DAO 가 무엇인가요?]]></title>
            <link>https://velog.io/@chae_ag/Model-DTO-VO-DAO-%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94</link>
            <guid>https://velog.io/@chae_ag/Model-DTO-VO-DAO-%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94</guid>
            <pubDate>Sun, 03 Sep 2023 17:18:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h2 id="model이란">Model이란?</h2>
</blockquote>
<p>객체지향프로그래밍에서 클래스를 역할에 따라 부르는 이름을 <strong>모델(Model)</strong>이라고 부른다.
<br></p>
<p>모델의 종류는 크게 메인클래스, DTO, VO, DAO, Utility 등이 있다.
<br></p>
<table>
<thead>
<tr>
<th>메인클래스</th>
<th>DTO(Data Transfer Object), VO(Value Object)</th>
<th>DAO(Data Access Object)</th>
<th>Utility</th>
</tr>
</thead>
<tbody><tr>
<td>시작 클래스(모델)</td>
<td>데이터를 담는(이동) 모델</td>
<td>데이터를 처리(DB)를 하는 모델, 주로 CRUD 처리를 한다.</td>
<td>도움(Utility)을 주는 모델</td>
</tr>
<tr>
<td><br></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<h2 id="dto-vo-모델이란">DTO, VO 모델이란?</h2>
</blockquote>
<ul>
<li>데이터를 하나로 묶어야 될 경우 <strong>데이터를 하나로 수집하는 역할</strong>을 하는 바구니가 필요. 이 바구니 역할을 하는 것이 DTO, VO 모델<br></li>
<li>DTO와 VO는 서로 비슷한 개념이지만 DTO는 값을 전달하는 동작을 수행하고 VO는 값 그자체이다.
그렇기 때문에 VO는 setter와 같은 새로운 값 지정 메서드 절대 사용 ❌</li>
</ul>
<br>
예를 들어, 객체의 정보를 입력받아서 메인메서드가 아닌 다른 메서드에서 출력을 시키고 싶을 때, 아래와 같이 출력을 하는 메서드를 만들고

<p><img src="https://velog.velcdn.com/images/chae_ag/post/c89869fe-e3a3-45c8-8a2c-79ac1eb34618/image.png" alt=""></p>
<p>해당 메서드에 객체의 정보를 인자로 일일히 넘겨주어야 메인메서드가 아닌 다른 객체의 정보를 출력할 수 있다.
인자가 한두개면 상관 없지만 100개, 1000개처럼 수많은 인자를 다 적기엔 너무 힘들다.</p>
<p><strong>👉 수많은 인자를 가지고 있는 모델을 만들어 사용하자! 그것이 DTO, VO</strong></p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/7f0f9b77-114c-455c-92c6-9eb5e8657f1b/image.png" alt=""> </p>
<p>DTO 클래스를 만든다 ~</p>
<p><img src="https://velog.velcdn.com/images/chae_ag/post/41d70f0e-8c82-42c1-9b38-a9022968c672/image.png" alt=""></p>
<p>메인 클래스와 출력 메서드 부분을 위와 같이 수정하면 처음과 똑같이 작동한다. 처음보다 훨씬 간결해진 것을 확인할 수 있다. 
😍 DTO와 VO를 사용하는 이유 증명!</p>
<blockquote>
<h2 id="dao란">DAO란?</h2>
</blockquote>
<p>데이터베이스에 데이터를 저장, 수정, 검색, 삭제를 하기 위해서 만들어지는 모델</p>
<p>CRUD 동작을 가지고있는 클래스</p>
<p>비지니스 로직을 처리하는 클래스</p>
<h3 id="🤔-비지니스-로직이란">🤔 비지니스 로직이란?</h3>
<p>홈페이지 회원가입을 예로 들어본다.
유저는 회원가입 폼에 양식을 입력하고 회원가입 버튼을 누르면 회원가입이 진행된다.
개발자는 이 과정들을 구현하기 위해 생각보다 많은 코드를 짜야한다.
이 때 아이디 중복찾기를 예시로 들면 아래와 같다.</p>
<p><code>1. 회원이 작성한 아이디 값을 저장</code></p>
<p><code>2. 회원정보가 있는 데이터베이스 연결</code></p>
<p><code>3. 데이터베이스에 회원이 작성한 아이디 값이 있는지 중복검사</code></p>
<p><code>4. 회원의 아이디가 이미 있는지 없는지 여부를 데이터화 하여 저장</code></p>
<p><code>5. 데이터베이스 연결 끊기</code></p>
<p><code>6. View 영역에게 가공된 데이터 전달</code></p>
<p>이와 같은 과정을 우리는 비지니스 로직이라고 한다.</p>
<p>참고
<a href="https://mommoo.tistory.com/67">https://mommoo.tistory.com/67</a></p>
]]></description>
        </item>
    </channel>
</rss>