<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>viviaHAM.log</title>
        <link>https://velog.io/</link>
        <description>Backend Engineer</description>
        <lastBuildDate>Thu, 23 Jul 2026 16:04:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>viviaHAM.log</title>
            <url>https://velog.velcdn.com/images/viviamm7-code/profile/359a2b28-156a-4f61-8bc0-c880cc169eca/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. viviaHAM.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/viviamm7-code" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Java] Hash]]></title>
            <link>https://velog.io/@viviamm7-code/Java-Hash</link>
            <guid>https://velog.io/@viviamm7-code/Java-Hash</guid>
            <pubDate>Thu, 23 Jul 2026 16:04:34 GMT</pubDate>
            <description><![CDATA[<h2 id="1-set">1. Set</h2>
<p><code>Set</code> 은 유일한 요소들의 컬렉션이다.</p>
<h4 id="유일성">유일성</h4>
<ul>
<li><code>Set</code> 에는 중복된 요소가 존재하지 않는다.</li>
</ul>
<h4 id="순서-미보장">순서 미보장</h4>
<ul>
<li><p>대부분의 <code>Set</code> 구현에서는 요소들의 순서를 보장하지 않는다.</p>
</li>
<li><p>입력 순서와 출력 순서가 다를 수 있다.</p>
</li>
</ul>
<h4 id="빠른-검색">빠른 검색</h4>
<ul>
<li><p><code>Set</code> 은 요소의 유무를 빠르게 확인할 수 있도록 최적화 돼 있다.</p>
</li>
<li><p>데이터의 중복을 방지하고 빠른 조회가 가능하다.</p>
</li>
</ul>
<hr>
<h2 id="2-해시-알고리즘---index-사용">2. 해시 알고리즘 - index 사용</h2>
<p>배열에 중복 데이터가 있는지 확인하려면 항상 전체 데이터를 확인해야 한다.
-&gt; 성능이 나쁘다.</p>
<hr>
<h2 id="3-해시-알고리즘---메모리-낭비">3. 해시 알고리즘 - 메모리 낭비</h2>
<p>만약 입력 값의 범위가 <code>int</code> 인 모든 범위를 입력할 수 있도록 하려면 심각한 메모리 낭비를 초래한다.</p>
<hr>
<h2 id="4-해시-알고리즘---나머지-연산">4. 해시 알고리즘 - 나머지 연산</h2>
<p>저장할 수 있는 배열의 크기를 10 이라고 가정하고, 그 크기에 맞춰 입력하는 데이터를 %10 나머지 연산을 한 결과 값을 인덱스로 하여 배열에 저장해보자.</p>
<p>나머지 연산을 통해 구한 인덱스를 해시 인덱스라 한다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/4606ade1-27b9-434c-9b05-8a7560a1f6e1/image.png" alt=""></p>
<ul>
<li><p>입력 값의 범위가 넓어도 배열의 크기를 제한하고, 나머지 연산을 통해 메모리가 낭비되는 문제도 해결할 수있다.</p>
</li>
<li><p>해시 인덱스를 사용해 <code>O(1)</code> 의 성능으로 데이터를 저장하고, 조회할 때도 같은 성능으로 조회할 수 있다.</p>
</li>
</ul>
<hr>
<h2 id="5-해시-알고리즘의-한계---해시-충돌">5. 해시 알고리즘의 한계 - 해시 충돌</h2>
<p>만약 데이터의 입력 값으로 <code>9, 99</code> 가 들어온다면 나머지 연산 후 같은 곳에 2개의 데이터가 저장될 것이다.
다른 값을 입력했는데 같은 해시 코드가 나오는 이것이 해시 충돌이다.</p>
<p>이를 해결하는 방법은 해시 충돌을 인정하는 것이다.
<strong>해시 충돌이 일어났을 때 해시 인덱스와 함께 저장하는 것이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/429d9c4c-a28a-4ec3-828c-87327b2268bb/image.png" alt=""></p>
<hr>
<h2 id="6-해시-충돌-구현">6. 해시 충돌 구현</h2>
<pre><code class="language-java">public class HashStart {

    static final int CAPACITY = 10;

    public static void main(String[] args) {
         //{1, 2, 5, 8, 14, 99 ,9}
        LinkedList&lt;Integer&gt;[] buckets = new LinkedList[CAPACITY];
        for (int i = 0; i &lt; CAPACITY; i++) {
               buckets[i] = new LinkedList&lt;&gt;();
         }

         add(buckets, 1);
        add(buckets, 2);
        add(buckets, 5);
        add(buckets, 8);
        add(buckets, 14);
        add(buckets, 99);
        add(buckets, 9); //중복
         System.out.println(&quot;buckets = &quot; + Arrays.toString(buckets));

         //검색
         int searchValue = 9;
         boolean contains = contains(buckets, searchValue);
         System.out.println(&quot;buckets.contains(&quot; + searchValue + &quot;) = &quot; + contains);
    }

     private static void add(LinkedList&lt;Integer&gt;[] buckets, int value) {
         int hashIndex = hashIndex(value);
         LinkedList&lt;Integer&gt; bucket = buckets[hashIndex]; // O(1)
         if (!bucket.contains(value)) { // O(n)
             bucket.add(value);
         }
     }

     private static boolean contains(LinkedList&lt;Integer&gt;[] buckets, int searchValue) {
         int hashIndex = hashIndex(searchValue);
         LinkedList&lt;Integer&gt; bucket = buckets[hashIndex]; // O(1)
         return bucket.contains(searchValue); // O(n)
    }

    static int hashIndex(int value) {
         return value % CAPACITY;
     }
}</code></pre>
<blockquote>
<h3 id="해시-인덱스-충돌-확률">해시 인덱스 충돌 확률</h3>
</blockquote>
<p>해시 출동이 발생하면 데이터를 추가하거나 조회할 때, 연결 리시트 내부에서 <code>O(n)</code> 의 추가 연산을 해야 하므로 성능이 떨어진다. 따라서 해시 충돌은 가급적 발생하지 않도록 해야 한다.</p>
<p>해시 충돌이 발생할 확률은 입력하는 데이터의 수와 배열의 크기와 관련이 있다. 입력하는 데이터의 수와 비교해서 배열의 크기가 클 수록 충돌 확률은 낮아진다.</p>
<p>상황에 따라 다르겠지만 보통 <code>75%</code> 를 적절한 크기로 보고 기준으로 잡는 것이 효과적이다.</p>
<p>해시 인덱스를 사용하는 방식은 사실 최악의 경우 <code>(O(n))</code> 는 거의 발생하지 않는다. 배열의 크기만 적절하게 잡아주면 대부분 <code>O(1)</code> 에 가까운 매우 빠른 성능을 보여준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] List]]></title>
            <link>https://velog.io/@viviamm7-code/Java-List</link>
            <guid>https://velog.io/@viviamm7-code/Java-List</guid>
            <pubDate>Thu, 23 Jul 2026 15:41:30 GMT</pubDate>
            <description><![CDATA[<h2 id="1-인터페이스-도입">1. 인터페이스 도입</h2>
<p>자료구조에서 다형성과 OCP 원칙을 잘 활용할 수 있다.</p>
<h3 id="list-자료구조">List 자료구조</h3>
<ul>
<li><p>순서가 있고, 중복을 허용하는 자료 구조를 List라 한다.</p>
</li>
<li><p><code>ArrayList</code>와 <code>LinkedList</code>의 공통 기능을 인터페이스로 뽑아서 추상화하면 다형성을 활용한 다양한 이득을 얻을 수 있다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/384a977e-7a8b-49b5-abaa-72d2c717ab96/image.png" alt=""></p>
</li>
</ul>
<hr>
<h2 id="2-의존관계-주입">2. 의존관계 주입</h2>
<pre><code class="language-java">public class Processor {

    private final MyList&lt;Integer&gt; list;

    public Processor(MyList&lt;integer&gt; list) {
        this.list = list;
    }

    public void logic(int size) {
        for (int i = 0; i &lt; size; i++) {
            list.add(0, i);
        }
    }
}

main() {
    new Processor(new MyArrayList()); //ArrayList 사용하고 싶을 때 사용
    new Processor(new MyLinkedList()); // LinkedList 사용하고 싶을 때 사용
}</code></pre>
<ul>
<li><p><code>Processor</code> 가 <code>MyArrayList()</code> , <code>MyLinkedList()</code> 에 직접 의존하는게 아니라 <code>MyList()</code> 인터페이스에 의존해 원하는 클래스를 원할 떄 꺼내서 사용하면 된다.</p>
</li>
<li><p>이렇게 인터페이스에 의존하면 <code>Processor</code> 의 코드를 전혀 변경하지 않고, 원하는 리스트 전략을 런타임에 지정할 수 있다.</p>
</li>
</ul>
<h3 id="생성자-주입">생성자 주입</h3>
<pre><code class="language-java">public Processor(MyList&lt;integer&gt; list) {
    this.list = list;
}</code></pre>
<ul>
<li><code>Processor</code> 생성자를 통해 런타임 의존관계를 주입하는 것을 생성자 주입이라 한다.</li>
</ul>
<hr>
<h2 id="3-java의-list">3. Java의 List</h2>
<h3 id="1-collection-인터페이스">1. Collection 인터페이스</h3>
<ul>
<li><p><code>Collection</code> 인터페이스는 <code>java.util</code> 패키지의 컬렉션 프레임워크의 핵심 인터페이스 중 하나이다. </p>
</li>
<li><p><code>List</code>, <code>Set</code>, <code>Queue</code> 와 같은 다양한 하위 인터페이스와 함꼐 사용되며, 데이터를 각 형태에 맞게 관리할 수 있다.</p>
</li>
</ul>
<h3 id="2-list-인터페이스">2. List 인터페이스</h3>
<ul>
<li><p><code>List</code> 인터페이스는 <code>java.util</code> 패키지에 있는 컬렉션 프레임워크의 일부이다.</p>
</li>
<li><p>Collection 인터페이스에게 상속받는다.</p>
</li>
<li><p>객체들의 순서가 있는 컬렉션을 나타내며, 같은 객체의 중복 저장을 허용한다.</p>
</li>
<li><p>배열과 비슷하지만, 크기가 동적으로 변화하는 컬렉션을 다룰 때 유연하게 사용가능 하다.</p>
</li>
</ul>
<blockquote>
<h3 id="list-실제-성능">List 실제 성능</h3>
</blockquote>
<p>이론적으로 <code>LinkedList</code> 의 중간 삽입 연산은 <code>ArrayList</code> 보다 빠르다.
그러나 실제 성능은 데이터 순차적 접근 속도, 메모리 할당 및 해제 비용, CPU 캐시 활용도 등 다양한 요소에 영향을 받는다.</p>
<p>또한 자바의 <code>ArrayList</code> 는 데이터를 한 칸씩 이동시키지 않고, 메모리 고속 복사 연산을 사용한다.</p>
<p>따라서 여러가지 요소에 의해 <code>ArrayList</code> 가 실제 사용 환경에서 더 나은 성능을 보여주는 경우가 많다.</p>
<ul>
<li><p>실무에서는 주로 <code>ArrayList</code> 를 사용한다.</p>
</li>
<li><p>만약 많은 데이터를 반복적으로 추가 / 삭제 하는 일이 많을 때만 <code>LinkedList</code> 를 고려한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] LinkedList]]></title>
            <link>https://velog.io/@viviamm7-code/Java-LinkedList</link>
            <guid>https://velog.io/@viviamm7-code/Java-LinkedList</guid>
            <pubDate>Thu, 23 Jul 2026 15:13:01 GMT</pubDate>
            <description><![CDATA[<h2 id="1-arraylist-의-단점">1. ArrayList 의 단점</h2>
<p>ArrayList는 내부에 배열을 사용해서 데이터를 보관하고 관리한다.</p>
<ul>
<li><p>데이터 추가시 기존 데이터들 오른쪽 이동</p>
<ul>
<li>데이터 삭제시 기존 데이터들 왼쪽 이동</li>
</ul>
</li>
</ul>
<p>이렇게 앞이나 중간에 데이터를 추가하거나 삭제하는 경우 많은 데이터를 이동하기 때문에 성능이 좋지 않다.</p>
<hr>
<h2 id="2-노드와-연결">2. 노드와 연결</h2>
<p>낭비되는 메모리 없이 필요한 만큼만 메모리를 확보해서 사용한다.
노드를 만들고 각 노드를 서로 연결하는 방식이다.</p>
<h4 id="node">Node</h4>
<pre><code class="language-java">public class Node {
    Object item;
    Node next;
}</code></pre>
<p>노드 클래스는 내부에 저장할 데이터 <code>item</code> 과, 다음으로 연결할 노드의 참조인 <code>next</code> 를 가진다.</p>
<hr>
<h2 id="3-직접-구현하는-linkedlist">3. 직접 구현하는 LinkedList</h2>
<p>노드와의 연결 구조를 통해 리스트로 만든 자료 구조가 <code>LinkedList</code> 이다.</p>
<pre><code class="language-java">public class MyLinkedList&lt;E&gt; {

    private Node&lt;E&gt; first;
    private int size = 0;

    public void add(E e) {
        Node&lt;E&gt; newNode = new Node&lt;&gt;(e);
        if (first == null) {
            first = newNode;
        } else {
            Node&lt;E&gt; lastNode = getLastNode();
            lastNode.next = newNode;
        }
        size++;
    }

    private Node&lt;E&gt; getLastNode() {
        Node&lt;E&gt; x = first;
        while (x.next != null) {
            x = x.next;
        }
        return x;
    }

    public void add(int index, E e) {
        Node&lt;E&gt; newNode = new Node&lt;&gt;(e);
        if (index == 0) {
            newNode.next = first;
            first = newNode;
        } else {
            Node&lt;E&gt; prev = getNode(index - 1);
            newNode.next = prev.next;
            prev.next = newNode;
        }
        size++;
    }

    public E remove(int index) {
        Node&lt;E&gt; node = getNode(index);
        E removedItem = node.item;
        if (index == 0) {
            first = node.next;
        } else {
            Node&lt;E&gt; prev = getNode(index - 1);
            prev.next = node.next;
        }
        node.next = null;
        node.item = null;
        size--;
        return removedItem;
    }

    public E set(int index, E element) {
        Node&lt;E&gt; x = getNode(index);
        E oldValue = x.item;
        x.item = element;
        return oldValue;
    }

    public E get(int index) {
        return getNode(index).item;
    }

    public int indexOf(E e) {
        int index = 0;
        for (Node&lt;E&gt; x = first; x != null; x = x.next) {
            if (x.item.equals(e)) return index;
            index++;
        }
        return -1;
    }

    public int size() {
        return size;
    }

    @Override
    public String toString() {
        return &quot;MyLinkedListV1{&quot; +
                &quot;first=&quot; + first +
                &quot;, size=&quot; + size +
                &#39;}&#39;;
    }

    private Node&lt;E&gt; getNode(int index) {
        Node&lt;E&gt; x = first;
        for (int i = 0; i &lt; index; i++) {
            x = x.next;
        }
        return x;
    }

    private static class Node&lt;E&gt; {

        E item;
        Node&lt;E&gt; next;

        public Node(E item) {
            this.item = item;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            Node&lt;E&gt; x = this;
            sb.append(&quot;[&quot;);
            while (x != null) {
                sb.append(x.item);
                if (x.next != null) sb.append(&quot;-&gt;&quot;);
                x = x.next;
            }
            sb.append(&quot;]&quot;);
            return sb.toString();
        }
    }
}</code></pre>
<ul>
<li><h4 id="private-nodee-first"><code>private Node&lt;E&gt; first</code></h4>
<p>첫 노드의 위치를 가리킨다.</p>
</li>
<li><h4 id="private-int-size--0"><code>private int size = 0</code></h4>
<p>자료 구조에 입력된 데이터의 사이즈이다.</p>
</li>
</ul>
<h3 id="마지막에-데이터-추가">마지막에 데이터 추가</h3>
<pre><code class="language-java">public void add(E e) {
    Node&lt;E&gt; newNode = new Node&lt;&gt;(e);
    if (first == null) {
        first = newNode;
    } else {
        Node&lt;E&gt; lastNode = getLastNode();
        lastNode.next = newNode;
    }
    size++;
}

private Node&lt;E&gt; getLastNode() {
    Node&lt;E&gt; x = first;
    while (x.next != null) {
        x = x.next;
    }
    return x;
}</code></pre>
<ul>
<li>첫 노드 추가라면 first에 연결한다.</li>
<li>마지막 노드를 찾아 새로운 노드를 가르키도록 새로운 노드를 추가한다.</li>
<li><h4 id="while-xnext--null"><code>while (x.next != null)</code></h4>
다음 노드가 없으면 이 노드는 마지막 노드이다.</li>
</ul>
<h3 id="특정-위치에-데이터-추가">특정 위치에 데이터 추가</h3>
<pre><code class="language-java">public void add(int index, E e) {
    Node&lt;E&gt; newNode = new Node&lt;&gt;(e);
    if (index == 0) {
        newNode.next = first;
        first = newNode;
    } else {
        Node&lt;E&gt; prev = getNode(index - 1);
        newNode.next = prev.next;
        prev.next = newNode;
    }
    size++;
}

private Node&lt;E&gt; getNode(int index) {
    Node&lt;E&gt; x = first;
    for (int i = 0; i &lt; index; i++) {
        x = x.next;
    }
    return x;
}</code></pre>
<ul>
<li><p><code>index == 0</code> 이면 
<code>newNode.next = first;</code> 다음 노드에 현재 첫 노드를 옮기고
<code>first = newNode;</code> newNode를 첫 노드로 수정한다.</p>
</li>
<li><p><code>getNode(index - 1);</code> : 추가하려는 index 앞의 노드를 찾아서 새롭게 추가하는 노드를 가리키도록 하고, 추가되는 노드는 prev 노드가 가리키던 노드를 가리키도록 수정한다.</p>
</li>
</ul>
<h3 id="특정-위치의-데이터-삭제">특정 위치의 데이터 삭제</h3>
<pre><code class="language-java">public E remove(int index) {
    Node&lt;E&gt; node = getNode(index);
    E removedItem = node.item;
    if (index == 0) {
        first = node.next;
    } else {
        Node&lt;E&gt; prev = getNode(index - 1);
        prev.next = node.next;
    }
    node.next = null;
    node.item = null;
    size--;
    return removedItem;
}

private Node&lt;E&gt; getNode(int index) {
    Node&lt;E&gt; x = first;
    for (int i = 0; i &lt; index; i++) {
        x = x.next;
    }
    return x;
}</code></pre>
<ul>
<li><p><code>index == 0</code> 이면
<code>first = node.next</code> 삭제하는 노드의 다음 노드를 first가 가리키도록 한다.</p>
</li>
<li><p><code>prev.next = node.next;</code> : 삭제하는 prev 노드가 삭제하는 노드가 가리키는 노드를 가리키도록 수정한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] ArrayList]]></title>
            <link>https://velog.io/@viviamm7-code/Java-ArrayList</link>
            <guid>https://velog.io/@viviamm7-code/Java-ArrayList</guid>
            <pubDate>Tue, 21 Jul 2026 14:23:27 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배열의-특징">1. 배열의 특징</h2>
<p>자바는 배열뿐만 아니라, 컬렉션 프레임워크라는 이름으로 다양한 자료 구조를 제공한다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/1c954a70-99b1-4a15-bb12-d8ea5b79aa19/image.png" alt="">  </p>
<ul>
<li><p>배열에서 데이터를 찾을 때 인덱스를 사용하면 매우 빠르게 데이터를 찾을 수 있다.</p>
</li>
<li><p>인덱스를 통한 입력, 변경, 조회의 경우 한번의 계산으로 데이터의 위치를 찾을 수 있다.</p>
</li>
</ul>
<h3 id="배열의-검색">배열의 검색</h3>
<p>배열에 들어있는 데이터를 찾는 것을 검색이라 한다.</p>
<ul>
<li><p>배열의 인덱스 사용 : O(1)</p>
</li>
<li><p>배열의 순차 검색 : O(n)</p>
</li>
</ul>
<h3 id="배열-데이터-추가">배열 데이터 추가</h3>
<p>배열의 특정 위치에 데이터를 추가하려면 기존 데이터를 한칸씩 오른쪽으로 이동해야 한다.
<code>O(n)</code></p>
<h3 id="배열의-한계">배열의 한계</h3>
<p>배열은 가장 기본적은 자료구조이고, 특히 인덱스를 사용할 때 최고의 효율이 나온다.
하지만 배열의 크기를 배열을 생성하는 시점에 정해야돼 동적으로 정할 수 없다는  단점이 있다.</p>
<hr>
<h2 id="2-arraylist-직접-구현">2. ArrayList 직접 구현</h2>
<p>배열의 길이를 동적으로 변경할 수 없고 데이터를 추가할 때의 불편함을 해소해 제공하는 자료구조를 <code>List</code> 라 한다.</p>
<h3 id="직접-구현해보기">직접 구현해보기</h3>
<pre><code class="language-java">public class MyArrayList&lt;E&gt; {

    private static final int DEFAULT_CAPACITY = 5;

    private Object[] elementData;
    private int size = 0;

    public MyArrayList() {
        elementData = new Object[DEFAULT_CAPACITY];
    }

    public MyArrayList(int initialCapacity) {
        elementData = new Object[initialCapacity];
    }

    public int size() {
        return size;
    }

    public void add(E e) {

        if (size == elementData.length) {
            grow();
        }

        elementData[size] = e;
        size++;
    }

    public void add(int index, E e) {

        if (size == elementData.length) {
            grow();
        }

        //데이터 이동
        shiftRightFrom(index);
        elementData[index] = e;
        size++;
    }

    private void shiftRightFrom(int index) {
        for (int i = size; i &gt; index; i--) {
            elementData[i] = elementData[i - 1];
        }
    }

    private void grow() {
        elementData = Arrays.copyOf(elementData, elementData.length * 2);
    }

    @SuppressWarnings(&quot;unchecked&quot;)
    public E get(int index) {
        return (E) elementData[index];
    }

    public E set(int index, E e) {
        E o = get(index);
        elementData[index] = e;
        return o;
    }

    public E remove(int index) {
        E oldValue = get(index);
        shiftLeftFrom(index);

        size--;
        elementData[size] = null;
        return oldValue;
    }

    private void shiftLeftFrom(int index) {
        for (int i = index; i &lt; size - 1; i++) {
            elementData[i] = elementData[i + 1];
        }
    }

    public int indexOf(E e) {
        for (int i = 0; i &lt; size; i++) {
            if (elementData[i].equals(e)) return i;
        }
        return -1;
    }

    public String toString() {
        return Arrays.toString(Arrays.copyOf(elementData, size))
                + &quot; size = &quot; + size
                + &quot;, capacity = &quot; + elementData.length;
    }
}</code></pre>
<ul>
<li><p>먼저 상수 필드로 배열의 초기 용량 값을 정한다.</p>
</li>
<li><p><code>private Object[] elementData;</code> : 다양한 타입의 데이터를 사용하기 위해 Object 배열을 사용했다.</p>
</li>
</ul>
<h3 id="동적으로-배열의-크기-늘리기">동적으로 배열의 크기 늘리기</h3>
<pre><code class="language-java">public void add(E e) {

    if (size == elementData.length) {
        grow()
    }

    elementData[size] = e;
    size++;
}

private void grow() {
    elementData = Arrays.copyOf(elementData,  elementData.length * 2);
}</code></pre>
<ul>
<li><p>데이터를 추가할 때 size가 배열의 크기와 같아지면 더는 데이터를 추가할 수 없다.</p>
</li>
<li><p>이때 grow()를 호출해 기존 배열을 복사한 새로운 배열을 만들고 기존 배열 2배의 크기로 만들어 준다.</p>
</li>
</ul>
<h3 id="인덱스-위치에-데이터를-추가">인덱스 위치에 데이터를 추가</h3>
<pre><code class="language-java">public void add(int index, E e) {

    if (size == elementData.length) {
        grow();
    }

    //데이터 이동
    shiftRightFrom(index);
    elementData[index] = e;
    size++;
}

private void grow() {
    elementData = Arrays.copyOf(elementData,  elementData.length * 2);
}

private void shiftRightFrom(int index) {

    for (int i = size; i &gt; index; i--) {
        elementData[i] = elementData[i - 1];
    }
}</code></pre>
<ul>
<li>기존에 인덱스부터 배열의 끝까지 존재했던 데이터를 한칸씩 오른쪽으로 밀어주고 인덱스에 데이터를 추가한다.</li>
</ul>
<h3 id="인덱스-위치의-데이터-삭제">인덱스 위치의 데이터 삭제</h3>
<pre><code class="language-java">public E remove(int index) {
    E oldValue = get(index);
    shiftLeftFrom(index);

    size--;
    elementData[size] = null;
    return oldValue;
}

private void shiftLeftFrom(int index) {
    for (int i = index; i &lt; size - 1; i++) {
    elementData[i] = elementData[i + 1];
    }
}</code></pre>
<ul>
<li>기존에 인덱스 다음부터 배여르이 끝까지 존재하던 데이터를 왼쪽으로 한칸씩 밀어주고 인덱스의 데이터를 삭제한다.</li>
</ul>
<hr>
<h2 id="3-arraylist의-단점">3. ArrayList의 단점</h2>
<ul>
<li><p>정확한 크기를 미리 알지 못하면 메모리가 낭비된다.</p>
</li>
<li><p>데이터를 중간에 추가하거나 삭제할 때 비효율적이다.</p>
</li>
</ul>
<blockquote>
<h4 id="이러한-단점을-해결한-자료구조는-linkedlist-라-한다">이러한 단점을 해결한 자료구조는 <code>LinkedList</code> 라 한다.</h4>
</blockquote>
<p><code>LinkedList</code> 는 <code>ArrayList</code> 보다 중간 삽입, 삭제가 더 개선돼 빠르지만, 실제로는 원하는 위치까지 찾아가는 비용이 있고 캐시 효율도 안 좋아서 생각보다 잘 사용하진 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Generic]]></title>
            <link>https://velog.io/@viviamm7-code/Java-Generic</link>
            <guid>https://velog.io/@viviamm7-code/Java-Generic</guid>
            <pubDate>Tue, 21 Jul 2026 13:26:19 GMT</pubDate>
            <description><![CDATA[<h2 id="1-제네릭이-필요한-이유">1. 제네릭이 필요한 이유</h2>
<p>제네릭을 사용하면 코드 재사용과 타입 안전성을 확보할 수 있다.</p>
<pre><code class="language-java">public class Box&lt;T&gt; {

    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}</code></pre>
<ul>
<li><p>&lt;&gt;를 사용한 클래스를 제네릭 클래스라고 한다.</p>
</li>
<li><p>제네릭 클래스를 사용할 땐 타입을 미리 결정하지 않는다.</p>
</li>
<li><p><code>&lt;T&gt;</code> 의 T를 타입 매개변수라고 하고, 이후에 Integer, String 같은 타입으로 변한다.</p>
</li>
<li><p>클래스 내부에 T 타입이 필요한 곳에 T value와 같이 타입 매개변수를 적어두면 된다.</p>
</li>
</ul>
<pre><code class="language-java">Box&lt;Integer&gt; integerBox = new Box&lt;&gt;(); // Integer로 생성
integerBox.setValue(10);
Integer integer = integerBox.getValue();

Box&lt;String&gt; stringBox = new Box&lt;&gt;(); // String으로 생성
stringBox.setValue(&quot;hello&quot;);
String string = stringBox.getValue();

System.out.println(integer); //10
System.out.println(string); // &quot;hello&quot;</code></pre>
<ul>
<li>Generic 타입은 생성 시점에 &lt;&gt; 사이에 원하는 타입을 지정한다.</li>
</ul>
<hr>
<h2 id="2-제네릭-용어와-관례">2. 제네릭 용어와 관례</h2>
<p>제네릭의 핵심은 사용할 타입을 미리 결정하지 않는다는 점이다.
실제 생성 시점에 타입을 결정하는 것이다.</p>
<blockquote>
<h3 id="용어-정리">용어 정리</h3>
</blockquote>
<h4 id="제네릭generic-단어">제네릭(Generic) 단어</h4>
<ul>
<li><p>제네릭이라는 단어는 일반적인, 범용적인이라는 영어 단어 뜻이다.</p>
</li>
<li><p>풀어보면 특정 타입에 속한 것이 아니라 일반적으로, 범용적으로 사용할 수 있다는 뜻이다.</p>
</li>
</ul>
<h4 id="제네릭-타입generic-type">제네릭 타입(Generic Type)</h4>
<ul>
<li><p>클래스나 인터페이스를 정의할 때 타입 매개변수를 사용하는 것을 말한다.</p>
</li>
<li><p>제네릭 클래스, 제네릭 인터페이스를 모두 합쳐서 제네릭 타입이라 한다.</p>
</li>
<li><p>타입은 클래스, 인터페이스, 기본형(int 등)을 모두 합쳐서 부르는 말이다.</p>
</li>
<li><p>예: class Box&lt;T&gt; { private T t; }</p>
</li>
<li><p>여기에서 Box&lt;T&gt; 를 제네릭 타입이라 한다.</p>
</li>
</ul>
<h4 id="타입-매개변수type-parameter">타입 매개변수(Type Parameter)</h4>
<ul>
<li>제네릭 타입이나 메서드에서 사용되는 변수로, 실제 타입으로 대체된다.</li>
<li>예: Box&lt;T&gt;</li>
<li>여기에서 T 를 타입 매개변수라 한다.</li>
</ul>
<h4 id="타입-인자type-argument">타입 인자(Type Argument)</h4>
<ul>
<li><p>제네릭 타입을 사용할 때 제공되는 실제 타입이다.</p>
</li>
<li><p>예: Box&lt;Integer&gt;</p>
</li>
<li><p>여기에서 Integer 를 타입 인자라 한다.</p>
</li>
</ul>
<blockquote>
<h3 id="제네릭-명명-관례">제네릭 명명 관례</h3>
</blockquote>
<p>타입 매개변수는 일반적인 변수명처럼 소문자로 사용해도 문제는 없다. 하지만 일반적으로 대문자를 사용하고 용도에 맞는 단어의 첫글자를 사용하는 관례를 따른다.</p>
<ul>
<li>E : Element</li>
<li>K : Key</li>
<li>N : Number</li>
<li>T : Type</li>
<li>V : Value</li>
<li>S, U, V etc : 2nd, 3rd, 4th types<h4 id="여러-타입-매개변수">여러 타입 매개변수</h4>
<pre><code class="language-java">class KeyValue&lt;K, V&gt; {}</code></pre>
</li>
</ul>
<hr>
<h2 id="3-타입-매개변수-제한">3. 타입 매개변수 제한</h2>
<p>타입 매개 변수를 특정 타입으로 제한할 수 있다.</p>
<pre><code class="language-java">public class ColorPlay&lt;T extends Color&gt; {...}</code></pre>
<ul>
<li><p><code>&lt;T extends Color&gt;</code> 을 통해  타입 매개변수 T를 Color과 그 자식들만 받을 수 있도록 제한한다.</p>
</li>
<li><p>자바 컴파일러는 T의 상한이 Color이라고 범위를 예측할 수 있다.</p>
</li>
<li><p>제네릭에 타입 매개변수 상한을 사용해 타입 안전성을 지키면서 상위 타입의 원하는 기능까지 사용할 수 있다.</p>
</li>
</ul>
<hr>
<h2 id="4-제네릭-메서드">4. 제네릭 메서드</h2>
<pre><code class="language-java">public class Box {

    public static &lt;T&gt; T genericMethod(T t) {
        return t;
    }
}

// main
Integer integer = Box.&lt;Integer&gt;genericMethod(10);</code></pre>
<ul>
<li><p>제네릭 메서드는 메서드를 호출할 때 타입 인자를 전달해서 타입을 지정하는 것이다.</p>
</li>
<li><p><code>Box.&lt;Integer&gt;genericMethod(10);</code> 처럼 타입을 지정하면서 메서드를 호출한다. </p>
</li>
</ul>
<h3 id="타입-매개변수-제한">타입 매개변수 제한</h3>
<p>제네릭 메서드도 제네릭 타입과 같이 타입 매개변수를 제한할 수 있다.</p>
<pre><code class="language-java">public static &lt;T extends Number&gt; T numberMethod(T t) {}</code></pre>
<h3 id="제네릭-메서드-타입-추론">제네릭 메서드 타입 추론</h3>
<pre><code class="language-java">Integer integer = Box.genericMethod(10);
Integer integer = Box.genericMethod(10.1); //error</code></pre>
<ul>
<li><p>제네릭 메서드 호출마다 타입 인자를 계속 전달하는 것은 매우 비효율이다.</p>
</li>
<li><p>자바 컴파일러는 호출 시 인자를 통해 제네릭 메서드의 타입을 추론한다.</p>
</li>
</ul>
<hr>
<h2 id="5-와일드-카드">5. 와일드 카드</h2>
<p>와일드 카드는 제네릭 타입을 더 편리하게 사용할수 있도록 도와준다.</p>
<pre><code class="language-java">public class WildCard {

    static void printWildcardV1(Box&lt;?&gt; box) {
        System.out.println(&quot;? = &quot; + box.getValue());
    }

    static void printWildcardV2(Box&lt;? extends Animal&gt; box) {
        Animal animal = box.getValue();
        System.out.println(&quot;이름 = &quot; + animal.getName());
    }

    static Animal printAndReturnWildcard(Box&lt;? extends Animal&gt; box) {
        Animal animal = box.getValue();
        System.out.println(&quot;이름 = &quot; + animal.getName());
        return animal;
    }
}</code></pre>
<ul>
<li><p>와일드 카드는 <code>?</code> 를 사용해서 정의한다.</p>
</li>
<li><p>와일드 카드는 이미 만들어진 제네릭 타입을 활용할 때 사용한다.</p>
</li>
<li><p>와일드 카드는 일반 메서드에 사용할 수 있고 단순히 매개변수로 제네릭 타입을 받을 수 있다.</p>
</li>
</ul>
<h3 id="상한-와일드-카드">상한 와일드 카드</h3>
<pre><code class="language-java">static void printWildcardV2(Box&lt;? extends Animal&gt; box) {
    Animal animal = box.getValue();
    System.out.println(&quot;이름 = &quot; + animal.getName());
}</code></pre>
<p> 제네릭 메서드와 마찬가지로 와일드카드에도 상한 제한을 둘 수 있다.</p>
<blockquote>
<h3 id="타입-매개변수가-꼭-필요한-경우">타입 매개변수가 꼭 필요한 경우</h3>
<p> 와일드카드는 제네릭을 정의할 때 사용하는 것이 아니다.
 만약 제네릭 타입으로 반환하고 싶다면 제네릭 메서드를 사용해야 하며, 와일드카드를 사용하면 전달한 타입을 명확하게 반환할 수 없다.</p>
</blockquote>
<hr>
<h2 id="6-타입-이레이저">6. 타입 이레이저</h2>
<p>이레이저는 지우개라는 뜻이다.</p>
<p>제네릭은 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 제네릭 정보가 삭제된다.
제네릭에 사용한 타입 매개변수가 모두 사라지는 것이다.</p>
<pre><code class="language-java">class EraserBox&lt;T&gt; {
    public boolean instanceCheck(T param) {
        return param instanceof T;
    }

    public void create() {
        return new T();
    }
}</code></pre>
<ul>
<li><p>여기서 <code>T</code> 는 런타임에 모두 Object가 되어버린다.</p>
</li>
<li><p>이렇게 되면 <code>instananceof</code> 는 항상 참이 되는 문제가 발생한다.</p>
</li>
<li><p><code>new T()</code> 도 항상 <code>new Object</code>가 되어 버린다.</p>
</li>
<li><p>따라서 자바는 타입 매개변수에 <code>instanceof</code> <code>new</code> 를 허용하지 않는다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] String 클래스]]></title>
            <link>https://velog.io/@viviamm7-code/Java-String-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@viviamm7-code/Java-String-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Tue, 21 Jul 2026 07:50:13 GMT</pubDate>
            <description><![CDATA[<h2 id="1-string-클래스---기본">1. String 클래스 - 기본</h2>
<h3 id="string-클래스-구조">String 클래스 구조</h3>
<pre><code class="language-java">public final class String //자바 21 기준
    implements java.io.Serializable, Comparable&lt;String&gt;, CharSequence,
               Constable, ConstantDesc {

    @Stable
    private final byte[] value; // 문자열 보관
    // ... 여러 필드

    // ... 여러 생성자

    // ... 여러 메서드
    public String concat(String str){...}
    public String replace(char oldChar, char newChar){...}
    ...
}</code></pre>
<p>String 클래스는 직접 다루기 불편한 char[ ]을 내부에 감추고 String 문자열로 다룰 수 있도록 제공한다.</p>
<h3 id="기능메서드">기능(메서드)</h3>
<p>String 클래스는 문자열로 처리할 수 있는 다양한 기능을 제공한다.</p>
<h4 id="length">length()</h4>
<ul>
<li>문자열 길이 반환</li>
</ul>
<h4 id="charatint-index">charAt(int index)</h4>
<ul>
<li>특정 index의 문자 반환</li>
</ul>
<h4 id="substringint-beginindex-int-endindex">subString(int beginIndex, int endIndex)</h4>
<ul>
<li>문자열의 부분 문자열 반환</li>
</ul>
<h4 id="indexofstring-str">indexOf(String str)</h4>
<ul>
<li>특정 문자열이 시작되는 인덱스 반환</li>
</ul>
<h4 id="tolowercase-touppercase">toLowerCase(), toUpperCase()</h4>
<ul>
<li>문자열을 소문자나 대문자로 반환</li>
</ul>
<h4 id="trim">trim()</h4>
<ul>
<li>문자열 양 끝의 공백을 제거</li>
</ul>
<h4 id="concatstring-str">concat(String str)</h4>
<ul>
<li>문자열 병합</li>
</ul>
<hr>
<h2 id="2-string-클래스---비교">2. String 클래스 - 비교</h2>
<p>String 클래스 비교할 때는 항상 equals() 비교를 해야 한다.</p>
<p>String 을 비교할 때 new String()으로 생성됐는지 리터널로 생성됐는지 알 수 없기 때문에 equals()로 동등성 비교를 해야 한다.</p>
<pre><code class="language-java">String str = &quot;hello&quot;;</code></pre>
<p>위와 같이 문자열 리터널을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위해 String Pool을 사용한다.</p>
<ul>
<li><p>JVM이 클래스를 로딩하고 문자열 리터럴을 처리하는 과정에서 String Pool에 해당 문자열을 등록하거나 기존 문자열을 재사용한다. (같은 문자열이 있으면 만들지 않음) : 캐싱</p>
</li>
<li><p>&quot;hello&quot; 문자열 리터널을 사용하므로 문자열 풀에서 str과 같은 참조를 사용한다.</p>
</li>
<li><p>문자열 풀 덕분에 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어 성능도 최적화된다.</p>
</li>
</ul>
<blockquote>
<h3 id="정리">정리</h3>
<p>문자열 리터럴은 String Pool에서 관리된다. 
같은 내용의 문자열 리터럴이 이미 존재하면 새로운 객체를 만들지 않고 기존 객체의 참조를 재사용한다. 
이를 통해 메모리 사용량과 문자열 객체 생성 비용을 줄일 수 있다.</p>
</blockquote>
<hr>
<h2 id="3-string-클래스---불변-객체">3. String 클래스 - 불변 객체</h2>
<p>String 은 불변객체이다.
변경이 필요한 경우는 기존 값을 변경하지 않고 새로운 결과를 반환해 할당해야 한다.</p>
<blockquote>
<h3 id="string-왜-불변으로-설계됐을까">String 왜 불변으로 설계됐을까?</h3>
</blockquote>
<p>만약 String 내부의 값을 변경할 수있다면, 기존에 String Pool에서  같은 문자를 참조하는 변수의 모든 문자가 함께 변경돼 버리는 문제가 발생한다.</p>
<p>strA가 참조하는 문자를 변경하면 strB의 문자도 함께 변경돼 사이드 이펙트 문제가 발생한다.</p>
<hr>
<h2 id="4-string-클래스---주요-메서드">4. String 클래스 - 주요 메서드</h2>
<blockquote>
<h3 id="문자열-정보-조회">문자열 정보 조회</h3>
</blockquote>
<h4 id="length-1">length()</h4>
<ul>
<li>문자열의 길이를 반환<h4 id="isempty">isEmpty()</h4>
</li>
<li>문자열이 비어 있는지 확인<h4 id="isblank">isBlank()</h4>
</li>
<li>문자열이 비어 있는지 확인<h4 id="charatint-index-1">charAt(int index)</h4>
</li>
<li>지정된 인덱스에 있는 문자를 반환</li>
</ul>
<blockquote>
<h3 id="문자열-비교">문자열 비교</h3>
</blockquote>
<h4 id="equalsobject-anobject">equals(Object anObject)</h4>
<ul>
<li>두 문자열이 동일한지 비교<h4 id="equalsignorecasestring-anotherstring">equalsIgnoreCase(String anotherString)</h4>
</li>
<li>두 문자열을 대소문자 구분 없이 비교<h4 id="comparetostring-anotherstring">compareTo(String anotherString)</h4>
</li>
<li>두 문자열을 사전 순으로 비교<h4 id="comparetoignorecasestring-str">compareToIgnoreCase(String str)</h4>
</li>
<li>두 문자열을 대소문자 구분 없이 사전적으로 비교<h4 id="startswithstring-prefix">startsWith(String prefix)</h4>
</li>
<li>문자열이 특정 접두사로 시작하는지 확인<h4 id="endswithstring-suffix">endsWith(String suffix)</h4>
</li>
<li>문자열이 특정 접미사로 끝나는지 확인</li>
</ul>
<blockquote>
<h3 id="문자열-검색">문자열 검색</h3>
</blockquote>
<h4 id="containscharsequence-s">contains(CharSequence s)</h4>
<ul>
<li>문자열이 특정 문자열을 포함하고 있는지 확인<h4 id="indexofstring-ch--indexofstring-ch-int-fromindex">indexOf(String ch) / indexOf(String ch, int fromIndex)</h4>
</li>
<li>문자열이 처음 등장하는 위치를 반환<h4 id="lastindexofstring-ch">lastIndexOf(String ch)</h4>
</li>
<li>문자열이 마지막으로 등장하는 위치를 반환</li>
</ul>
<blockquote>
<h3 id="문자열-조작--변환">문자열 조작 / 변환</h3>
</blockquote>
<h4 id="substringint-beginindex--substringint-beginindex-int-endindex">substring(int beginIndex) / substring(int beginIndex, int endIndex)</h4>
<ul>
<li>문자열의 부분 문자열을 반환<h4 id="concatstring-str-1">concat(String str)</h4>
</li>
<li>문자열의 끝에 다른 문자열을 붙임<h4 id="replacecharsequence-target-charsequence-replacement">replace(CharSequence target, CharSequence replacement)</h4>
</li>
<li>특정 문자열을 새 문자열로 대체<h4 id="replaceallstring-regex-string-replacement">replaceAll(String regex, String replacement)</h4>
</li>
<li>문자열에서 정규 표현식과 일치하는 부분을 새 문자열로 대체<h4 id="replacefirststring-regex-string-replacement">replaceFirst(String regex, String replacement)</h4>
</li>
<li>문자열에서 정규 표현식과 일치하는 첫 번째 부분을 새 문자열로 대체<h4 id="tolowercase--touppercase">toLowerCase() / toUpperCase()</h4>
</li>
<li>문자열을 소문자나 대문자로 변환<h4 id="trim-1">trim()</h4>
</li>
<li>문자열 양쪽 끝의 공백을 제거<h4 id="strip">strip()</h4>
</li>
<li>Whitespace 와 유니코드 공백을 포함해서 제거</li>
</ul>
<blockquote>
<h3 id="문자열-분할--조합">문자열 분할 / 조합</h3>
</blockquote>
<h4 id="splitstring-regex">split(String regex)</h4>
<ul>
<li>문자열을 정규 표현식을 기준으로 분할<h4 id="joincharsequence-delimiter-charsequence-elements">join(CharSequence delimiter, CharSequence... elements)</h4>
</li>
<li>주어진 구분자로 여러 문자열을 결합</li>
</ul>
<blockquote>
<h3 id="기타-유틸리티">기타 유틸리티</h3>
</blockquote>
<h4 id="valueofobject-obj">valueOf(Object obj)</h4>
<ul>
<li>다양한 타입을 문자열로 변환<h4 id="tochararray">toCharArray()</h4>
</li>
<li>문자열을 문자 배열로 변환<h4 id="formatstring-format-object-args">format(String format, Object... args)</h4>
</li>
<li>형식 문자열과 인자를 사용하여 새로운 문자열을 생성<h4 id="matchesstring-regex">matches(String regex)</h4>
</li>
<li>문자열이 주어진 정규 표현식과 일치하는지 확인</li>
</ul>
<blockquote>
<h3 id="참고">참고</h3>
<p>CharSequence는 String, StringBuilder의 상위 타입으로, 
다양한 객체를 받을 수 있다.</p>
</blockquote>
<hr>
<h2 id="5-stringbuilder---가변-string">5. StringBuilder - 가변 String</h2>
<p>불변인 String 클래스의 단점은 문자를 더하거나 변경할 때마다 새로운 객체를 생성해야 한다는 점이다.</p>
<p>문자를 자주 변경해야 하는 상황이면 String 객체를 만들고 GC 해야한다.
결과적으로 CPU, 메모리 자원을 더 많이 사용하게 된다.</p>
<h3 id="stringbuilder">StringBuilder</h3>
<p>이런 문제를 해결하기 위해 자바는 StringBuilder 라는 가변 Strin을 제공한다.</p>
<pre><code class="language-java">public final class StringBuilder
        extends AbstractStringBuilder
        implements java.io.Serializable, 
                    Comparable&lt;StringBuilder&gt;, 
                    CharSequence {
}

abstract class AbstractStringBuilder 
    implements Appendable, CharSequence {

    byte[] value;
}</code></pre>
<p>StringBuilder은 AbstaractStringBuilder를 상속해 value를 사용한다.
가변이므로 final이 안 붙어있다.</p>
<pre><code class="language-java">StringBuilder sb = new StringBuilder();

sb.append(&quot;1&quot;);
sb.insert(1, &quot;2&quot;);
sb.delete(0,1);
sb.reverse();</code></pre>
<p>StringBuilder은 보통 문자열을 변경하는 동안만 사용하다가 문자열 변경이 끝나면 안전한 불변 String으로 변환하는 것이 좋다.</p>
<hr>
<h2 id="6-string-최적화">6. String 최적화</h2>
<h3 id="string-리터럴-최적화">String 리터럴 최적화</h3>
<pre><code class="language-java">String ab = &quot;a&quot; + &quot;b&quot; ; </code></pre>
<p>자바는 문자열 리터럴끼리 + 할시 문자열 결합 연산을 하지 않고 자동으로 합쳐준다.
연산이 일어나지 않기 때문에 성능이 향상된다.</p>
<h3 id="string-변수-최적화">String 변수 최적화</h3>
<pre><code class="language-java">String a = &quot;a&quot;;
String b = &quot;b&quot;;
String ab = a + b;</code></pre>
<p>컴파일 시점에는 변수에 어떤 값이 들어있는지 알수 없기 때문에 단순하게 합칠 수  없다.</p>
<pre><code class="language-java">String ab = new StringBuidler().append(a).append(b).toString();</code></pre>
<p>자바에서 StringBuilder로 변환하여 문자열을 더한 뒤 다시 불변 String으로 변환하여 최적화한다.</p>
<blockquote>
<h3 id="stringbuilder를-직접-사용하는-것이-더-좋은-경우">StringBuilder를 직접 사용하는 것이 더 좋은 경우</h3>
</blockquote>
<ul>
<li>반복문에서 반복해서 문자를 연결할 때</li>
<li>조건문을 통해 동적으로 문자열을 조합할 때</li>
<li>복잡한 문자열의 특정 부분을 변경해야 할 때</li>
<li>매우 긴 대용량 문자열을 다룰 때</li>
</ul>
<blockquote>
<h3 id="참고-stringbuilder-vs-stringbuffer">참고: StringBuilder vs StringBuffer</h3>
<p>StrinBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다.</p>
</blockquote>
<ul>
<li><code>StringBuffer</code> 는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에서 안전하지만 동기화 오버헤드로 인해 성능이 느리다.</li>
<li><code>StringBuilder</code> 는 멀티 쓰레드 상황에서 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다.</li>
</ul>
<hr>
<h2 id="7-method-chaining">7. Method Chaining</h2>
<pre><code class="language-java">public Number plus(int plusNum) {
    num += plusNum;
    return this;
}</code></pre>
<p>plus 메서드를 보면 자기 자신(this)의 참조값을 반환한다.</p>
<pre><code class="language-java">Number num1 = new Number();
num1.add(1);
num1.add(2);
num1.add(3);

num2.add(1).add(2).add(3);</code></pre>
<p>아래 num2처럼 메서드 호출 결과로 자기 자신의 참조값을 반환하면, 반환돼 참조값을 사용해 메서드 호출을 계속 이어나갈 수 있다.
이런 기법을 <code>Method Chaining</code> 이라고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 불변 객체]]></title>
            <link>https://velog.io/@viviamm7-code/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4</link>
            <guid>https://velog.io/@viviamm7-code/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4</guid>
            <pubDate>Tue, 21 Jul 2026 05:09:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1-기본형과-참조형의-공유">1. 기본형과 참조형의 공유</h2>
<h4 id="기본형-primitive-type">기본형 (Primitive Type)</h4>
<p>하나의 값을 여러 변수에서 절대 공유하지 않음</p>
<h4 id="참조형-reference-type">참조형 (Reference Type)</h4>
<p>하나의 객체를 참조값을 통해 여러 변수에서 공유 가능</p>
<pre><code class="language-java">Member m1 = new Member();
m1.name = &quot;김진슝&quot;;

Member m2 = m1;

m2.name = &quot;홍길동&quot;;

System.out.println(m1.name); // 홍길동
System.out.println(m2.name); // 홍길동</code></pre>
<hr>
<h2 id="2-공유-참조와-사이드-이펙트">2. 공유 참조와 사이드 이펙트</h2>
<blockquote>
<h3 id="사이드-이펙트란">사이드 이펙트란?</h3>
</blockquote>
<p>참조형 객체를 여러 곳에서 공유할 때, 한 곳의 변경이 다른 곳에도 영향을 주는 현상</p>
<pre><code class="language-java">Member m1 = new Member();
m1.name = &quot;김진슝&quot;;

Member m2 = m1;

m2.name = &quot;홍길동&quot;;

System.out.println(m1.name); // 홍길동
System.out.println(m2.name); // 홍길동</code></pre>
<p>위 코드를 봤을 때 m2의 이름을 바꿨는데 m1 까지 바뀌었다.
실제로는 둘 다 같은 객체를 가리키기 때문에 (같은 인스턴스 참조) 객체 하나가 변경된 것이다.</p>
<blockquote>
<h3 id="해결-방안은">해결 방안은?</h3>
</blockquote>
<p>처음부터 다른 인스턴스를 참조하면 된다.</p>
<pre><code class="language-java">Member m1 = new Member(&quot;김진슝&quot;);
Member m2 = new Member(&quot;함지성&quot;);</code></pre>
<p>하지만 참조값을 공유한다고 해서 컴파일상에서 오류가 뜨는것이 아니기에 막을 방법이 없다.
-&gt; 이때 불변 객체를 사용 :: 객체의 값을 바꿀 수 없게 만들면 된다.</p>
<hr>
<h2 id="3-불변-객체---도입">3. 불변 객체 - 도입</h2>
<p>객체의 상태(객체 내부 값 : 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체라고 부른다.</p>
<pre><code class="language-java">public class Member {
    private final String name;

    public Member() {
        this.name = name;
    }

    public String getName() {
        return value
    }
}</code></pre>
<p>객체 내부의 값을 final로 선언하고 setter를 제거해, 생성자를 통해서 값을 설정하고 이후 변경이 불가능하게 했다.</p>
<pre><code class="language-java">Member m1 = new Member(&quot;김진슝&quot;);
Member m2 = m1;

m2 = new Member(&quot;홍길동&quot;);

System.out.println(m1.name); // 김진슝
System.out.println(m2.name); // 홍길동</code></pre>
<hr>
<h2 id="4-불변-객체---값-변경">4. 불변 객체 - 값 변경</h2>
<blockquote>
<h4 id="불변-객체를-사용하지만-값을-변경해야-되는-경우가-있다면">불변 객체를 사용하지만 값을 변경해야 되는 경우가 있다면?</h4>
</blockquote>
<pre><code class="language-java">public class Member {
    private final int age;

    public Member(int age) {
        this.age = age;
    }

    public Member add(int addAge) {
        int res =  age + addAge;
        return new Member(res);
    }

    public int  getAge() {
        return age;
    }
}</code></pre>
<ul>
<li>불변 객체는 기존 값을 변경하지 않고 계산 결과를 바탕으로 새로운 객체를 만들어서 반환</li>
<li>불변을 유지하면서 새로운 결과도 반환</li>
</ul>
<pre><code class="language-java">
Member m1 = new Member(3);
Member m2 = m1.add(10);

System.out.println(m1.getAge()); // 3
System.out.println(m2.getAge()); // 13</code></pre>
<hr>
<h2 id="5-불변-객체---정리">5. 불변 객체 - 정리</h2>
<p>자바에서 가장 많이 사용되는 String 클래스는 불변 객체이다.
또한 자바에서 기본으로 제공하는 Integer,  LocalDate 등 수 많은 클래스가 불변 객체로 설계되어 있다.</p>
<blockquote>
<h4 id="모든-클래스가-불변은-아니다">모든 클래스가 불변은 아니다</h4>
</blockquote>
<p>가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 경우에 만들어서 사용해야 한다.</p>
<p>캐시 안정성, 멀티 쓰레드 안정성, 엔티티 값 타입 등 수 많은 이유로 클래스를 불변으로 설계하곤 한다.</p>
<h3 id="불변-객체-조건">불변 객체 조건</h3>
<ul>
<li>클래스를 final로 만들어 상속을 제한</li>
<li>필드를 private final로 선언</li>
<li>setter를 제공하지 않음</li>
<li>생성자에서 모든 값을 설정</li>
<li>상태 변경이 필요하면 새 객체를 반환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Object 클래스]]></title>
            <link>https://velog.io/@viviamm7-code/Java-Object-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@viviamm7-code/Java-Object-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Mon, 20 Jul 2026 17:19:22 GMT</pubDate>
            <description><![CDATA[<h2 id="1-javalang-패키지">1. java.lang 패키지</h2>
<p>여기서 lang은 langauge의 줄임말로, 이 패키지는 자바언어를 이루는 가장 기본 클래스들을 보관하는 패키지를 말한다.</p>
<p>이 패키지는 자바 애플리케이션 단에서 자동으로 import 되어서 생략 가능하다.</p>
<blockquote>
<h4 id="javalang-의-대표-클래스들">java.lang 의 대표 클래스들</h4>
</blockquote>
<h4 id="1-object">1. Object</h4>
<ul>
<li>모든 자바 객체의 부모 클래스</li>
</ul>
<h4 id="2-string">2. String</h4>
<ul>
<li>문자열</li>
</ul>
<h4 id="3-integer--long--double">3. Integer / Long / Double</h4>
<ul>
<li>래퍼 타입, 기본형 데이터 타입을 객체로 만든 것</li>
</ul>
<h4 id="4-class">4. Class</h4>
<ul>
<li>클래스 메타 정보</li>
</ul>
<h4 id="5-system">5. System</h4>
<ul>
<li>시스템과 관련된 기본 기능들을 제공</li>
</ul>
<hr>
<h2 id="2-object-클래스">2. Object 클래스</h2>
<p>모든 클래스의 최상위 부모 클래스는 Object 클래스이다.</p>
<h4 id="공통-기능-제공">공통 기능 제공</h4>
<ul>
<li>Object는 모든 객체에 필요한 공통 기능을 제공</li>
<li>Object는 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 상속받을 수 있다.</li>
<li>toString(), equals(), getClass() 등등</li>
</ul>
<h4 id="다형성의-기본-구현">다형성의 기본 구현</h4>
<ul>
<li>다형성을 지원하는 기본적인 메커니즘 제공</li>
<li>Object는 모든 객체를 다 담을 수 있고, 다양한 타입의 객체를 통합적으로 처리 가능</li>
</ul>
<hr>
<h2 id="3-object-다형성">3. Object 다형성</h2>
<p>모든 Java 클래스는 Object를 직접 또는 간접적으로 상속하므로, Object 타입 변수는 모든 클래스의 객체를 참조할 수 있다.</p>
<pre><code class="language-java">class B { // Object 상속받음(직접 상속)
}

class A extends B {  // Object를 상속받은 B를 상속받음(간접 상속)
}</code></pre>
<blockquote>
<h4 id="object를-활용한-다형성의-한계">Object를 활용한 다형성의 한계</h4>
</blockquote>
<p>Object 타입은 모든 객체를 다형적으로 참조할 수 있다. 오버라이딩된 Object 메서드는 실제 객체의 메서드가 실행되지만, 참조 타입이 Object이므로 각 클래스에만 정의된 고유 메서드를 호출하려면 다운캐스팅이 필요하다. </p>
<pre><code class="language-java">class Dog {
    void bark() {
        System.out.println(&quot;멍멍&quot;);
    }

    @Override
    public String toString() {
        return &quot;강아지&quot;;
    }
}

Object obj = new Dog();

System.out.println(obj.toString()); // Dog에서 오버라이딩한 메서드 실행
// obj.bark(); // 컴파일 오류: Object에는 bark()가 없음</code></pre>
<p>-&gt; 공통으로 사용할 수 있는 메서드가 적어 다형성 활용에 한계가 있다.</p>
<hr>
<h2 id="4-object-배열">4. Object 배열</h2>
<p>Object 배열을 만들면 모든 객체를 담을 수 있는 배열이 만들어 진다.</p>
<pre><code class="language-java">Object obj[0] = new Cat();
Object obj[1] = new Dog();
Object obj[2] = new Lion();</code></pre>
<hr>
<h2 id="5-tostring">5. toString()</h2>
<p>Object.toString() 메서드는 객체의 정보를 문자열 형태로 제공한다.
모든 클래스에서 상속받아 사용 가능하다.</p>
<pre><code class="language-java">public String toString() {
        return getClass().getName() 
        + &quot;@&quot; 
        + Integer.toHexString(hashCode());
}</code></pre>
<ul>
<li>Object가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공</li>
</ul>
<h4 id="println">println()</h4>
<p>System.out.println() 메서드는 내부에서 String.valueOf()를 거쳐 toString()을 호출한다.</p>
<pre><code class="language-java">public static String valueOf(Object obj) {
        return (obj == null) ? &quot;null&quot; : obj.toString();
}</code></pre>
<ul>
<li>null인 객체를 toString() 붙여서 println() -&gt; NullPointerException</li>
<li>null인 객체를 그냥 println() -&gt; null 출력</li>
</ul>
<p>객체의 정보를 원하는 방식으로 제공하고 싶으면 toString()을 오버라이딩해서 사용</p>
<ul>
<li>오버라이딩 한 경우 객체의 참조값을 알고 싶으면</li>
<li><blockquote>
<p>Integer.toHexString(System.identityHashCode())</p>
</blockquote>
</li>
</ul>
<hr>
<h2 id="6-object와-ocp">6. Object와 OCP</h2>
<p>만약 Object 클래스가 없다면 클래스마다 toString 메서드를 정의해 작성해야 한다.</p>
<h3 id="구체적인-것에-의존">구체적인 것에 의존</h3>
<p>사용하는 클래스가 특정 구현 클래스를 직접 참조하므로, 구현 클래스를 다른 것으로 교체할 때 사용하는 코드도 수정해야 한다.</p>
<h3 id="추상적인-것에-의존">추상적인 것에 의존</h3>
<p>사용하는 클래스가 여러 구현 클래스의 공통 상위 타입인 인터페이스나 추상 클래스에 의존하므로, 구현체가 바뀌어도 사용하는 코드는 수정하지 않아도 된다.</p>
<h3 id="ocp-open---closed---principle-원칙이란">OCP (Open - Closed - Principle) 원칙이란?</h3>
<p>확장에는 열려 있고, 변경에는 닫혀 있어야 한다.</p>
<ul>
<li>새로운 기능은 추가할 수 있어야 하고,</li>
<li>기존 코드는 가능하면 수정하지 않아야 한다.</li>
<li>OCP는 새로운 요구사항이 생겼을 때 기존 코드를 고치는 대신 새로운 구현을 추가해서 기능을 확장하도록 설계하는 원칙이다.</li>
</ul>
<h4 id="자바-언어는-객체지향-언어-답게-언어-스스로-객체지향의-특징을-매우-잘-활용해야-한다">자바 언어는 객체지향 언어 답게 언어 스스로 객체지향의 특징을 매우 잘 활용해야 한다.</h4>
<hr>
<h2 id="7-equals---동일성과-동등성">7. equals() - 동일성과 동등성</h2>
<p>Object는 동등성 비교를 위한 equals() 메서드는 제공한다.</p>
<h4 id="1-동일성-identity">1. 동일성 (Identity)</h4>
<p><code>==</code> 연산자를 사용해서 두 객체가 참조가 동일한 객체를 가리키고 있는지 확인</p>
<h4 id="2-동등성-equality">2. 동등성 (Equality)</h4>
<p><code>equals()</code> 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인</p>
<pre><code class="language-java">public boolean equals(Object obj) {
        return (this == obj);
}</code></pre>
<ul>
<li>Object가 기본으로 제공하는 equals()는 ==으로 동일성 비교를 제공</li>
<li>동등성 비교를 사용해야되는 경우 equals 메서드를 재정의해서 사용</li>
<li>equals()를 재정의하면 일반적으로 hashCode()도 함께 재정의</li>
</ul>
<h3 id="object의-나머지-메서드들">Object의 나머지 메서드들</h3>
<h4 id="clone">clone()</h4>
<ul>
<li>객체를 복사할 때 사용</li>
</ul>
<h4 id="hashcode">hashCode()</h4>
<ul>
<li>객체를 숫자 형태의 해시값으로 표현하는 메서드</li>
<li>equals()가 true인 두 객체는 반드시 같은 hashCode()를 반환해야 한다.<blockquote>
<h4 id="같은-hashcode를-가진다고-해서-반드시-equals가-true인-것은-아니다">같은 hashCode()를 가진다고 해서 반드시 equals()가 true인 것은 아니다.</h4>
</blockquote>
</li>
</ul>
<h4 id="getclass">getClass()</h4>
<ul>
<li>객체의 실제 실행 클래스 정보를 반환</li>
<li>final 메서드라 재정의 불가</li>
</ul>
<h4 id="notify-notifyall-wait">notify(), notifyAll(), wait()</h4>
<ul>
<li>멀티 쓰레드용 메서드</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] PRG 패턴]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-PRG-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@viviamm7-code/Spring-PRG-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Thu, 09 Jul 2026 16:23:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="먼저-prg-패턴이-뭔가요">먼저 PRG 패턴이 뭔가요?</h3>
</blockquote>
<p>PRG 패턴은 Post/Redirect/Get의 줄임말로, 클라이언트가 POST 요청으로 데이터를 변경한 뒤 바로 결과 화면을 반환하지 않고, Redirect를 통해 GET 요청으로 결과 페이지를 다시 조회하게 만드는 웹 개발 패턴이다.</p>
<hr>
<h3 id="상품-도메인-흐름-예시">상품 도메인 흐름 예시</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/864f9af1-4b34-4382-9345-08ffda357d5c/image.png" alt="">상품을 등록하면 상품이 조회되고 상품 상세로 가는 플로우를 예로 들어보자!!!!!</p>
<hr>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/50d6b521-a507-47f6-bffa-20a55e2242d2/image.png" alt="">웹  브라우저의 새로고침은 마지막 요청을 다시 수행하는 개념이다.
상품을 등록하고 마지막 API는 POST로 상품을 저장할 것이고, 새로고침시 계속 중복 저장이 될 것이다.</p>
<ol>
<li>상품 등록 페이지를 조회한다.(GET)</li>
<li>상품을 서버에 저장하고 상품 상세 페이지로 보낸다.(POST)</li>
<li>새로고침시 마지막 POST API가 다시 호출된다.(데이터 중복)</li>
</ol>
<hr>
<blockquote>
<h3 id="그럼-이-중복-문제를-어떻게-해결할-수-있을까">그럼 이 중복 문제를 어떻게 해결할 수 있을까???</h3>
</blockquote>
<h3 id="prg-패턴-적용">PRG 패턴 적용</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/7c242017-81cf-4b33-a6d7-52fe458ba549/image.png" alt="">상품 등록 페이지에서 상품을 서버에 POST API 호출 후 저장한다. PRG 패턴을 적용하면 상품 상세 페이지로 리다이렉트 시키고 그 상품의 상품 상세 페이지를 조회한다. 그 후 새로고침을 하면 마지막 API인 GET API가 호출돼 상품 상세 페이지가 계속 조회될 것이다.</p>
<ol>
<li>상품 등록 페이지를 조회한다.(GET)</li>
<li>상품을 서버에 저장하고 상품 상세 페이지로 리다이렉트 시킨다.(POST)</li>
<li>상품 상세 페이지를 조회한다.(GET)</li>
<li>새로고침시 마지막 GET API가 다시 호출된다.(데이터 중복 x)</li>
</ol>
<hr>
<h3 id="redirectattributes">RedirectAttributes</h3>
<p>PRG 패턴을 적용할 때 리다이렉트 URL에 값을 전달해야 하는 경우가 있다.</p>
<p>예를 들어 상품을 등록한 뒤, 등록된 상품의 상세 페이지로 이동하려면 저장된 상품의 <code>id</code>가 필요하다.</p>
<pre><code class="language-java">@PostMapping(&quot;/add&quot;)
public String addItem(
                    @ModelAttribute Item item, 
                    RedirectAttributes redirectAttributes) {
    Item savedItem = itemRepository.save(item);

    redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());

    return &quot;redirect:/basic/items/{itemId}&quot;;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 스프링 MVC]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-MVC</link>
            <guid>https://velog.io/@viviamm7-code/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-MVC</guid>
            <pubDate>Thu, 09 Jul 2026 13:04:36 GMT</pubDate>
            <description><![CDATA[<h3 id="spring-mvc">Spring MVC</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/23bf93e6-d758-45d5-8dba-68f419612b42/image.png" alt=""></p>
<h3 id="dispatcherservlet-서블릿-등록">DispatcherServlet 서블릿 등록</h3>
<p>DispatcherServlet 도 부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, 서블릿으로 동작한다.</p>
<ul>
<li>DispatcherServlet FrameworkServlet HttpServletBean HttpServlet</li>
<li>스프링 부트는 DispatcherServlet 을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns=&quot;/&quot; )에 대해서 매핑한다.
더 자세한 경로가 우선순위가 높다. 그래서 기존에 등록한 서블릿도 함께 동작한다.</li>
</ul>
<h3 id="요청-흐름">요청 흐름</h3>
<ol>
<li>서블릿이 호출되면 HttpServlet 이 제공하는 service() 가 호출된다.<ul>
<li>스프링 MVC는 DispatcherServlet 의 부모인 FrameworkServlet 에서 service() 를 오버라이드 해두었다.</li>
</ul>
</li>
<li>FrameworkServlet.service() 를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch() 가 호출된다.</li>
</ol>
<h3 id="dispatcherservletdodispatch">DispatcherServlet.doDispatch()</h3>
<pre><code class="language-java">protected void doDispatch(HttpServletRequest request, HttpServletResponseresponse) throws Exception {

  HttpServletRequest processedRequest = request;
  HandlerExecutionChain mappedHandler = null;
  ModelAndView mv = null;

  // 1. 핸들러 조회
  mappedHandler = getHandler(processedRequest);
  if (mappedHandler == null) {
  noHandlerFound(processedRequest, response);
  return;

  }

  // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

  // 3. 핸들러 어댑터 실행 -&gt; 4. 핸들러 어댑터를 통해 핸들러 실행 -&gt; 5. ModelAndView 반환
  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  processDispatchResult(processedRequest, response, mappedHandler, mv,
  dispatchException);

}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView
mv, Exception exception) throws Exception {

  // 뷰 렌더링 호출
  render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request,
HttpServletResponse response) throws Exception {

  View view;
  String viewName = mv.getViewName();

  // 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
  view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

  // 8. 뷰 렌더링
  view.render(mv.getModelInternal(), request, response);
}
</code></pre>
<h4 id="1-handlermapping핸들러-매핑">1. HandlerMapping(핸들러 매핑)</h4>
<p>핸들러 매핑에서 내 컨트롤러를 찾을 수 있어야 한다.
예) 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/e31f8614-401f-44d6-8750-628c7c1cae6e/image.png" alt=""></p>
<h4 id="2-handleradapter핸들러-어댑터">2. HandlerAdapter(핸들러 어댑터)</h4>
<p>핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
예) Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야 한다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/e2814375-a893-4c6f-a04b-d885d1ab8cfd/image.png" alt=""></p>
<p>스프링 부트는 InternalResourceViewResolver 라는 뷰 리졸버를 자동으로 등록하는데, 이때 application.properties 에 등록한spring.mvc.view.prefix, spring.mvc.view.suffix 설정 정보를 사용해서 등록한다.</p>
<hr>
<h3 id="requestmapping">@RequestMapping</h3>
<ul>
<li>스프링은 애노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로 @RequestMapping 애노테이션을 사용하는 컨트롤러이다.</li>
<li>요청 정보를 매핑하고, 해당 URL이 호출되면 이 메서드가 호출된다. </li>
<li>애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 된다.</li>
</ul>
<h4 id="requestmappinghandlermapping">RequestMappingHandlerMapping</h4>
<ul>
<li>스프링 빈 중에서 @RequestMapping 또는 @Controller 가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.</li>
</ul>
<h4 id="requestmappinghandleradapter">RequestMappingHandlerAdapter</h4>
<h3 id="controller">@Controller</h3>
<p>스프링이 자동으로 스프링 빈으로 등록한다. </p>
<ul>
<li>내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨</li>
<li>스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.</li>
</ul>
<h3 id="model-파라미터">Model 파라미터</h3>
<ul>
<li>ViewName 직접 반환 뷰의 논리 이름을 반환할 수 있다</li>
</ul>
<h3 id="requestparam">@RequestParam</h3>
<ul>
<li>스프링은 HTTP 요청 파라미터를 @RequestParam 으로 받을 수 있다.</li>
</ul>
<h3 id="restcontroller">@RestController</h3>
<ul>
<li>@Controller 는 반환 값이 String 이면 뷰 이름으로 인식된다. 
그래서 뷰를 찾고 뷰가 랜더링 된다.</li>
<li>@RestController 는 반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력한다.</li>
</ul>
<h3 id="requestmapping의-http-메서드">@RequestMapping의 HTTP 메서드</h3>
<ul>
<li>method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출된다.</li>
<li>모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE</li>
</ul>
<h3 id="pathvariable경로-변수-사용">@PathVariable(경로 변수) 사용</h3>
<ul>
<li>@RequestMapping 은 URL 경로를 템플릿화 할 수 있는데, @PathVariable 을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.</li>
<li>@PathVariable 의 이름과 파라미터 이름이 같으면 생략할 수 있다.</li>
<li>다중 매핑 가능</li>
</ul>
<hr>
<blockquote>
<h3 id="http-요청---기본-헤더-조회">HTTP 요청 - 기본, 헤더 조회</h3>
<p>애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다</p>
</blockquote>
<h4 id="httpservletrequest">HttpServletRequest</h4>
<h4 id="httpservletresponse">HttpServletResponse</h4>
<h4 id="httpmethod">HttpMethod</h4>
<ul>
<li>HTTP 메서드를 조회한다<h4 id="locale">Locale</h4>
</li>
<li>Locale 정보를 조회한다.<h4 id="requestheader-multivaluemapstring-string-headermap">@RequestHeader MultiValueMap&lt;String, String&gt; headerMap</h4>
</li>
<li>모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.<h4 id="requestheaderhost-string-host">@RequestHeader(&quot;host&quot;) String host</h4>
</li>
<li>특정 HTTP 헤더를 조회한다.</li>
<li>속성<ul>
<li>필수 값 여부: required</li>
<li>기본 값 속성: defaultValue</li>
</ul>
</li>
</ul>
<h4 id="cookievaluevalue--mycookie-required--false-string-cookie">@CookieValue(value = &quot;myCookie&quot;, required = false) String cookie</h4>
<ul>
<li>특정 쿠키를 조회한다.</li>
<li>속성<ul>
<li>필수 값 여부: required</li>
<li>기본 값: defaultValue<h4 id="multivaluemap">MultiValueMap</h4>
</li>
</ul>
</li>
<li>MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.</li>
<li>HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다. </li>
<li>keyA=value1&amp;keyA=value2</li>
</ul>
<hr>
<h3 id="requestparam-1">@RequestParam</h3>
<ul>
<li>파라미터 이름으로 바인딩</li>
<li>HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name=&quot;xx&quot;) 생략 가능</li>
<li>String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능</li>
</ul>
<h3 id="responsebody">@ResponseBody</h3>
<ul>
<li>View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력</li>
</ul>
<h3 id="modelattribute">@ModelAttribute</h3>
<ol>
<li>Dto 객체를 생성한다.</li>
<li>요청 파라미터의 이름으로 Dto 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.</li>
</ol>
<h3 id="inputstreamreader">InputStream(Reader)</h3>
<ul>
<li>HTTP 요청 메시지 바디의 내용을 직접 조회</li>
</ul>
<h3 id="outputstreamwriter">OutputStream(Writer)</h3>
<ul>
<li>HTTP 응답 메시지의 바디에 직접 결과 출력</li>
</ul>
<h3 id="httpentity">HttpEntity</h3>
<ul>
<li>HTTP header, body 정보를 편리하게 조회</li>
<li>메시지 바디 정보를 직접 조회</li>
<li>요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X<h3 id="httpentity는-응답에도-사용-가능">HttpEntity는 응답에도 사용 가능</h3>
</li>
<li>메시지 바디 정보 직접 반환</li>
<li>헤더 정보 포함 가능</li>
<li>view 조회X</li>
</ul>
<hr>
<blockquote>
<h3 id="httpentity-를-상속받은-다음-객체들도-같은-기능을-제공">HttpEntity 를 상속받은 다음 객체들도 같은 기능을 제공</h3>
</blockquote>
<h4 id="requestentity">RequestEntity</h4>
<ul>
<li>HttpMethod, url 정보가 추가, 요청에서 사용<h4 id="responseentity">ResponseEntity</h4>
</li>
<li>HTTP 상태 코드 설정 가능, 응답에서 사용<pre><code class="language-java">return new ResponseEntity&lt;String&gt;(&quot;Hello World&quot;, responseHeaders,
HttpStatus.CREATED)</code></pre>
</li>
</ul>
<h4 id="requestbody">@RequestBody</h4>
<ul>
<li>@RequestBody 를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다.</li>
<li>헤더 정보가 필요하다면 HttpEntity 를 사용하거나 @RequestHeader 를 사용하면 된다.</li>
</ul>
<h4 id="responsebody-1">@ResponseBody</h4>
<ul>
<li>@ResponseBody 를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다.</li>
</ul>
<h4 id="요청-파라미터-vs-http-메시지-바디">요청 파라미터 vs HTTP 메시지 바디</h4>
<ul>
<li>요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute</li>
<li>HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody</li>
</ul>
<h4 id="restcontroller-1">@RestController</h4>
<ul>
<li>@Controller 대신에 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과가 있다. </li>
<li>뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터를 입력한다. </li>
<li>이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러이다.</li>
</ul>
<hr>
<blockquote>
<h3 id="스프링-mvc는-다음의-경우에-http-메시지-컨버터를-적용">스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용</h3>
</blockquote>
<h4 id="http-요청">HTTP 요청</h4>
<ul>
<li>@RequestBody , HttpEntity(RequestEntity) ,<h4 id="http-응답">HTTP 응답</h4>
</li>
<li>@ResponseBody , HttpEntity(ResponseEntity) , </li>
</ul>
<h4 id="http-메시지-컨버터는-http-요청-http-응답-둘-다-사용">HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용</h4>
<ul>
<li>canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크</li>
<li>read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능</li>
</ul>
<hr>
<h3 id="modelattribute-1">@ModelAttribute</h3>
<ul>
<li>요청 파라미터 처리</li>
<li>@ModelAttribute 는 Dto 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.</li>
</ul>
<h3 id="modelattribute---model-추가">@ModelAttribute - Model 추가</h3>
<ul>
<li>@ModelAttribute 는 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Logging]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-Logging</link>
            <guid>https://velog.io/@viviamm7-code/Spring-Logging</guid>
            <pubDate>Wed, 08 Jul 2026 04:03:08 GMT</pubDate>
            <description><![CDATA[<p>운영 시스템에서는 System.out.println() 같은 시스템 콘솔을 사용해서 필요한 정보를 출력하지 않고, 별도의 로깅 라이브러리를 사용해서 로그를 출력해야 한다.</p>
<h4 id="로깅-라이브러리">로깅 라이브러리</h4>
<p>스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리( spring-boot-starter-logging )가 함께 포함된다.
스프링 부트 로깅 라이브러리는 기본으로 다음 로깅 라이브러리를 사용한다.</p>
<ul>
<li>SLF4J - <a href="http://www.slf4j.org">http://www.slf4j.org</a></li>
<li>Logback - <a href="http://logback.qos.ch">http://logback.qos.ch</a></li>
</ul>
<p>로그 라이브러리는 Logback, Log4J, Log4J2 등등 수 많은 라이브러리가 있는데, 그것을 통합해서 인터페이스로 제공하는 것이 바로 SLF4J 라이브러리다.
SLF4J는 인터페이스이고, 그 구현체로 Logback 같은 로그 라이브러리를 선택하면 된다.</p>
<h4 id="로그-선언">로그 선언</h4>
<pre><code class="language-java">private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)</code></pre>
<ul>
<li>@Slf4j : 롬복 사용 가능</li>
</ul>
<h4 id="로그-호출">로그 호출</h4>
<ul>
<li>log.info(&quot;hello&quot;)</li>
<li>System.out.println(&quot;hello&quot;) -&gt; 실무에선 다 찍히니까 사용하면 안됨</li>
</ul>
<h4 id="테스트">테스트</h4>
<ul>
<li>로그가 출력되는 포멧 확인</li>
<li>시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지</li>
</ul>
<blockquote>
<p>LEVEL: TRACE &gt; DEBUG &gt; INFO &gt; WARN &gt; ERROR
개발 서버는 debug 출력
운영 서버는 info 출력</p>
</blockquote>
<ul>
<li>@Slf4j 로 변경</li>
</ul>
<h3 id="올바른-로그-사용법">올바른 로그 사용법</h3>
<h4 id="️-logdebugdatadata">‼️ log.debug(&quot;data=&quot;+data)</h4>
<p>로그 출력 레벨을 info로 설정해도 해당 코드에 있는 &quot;data=&quot;+data가 실제 실행이 되어 버린다.
의미 없는 연산을 해 log를 찍지 않는데도 메모리나 CPU 낭비가 될 수 있다.</p>
<h4 id="👍🏻-logdebugdata-data">👍🏻 log.debug(&quot;data={}&quot;, data)</h4>
<p>로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 
따라서 앞과 같은 의미없는 연산이 발생하지 않는다.</p>
<h4 id="로그-사용시-장점">로그 사용시 장점</h4>
<ul>
<li>쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.</li>
<li><strong>로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절할 수 있다.</strong></li>
<li>시스템 아웃 콘솔에만 출력하는 것이 아니라, <strong>파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다</strong>. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.</li>
<li>성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] MVC 패턴 / Framework]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-MVC-%ED%8C%A8%ED%84%B4-Framework</link>
            <guid>https://velog.io/@viviamm7-code/Spring-MVC-%ED%8C%A8%ED%84%B4-Framework</guid>
            <pubDate>Fri, 03 Jul 2026 16:10:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h3 id="mvc-패턴이-어떻게-나오게-됐을까">MVC 패턴이 어떻게 나오게 됐을까???</h3>
</blockquote>
<h4 id="서블릿과-jsp의-한계">서블릿과 JSP의 한계</h4>
<ul>
<li>서블릿으로 개발할 때는 뷰(View)화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여서 지저분하고 복잡하다.</li>
<li>JAVA 코드, 데이터를 조회하는 리포지토리 등등 다양한 코드가 모두 JSP에 노출되어 있다. </li>
<li><blockquote>
<p>JSP가 너무 많은 역할을 한다.</p>
</blockquote>
</li>
</ul>
<h4 id="mvc-패턴의-등장">MVC 패턴의 등장</h4>
<p>비즈니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일만 해 서로 역할 분리</p>
<h4 id="변경의-라이프-사이클">변경의 라이프 사이클</h4>
<p>진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이다. 예를 들어서 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다. </p>
<h4 id="기능-특화">기능 특화</h4>
<p>JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다.</p>
<h4 id="model-view-controller">Model View Controller</h4>
<p>MVC 패턴은 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다.</p>
<ul>
<li><strong>컨트롤러</strong>: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를조회해서 모델에 담는다.</li>
<li><strong>모델</strong>: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.</li>
<li><strong>뷰</strong>: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.</li>
</ul>
<hr>
<h3 id="mvc-패턴">MVC 패턴</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/eed27647-fcf2-46f0-866d-7f9f49aedddb/image.png" alt=""></p>
<ul>
<li>서블릿을 컨트롤러로 사용</li>
<li>JSP를 뷰로 사용</li>
<li>Model은 HttpServletRequest 객체를 사용</li>
</ul>
<p>request는 내부에 데이터 저장소를 가지고 있는데, request.setAttribute(), request.getAttribute() 를 사용하면 데이터를 보관하고, 조회할 수 있다.</p>
<h4 id="dispatcherforward">dispatcher.forward()</h4>
<p>다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.</p>
<h4 id="redirect-vs-forward">redirect vs forward</h4>
<p>리다이렉트는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다.
따라서 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다. 반면에 포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.</p>
<h4 id="httpservletrequest를-model로-사용한다">HttpServletRequest를 Model로 사용한다.</h4>
<p>request가 제공하는 setAttribute() 를 사용하면 request 객체에 데이터를 보관해서 뷰에 전달할 수 있다.
뷰는 request.getAttribute() 를 사용해서 데이터를 꺼내면 된다.</p>
<hr>
<h3 id="mvc-패턴---한계">MVC 패턴 - 한계</h3>
<h4 id="포워드-중복">포워드 중복</h4>
<p>View로 이동하는 코드가 항상 중복 호출되어야 한다. 물론 이 부분을 메서드로 공통화해도 되지만, 해당 메서드도 항상 직접 호출해야 한다.</p>
<pre><code class="language-java">RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);</code></pre>
<h4 id="viewpath에-중복">ViewPath에 중복</h4>
<pre><code class="language-java">String viewPath = &quot;/WEB-INF/views/new-form.jsp&quot;;</code></pre>
<h4 id="사용하지-않는-코드">사용하지 않는 코드</h4>
<p>다음 코드를 사용할 때도 있고, 사용하지 않을 때도 있다. 특히 response는 현재 코드에서 사용되지 않는다. </p>
<pre><code class="language-java">HttpServletRequest request, HttpServletResponse response</code></pre>
<h4 id="공통-처리가-어렵다">공통 처리가 어렵다.</h4>
<p>기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것이다. 단순히 공통 기능을 메서드로 뽑으면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 것이다.</p>
<p>이 문제를 해결하려면 컨트롤러 호출 전에 먼저 공통 기능을 처리해야 한다.
<strong>프론트 컨트롤러(Front Controller) 패턴</strong>을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/1f0a81da-2daf-4dc3-a616-60e5a153d02a/image.png" alt=""></p>
<h3 id="frontcontroller-패턴-특징">FrontController 패턴 특징</h3>
<ul>
<li>프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음</li>
<li>프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출</li>
<li>공통 처리 가능</li>
<li>프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨</li>
</ul>
<hr>
<h3 id="v1---프론트-컨트롤러">V1 - 프론트 컨트롤러</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/9205b161-dd1d-4c7d-8577-f09cd988b14f/image.png" alt=""></p>
<hr>
<h3 id="v2---view-분리">V2 - view 분리</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/98e05041-9946-48cc-a63e-504d26fefabe/image.png" alt=""></p>
<p>모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고, 깔끔하지 않다. </p>
<pre><code class="language-java">String viewPath = &quot;/WEB-INF/views/new-form.jsp&quot;;
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);</code></pre>
<hr>
<h3 id="v3---model-추가">V3 - Model 추가</h3>
<h4 id="서블릿-종속성-제거">서블릿 종속성 제거</h4>
<blockquote>
<h4 id="컨트롤러-입장에서-httpservletrequest-httpservletresponse이-꼭-필요할까">컨트롤러 입장에서 HttpServletRequest, HttpServletResponse이 꼭 필요할까?</h4>
</blockquote>
<p>요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 지금 구조에서는 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
그리고 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.</p>
<h4 id="뷰-이름-중복-제거">뷰 이름 중복 제거</h4>
<p>컨트롤러에서 지정하는 뷰 이름에 중복이 있는 것을 확인할 수 있다.
컨트롤러는 뷰의 논리 이름을 반환하고, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하게 하면 된다.
이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 프론트 컨트롤러만 고치면 된다.</p>
<ul>
<li>/WEB-INF/views/new-form.jsp -&gt; new-form</li>
<li>/WEB-INF/views/save-result.jsp -&gt; save-result</li>
<li>/WEB-INF/views/members.jsp -&gt; members
<img src="https://velog.velcdn.com/images/viviamm7-code/post/77741ebd-3eff-4d6d-91ec-7545e5a8591d/image.png" alt=""></li>
</ul>
<hr>
<h3 id="v4---단순하고-실용적인-컨트롤러">V4 - 단순하고 실용적인 컨트롤러</h3>
<p>v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. 그런데 실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 번거롭다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/2b009e6f-6efd-4e2f-b87e-fa74ceecbf0a/image.png" alt="">
기본적인 구조는 V3와 같다. 
대신에 컨트롤러가 ModelView 를 반환하지 않고, ViewName 만 반환한다.</p>
<h4 id="모델-객체-전달">모델 객체 전달</h4>
<pre><code class="language-java">Map&lt;String, Object&gt; model = new HashMap&lt;&gt;(); //추가</code></pre>
<p>모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.</p>
<h4 id="뷰의-논리-이름을-직접-반환">뷰의 논리 이름을 직접 반환</h4>
<pre><code class="language-java">String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName); </code></pre>
<p>컨트롤로가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰를 찾을 수 있다.</p>
<hr>
<h3 id="v5---유연한-컨트롤러">V5 - 유연한 컨트롤러</h3>
<blockquote>
<h4 id="만약-어떤-개발자는-v3-방식으로-개발하고-싶고-어떤-개발자는-v4-방식으로-개발하고-싶다면-어떻게-해야할까">만약 어떤 개발자는 V3 방식으로 개발하고 싶고, 어떤 개발자는 V4 방식으로 개발하고 싶다면 어떻게 해야할까?</h4>
</blockquote>
<h4 id="어댑터-패턴">어댑터 패턴</h4>
<p>ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다. 이럴 때 어댑터 패턴을 사용하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/1acd1cde-12b6-408b-af8c-d32c6c851d6d/image.png" alt=""></p>
<h4 id="핸들러-어댑터">핸들러 어댑터</h4>
<p>중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.</p>
<h4 id="핸들러">핸들러</h4>
<p>컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] HTTP Servlet ]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-%EC%84%9C%EB%B8%94%EB%A6%BF</link>
            <guid>https://velog.io/@viviamm7-code/Spring-%EC%84%9C%EB%B8%94%EB%A6%BF</guid>
            <pubDate>Fri, 03 Jul 2026 03:38:17 GMT</pubDate>
            <description><![CDATA[<h4 id="servletcomponentscan">@ServletComponentScan</h4>
<p>스프링 부트는 서블릿을 직접 등록해서 사용할 수 있도록 @ServletComponentScan 을 지원</p>
<h4 id="webservlet-서블릿-애노테이션">@WebServlet 서블릿 애노테이션</h4>
<ul>
<li>name: 서블릿 이름</li>
<li>urlPatterns: URL 매핑</li>
</ul>
<hr>
<h3 id="httpservletrequest-역할">HttpServletRequest 역할</h3>
<h4 id="임시-저장소-기능">임시 저장소 기능</h4>
<p>해당 HTTP 요청이 시작부터 끝날 때 까지 유지되는 임시 저장소 기능</p>
<ul>
<li>저장: request.setAttribute(name, value)</li>
<li>조회: request.getAttribute(name)</li>
</ul>
<h4 id="세션-관리-기능">세션 관리 기능</h4>
<p>request.getSession(create: true)</p>
<h4 id="httpservletrequest-헤더-기타정보-조회">HttpServletRequest 헤더, 기타정보 조회</h4>
<pre><code class="language-java">package hello.servlet.basic.request;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Enumeration;

@WebServlet(name = &quot;requestHeaderServlet&quot;, urlPatterns = &quot;/request-header&quot;)
public class RequestHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        printStartLine(request);
        printHeaders(request);
        printHeaderUtils(request);
        printEtc(request);
    }

    //start line 정보
    private void printStartLine(HttpServletRequest request) {
        System.out.println(&quot;--- REQUEST-LINE - start ---&quot;);
        System.out.println(&quot;request.getMethod() = &quot; + request.getMethod()); //GET
        System.out.println(&quot;request.getProtocol() = &quot; + request.getProtocol()); //HTTP / 1.1
        System.out.println(&quot;request.getScheme() = &quot; + request.getScheme()); //http
        // http://localhost:8080/request-header
        System.out.println(&quot;request.getRequestURL() = &quot; + request.getRequestURL());
        // /request-header
        System.out.println(&quot;request.getRequestURI() = &quot; + request.getRequestURI());
        //username=hi
        System.out.println(&quot;request.getQueryString() = &quot; +
                request.getQueryString());
        System.out.println(&quot;request.isSecure() = &quot; + request.isSecure()); //https 사용유무
        System.out.println(&quot;--- REQUEST-LINE - end ---&quot;);
        System.out.println();
    }

    //Header 모든 정보
    private void printHeaders(HttpServletRequest request) {
        System.out.println(&quot;--- Headers - start ---&quot;);
/*
         Enumeration&lt;String&gt; headerNames = request.getHeaderNames();
         while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            System.out.println(headerName + &quot;: &quot; + request.getHeader(headerName));
         }

 */

        request.getHeaderNames().asIterator()
                .forEachRemaining(headerName -&gt; System.out.println(headerName + &quot;: &quot;
                        + request.getHeader(headerName)));

        System.out.println(&quot;--- Headers - end ---&quot;);
        System.out.println();
    }

    //Header 편리한 조회
    private void printHeaderUtils(HttpServletRequest request) {
        System.out.println(&quot;--- Header 편의 조회 start ---&quot;);
        System.out.println(&quot;[Host 편의 조회]&quot;);
        System.out.println(&quot;request.getServerName() = &quot; +
                request.getServerName()); //Host 헤더
        System.out.println(&quot;request.getServerPort() = &quot; +
                request.getServerPort()); //Host 헤더
        System.out.println();
        System.out.println(&quot;[Accept-Language 편의 조회]&quot;);
        request.getLocales().asIterator()
                .forEachRemaining(locale -&gt; System.out.println(&quot;locale = &quot; +
                        locale));
        System.out.println(&quot;request.getLocale() = &quot; + request.getLocale());
        System.out.println();
        System.out.println(&quot;[cookie 편의 조회]&quot;);
        if (request.getCookies() != null) {
            for (Cookie cookie : request.getCookies()) {
                System.out.println(cookie.getName() + &quot;: &quot; + cookie.getValue());
            }
        }
        System.out.println();
        System.out.println(&quot;[Content 편의 조회]&quot;);
        System.out.println(&quot;request.getContentType() = &quot; +
                request.getContentType());
        System.out.println(&quot;request.getContentLength() = &quot; +
                request.getContentLength());
        System.out.println(&quot;request.getCharacterEncoding() = &quot; +
                request.getCharacterEncoding());
        System.out.println(&quot;--- Header 편의 조회 end ---&quot;);
    }

    //기타 정보
    private void printEtc(HttpServletRequest request) {
        System.out.println(&quot;--- 기타 조회 start ---&quot;);
        System.out.println(&quot;[Remote 정보]&quot;);
        System.out.println(&quot;request.getRemoteHost() = &quot; +
                request.getRemoteHost()); //
        System.out.println(&quot;request.getRemoteAddr() = &quot; +
                request.getRemoteAddr()); //
        System.out.println(&quot;request.getRemotePort() = &quot; +
                request.getRemotePort()); //
        System.out.println();
        System.out.println(&quot;[Local 정보]&quot;);
        System.out.println(&quot;request.getLocalName() = &quot; + request.getLocalName()); //
        System.out.println(&quot;request.getLocalAddr() = &quot; + request.getLocalAddr()); //
        System.out.println(&quot;request.getLocalPort() = &quot; + request.getLocalPort()); //
        System.out.println(&quot;--- 기타 조회 end ---&quot;);
        System.out.println();
    }
}
</code></pre>
<h3 id="http-요청-데이터">HTTP 요청 데이터</h3>
<p>클라이언트 -&gt; 서버 보내는 방법</p>
<h4 id="get---쿼리-파라미터">GET - 쿼리 파라미터</h4>
<ul>
<li>/url<strong>?username=hello&amp;age=20</strong></li>
<li>메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달</li>
<li>예) 검색, 필터, 페이징등에서 많이 사용하는 방식<pre><code class="language-java">package hello.servlet.basic.request;
</code></pre>
</li>
</ul>
<p>import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;</p>
<p>import java.io.IOException;
import java.util.Enumeration;</p>
<p>@WebServlet(name = &quot;requestParamServlet&quot;, urlPatterns = &quot;/request-param&quot;)
public class RequestParamServlet extends HttpServlet {</p>
<pre><code>@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    System.out.println(&quot;[전체 파라미터 조회] - start&quot;);</code></pre><p> /*     Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
           String paramName = parameterNames.nextElement();
           System.out.println(paramName + &quot;=&quot; +
           request.getParameter(paramName));
        }
 */</p>
<pre><code>    request.getParameterNames().asIterator()
            .forEachRemaining(paramName -&gt; System.out.println(paramName +
                    &quot;=&quot; + request.getParameter(paramName)));
    System.out.println(&quot;[전체 파라미터 조회] - end&quot;);
    System.out.println();
    System.out.println(&quot;[단일 파라미터 조회]&quot;);
    String username = request.getParameter(&quot;username&quot;);
    System.out.println(&quot;request.getParameter(username) = &quot; + username);
    String age = request.getParameter(&quot;age&quot;);
    System.out.println(&quot;request.getParameter(age) = &quot; + age);
    System.out.println();
    System.out.println(&quot;[이름이 같은 복수 파라미터 조회]&quot;);
    System.out.println(&quot;request.getParameterValues(username)&quot;);
    String[] usernames = request.getParameterValues(&quot;username&quot;);

    for (String name : usernames) {
        System.out.println(&quot;username=&quot; + name);
    }
    response.getWriter().write(&quot;ok&quot;);
}</code></pre><p>}</p>
<pre><code>
#### POST - HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&amp;age=20
- 예) 회원 가입, 상품 주문, HTML Form 사용
```java
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
   &lt;meta charset=&quot;UTF-8&quot;&gt;
   &lt;title&gt;Title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;form action=&quot;/request-param&quot; method=&quot;post&quot;&gt;
   username: &lt;input type=&quot;text&quot; name=&quot;username&quot; /&gt;
   age: &lt;input type=&quot;text&quot; name=&quot;age&quot; /&gt;
   &lt;button type=&quot;submit&quot;&gt;전송&lt;/button&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><h4 id="http-message-body에-데이터를-직접-담아서-요청">HTTP message body에 데이터를 직접 담아서 요청</h4>
<ul>
<li>HTTP API에서 주로 사용, JSON, XML, TEXT</li>
<li>데이터 형식은 주로 JSON 사용</li>
<li>POST, PUT, PATCH<pre><code class="language-java">package hello.servlet.basic.request;
</code></pre>
</li>
</ul>
<p>import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StreamUtils;
import tools.jackson.databind.ObjectMapper;</p>
<p>import java.io.IOException;
import java.nio.charset.StandardCharsets;</p>
<p>@WebServlet(name = &quot;requestBodyJsonServlet&quot;, urlPatterns = &quot;/request-body-json&quot;)
public class RequestBodyJsonServlet extends HttpServlet {</p>
<pre><code>private ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    ServletInputStream inputStream = request.getInputStream();
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

    System.out.println(&quot;messageBody = &quot; + messageBody);

    HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
    System.out.println(&quot;helloData.getUsername() = &quot; + helloData.getUsername());
    System.out.println(&quot;helloData.getAge() = &quot; + helloData.getAge());
}</code></pre><p>}</p>
<pre><code>
---

### HttpServletResponse 역할

#### HTTP 응답 메시지 생성

- HTTP 응답코드 지정
- 헤더 생성
- 바디 생성

#### 편의 기능 제공
- Content-Type, 쿠키, Redirect

```java
package hello.servlet.basic.response;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = &quot;responseHeaderServlet&quot;, urlPatterns = &quot;/response-header&quot;)
public class ResponseHeaderServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setStatus(HttpServletResponse.SC_OK);

        response.setHeader(&quot;Content-Type&quot;, &quot;text/plain;charset=UTF-8&quot;);
        response.setHeader(&quot;Cache-Control&quot;, &quot;no-cache, no-store, must-revalidate&quot;);
        response.setHeader(&quot;Pragma&quot;, &quot;no-cache&quot;);
        response.setHeader(&quot;my-header&quot;, &quot;hihi&quot;);

        content(response);
        cookie(response);
        redirect(response);

        PrintWriter writer = response.getWriter();
        writer.write(&quot;ok&quot;);

    }

    private void content(HttpServletResponse response) {
        //Content-Type: text/plain;charset=utf-8
        //Content-Length: 2
        //response.setHeader(&quot;Content-Type&quot;, &quot;text/plain;charset=utf-8&quot;);
        response.setContentType(&quot;text/plain;charset=UTF-8&quot;);
        response.setCharacterEncoding(&quot;utf-8&quot;);
        //response.setContentLength(2); //생략시 자동 생성
    }

    private void cookie(HttpServletResponse response) {
        //Set-Cookie: myCookie=good; Max-Age=600;
        //response.setHeader(&quot;Set-Cookie&quot;, &quot;myCookie=good; Max-Age=600&quot;);
        Cookie cookie = new Cookie(&quot;myCookie&quot;, &quot;good&quot;);
        cookie.setMaxAge(600); //600초
        response.addCookie(cookie);
    }

    private void redirect(HttpServletResponse response) throws IOException {
        //Status Code 302
        //Location: /basic/hello-form.html
        //response.setStatus(HttpServletResponse.SC_FOUND); //302
        //response.setHeader(&quot;Location&quot;, &quot;/basic/hello-form.html&quot;);
        response.sendRedirect(&quot;/basic/hello-form.html&quot;);
    }
}</code></pre><h3 id="http-응답-데이터">HTTP 응답 데이터</h3>
<ul>
<li>단순 텍스트 응답</li>
<li>HTML 응답</li>
<li>HTTP API - MessageBody JSON 응답</li>
</ul>
<pre><code class="language-java">package hello.servlet.basic.response;

import hello.servlet.basic.HelloData;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;

@WebServlet(name = &quot;responseJsonServlet&quot;, urlPatterns = &quot;/response-json&quot;)
public class ResponseJsonServlet extends HttpServlet {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        HelloData helloData = new HelloData();
        helloData.setUsername(&quot;ham&quot;);
        helloData.setAge(25);

        String result = objectMapper.writeValueAsString(helloData);
        response.getWriter().println(result);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Network] 웹애플리케이션 이해 ]]></title>
            <link>https://velog.io/@viviamm7-code/Network-%EC%9B%B9%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9D%B4%ED%95%B4</link>
            <guid>https://velog.io/@viviamm7-code/Network-%EC%9B%B9%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9D%B4%ED%95%B4</guid>
            <pubDate>Fri, 03 Jul 2026 01:51:17 GMT</pubDate>
            <description><![CDATA[<h4 id="웹서버web-server">웹서버(Web Server)</h4>
<ul>
<li>HTTP 기반으로 동작</li>
<li>정적 리소스 제공</li>
<li>정적 HTML, NGINX, APACHE</li>
</ul>
<h4 id="웹-어플리케이션-서버web-application-server">웹 어플리케이션 서버(Web Application Server)</h4>
<ul>
<li>HTTP 기반으로 동작</li>
<li>프로그램 코드를 실행해서 애플리케이션 로직 수행</li>
<li>동적 HTML, 서블릿, JSP, Tomcat, Jetty, 스프링 MVC</li>
</ul>
<blockquote>
<h4 id="과거-was가-너무-많은-역할-담당-서버-과부하-was-장애시-오류-화면도-노출-불가">과거 WAS가 너무 많은 역할 담당, 서버 과부하, WAS 장애시 오류 화면도 노출 불가</h4>
<p>-&gt; 정적 리소스는 Web Server가 처리, WAS는 중요한 애플리케이션 로직 처리 전담</p>
</blockquote>
<h4 id="서블릿">서블릿</h4>
<ul>
<li>urlPatterns의 URL이 호출되면 서블릿 코드가 실행</li>
<li>HTTP 요청 정보를 편리하게 사용할 수 있는 HttpServletRequest</li>
<li>HTTP 응답 정보를 편리하게 사용할 수 있는 HttpServletResponse</li>
</ul>
<h4 id="서블릿-컨테이너">서블릿 컨테이너</h4>
<ul>
<li>톰캣처럼 서블릿을 지원하는 WAS</li>
<li>서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기 관리</li>
<li>서블릿 객체는 싱글톤으로 관리</li>
<li>JSP도 서블릿으로 변환 되어서 사용</li>
<li><strong>동시 요청을 위한 멀티 쓰레드 처리 지원</strong> </li>
</ul>
<hr>
<h4 id="쓰레드">쓰레드</h4>
<ul>
<li>애플리케이션 코드를 하나하나 순차적으로 실행하는 것</li>
<li>자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행</li>
<li>쓰레드가 없다면 자바 애플리케이션 실행이 불가능</li>
<li>쓰레드는 한번에 하나의 코드 라인만 수행</li>
<li>동시 처리가 필요하면 쓰레드를 추가로 생성</li>
</ul>
<h4 id="쓰레드-풀">쓰레드 풀</h4>
<ul>
<li>쓰레드가 모여있는 장소</li>
<li>쓰레드 사용 후 다시 쓰레드 풀에 반납</li>
<li>쓰레드 풀 내 모든 쓰레드가 사용중이면 쓰레드 대기하거나 쓰레드거절</li>
</ul>
<blockquote>
<h4 id="was의-주요-튜닝-포인트는-최대-쓰레드max-thread수-이다">WAS의 주요 튜닝 포인트는 최대 쓰레드(max thread)수 이다.</h4>
</blockquote>
<h4 id="이-값이-너무-낮으면">이 값이 너무 낮으면?</h4>
<p>동시 요청이 많으면, 서버 리소스는 여유롭지만, 클라이언트는 금방 응답 지연</p>
<h4 id="이-값이-너무-높으면">이 값이 너무 높으면?</h4>
<p>동시 요청이 많으면, CPU, 메모리 리소스 임계점 초과로 서버 다운</p>
<h4 id="적정-숫자는">적정 숫자는?</h4>
<p>애플리케이션 로직의 복잡도, CPU, 메모리, IO 리소스 상황에 따라 다름
최대한 실제 서비스와 유사하게 성능 테스트 시도!!!!</p>
<ul>
<li>아파치 ab,  jmeter, nGrinder</li>
</ul>
<p>단일 요청 - 쓰레드 하나 사용</p>
<h4 id="요청-마다-쓰레드-생성">요청 마다 쓰레드 생성?</h4>
<ul>
<li>쓰레드는 컨텍스트 스위칭 비용 발생</li>
<li>쓰레드는 생성 비용이 매우 비쌈</li>
</ul>
<p>-&gt; 다중 요청 - 멀티 쓰레드 사용</p>
<hr>
<h4 id="ssr---서버-사이드-렌더링">SSR - 서버 사이드 렌더링</h4>
<ul>
<li>HTML 최종 결과를 서버에서 만들어서 웹 브라우저에 전달</li>
<li>JSP, 타임리프</li>
</ul>
<h4 id="csr---클라이언트-사이드-렌더링">CSR - 클라이언트 사이드 렌더링</h4>
<ul>
<li>HTML 결과를 자바스크립트를 사용해 웹 브라우저에서 동적으로 생성해서 적용</li>
<li>React, Vue.js</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Network] HTTP 웹]]></title>
            <link>https://velog.io/@viviamm7-code/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP-%EC%9B%B9</link>
            <guid>https://velog.io/@viviamm7-code/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-HTTP-%EC%9B%B9</guid>
            <pubDate>Thu, 02 Jul 2026 09:28:51 GMT</pubDate>
            <description><![CDATA[<h4 id="ip-프로토콜-사용-시-클라이언트-입장">IP 프로토콜 사용 시 클라이언트 입장</h4>
<ul>
<li>대상 서버가 패킷을 받을 수 있는 상태인지 모름</li>
<li>패킷 소실</li>
<li>패킷 전달 순서 문제 발생</li>
</ul>
<p><strong>-&gt; TCP/UDP 로 해결</strong></p>
<h4 id="tcp-특징-transmission-control-protocaol">TCP 특징 (Transmission Control Protocaol)</h4>
<ul>
<li>연결 지향 - TCP 3 way handshake (가상 연결) </li>
<li>데이터 전달 보증</li>
<li>순서 보장</li>
<li><blockquote>
<p>순서 안 맞게 보내면 다시 보내라고 요청</p>
</blockquote>
</li>
</ul>
<h4 id="tcp-3-way-handshake-연결-과정">TCP 3 way handshake 연결 과정</h4>
<ol>
<li>SYN(접속 요청)</li>
<li>SYN+ACK</li>
<li>ACK(요청 수락)</li>
<li>데이터 전송</li>
</ol>
<h4 id="udp-특징-user-datagram-protocol">UDP 특징 (User Datagram Protocol)</h4>
<ul>
<li>연결 지향 x</li>
<li>데이터 전달 보증 x</li>
<li>순서 보장 x</li>
<li>단순, 빠름!!!!</li>
<li>IP와 거의 같지만 + PORT + 체크섬</li>
</ul>
<blockquote>
<h4 id="dns-란--domain-name-system">DNS 란 ? (Domain Name System)</h4>
</blockquote>
<ul>
<li>도메인 명을 IP주소로 변환</li>
<li>도메인 명은 그대로 , IP 주소는 변경해서 쓰면됨</li>
</ul>
<h4 id="uriuniform-resource-identifier">URI(Uniform Resource Identifier)</h4>
<ul>
<li><p>URL (Resource Locator) - 리소스가 있는 위치 지정(변함)
EX) <a href="https://www.google.com:443/search?q=hello&amp;hl=ko">https://www.google.com:443/search?q=hello&amp;hl=ko</a></p>
<p><strong>scheme</strong> : http, https, ftp
<strong>host</strong> : <a href="http://www.google.com">www.google.com</a> (호스트명)
<strong>port</strong> : 80, 443 (생략가능)
<strong>path</strong> : search
<strong>query</strong> : ?q=hello&amp;hl=ko (key=value 형태, ?로 시작. &amp;로 추가,   query parameter라 부름)
<strong>fragment</strong> : html 내부 북마크 등에 사용</p>
</li>
<li><p>URN (Resource Name) - 리소스에 이름을 부여(고정값)</p>
</li>
</ul>
<hr>
<blockquote>
<h4 id="http-hypertext-transfer-protocol">HTTP (HyperText Transfer Protocol)</h4>
</blockquote>
<ul>
<li>모든 데이터를 HTTP에 담아 전송</li>
</ul>
<p>TCP : HTTP/1.1 , HTTP/2
UDP : HTTP/3</p>
<h4 id="무상태-프로토콜stateless">무상태 프로토콜(Stateless)</h4>
<ul>
<li>서버가 클라이언트의 상태를 보존 x</li>
<li>응답 서버를 쉽게 바꿀 수 있다 -&gt; 무한한 서버 증설 가능</li>
<li>아무 서버나 호출해도 됨</li>
</ul>
<h4 id="비-연결성">비 연결성</h4>
<ul>
<li>HTTP는 기본이 연결을 유지하지 않는 모델</li>
<li>일반적으로 초 단위의 이하의 빠른 속도로 응답</li>
<li>서버 자원을 매우 효율적으로 사용</li>
<li><blockquote>
<p>TCP/IP 연결을 새로 맺어야함 - 3 way handshake 시간 추가
지금은 HTTP <strong>지속 연결(Persistent Connections)</strong>로 문제 해결
HTTP/2, HTTP/3에서는 더 최적화</p>
</blockquote>
</li>
</ul>
<blockquote>
<h3 id="http-메시지---헤더--공백--바디">HTTP 메시지 -&gt; 헤더 + 공백 + 바디</h3>
</blockquote>
<h4 id="http-헤더">HTTP 헤더</h4>
<ul>
<li>HTTP 전송에 필요한 모든 부가 정보 (Content-Type, Content-Length 등등)</li>
</ul>
<h4 id="http-바디">HTTP 바디</h4>
<ul>
<li>실제 전송한 데이터(HTML 문서, 이미지 영상, JSON 등등 모든 데이터)</li>
</ul>
<h4 id="http-메서드-종류">HTTP 메서드 종류</h4>
<ul>
<li>GET : 리소스 조회</li>
<li>POST : 요청 데이터 처리, 주로 등록</li>
<li>PUT : 리소스를 완전히 대체, 해당 리소스 없으면 생성(덮어버림)</li>
<li>PATCH : 리소스 부분 변경</li>
<li>DELETE : 리소스 삭제</li>
<li>기타 메서드 (HEAD, OPTIONS, CONNECT, TRACE)</li>
</ul>
<h4 id="http-메서드-속성">HTTP 메서드 속성</h4>
<ul>
<li>안전(Safe) - 호출해도 리소스를 변경하지 않는다. GET - Safe / POST - Safe x</li>
<li>멱등(Idempotent) - 몇 번을 호출하던 결과가 똑같다. GET - 멱등 / POST - 멱등 x</li>
<li><blockquote>
<p>자동 복구 메커니즘</p>
</blockquote>
</li>
<li>캐시가능(Cacheable) - 리소스를 캐싱해서 사용. 실무에선 거의 GET만 캐시가능</li>
</ul>
<blockquote>
<h4 id="클라이언트에서-서버로-데이터-전송">클라이언트에서 서버로 데이터 전송</h4>
</blockquote>
<ul>
<li>쿼리 파라미터로 데이터 전송 - GET</li>
<li>메시지 바디로 데이터 전송 - POST, PUT, PATCH</li>
</ul>
<h4 id="파일-관리-시스템">파일 관리 시스템</h4>
<p>PUT 사용 시 클라이언트가 리소스 URI를 알고 있어야 한다.</p>
<blockquote>
<h4 id="스토어store">스토어(Store)</h4>
</blockquote>
<ul>
<li>클라이언트가 관리하는 리소스 저장소</li>
<li>클라이언트가 리소스의 URI를 알고 관리</li>
</ul>
<hr>
<h3 id="http-상태코드">HTTP 상태코드</h3>
<ul>
<li><p>2xx(Successful)</p>
<ul>
<li>200 OK : 요청 성공</li>
<li>201 Created : 요청 성공해서 새로운 리소스가 생성됨</li>
<li>202 Accepted : 요청이 접수되었으나 처리가 완료되지 않았음</li>
<li>204 No Content : 서버가 요청을 성공적으로 수행했지만, 응답 payload 본문에 보낼 데이터가 없음</li>
</ul>
</li>
<li><p>3xx(Redirection)</p>
<ul>
<li>영구 리다이렉션 - 301 Moved Permanently, 308 Permanent Redirect</li>
<li>일시 리다이렉션 
302 Found : GET으로 변할 수 있음(모호함)
307 Temporary Redirect : 메서드가 변하면 안됨
303 See Other : 메서드가 GET으로 변경</li>
</ul>
<blockquote>
<h4 id="post로-주문-후에-웹-브라우저를-새로고침하면">Post로 주문 후에 웹 브라우저를 새로고침하면?</h4>
<p>새로고침은 다시 요청이므로 중복 주문이 들어감
-&gt; PRG : Post/Redirect/Get 로 해결</p>
<pre><code>1. Post로 주문후에 새로 고침으로 인한 중복 주문 방지
2. Post로 주문후에 주문 결과 하면을 GET 메서드로 리다이렉트
3. 새로고침해도 결과 화면을 GET으로 조회
4. 중복 주문 대신에 결과 화면만 GET으로 다시 요청</code></pre></blockquote>
<ul>
<li>기타 리다이렉션
304 Not Modified : 캐시를 목적으로 사용(캐시로 리다이렉트)</li>
</ul>
</li>
<li><p>4xx(Client Error)</p>
<ul>
<li>400 Bad Request : 클라이언트가 잘못된 요청을 해서 서버가 요청을 처리할 수 없음</li>
<li>401 Unauthorized : 인증되지 않음</li>
<li>403 Forbidden : 서버가 요청을 이해했지만 승인을 거부함</li>
<li>404 Not Found : 요청 리소스를 찾을 수 없음</li>
</ul>
</li>
<li><p>5xx(Server Error)</p>
<ul>
<li>500 Internal Server Error : 서버 문제로 오류 발생</li>
<li>503 Service Unavailable : 서비스 이용 불가</li>
</ul>
</li>
</ul>
<h4 id="http-헤더-부가-정보들">HTTP 헤더 부가 정보들</h4>
<ol>
<li><p>표현
Content-Type : 미디어 타입, 문자 인코딩 (text/html;, application/json)
Content-Encoding : 표현 데이터를 압축 (gzip)
Content-Language : 표현 데이터의 자연 언어 (ko, en)
Content-Length : 표현 데이터의 길이 (바이트 단위)</p>
</li>
<li><p>협상 (<strong>클라이언트가 선호</strong>하는 표현 요청)
Accept : 미디어 타입
Accept-Charset : 문자 인코딩
Accept-Encoding : 압축 인코딩
Accept-Language : 자연 언어</p>
</li>
<li><p>전송 방식</p>
</li>
</ol>
<ul>
<li>단순 전송 : Content-Length</li>
<li>압축 전송 : Content-Encoding</li>
<li>분할 전송 : Transfer-Encoding</li>
<li>범위 전송 : Range, Content-Range</li>
</ul>
<ol start="4">
<li>일반 정보</li>
</ol>
<ul>
<li>From : 유저 에이전트의 이메일 정보</li>
<li>Referer : 이전 웹 페이지 주소</li>
<li>User-Agent : 유저 에이전트 애플리케이션 정보, 통계 정보</li>
<li>Server : 요청을 처리하는 ORIGIN 서버의 소프트웨어 정보</li>
<li>Date : 메시지가 발생한 날짜와 시간</li>
</ul>
<ol start="5">
<li>특별한 정보</li>
</ol>
<ul>
<li>Host : 요청한 호스트 정보(도메인), 필수 헤더</li>
<li>Location : 페이지 리다이렉션(3xx)</li>
<li>Allow : 허용 가능한 HTTP 메서드(405)</li>
<li>Retry-After : 유저 에이전트가 다음 요청을 하기까지 기다려야 하는 시간(503)</li>
</ul>
<ol start="6">
<li>인증 </li>
</ol>
<ul>
<li>Authorization : 클라이언트 인증 정보를 서버에 전달</li>
<li>WWW-Authenticate : 리소스 접근 시 필요한 인증 방법 정의(401)</li>
</ul>
<ol start="7">
<li>쿠키</li>
</ol>
<ul>
<li><p>Set-Cookie : 서버에서 클라이언트로 쿠키 전달(응답)</p>
<ul>
<li>생명주기 : Expires, max-age</li>
<li>도메인 : 명시하면 명시한 문서 기준 도메인 + 서브 도메인 포함
생략하면 현재 문서 기준 도메인만 적용</li>
<li>경로 : 이 경로를 포함한 하위 경로 페이지만 쿠키 접근</li>
<li>보안<pre><code> 1. Secure : https 만 전송
 2. HttpOnly : 자바스크립트에서 접근 불가, HTTP 전송에만 사용
 3. SameSite : 요청 도메인과 쿠키에 설정된 도메인이 같을 때만 쿠키 전송</code></pre></li>
</ul>
</li>
<li><p>Cookie : 클라이언트가 서버에서 받은 쿠키를 저장하고, HTTP 요청시 서버로 전달</p>
</li>
</ul>
<hr>
<h3 id="브라우저-캐시--유효-시간동안-데이터-캐싱">브라우저 캐시 : 유효 시간동안 데이터 캐싱</h3>
<blockquote>
<h4 id="만약-캐시-시간이-초과했을-때-데이터가-같은-경우-다시-서버에게-요청해야되는가-">만약 캐시 시간이 초과했을 때 데이터가 같은 경우 다시 서버에게 요청해야되는가 ??</h4>
</blockquote>
<h4 id="last-modified-로-검증">Last-Modified 로 검증</h4>
<ol>
<li>응답 헤더에 Last-Modified를 내리고 웹 브라우저 캐시에 저장</li>
<li>최종 수정일 조회 요청 후 캐시와 서버의 날짜 비교</li>
<li>날짜가 같다면 서버에서 헤더만 내리고 304 내림 (바디를 다시 안내려도 됨)</li>
<li>바디는 브라우저 캐시의 바디 값 재사용</li>
<li>날짜가 다르면 새로운 데이터(헤더+바디) 내리고 200 OK 내림</li>
</ol>
<blockquote>
<h4 id="비교-날짜가-다르지만-내린-데이터가-같거나-1초-미만-단위로-차이날-때는-">비교 날짜가 다르지만 내린 데이터가 같거나 1초 미만 단위로 차이날 때는 ??</h4>
</blockquote>
<h4 id="etagentity-tag로-검증">ETag(Entity Tag)로 검증</h4>
<ol>
<li>캐시용 데이터에 임의의 고유한 버전 이름(ETag)을 달아둠</li>
<li>ETag만 서버에 보내서 같으면 유지, 다르면 다시 받기</li>
</ol>
<p>-&gt; 캐시 제어 로직을 서버에서 완전히 관리 (클라이언트는 캐시 메커니즘을 모름)</p>
<h4 id="캐시-제어-헤더">캐시 제어 헤더</h4>
<ul>
<li>Cache-Control : 캐시 제어 (max-age, no-cache, no-store) = 이걸로 다 함</li>
<li>Pragma : 캐시 제어(하위 호환)</li>
<li>Expires : 캐시 유효 기간(하위 호환)</li>
</ul>
<blockquote>
<h4 id="원-서버가-멀-경우-프록시-캐시-도입">원 서버가 멀 경우 프록시 캐시 도입</h4>
</blockquote>
<ul>
<li>클라이언트 캐시에서 원 서버로 가기위해 그 사이에 프록시 캐시 서버를 둠</li>
</ul>
<h4 id="cache-control--no-cache">Cache-Control : no-cache</h4>
<ul>
<li>항상 원 서버에 검증</li>
<li>프록시 캐시에서 원 서버에 접근할 수 없는 경우 오류 보다 오래된 데이터라도 보여줌</li>
</ul>
<h4 id="cache-control--no-store">Cache-Control : no-store</h4>
<ul>
<li>데이터에 민감한 정보가 있으므로 저장하면 안됨(메모리에서 사용하고 빠르게 삭제)</li>
</ul>
<h4 id="cache-control--must-revalidate">Cache-Control : must-revalidate</h4>
<ul>
<li>캐시 만료후 최초 조회시 원 서버에 검증</li>
<li>프록시 캐시에서 원 서버에 접근할 수 없는 경우, 항상 오류 발생(504)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[KT x Goorm] 부트캠프 회고 (우수 수료생)]]></title>
            <link>https://velog.io/@viviamm7-code/KT-x-Goorm-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@viviamm7-code/KT-x-Goorm-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 17 Jun 2026 11:29:51 GMT</pubDate>
            <description><![CDATA[<h4 id="본-콘텐츠는-구름-서포터즈-활동으로-지원을-받아-작성된-교육생의-실제-경험-후기입니다">본 콘텐츠는 구름 서포터즈 활동으로 지원을 받아 작성된 교육생의 실제 경험 후기입니다.</h4>
<blockquote>
<p><a href="https://deepdive.goorm.io/">KT x Goorm 백엔드 부트캠프</a></p>
</blockquote>
<h2 id="개요">개요</h2>
<p>마지막 프로젝트에서를 소개하면서 부트캠프 회고를 해보려고 한다.
일단 마지막 프로젝트에서는 지도를 통해 서비스를 만드는 모바일 웹 프로젝트를 진행했고, 우리는 카카오 지도를 가지고 로컬 맛집이라는 주제를 담아 기획을 시작했다.
백엔드와 프론트를 도메인별로 동시에 맡아 풀스택으로 진행했다.
내가 맡은 파트는 로그인 / 방문 인증 / 북마크 / 마이페이지(회원탈퇴) 이렇게 진행했다.</p>
<h2 id="도메인-별-진행">도메인 별 진행</h2>
<h3 id="1-로그인">1. 로그인</h3>
<p>로그인 기능은 카카오 소셜 로그인을 기반으로 구현했다. 프로젝트 초반에는 인증 방식을 어떻게 가져갈지에 대한 고민이 많았다. 세션을 사용할지, JWT를 사용할지, JWT를 사용한다면 Access Token과 Refresh Token을 어디에 저장할지에 대해 현업 개발자 친구에게도 물어보고 블로그도 뒤져보면서 같은 고민을 했던 사람들의 의견을 참고했다.</p>
<p>최종적으로는 Refresh Token은 HttpOnly Cookie에 저장하고, Access Token은 프론트 메모리에서 관리하는 방식으로 구현했다. Refresh Token을 쿠키에 저장한 이유는 JavaScript에서 직접 접근할 수 없도록 하여 보안성을 높이기 위해서였다. 반면 Access Token은 API 요청 시 Authorization Header에 담아 사용해야 했기 때문에 프론트 메모리에서 관리하도록 했다.</p>
<p>이 과정에서 단순히 “JWT를 쓴다”가 아니라, 토큰을 어디에 저장하고 어떤 방식으로 재발급할지까지 고민할 수 있었다. 특히 Access Token이 만료되었을 때 Refresh Token을 통해 재발급하는 흐름을 구현하면서 인증 구조에 대한 이해도가 많이 올라갔다.</p>
<h3 id="2-방문-인증">2. 방문 인증</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/bf997bab-5074-48a2-9e72-39ca3e85cec1/image.png" alt=""></p>
<p>방문 인증은 이번 프로젝트에서 가장 핵심적인 기능 중 하나였다. 사용자가 실제로 가게 근처에 있는지를 확인해야 했기 때문에, 브라우저의 Geolocation API와 데이터베이스의 공간 데이터 처리 기능인 PostGIS를 함께 사용했다.</p>
<p>프론트에서는 사용자의 현재 위치를 가져오기 위해 Geolocation API를 사용했다. 지도 화면에서는 사용자의 위치가 UX상 실시간으로 반영되는 것처럼 보이도록 위치 변화를 감지했고, 방문 인증 버튼을 클릭했을 때는 현재 위치를 다시 조회해 서버로 전달했다.</p>
<p>백엔드에서는 프론트에서 전달한 사용자 좌표를 그대로 신뢰하지 않고 재검증을 진행했다. PostGIS 함수를 사용해 DB에 저장된 가게 위치와 사용자 위치 사이의 거리를 다시 계산했다. 가게 위치는 ST_X, ST_Y를 통해 경도와 위도로 가공해 프론트에 내려주었고, 실제 방문 인증 저장 전에는 ST_Distance를 사용해 서버에서 최종 검증을 수행했다.</p>
<p>이때 사용자가 가게 반경 50m 이내에 있을 경우에만 방문 인증 기록을 저장하도록 구현했다. 프론트에서 1차로 사용자 경험을 위한 검증을 하고, 백엔드에서 최종 검증을 수행하는 구조로 가져가면서 보안성과 사용자 경험을 모두 고려할 수 있었다.</p>
<h3 id="3-북마크나의-맛집">3. 북마크(나의 맛집)</h3>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/e2e5768b-1f98-473a-be67-dc765c32a48d/image.png" alt=""></p>
<p>북마크 기능은 사용자가 마음에 드는 맛집을 저장하고 관리할 수 있도록 CRUD 형태로 구현했다. 단순히 맛집 하나를 저장하는 기능에서 끝나는 것이 아니라, 사용자가 북마크 카테고리를 만들고, 특정 카테고리에 맛집을 저장하거나 제거할 수 있도록 구성했다.</p>
<p>여러 카테고리에 어떻게 북마크를 집어넣을까 고민하다가 구글 맵의 구현 방법을 참고했다.</p>
<p>이 기능을 구현하면서 사용자별 데이터 접근 제어를 신경 썼다. 예를 들어, 사용자가 요청한 북마크 카테고리가 실제로 본인의 카테고리인지 검증한 뒤 저장하거나 삭제하도록 처리했다.</p>
<p>또한 프론트에서는 가게 상세 페이지에서 현재 맛집이 어떤 폴더에 저장되어 있는지 보여주고, 사용자가 체크박스처럼 저장 상태를 변경할 수 있도록 구현했다. 백엔드 API 응답 구조를 프론트 화면 흐름에 맞춰 설계하는 경험도 할 수 있었다.</p>
<h3 id="4-마이페이지-및-회원탈퇴">4. 마이페이지 및 회원탈퇴</h3>
<p>마이페이지에서는 사용자 정보 조회와 회원탈퇴 기능을 담당했다. 회원탈퇴는 단순히 회원 데이터를 삭제하는 방식이 아니라, 기존 리뷰나 북마크, 방문 인증 기록과의 관계를 고려해야 했다.</p>
<p>그래서 회원 데이터를 바로 삭제하기보다는 개인정보에 해당하는 값은 null 처리하거나 익명화하고, 사용자의 상태값을 관리하는 방식으로 구현했다. 이미 작성된 리뷰나 서비스 데이터는 유지하면서도, 탈퇴한 사용자의 개인정보는 남지 않도록 처리했다.</p>
<p>또한 카카오 소셜 로그인을 사용했기 때문에, 서비스 내부 회원탈퇴뿐만 아니라 카카오 개발자의 어드민 키를 발급받아 카카오 연결 끊기까지 함께 처리했다.</p>
<h2 id="프로젝트를-진행하며-느낀-점">프로젝트를 진행하며 느낀 점</h2>
<p>이번 프로젝트는 단순히 기능을 구현하는 것보다, 실제 서비스처럼 동작하기 위해 고려해야 할 점이 많았다. 로그인에서는 보안과 사용자 편의성을 함께 고민해야 했고, 방문 인증에서는 프론트의 UX와 백엔드의 검증 책임을 분리해야 했다. 북마크와 회원탈퇴에서는 데이터 관계와 권한 검증을 신경 써야 했다.</p>
<p>특히 도메인별로 백엔드와 프론트를 동시에 맡아 진행하면서, API를 설계할 때 프론트에서 어떻게 사용할지를 함께 고민할 수 있었다. 이전에는 백엔드 로직을 완성하는 것에 집중했다면, 이번 프로젝트에서는 화면 흐름과 사용자 경험까지 함께 고려하게 되었다.</p>
<p>또한 팀 프로젝트를 진행하면서 협업의 중요성도 많이 느꼈다. 기능이 서로 연결되어 있다 보니, 내가 만든 API가 다른 팀원의 화면과 맞지 않으면 바로 문제가 발생했다. 그래서 API 명세를 공유하고, 필요한 응답값을 조율하고, 문제가 생기면 빠르게 소통하는 과정이 중요했다.</p>
<h2 id="우수-수료생-선정">우수 수료생 선정</h2>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/fe392ed3-1854-4e2d-9a8c-d5c10c9a6c3e/image.png" alt=""></p>
<p>부트캠프를 마무리하면서 예상치도 못한 우수 수료생으로 선정되었다.
사실 3차 프로젝트때 수상을 하지 못했기 때문에 우수 수료생을 거의 포기했었다.
그래도 지금까지 해온게 있기 때문에 꾸준히 열심히 했다.
내가 6개월 동안 해온것들은</p>
<ul>
<li>출석률 99%</li>
<li>구름 서포터즈 활동</li>
<li>해커톤 수상(장려상)</li>
<li>EXP 미션 모두 완료</li>
<li>코딩테스트 매달 점수 따기</li>
</ul>
<p>등등 꾸준히 열심히 했다.
이 종합적인 것들을 합산해서 내가 우수 수료생으로 수료할 수 있지 않았나 싶다.
정말 스프링의 스자도 모르도 백엔드의 백자도 모르던 내가 이 부트캠프를 통해서 프로젝트 경험도 쌓을 뿐더러 시스템의 구조와 흐름을 이해하고 앞으로 무엇을 해야하는 지 깨닫게 됐다.</p>
<p>우수 수료생 혜택으로는 </p>
<ul>
<li>kt cloud 서류 우대 or 구름 인턴십</li>
<li>제주 펜트하우스 1박2일권</li>
<li>장학금 200만원</li>
</ul>
<p>등이 있다.
앞으로 취준에 많은 도움이 될 거같다.</p>
<p>아.. 이제 서류 넣어야지</p>
<p><a href="https://github.com/Dev-DdogalMap">또갈지도 깃허브</a></p>
<blockquote>
<p><a href="https://deepdive.goorm.io">#구름딥다이브 #구름부트캠프 #부트캠프후기
#KDT후기 #구름서포터즈 #DEEPDIVE</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] JWT 인증 구현기]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-JWT-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0</link>
            <guid>https://velog.io/@viviamm7-code/Spring-JWT-%EC%9D%B8%EC%A6%9D-%EA%B5%AC%ED%98%84%EA%B8%B0</guid>
            <pubDate>Sat, 06 Jun 2026 06:46:09 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>마지막 프로젝트 때 로그인 인증 방식과 사용자 인증을 어떤식으로 굉장히 많은 고민을 했다. 세션과 JWT 중에는 뭘 쓸지, JWT를 쓴다면 토큰 관리는 어떻게 할지,  프론트에게 토큰을 어떻게 전달할 지, 많은 고민들을 공유해보려고 한다.</p>
<p>물론 3차 프로젝트때도 JWT를 사용해 토큰을 관리해보았지만 그 당시에는 리프레시토큰을 쿠키에 왜 넣어서 보관하는지도 리프레시토큰을 어떻게 쓰는지도 액세스 토큰을 어디에 보관해야되는지의 고민도 자세히 해보지 않고 사용해서 이번엔 정말 근거있게 선택해보자 생각했다.</p>
<hr>
<h2 id="1-왜-세션-대신-jwt를-선택했나">1. 왜 세션 대신 JWT를 선택했나?</h2>
<p>이번 프로젝트는 프론트는 React로 동작하고, 백엔드는 Spring Boot API 서버로 동작한다.
그리고 배포 환경에서도 프론트 도메인과 백엔드 도메인이 분리된다.</p>
<p>이런 구조에서는 API 요청마다 인증 정보를 명확하게 전달하는 JWT 방식이 잘 맞는다고 판단했고 크게 세 가지 이유가 있다.</p>
<h3 id="첫-번째는-서버-확장성이다">첫 번째는 서버 확장성이다.</h3>
<p>세션 방식은 서버가 로그인 상태를 저장해야 한다.
반면 JWT는 서버가 토큰 검증만 하면 되기 때문에 서버가 여러 대로 늘어나도 세션 공유 문제가 상대적으로 적다.</p>
<h3 id="두-번째는-프론트와-백엔드-분리-구조에-잘-맞기-때문이다">두 번째는 프론트와 백엔드 분리 구조에 잘 맞기 때문이다.</h3>
<p>React 프론트에서 API 요청을 보낼 때 Authorization: Bearer accessToken 형태로 인증 정보를 명시적으로 보낼 수 있다.</p>
<h3 id="마지막은-데이터베이스-부하가-적다는-점이다">마지막은 데이터베이스 부하가 적다는 점이다.</h3>
<p>세션 방식은 서버가 로그인 상태를 세션 저장소에서 관리해야 한다. 서버가 여러 대로 늘어나면 세션 저장소를 공유해야 하므로 Redis 같은 외부 저장소가 필요할 수 있다. 반면 JWT는 서버가 토큰의 서명과 만료시간을 검증하면 되기 때문에 세션 저장소 의존도가 낮다.</p>
<h4 id="jwt도-단점이-있다">JWT도 단점이 있다.</h4>
<p>JWT는 한 번 발급하면 기본적으로 서버가 상태를 저장하지 않기 때문에, 토큰 만료 전까지는 강제 폐기가 어렵다.
그래서 <strong>accessToken의 만료 시간을 짧게 가져가고, refreshToken을 별도로 관리</strong>하는 방식을 선택했다.</p>
<hr>
<h2 id="2-jwt-토큰을-어떻게-관리할지">2. JWT 토큰을 어떻게 관리할지?</h2>
<p>토큰을 어디에 저장하고 어떻게 만료/재발급할지 잘못 설계하면 보안 위험이 커질 수 있다.</p>
<p>localStorage는 JavaScript로 접근 가능하기 때문에 XSS 공격이 발생해서 악성 스크립트가 실행되면 localStorage에 있는 토큰을 쉽게 읽을 수 있다.</p>
<p>물론 XSS는 애초에 막아야 하는 문제다.
하지만 토큰 저장 방식을 설계할 때도 XSS 발생 시 피해를 줄일 수 있는 방향을 선택하는 것이 좋다고 판단했다.</p>
<p>그래서 백에서 토큰을 내려줬을 때 프론트 메모리에 넣는 방법을 선택했다.
프론트는 React를 사용하고있어 useState 메모리를 선택했다.</p>
<p>백엔드는 로그인 성공 시 refreshToken을 HttpOnly Cookie로 저장한다. 이후 프론트는 refreshToken api를 호출하고, 이때 브라우저가 refreshToken 쿠키를 자동으로 함께 보낸다. 백엔드는 refreshToken을 검증한 뒤 accessToken을 응답 body로 내려주고, 프론트는 이 accessToken을 React useState에 저장한다.</p>
<hr>
<h2 id="3-accesstoken과-refreshtoken을-나눈-이유">3. AccessToken과 RefreshToken을 나눈 이유</h2>
<p>처음에는 accessToken 하나만 사용해도 되지 않을까 생각했다.</p>
<p>하지만 보안 상 accessToken이 탈취되면 만료될 때까지 공격자가 계속 사용할 수 있기 때문이다.</p>
<p>그래서 토큰을 두 종류로 나눴다.</p>
<h4 id="1-access-token">1. Access Token</h4>
<p>→ 실제 API 인증에 사용
→ 만료 시간을 짧게 설정</p>
<h4 id="2-refresh-token">2. Refresh Token</h4>
<p>→ Access Token 재발급에 사용
→ 만료 시간을 길게 설정</p>
<p>이렇게 하면 accessToken의 만료 시간을 짧게 가져가도 사용자는 계속 로그인 상태를 유지할 수 있며 보안성과 UX까지 챙길 수 있다고 생각했다.</p>
<h2 id="4--프론트에서의-통신-방법">4.  프론트에서의 통신 방법</h2>
<p>원래 프로젝트에서는 백엔드와 통신할 때 공통 axios 인스턴스를 사용했다.</p>
<p>이 방식은 로그인 없이 접근 가능한 API에서는 문제가 없었다.
예를 들어 음식점 지도 조회, 음식점 미리보기, 음식 카테고리 조회처럼 누구나 접근 가능한 public API는 accessToken이 필요하지 않다.</p>
<p>하지만 로그인한 사용자만 사용할 수 있는 API에서는 문제가 생겼다.</p>
<p>예를 들어 방문 인증, 찜하기, 내 정보 조회, 채팅방 생성 같은 API는 백엔드가 “이 요청을 보낸 사용자가 누구인지” 알아야 돼서 요청에 accessToken을 함께 보내야 한다.</p>
<p>하지만 문제가 있었다!!!!!</p>
<p><strong>첫 번째, 인증이 필요한 모든 API마다 Authorization 헤더를 직접 붙여야 한다.</strong></p>
<p><strong>두 번째, accessToken이 만료되었을 때의 처리를 모든 API마다 반복해서 작성해야 한다.</strong></p>
<p>그래서 인증이 필요한 API 요청을 위한 공통 함수 <code>authFetch</code>를 만들었다.</p>
<p><code>authFetch</code>의 역할은 이다.</p>
<pre><code class="language-text">1. 현재 React 메모리에 저장된 accessToken을 전달받는다.
2. accessToken을 Authorization 헤더에 붙여 API 요청을 보낸다.
3. 응답이 성공이면 그대로 반환한다.
4. 응답이 401 또는 403이면 accessToken 만료로 판단한다.
5. /api/auth/refresh를 호출한다.
6. refreshToken 쿠키를 통해 새 accessToken을 발급받는다.
7. 새 accessToken을 React 메모리에 저장한다.
8. 처음 실패했던 API를 새 accessToken으로 다시 요청한다.</code></pre>
<p>실제로 방문 인증 API에서 다음 흐름을 확인했다.</p>
<pre><code class="language-text">POST /api/visit/visit-verification → 403
POST /api/auth/refresh             → 200
POST /api/visit/visit-verification → 200</code></pre>
<p>첫 번째 요청은 만료된 accessToken으로 인해 실패했다.
그 후 <code>authFetch</code>가 <code>/api/auth/refresh</code>를 호출했고, 새 accessToken을 받은 뒤 원래 방문 인증 API를 다시 호출했다.
결과적으로 사용자는 다시 로그인하지 않고 방문 인증을 완료할 수 있었다.</p>
<p>여기서 중요한 점은 <code>/api/auth/refresh</code> 자체를 <code>authFetch</code>로 호출하는 것은 아니라는 점이다.</p>
<p>구조는 다음과 같다.</p>
<pre><code class="language-text">authFetch
→ 일반 인증 API 호출
→ 401/403 감지
→ refreshAccessToken() 호출
→ /api/auth/refresh 호출
→ 새 accessToken 저장
→ 원래 API 재요청</code></pre>
<p>즉 <code>authFetch</code>는 인증이 필요한 일반 API 요청을 감싸는 함수이고, 내부에서 필요할 때만 refresh API를 호출한다.</p>
<p>그래서 우리 프로젝트에서는 API 요청 기준을 다음과 같이 정했다.</p>
<pre><code class="language-text">로그인 없이 접근 가능한 public API
→ 기존 axios 사용

로그인한 사용자만 접근 가능한 API
→ authFetch 사용</code></pre>
<p><code>accessToken</code>은 AuthContext의 useState에 저장된 값이다.</p>
<pre><code class="language-tsx">const [accessToken, setAccessToken] = useState&lt;string | null&gt;(null);</code></pre>
<p>컴포넌트에서는 <code>useAuth()</code>를 통해 이 값을 꺼내고, API 함수에 전달한다.</p>
<pre><code class="language-tsx">const { accessToken, refreshAccessToken } = useAuth();</code></pre>
<p>이렇게 분리해 public API는 단순하게 유지하고, 인증이 필요한 API에서는 accessToken 만료와 재발급 처리를 자동화했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kt x Goorm] 3차 프로젝트 n8n으로 자동화 플로우 구축하기]]></title>
            <link>https://velog.io/@viviamm7-code/Kt-x-Goorm-3%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@viviamm7-code/Kt-x-Goorm-3%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 25 May 2026 14:54:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="n8n으로-인스타그램-웹-크롤링부터-ai-분석-이메일-발송까지-자동화하기">n8n으로 인스타그램 웹 크롤링부터 AI 분석, 이메일 발송까지 자동화하기</h4>
</blockquote>
<h1 id="☁️-개요">☁️ 개요</h1>
<p>언더 뮤지션들을 홍보하는 목적으로 3차 프로젝트를 진행했고 마침내 끝이 났다.
3차 프로젝트를 진행하면서 사용자의 인스타그램 지표를 크롤링해 가져와 AI 분석을 수행하고, 분석 결과를 이메일로 발송하는 자동화 플로우를 구축했다.</p>
<p>실제 도메인 사이트 : <a href="https://www.musicpeak.site">https://www.musicpeak.site</a></p>
<hr>
<h1 id="☁️-n8n-플로우">☁️ n8n 플로우</h1>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/cdaa57da-f160-4a51-84db-b5def066423b/image.png" alt="">내가 구축한 플로우에는 7개의 노드가 들어감. 각 노드별로 적어보겠심다</p>
<h2 id="1-webhook-노드">1. Webhook 노드</h2>
<p>가장 첫 번째 노드는 Webhook이다.</p>
<p>사용자가 서비스에서 분석 요청을 하면 Spring Boot 백엔드는 먼저 분석 Job을 생성하고, n8n Webhook URL로 요청을 보낸다.</p>
<p>백엔드는 n8n에게 뒷 작업을 위임한다.</p>
<p>Webhook으로 전달하는 데이터는 대략 다음과 같은 형태이다.</p>
<pre><code class="language-yaml">{
  &quot;analysisJobId&quot;: 1,
  &quot;promotionId&quot;: 10,
  &quot;musicianId&quot;: 3,
  &quot;instagramUsername&quot;: &quot;example_user&quot;,
  &quot;sinceDate&quot;: &quot;2026-05-01&quot;,
  &quot;email&quot;: &quot;test@example.com&quot;
}</code></pre>
<p>여기서 중요한 값은 analysisJobId이다.
이 값이 있어야 이후 단계에서 현재 분석 요청이 어떤 Job인지 추적할 수 있다.</p>
<p>Webhook
→ 백엔드에서 전달한 분석 요청 데이터 수신
→ 이후 노드에서 사용할 기본 데이터 확보</p>
<p>처음에는 단순히 username만 넘겨도 되지 않을까 생각했지만, 실제로는 분석 Job 상태 관리와 결과 저장을 위해 analysisJobId, promotionId, email 같은 값들이 함께 필요했다.</p>
<hr>
<h2 id="2-running-노드">2. RUNNING 노드</h2>
<p>두 번째 노드는 RUNNING이다.</p>
<p>Webhook으로 요청을 받은 직후, 해당 분석 Job의 상태를 RUNNING으로 변경한다.</p>
<p>처음 사용자가 분석을 요청했을 때 Job 상태는 보통 PENDING으로 저장된다.</p>
<p>n8n 플로우가 시작되면 백엔드 API를 호출해서 상태를 RUNNING으로 바꾼다.</p>
<p>Webhook 요청 수신
→ analysisJobId 확인
→ 백엔드 상태 변경 API 호출
→ status = RUNNING</p>
<p>이 단계를 둔 이유는 프론트나 백엔드에서 현재 분석이 실제로 진행 중인지 확인할 수 있게 하기 위해서다.</p>
<p>만약 이 상태 변경이 없다면, 사용자는 분석 요청을 했지만 실제로 크롤링이 시작되었는지 알기 어렵다.</p>
<hr>
<h2 id="3-context-노드">3. Context 노드</h2>
<p>세 번째 노드는 Context이다.</p>
<p>이 노드에서는 이후 크롤링과 AI 분석에 필요한 데이터를 정리한다.</p>
<p>Webhook에서 받은 데이터가 그대로 모든 노드에서 쓰기 좋은 형태는 아니기 때문에, 필요한 값만 추출해서 하나의 Context로 구성했다.</p>
<p>이 Context 노드를 둔 이유는 뒤쪽 노드들이 복잡해지는 것을 막기 위해서다.</p>
<p>각 노드마다 Webhook 원본 데이터를 계속 찾아서 쓰면 표현식이 길어지고 유지보수가 어려워진다.
그래서 중간에 Context를 만들어두고, 이후 노드에서는 정리된 값을 기준으로 동작하도록 구성했다.
이렇게 해두면 Web Crawling 노드나 Save Crawling-result 노드에서 필요한 값을 쉽게 가져다 쓸 수 있다.</p>
<hr>
<h2 id="4-web-crawling">4. Web Crawling</h2>
<p>네 번째 노드는 Web Crawling이다.</p>
<p>이 단계에서는 인스타그램 사용자명을 기반으로 게시글 데이터를 수집한다.</p>
<p>n8n에서는 HTTP Request 노드 또는 크롤러 서버 호출 방식을 사용할 수 있는데, 이번 프로젝트에서는 크롤러를 별도로 두고 n8n에서 해당 크롤러 API를 호출하는 방식으로 구성했다.</p>
<p>이 단계가 전체 플로우에서 가장 무거운 작업이었다.</p>
<p>웹 크롤링은 외부 사이트 상태, 네트워크, 브라우저 실행 환경, 서버 메모리 등에 영향을 많이 받는다.
특히 Playwright나 Chromium 기반 크롤러를 사용할 경우 서버 리소스를 많이 사용한다.</p>
<p>그래서 크롤링은 백엔드 요청 흐름 안에서 직접 처리하기보다, n8n 플로우 안에서 별도 단계로 분리하는 것이 더 관리하기 좋았다.</p>
<hr>
<h2 id="5-conversion-json">5. Conversion JSON</h2>
<p>다섯 번째 노드는 Conversion JSON이다.</p>
<p>이 단계에서 크롤링 결과를 백엔드와 AI 분석에서 사용하기 좋은 JSON 형태로 변환한다.</p>
<p>예를 들면 크롤러 결과를 다음과 같은 형태로 정리할 수 있다.</p>
<pre><code class="language-yaml">{
  &quot;analysisJobId&quot;: 1,
  &quot;promotionId&quot;: 10,
  &quot;instagramUsername&quot;: &quot;example_user&quot;,
  &quot;posts&quot;: [
    {
      &quot;caption&quot;: &quot;신곡 발매 안내&quot;,
      &quot;mediaUrl&quot;: &quot;https://...&quot;,
      &quot;permalink&quot;: &quot;https://...&quot;,
      &quot;timestamp&quot;: &quot;2026-05-01T12:00:00&quot;,
      &quot;likeCount&quot;: 120,
      &quot;commentCount&quot;: 15,
      &quot;shareCount&quot;: 4,
      &quot;profileVisitCount&quot;: 20
    }
  ]
}</code></pre>
<p>이 변환 단계를 따로 둔 이유는 역할을 분리하기 위해서다.</p>
<p>크롤러는 데이터를 수집하는 역할만 하고,n8n의 Conversion JSON 노드는 데이터를 사용하기 좋은 형태로 가공하는 역할을 한다.</p>
<p>이렇게 나누면 크롤러 응답 구조가 조금 바뀌더라도, 전체 플로우를 다 수정하지 않고 변환 노드만 수정하면 된다.</p>
<hr>
<h2 id="6-save-crawling-result">6. Save Crawling-result</h2>
<p>여섯 번째 노드는 Save Crawling-result이다.</p>
<p>Conversion JSON에서 정리한 데이터를 백엔드 API로 전달해 DB에 저장한다.</p>
<p>크롤링과 AI 분석을 완전히 하나의 덩어리로 묶지 않고, 중간 결과를 저장하는 구조로 만들었다.
이런 구조로 설계해섯 실패 지점을 더 명확하게 추적할 수 있었다.</p>
<hr>
<h2 id="7-ai-analysis--send-email">7. AI Analysis + Send email</h2>
<p>마지막 노드는 AI Analysis + Send email이다.</p>
<p>이 단계에서는 저장된 크롤링 데이터를 기반으로 AI 분석을 수행하고, 분석 결과를 DB에 저장하고, 분석 결과를 사용자 이메일로 발송한다.</p>
<p>AI 분석에서는 단순히 게시글을 요약하는 것이 아니라, 음악 홍보 관점에서 개선점을 도출하도록 설계했다.</p>
<p>AI에게 요청할 때는 응답 형식을 명확히 지정했다.</p>
<p>이렇게 형식을 정해줘서 백엔드에서 분석 결과를 저장하거나, 이메일 본문에 넣을 때 다루기 쉬워졌다.</p>
<hr>
<h1 id="☁️-사용자-플로우">☁️ 사용자 플로우</h1>
<p>사용자가 진단을 받기 위해서는 홍보를 만들고 그 홍보를 진단받는다.</p>
<h2 id="1-홍보-만들기">1. 홍보 만들기</h2>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/fba0de4e-f2a4-43e2-a660-65217610b2a5/image.png" alt=""></p>
<hr>
<h2 id="2-홍보-진단받기">2. 홍보 진단받기</h2>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/b3ce23fb-18db-4bb6-a675-458e74091536/image.png" alt="">위에 진단 신청하기를 누르면 바로 n8n 플로우로 들어가지 않고 먼저 검증 크롤러가 크롤링한다. 사용자의 인스타 계정이 차단 계정인지, 비공개 계정인지, 실제 있는 계정인지 검증하고 검증된 사용자만 다음 n8n 플로우로 들어올 수 있다.</p>
<hr>
<h2 id="3-메일-받기">3. 메일 받기</h2>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/c3ac23a6-ba8e-465a-a69c-ede1d9919d71/image.png" alt=""></p>
<hr>
<h2 id="4-진단-결과-확인하기">4. 진단 결과 확인하기</h2>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/89695d9d-19b8-43a3-b1ad-6d87b2903ed9/image.png" alt=""></p>
<hr>
<h1 id="☁️-마무리">☁️ 마무리</h1>
<p>이번 프로젝트에서는 n8n을 활용해 인스타그램 웹 크롤링, AI 분석, 이메일 발송까지 이어지는 자동화 플로우를 구축했다.</p>
<p>처음에는 단순히 자동으로 이메일을 보내는 기능이라고 생각했지만, 실제로 구현해보니 중요한 것은 자동화 자체보다 각 단계의 책임을 나누고 실패 지점을 추적할 수 있게 만드는 것이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] GitHub Actions을 통한 CI/CD 자동화]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-GitHub-Actions%EC%9D%84-%ED%86%B5%ED%95%9C-CICD-%EC%9E%90%EB%8F%99%ED%99%94</link>
            <guid>https://velog.io/@viviamm7-code/Spring-GitHub-Actions%EC%9D%84-%ED%86%B5%ED%95%9C-CICD-%EC%9E%90%EB%8F%99%ED%99%94</guid>
            <pubDate>Mon, 18 May 2026 08:32:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<h4 id="ec2--docker-compose-기반-github-actions-cicd-자동배포-플로우">EC2 + Docker Compose 기반 GitHub Actions CI/CD 자동배포 플로우</h4>
</blockquote>
<p>이번 프로젝트에서의 배포 자동화는 develop 브랜치에 PR 시 GitHub Actions가 Docker 이미지를 새로 빌드하고, Docker Hub에 push한 뒤, EC2 서버에서 Docker Compose를 통해 최신 이미지로 재배포하는 구조로 구성해 보았다.</p>
<hr>
<blockquote>
<h2 id="github-actions를-선택한-이유">GitHub Actions를 선택한 이유</h2>
</blockquote>
<h3 id="1-github-pr-흐름과-바로-연결됨">1. GitHub PR 흐름과 바로 연결됨</h3>
<p>우리는 프로젝트에서 이미 GitHub를 통해 코드를 관리하고 있었음.</p>
<pre><code>PR 생성 → CI 테스트
develop merge → Docker 빌드/푸시 → EC2 배포</code></pre><p>이 플로우를 코드 관리하는 환경인 Github에서 그대로 사용할 수 있기에 편할 거 같았다.</p>
<h3 id="2-별도-서버-운영이-필요-없음">2. 별도 서버 운영이 필요 없음</h3>
<p>Jenkins 나 다른 자동화 CI/CD 환경을 비교해 생각해봤을 때 우리의 프로젝트 규모에 맞으려면 서버를 띄우지 않는게 맞다고 판단했다.
GitHub Actions는 GitHub-hosted runner를 쓰면 GitHub가 실행 환경을 제공해줘서, 별도 CI 서버를 직접 운영하지 않아도 됨.</p>
<ul>
<li><a href="https://docs.github.com/ko/actions/concepts/runners/github-hosted-runners?utm_source=chatgpt.com">깃허브 호스팅 Docs</a></li>
</ul>
<h3 id="3-secret-키-관리가-편함">3. Secret 키 관리가 편함</h3>
<p>Docker Hub 계정, EC2 SSH 키, 서버 IP 같은 민감한 값은 GitHub Secrets에 넣고 workflow에서 참조하면 됨.
그래서 코드에 비밀번호나 pem 키를 직접 올리지 않고도 배포 자동화를 구성할 수 있었음.</p>
<ul>
<li><a href="https://docs.github.com/ko/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets?utm_source=chatgpt.com">깃허브 시크릿 키 Docs</a></li>
</ul>
<hr>
<blockquote>
<h2 id="cicd-자동화-환경세팅-중-발생한-이슈들">CI/CD 자동화 환경세팅 중 발생한 이슈들</h2>
</blockquote>
<h3 id="1-docker-hub-이미지-이슈">1. Docker Hub 이미지 이슈</h3>
<p>처음 docker compose pull을 했을 때 not found 에러가 발생했다.
docker-compose.yml에 image: jinsungzz/hoppin만 적혀 있어서 기본 태그가 latest로 해석됐다. 
그리고 Docker Hub에도 아직 이미지가 없었다.</p>
<h4 id="해결-방안">해결 방안</h4>
<ul>
<li>docker hub에 이미지 태그를 staging으로 통일 시켜 hoppin repository를 생성했다.<pre><code class="language-js">image: jinsungzz/hoppin:staging</code></pre>
</li>
</ul>
<h3 id="2-dockerfile-이슈">2. Dockerfile 이슈</h3>
<p>먼저 프로젝트 루트에 Dockerfile을 만들었다.</p>
<pre><code class="language-dockerfile">FROM eclipse-temurin:17-jdk

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</code></pre>
<p>처음 빌드할 때 아래 에러가 발생했다.</p>
<pre><code class="language-dockerfile">COPY build/libs/*.jar app.jar
lstat /build/libs: no such file or directory</code></pre>
<h4 id="해결-방안-1">해결 방안</h4>
<ul>
<li>Dockerfile은 build/libs/*.jar가 있다고 가정한다. 
그런데 ./gradlew bootJar를 먼저 안 해서 jar가 없었음<pre><code class="language-java">./gradlew clean bootJar
docker build -t jinsungzz/hoppin:staging .</code></pre>
jar 파일 생성 후 빌드하도록 수정했다.</li>
</ul>
<h3 id="3-mac-아키텍쳐-이슈">3. Mac 아키텍쳐 이슈</h3>
<p>로컬에서 이미지를 push한 뒤 EC2에서 이미지를 pull 했더니 아래 에러가 발생했다.</p>
<pre><code class="language-java">no matching manifest for linux/amd64</code></pre>
<h4 id="해결-방안-2">해결 방안</h4>
<p>나는 맥북 M2 아키텍쳐 모델을 사용 중이다. </p>
<ul>
<li>M2의 기본 빌드는 amd64</li>
<li>에러를 보면 EC2의 빌드는 linux/amd64</li>
<li><blockquote>
<p><strong>M2와 EC2의 빌드를 통일해 주어야 한다.</strong></p>
</blockquote>
<pre><code class="language-bash">docker buildx build --platform linux/amd64 -t jinsungzz/hoppin:staging --push .</code></pre>
이 코드를 통한 빌드로 로컬에서 push 할 때 이미지를 linux/amd64 아키텍쳐로 수정 후 빌드하게헀다.</li>
</ul>
<h3 id="4-github-secrets-이슈">4. Github Secrets 이슈</h3>
<p>나는 분명 Secrets 키 값을 입력했는데 Actions 로그인했는데 아래 에러가 발생했다.</p>
<pre><code class="language-java">Must provide --username with --password-stdin
Run echo &quot;&quot; | docker login -u &quot;&quot; --password-stdin</code></pre>
<p>시크릿 값이 비어있다는 에러이다.
그래서 다시 시크릿 값을 잘못 넣었나? 생각해 계속 수정해서 넣어도 똑같은 에러를 받았다.
원초적으로 값이 비어있다는 것에 관점을 두고 생각해봤다.</p>
<ul>
<li>원래의 입력은 아래처럼 하나의 시크릿에 모든 환경변수를 다 넣었다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/f9825f89-1735-47e6-9b7a-0f6ce69cabf3/image.png" alt=""></li>
</ul>
<h4 id="해결-방안-3">해결 방안</h4>
<p>Name과 Secret에 1:1로 환경변수를 적용해 주었다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/7aa36489-c18a-44e4-96c2-f2d621d46c03/image.png" alt="">
위의 STAGING_SSH_KEY 값에는 .pem의 키 값을 넣어줘야되는데</p>
<pre><code class="language-java">-----BEGIN OPENSSH PRIVATE KEY-----
중간 키 내용 전체
-----END OPENSSH PRIVATE KEY-----</code></pre>
<p>이런 형태로 되어 있다.
이때 BEGIN / END 내용까지 모두 시크릿 값에 넣어줘야 한다!!!!!</p>
<hr>
<blockquote>
<h2 id="ci-scripts">CI scripts</h2>
</blockquote>
<h4 id="1-언제-실행되는가-">1. 언제 실행되는가 ?</h4>
<p>pr 시 브랜치가 main, develop인지 확인하기 (pr 브랜치 기준으로 실행)</p>
<pre><code class="language-yaml">on:
  pull_request:
    branches: [ develop, main ]</code></pre>
<h4 id="2-실행환경">2. 실행환경</h4>
<p>Github가 제공하는 ubuntu 서버에서 CI가 실행됨</p>
<pre><code class="language-yaml">runs-on: ubuntu-latest</code></pre>
<h4 id="3-코드-가져오기">3. 코드 가져오기</h4>
<p>PR에 올라온 코드를 GitHub Actions 실행 환경으로 가져옴</p>
<pre><code class="language-yaml">- name: Checkout
  uses: actions/checkout@v4</code></pre>
<h4 id="4-파일-구조-확인">4. 파일 구조 확인</h4>
<p>파일이 실제로 있는지 디버깅</p>
<pre><code class="language-yaml">- name: Show files
  run: find . -maxdepth 3 -type f | sort</code></pre>
<h4 id="5-jdk-17-세팅">5. JDK 17 세팅</h4>
<p> Spring Boot 빌드에 필요한 Java 17을 설치함</p>
<pre><code class="language-yaml"> - name: Set up JDK 17
   uses: actions/setup-java@v4
   with:
      distribution: temurin
      java-version: 17</code></pre>
<h4 id="6-gradlew-실행-권한-부여">6. gradlew 실행 권한 부여</h4>
<p> 리눅스 환경에서는 gradlew에 실행 권한이 없으면 실행이 안 돼서 ./gradlew 실행 전에 권한을 부여했음</p>
<pre><code class="language-yaml"> - name: Grant execute permission
   run: chmod +x ./gradlew</code></pre>
<h4 id="7-테스트-실행">7. 테스트 실행</h4>
<pre><code class="language-yaml"> - name: Run tests
   run: ./gradlew clean test</code></pre>
<h4 id="8-빌드-확인">8. 빌드 확인</h4>
<p>7번에 테스트를 진행했기 때문에 -x test로 테스트를 제외하고 빌드만 확인함</p>
<pre><code class="language-yaml">- name: Build check
  run: ./gradlew build -x test</code></pre>
<hr>
<blockquote>
<h2 id="ci-코드">CI 코드</h2>
</blockquote>
<pre><code class="language-yaml">name: PR to Develop CI

on:
  pull_request:
    branches: [ develop, main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Show files
        run: find . -maxdepth 3 -type f | sort

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Grant execute permission
        run: chmod +x ./gradlew

      - name: Run tests
        run: ./gradlew clean test

      - name: Build check
        run: ./gradlew build -x test</code></pre>
<hr>
<blockquote>
<h2 id="cd-scripts">CD scripts</h2>
</blockquote>
<h4 id="1-언제-실행되는가">1. 언제 실행되는가?</h4>
<p>develop 브랜치에 push가 발생하면 실행됨
즉 PR이 merge돼 develop이 업데이트되면 자동 배포가 시작됨</p>
<pre><code class="language-yaml">on:
  push:
    branches:
      - develop</code></pre>
<h4 id="2-실행환경-1">2. 실행환경</h4>
<p>GitHub가 제공하는 Ubuntu runner에서 실행됨
GitHub Actions 서버에서 빌드하고 이미지를 push함</p>
<pre><code class="language-yaml">runs-on: ubuntu-latest</code></pre>
<h4 id="3-코드가져오고-jdk-17-세팅">3. 코드가져오고 JDK 17 세팅</h4>
<p>현재 develop 브랜치의 코드를 Actions runner로 가져와 스프링 부트를 실행하기 위해 JDK 17을 세팅함</p>
<pre><code class="language-yaml">- name: Checkout
  uses: actions/checkout@v4

- name: Set up JDK
  uses: actions/setup-java@v4</code></pre>
<h4 id="4-gradlew-실행-권한-부여-후-jar-빌드검증">4. gradlew 실행 권한 부여 후 jar 빌드/검증</h4>
<p>Linux 환경에서 ./gradlew를 실행할 수 있도록 권한을 부여하고 Spring Boot 실행 jar 파일을 만들고 검증함
검증 파일은 개발한 파일 넣으면 됨</p>
<pre><code class="language-yaml">- name: Grant execute permission
  run: chmod +x ./gradlew

- name: Build jar
  run: ./gradlew clean bootJar -x test

- name: Verify jar contents
  run: |
      jar tf build/libs/*.jar | grep BOOT-INF/classes/application
      jar tf build/libs/*.jar | grep -E &quot;AiController|AuthController|MeController&quot;</code></pre>
<h4 id="5-docker-hub-로그인-및-docker-buildx-세팅">5. Docker Hub 로그인 및 Docker Buildx 세팅</h4>
<p>도커 허브 로그인 후 환경을 맞추기위해 어느 환경에서도 빌드해도 상관없는 Buildx 로 세팅</p>
<pre><code class="language-yaml"> - name: Docker login
   run: echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin

 - name: Set up Docker Buildx
   uses: docker/setup-buildx-action@v3</code></pre>
<h4 id="6-docker-이미지-빌드-후-push--이미지-내부-검증">6. Docker 이미지 빌드 후 push + 이미지 내부 검증</h4>
<p>검증 시 개발한 파일을 넣으면 되는데 일단 임시로 넣어둠</p>
<pre><code class="language-yaml">docker buildx build \
  --no-cache \
  --platform linux/amd64 \
  -t jinsungzz/hoppin:staging \
  --push .

- name: Verify docker image contents
  run: |
      docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c &#39;jar tf /app.jar | grep BOOT-INF/classes/application&#39;
      docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c &#39;jar tf /app.jar | grep -E &quot;AiController|AuthController|MeController&quot;&#39;</code></pre>
<ul>
<li>--no-cache : Docker 캐시 사용 안 함</li>
<li>--platform linux/amd64 : EC2에서 실행 가능한 amd64 이미지로 빌드</li>
<li>-t jinsungzz/hoppin:staging : 이미지 이름과 태그 지정</li>
<li>--push : 빌드 후 Docker Hub에 바로 push</li>
<li>. : 현재 프로젝트 루트의 Dockerfile 사용</li>
</ul>
<h4 id="7-ec2-배포에-사용되는-명령어">7. EC2 배포에 사용되는 명령어</h4>
<pre><code class="language-yaml">- name: Deploy to staging EC2
  uses: appleboy/ssh-action@v1.0.3</code></pre>
<p>GitHub Actions가 EC2에 SSH로 접속해서 배포 명령을 실행함
접속 정보는 Secrets에서 가져옴</p>
<pre><code class="language-yaml">script: |
    echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin
    cd /home/ubuntu/app</code></pre>
<p>EC2에서도 Docker Hub에서 private/public 이미지를 pull할 수 있도록 로그인하고, 배포 디렉토리로 이동</p>
<pre><code class="language-bash">docker compose down
docker rm -f hoppin 2&gt;/dev/null || true
docker image rm jinsungzz/hoppin:staging 2&gt;/dev/null || true
docker system prune -af</code></pre>
<p>이걸 넣은 이유는 배포했는데 이전 이미지/컨테이너가 계속 남아서 안 바뀌는 문제를 막기 위해 이전 컨테이너와 이미지를 정리하는 것임</p>
<pre><code class="language-bash">docker network inspect hoppin-net &gt;/dev/null 2&gt;&amp;1 || docker network create hoppin-net

docker pull jinsungzz/hoppin:staging

docker compose up -d --force-recreate
</code></pre>
<p>네트워크 확인/생성 + 최신 이미지 pull + Docker Compose 재기동</p>
<hr>
<blockquote>
<h2 id="cd-코드">CD 코드</h2>
</blockquote>
<pre><code class="language-yaml">name: Deploy Staging

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Grant execute permission
        run: chmod +x ./gradlew

      - name: Build jar
        run: ./gradlew clean bootJar -x test

      - name: Verify jar contents
        run: |
          jar tf build/libs/*.jar | grep BOOT-INF/classes/application
          jar tf build/libs/*.jar | grep -E &quot;AiController|AuthController|MeController&quot;

      - name: Docker login
        run: echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and Push Docker image
        run: |
          docker buildx build \
            --no-cache \
            --platform linux/amd64 \
            -t jinsungzz/hoppin:staging \
            --push .

      - name: Verify docker image contents
        run: |
          docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c &#39;jar tf /app.jar | grep BOOT-INF/classes/application&#39;
          docker run --rm --entrypoint sh jinsungzz/hoppin:staging -c &#39;jar tf /app.jar | grep -E &quot;AiController|AuthController|MeController&quot;&#39;

      - name: Deploy to staging EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.STAGING_USER }}
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            echo &quot;${{ secrets.DOCKER_PASSWORD }}&quot; | docker login -u &quot;${{ secrets.DOCKER_USERNAME }}&quot; --password-stdin
            cd /home/ubuntu/app

            docker compose down
            docker rm -f hoppin 2&gt;/dev/null || true
            docker image rm jinsungzz/hoppin:staging 2&gt;/dev/null || true
            docker system prune -af

            docker network inspect hoppin-net &gt;/dev/null 2&gt;&amp;1 || docker network create hoppin-net

            docker pull jinsungzz/hoppin:staging
            docker compose up -d --force-recreate</code></pre>
<hr>
<blockquote>
<h3 id="위-workflow는-자동-배포-파이프라인이다">위 workflow는 자동 배포 파이프라인이다</h3>
</blockquote>
<p>develop 업데이트
→ jar 빌드
→ Docker 이미지 빌드
→ Docker Hub push
→ EC2 pull
→ Docker Compose 재실행</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 배포환경에서 소셜로그인 구현 (Kakao/Google/Naver)]]></title>
            <link>https://velog.io/@viviamm7-code/Spring-%EB%B0%B0%ED%8F%AC%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-KakaoGoogleNaver</link>
            <guid>https://velog.io/@viviamm7-code/Spring-%EB%B0%B0%ED%8F%AC%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-KakaoGoogleNaver</guid>
            <pubDate>Mon, 27 Apr 2026 07:21:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/be374ccf-8e15-456c-8433-cd342d318c93/image.png" alt=""></p>
<blockquote>
<h2 id="배포-환경에서의-소셜-로그인">배포 환경에서의 소셜 로그인</h2>
</blockquote>
<p>로컬에서는 localhost 기준으로 Redirect URI를 등록하면 소셜 로그인을 테스트할 수 있다.
하지만 실제 배포 환경 소셜로그인을 사용하기 위해서 각 플랫폼마다 요구하는 조건이 있다.
이번 프로젝트에서는 네이버, 카카오, 구글 로그인을 붙이면서 그 조건들의 차이를 확인했다.</p>
<blockquote>
<h3 id="먼저-spring-security의-oauth2를-사용해-리다이렉트콜백-url을-baseurlloginoauth2coderegistrationid-로-설정했다">먼저 Spring Security의 Oauth2를 사용해 리다이렉트(콜백) URL을 {BaseURL}/login/oauth2/code/{registrationId} 로 설정했다.</h3>
</blockquote>
<h3 id="인증검증은-jwt-accesstoken사용-중이다">인증/검증은 JWT accessToken사용 중이다.</h3>
<h2 id="1-naver">1. Naver</h2>
<p><a href="https://developers.naver.com/main/">- Naver Developers 사이트</a></p>
<p>위 사이트에 들어가서 먼저 Application을 등록해야 한다.
등록하면 주는 클라이언트 ID, 시크릿 키는 API 호출 시 환경변수 등록을 위해 저장해 준다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/f2ec3982-84e1-4dd3-96c4-a5d39f240178/image.png" alt="">
등록 후 Redirect URL이 가장 중요한데 나는 로컬 테스트용 하나 배포용 하나 이렇게 등록했다.
도메인까지 입력 후 뒤에 /login/oauth2/code/naver을 추가해준다.</p>
<p>네이버는 배포환경에서 소셜로그인을 사용하기 위해서는 검수 요청을 받아야한다.
웹 사이트의 로그인이 완료되기까지의 플로우를 캡쳐해서 네이버 개발자 센터에 검수 받으면 된다.</p>
<p><a href="https://developers.naver.com/docs/login/verify/verify.md">네이버 개발자 검수 요청 가이드
</a></p>
<h2 id="2-google">2. Google</h2>
<p><a href="https://console.cloud.google.com/">- Google Cloud 사이트
</a>
위 사이트 들어가서 새 프로젝트를 생성한다.
OAuth 2.0 클라이언트 ID를 만들어야되는데 API 및 서비스 -&gt; 사용자 인증 정보 들어가서 만들면 된다.
여기서도 구글 클라이언트 아이디랑 시크릿 키 주니까 잘 저장해 준다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/5a6d9891-88f4-4346-8dc3-26758b59cfd0/image.png" alt="">
여기서도 Redirect URL 가장 중요한데 구글에서는 특히나 중요하다.
구글은 도메인을 ip주소로 입력하면 리다이렉션을 허용해주지 않는다.</p>
<p>예를 들어 EC2로 배포 시  x.x.x.x:8080 이라는 도메인을 주는데 이렇게 입력하면 안되고 도메인 주소를 구매 후 리다이렉션 창에 입력해야 승인해준다.</p>
<p>우리는 가비아에서 도메인 하나를 구매 후 입력했다. (로컬용 하나, 배포용 하나)
마찬가지로 도메인 + /login/oauth2/code/google</p>
<h2 id="3-kakao">3. Kakao</h2>
<p><a href="https://developers.kakao.com/">- 카카오 개발자 사이트</a>
카카오가 소셜로그인 중에서는 가장 간단하다.
얘는 위 사이트 들어가서 앱 만들고 비즈앱 등록하면 사실상 끝이다.</p>
<p>먼저 앱을 만들고 비즈앱을 등록한다.
그리고 앱 -&gt; 플랫폼 키를 들어가서 클라이언트 아이디와 시크릿 키를 저장한다.
<img src="https://velog.velcdn.com/images/viviamm7-code/post/9455981a-7ef0-47d8-a45c-504e860cde67/image.png" alt=""></p>
<p>여기도 리다이렉트 URL이 가장 중요하다.
도메인 + /login/oauth2/code/kakao 추가해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/viviamm7-code/post/437f9c98-73cc-4b33-bdcf-8e4e00f90995/image.png" alt="">카카오는 또 하나 필요한게 카카오 로그인 사용 설정을 ON으로 바꿔줘야 한다.</p>
<hr>
<blockquote>
<h2 id="소셜로그인-구현-플로우">소셜로그인 구현 플로우</h2>
</blockquote>
<ol>
<li>프론트 로그인 버튼 클릭
↓</li>
<li>프론트가 백엔드 로그인 시작 URL로 이동
<a href="https://api.musicpeak.site/oauth2/authorization/%7BregistrationId%7D">https://api.musicpeak.site/oauth2/authorization/{registrationId}</a>
↓</li>
<li>Spring Security가 소셜 로그인 페이지로 redirect
↓</li>
<li>사용자가 플랫폼에서 로그인/동의
↓</li>
<li>플랫폼이 백엔드 callback URL로 code 전달
<a href="https://api.musicpeak.site/login/oauth2/code/%7BregistrationId%7D">https://api.musicpeak.site/login/oauth2/code/{registrationId}</a>
↓</li>
<li>Spring Security가 code로 플랫폼 access token 요청
↓</li>
<li>Spring Security가 플랫폼 사용자 정보 조회
↓</li>
<li>CustomOAuth2UserService 실행<ul>
<li>기존 회원인지 확인</li>
<li>없으면 회원가입</li>
<li>musicianId를 OAuth2User attributes에 넣음
↓</li>
</ul>
</li>
<li>OAuth2SuccessHandler 실행<ul>
<li>musicianId 꺼냄</li>
<li>JWT accessToken/refreshToken 생성</li>
<li>쿠키에 저장
↓</li>
</ul>
</li>
<li>백엔드가 프론트 성공 페이지로 redirect
<a href="https://www.musicpeak.site/auth/success">https://www.musicpeak.site/auth/success</a>
↓</li>
<li>프론트 AuthSuccess 페이지에서 /api/me 호출
credentials: &quot;include&quot;
↓</li>
<li>백엔드 JwtAuthenticationFilter가 쿠키의 accessToken 검증
↓</li>
<li>/api/me 성공 응답
↓</li>
<li>프론트가 메인페이지로 이동
<a href="https://www.musicpeak.site">https://www.musicpeak.site</a></li>
</ol>
]]></description>
        </item>
    </channel>
</rss>