<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Make good code</title>
        <link>https://velog.io/</link>
        <description>개발자가 되는 그날까지</description>
        <lastBuildDate>Tue, 11 Mar 2025 13:42:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Make good code</title>
            <url>https://velog.velcdn.com/images/yoon_bly/profile/e170e8da-df45-4fde-a28a-b68fc760ca9c/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Make good code. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yoon_bly" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[정보처리기사 실기 코딩 문제 정리]]></title>
            <link>https://velog.io/@yoon_bly/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B0-%EC%BD%94%EB%94%A9-%EB%AC%B8%EC%A0%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yoon_bly/%EC%A0%95%EB%B3%B4%EC%B2%98%EB%A6%AC%EA%B8%B0%EC%82%AC-%EC%8B%A4%EA%B8%B0-%EC%BD%94%EB%94%A9-%EB%AC%B8%EC%A0%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 11 Mar 2025 13:42:59 GMT</pubDate>
            <description><![CDATA[<h1 id="c언어">C언어</h1>
<hr>
<h2 id="1-sizeof">1. sizeof()</h2>
<ul>
<li>자료형의 byte 크기를 나타냄<pre><code class="language-C">#include &lt;stdio.h&gt;
</code></pre>
</li>
</ul>
<p>int main()
{
    int num1 = 0;
    int size;</p>
<pre><code>size = sizeof num1;    // 변수 num1의 자료형 크기를 구함

printf(&quot;num1의 크기: %d\n&quot;, size);

return 0;</code></pre><p>}</p>
<p>// 출력
// num1의 크기: 4</p>
<pre><code>## 2. switch case 문
```C
#define _CRT_SECURE_NO_WARNINGS    // scanf 보안 경고로 인한 컴파일 에러 방지
#include &lt;stdio.h&gt;

int main()
{
    int num1;

    scanf(&quot;%d&quot;, &amp;num1);    // 값을 입력받음

    // switch의 case에서 break 삭제
    switch (num1)
    {
    case 1:    // 1일 때는 아래 case 2, default가 모두 실행됨
        printf(&quot;1입니다.\n&quot;);
    case 2:    // 2일 때는 아래 default까지 실행됨
        printf(&quot;2입니다.\n&quot;);
    default:
        printf(&quot;default\n&quot;);
    }

    return 0;
}</code></pre><ul>
<li>switch 문의 case에 break가 없다면, <strong>밑의 case문들과 deafult까지</strong> 출력하게 된다. (fall through)</li>
</ul>
<h2 id="isdigit">isdigit()</h2>
<p>숫자인지 아닌지 판단하는 함수</p>
<pre><code class="language-C">#include &lt;stdio.h&gt;
#include &lt;ctype.h&gt;

int main()
{
    char arr[13] = &quot;ABCDEF123456&quot;;
    printf(&quot;arr : %s \n\n&quot;, arr);

    for (int i = 0; i &lt; 12; i++)
    {
        printf(&quot;arr[%d] = %c    isdigit: %d \n&quot;,i,arr[i], isdigit(arr[i]));
    }
    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/ffa667f7-ed85-4128-95b4-052532c2d53d/image.png" alt=""></p>
<h2 id="포인터">포인터</h2>
<h1 id="java">Java</h1>
<hr>
<h2 id="1-extends---상속">1. extends -&gt; 상속</h2>
<p>ex)</p>
<pre><code class="language-java">class SuperObject {
    public void paint(){
        draw();
    }

    public void draw(){
        draw();
        System.out.println(&quot;Super Object&quot;);
    }
}


class SubObject extends SuperObject {
    public void paint(){
        super.draw();
    }

    public void draw(){
        System.out.println(&quot;Sub Object&quot;);
    }
}


public class Test{
    public static void main(String[] args){
        SuperObject a = new SubObject();
        a.paint();
    } 
}</code></pre>
<blockquote>
<p>클래스 SubObject를 정의하고 부모 클래스로 SuperObject를 지정하면 SuperObject에 속한 변수와 메서드를 상속받습니다.</p>
</blockquote>
<blockquote>
<p>자식 클래스 생성자로 인스턴스를 생성할 때 자료형을 부모 클래스로 지정하면 생성된 인스턴스는 부모 클래스로 묵시적 클래스 형 변환이 됩니다. 부모와 자식 클래스간 같은 메서드가 존재하면 호출되는 메서드는 생성된 인스턴스에 따라 결정됩니다.</p>
</blockquote>
<blockquote>
<p>a.paint()는 클래스 형 변환을 수행하였고 print()메서드가 자식 클래스에서 재정의를 통해 오버라이딩 된 메서드이므로 자식 클래스의 paint 메서드가 수행됩니다.</p>
</blockquote>
<blockquote>
<p>부모 클래스를 호출하는 super를 사용했으므로 부모 클래스의 draw() 메서드를 수행합니다.</p>
</blockquote>
<blockquote>
<p>부모 클래스 draw()에서 처음에 클래스 형 변환을 수행하였고 draw()메서드가 자식 클래스에서 재정의를 통해 오버라이딩 된 메서드이므로 자식 클래스의 draw()메서드를 수행합니다.</p>
</blockquote>
<blockquote>
<p>자식 메서드 draw를 수행하면서 &#39;Sub Object가 수행되고&#39; 다시 부모 draw()로 돌아가 나머지 &#39;Super Object&#39;를 수행합니다.</p>
</blockquote>
<hr>
<ul>
<li>부모클래스 변수명 = New 자식생성자<ul>
<li>```java
SuperObject a = new SubObject();</li>
<li>부모 클래스 객체 생성. but, 자식객체의 생성자 사용<ul>
<li>부모 클래스와 자식 클래스에 동일한 속성이나 메소드가 있다면, 자식 클래스로 재정의</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2--와-equals">2. == 와 .equals()</h2>
<ul>
<li>== 은 객체의 참조(주소)를 비교한다.</li>
<li>.equals()는 말그대로 해당 참조에 있는 데이터를 비교함</li>
</ul>
<pre><code class="language-java">
public class Main {
    public static void main(String[] args) {
        String str1 = &quot;Programming&quot;;
        String str2 = &quot;Programming&quot;;
        String str3 = new String(&quot;Programming&quot;);

        System.out.println(str1 == str2);      // ①
        System.out.println(str1 == str3);      // ②
        System.out.println(str1.equals(str3)); // ③
        System.out.print(str2.equals(str3));   // ④
    }
}

//결과
true
false
true
true
</code></pre>
<p>1️⃣ String str1 = &quot;Programming&quot;;</p>
<p>문자열 리터럴 &quot;Programming&quot;을 <strong>String Constant Pool(문자열 상수 풀)</strong>에 저장.
str1이 &quot;Programming&quot;을 가리킴.</p>
<p>2️⃣ String str2 = &quot;Programming&quot;;</p>
<p>동일한 문자열 &quot;Programming&quot;이 이미 String Pool에 존재.
str2는 str1과 동일한 &quot;Programming&quot;을 참조.</p>
<p>3️⃣ String str3 = new String(&quot;Programming&quot;);</p>
<p>new String(&quot;Programming&quot;)은 Heap 영역에 새로운 문자열 객체를 생성.
str3은 String Pool의 &quot;Programming&quot;을 참조하지 않고, 새로 생성된 객체를 참조</p>
<p><strong>해설</strong>
따라서 str1, str2는 동일한 Programming을 참조 하지만, 생성자를 통한 str3은 힙영역에 저장되기 때문에 str1, str2와 다른 주소를 갖고 있다.</p>
<h1 id="python">Python</h1>
<hr>
<h2 id="리스트와-세트">리스트와 세트</h2>
<p>리스트 생성 방법</p>
<pre><code class="language-python">a = [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]</code></pre>
<p>세트 생성 방법</p>
<pre><code class="language-python">a = {&#39;a&#39;, &#39;b&#39;, &#39;c&#39;}</code></pre>
<p>리스트 </p>
<ul>
<li>순서가 있음</li>
<li>중복된 데이터가 있을 수 있음</li>
<li>여러 자료형을 저장할 수 있음 
ex)  <code>a = [3, 1.234, &#39;ABC&#39;]</code></li>
<li>선언 시, 크기를 적지 않아도 됨</li>
</ul>
<p>세트</p>
<ul>
<li>순서가 정해져있지 않음</li>
<li>중복된 데이터는 저장되지 않음</li>
</ul>
<p>리스트 관련 주요 메서드</p>
<ul>
<li>apppend(값)<ul>
<li>리스트에 값을 추가한다.</li>
</ul>
</li>
</ul>
<p>Ex)</p>
<pre><code class="language-python">[1, 2, 3].append(4) -&gt; [1, 2, 3, 4]</code></pre>
<ul>
<li>pop(위치)<ul>
<li>리스트의 &#39;위치&#39;에 있는 값을 출력하고 해당 요소를 삭제한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">[10, 11, 12].pop(1) -&gt; 11 출력 -&gt; [10, 12]</code></pre>
<ul>
<li>index(값)<ul>
<li>리스트에서 &#39;값&#39;이 저장된 요소의 위치를  반환한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">[10, 11, 12].index(2) -&gt; 2</code></pre>
<ul>
<li>count(값)<ul>
<li>리스트에서 &#39;값&#39;이 저장되어 있는 요소들의  개수를 반환한다.</li>
</ul>
</li>
</ul>
<p>ex) </p>
<pre><code class="language-python">[1, 0, 1, 1, 0].count(1) -&gt; 3</code></pre>
<ul>
<li>extend(리스트)<ul>
<li>리스트의 끝에 새로운 리스트를 추가하여 확장한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">[&#39;a&#39;, &#39;b&#39;].extend([&#39;c&#39;, &#39;d&#39;]) -&gt; [&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;]</code></pre>
<ul>
<li>reverse()<ul>
<li>리스트의 순서를 역순으로 뒤집는다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">[10, 11, 12].reverse() -&gt; [12, 11, 10]</code></pre>
<ul>
<li>sort()<ul>
<li>리스트를 정렬하며, 기본값은 오름차순이다.</li>
</ul>
</li>
</ul>
<p>ex) </p>
<pre><code class="language-python">   [2, 1, 3].sort() -&gt; [1, 2, 3] </code></pre>
<pre><code class="language-python">   [2, 1, 3].sort(reverse = True) -&gt; [3, 2, 1]</code></pre>
<ul>
<li>copy()<ul>
<li>리스트를 복사한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">a = [1,2,3]
b = a.copy()</code></pre>
<p>세트 관련 주요 메서드</p>
<ul>
<li>pop()<ul>
<li>세트의 값을 출력하고 요소를 삭제한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">{10, 11, 12}.pop() -&gt; 10 출력 -&gt; {11, 12}</code></pre>
<ul>
<li>add(값)<ul>
<li>세트에 값을 추가한다.</li>
</ul>
</li>
</ul>
<p>ex) </p>
<pre><code class="language-python">{10, 11, 12}.add(13) -&gt; {10, 11, 12, 13}</code></pre>
<ul>
<li>update(세트)<ul>
<li>세트에 새로운 &#39;세트&#39;를 추가하여 확장한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-python">{&#39;a&#39;, &#39;b&#39;, &#39;c&#39;}.update({&#39;c&#39;, &#39;d&#39;}) -&gt; {&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;}</code></pre>
<ul>
<li>remove(값)<ul>
<li>세트에서 &#39;값&#39;을 찾아 해당 요소를 삭제한다.</li>
</ul>
</li>
</ul>
<p>ex)</p>
<pre><code class="language-pyhton">{10, 11, 12}.remove(11) -&gt; {10, 12}</code></pre>
<h2 id="튜플">튜플</h2>
<p>튜플 생성 방법</p>
<pre><code class="language-python">tuple1 = (1, &#39;b&#39;, &#39;c&#39;)
tuple2 = tuple([&#39;a&#39;, &#39;b&#39;]) </code></pre>
<p>튜플</p>
<ul>
<li>순서가 있음</li>
<li>중복된 데이터가 있을 수 있음</li>
<li>여러 자료형을 저장할 수 있음</li>
<li><strong>리스트와는 다르게 수정 및 삭제 불가능</strong></li>
<li>값이 한개만 들어갈 경우 값을 넣고 콤마(,)를 넣어줘야 함.</li>
</ul>
<p>ex) </p>
<pre><code class="language-python">tuple = (1,)</code></pre>
<h2 id="range">Range</h2>
<ul>
<li>Range는 연속된 숫자를 생성하는 것으로, 리스트나 반복문에서 많이 사용된다.</li>
</ul>
<h3 id="형식">&lt;형식&gt;</h3>
<p><code>range(최종값)</code> </p>
<ul>
<li>0에서 최종값 - 1까지 연속된 숫자를 생성한다.</li>
</ul>
<p><code>range(초기값, 최종값)</code></p>
<ul>
<li>초기값에서 최종값 - 1까지 연속된 숫자를 생성한다.</li>
</ul>
<p><code>range(초기값, 최종값, 증가값)</code></p>
<ul>
<li>초기값에서 최종값 - 1까지 증가값만큼 증가하면서 숫자를 생성한다.</li>
<li>증가값이 음수인 경우 초기값에서 <strong>최종값 + 1</strong>까지 증가값 만큼 감소하면서 숫자를 생성한다.</li>
</ul>
<h2 id="slice">Slice</h2>
<ul>
<li>Slice는 문자열이나 리스트와 같은 순차형 객체에서 일부를 잘라 반환하는 기능이다.</li>
</ul>
<p><code>객체명[초기위치:최종위치]</code></p>
<ul>
<li>초기위치에서 최종위치 - 1까지의 요소들을 가져온다.</li>
</ul>
<p><code>객체명[초기위치:최종위치:증가값]</code></p>
<ul>
<li>초기위치에서 최종위치 - 1까지 증가값 만큼 증가하면서 해당 위치의 요소들을 가져온다.</li>
<li>증가값이 음수인 경우 초기위치에서 <strong>최종위치 + 1</strong>까지 증가값만큼 감소하면서 해당 위치의 요소들을 가져온다.</li>
</ul>
<p><code>객체명[:] 또는 객체명[::]</code></p>
<ul>
<li>객체의 모든 요소를 반환한다.</li>
</ul>
<p><code>객체명[초기위치:]</code></p>
<ul>
<li>객체의 초기위치에서 마지막 위치까지의 요소들을 반환한다.</li>
</ul>
<p><code>객체명[:최종위치]</code></p>
<ul>
<li>객체의 0번째 위치에서 최종위치 - 1까지의 요소들을 반환한다.</li>
</ul>
<p><code>객체명[::증가값]</code></p>
<ul>
<li>객체의 0번째 위치에서 마지막 위치까지 증가값만큼 증가하면서 해당 위치의 요소들을 반환한다.</li>
</ul>
<h2 id="대문자-변환upper-capitalize">대문자 변환(upper, capitalize..)</h2>
<ul>
<li>upper() : 모든알파벳을 대문자로 변환</li>
<li>capitalize() : 맨 첫글자만 대문자로 변환 </li>
<li>title() : 알파벳 외의 문자(숫자, 특수기호, 띄어쓰기 등)로 나누어져 있는 영단어들의 첫 글자를 모두 대문자로</li>
</ul>
<pre><code class="language-python">A=&#39;abcd&#39;
print(A.upper()) #ABCD
print(A.capitalize()) #Abcd
print(A.title()) #Abcd

B=&#39;a2b3c4&#39;
print(B.upper()) #A2B3C4
print(B.capitalize()) #A2b3c4
print(B.title()) #A2B3C4

C=&quot;abc-def efg&quot;
print(C.upper()) #ABC-DEF EFG
print(C.capitalize()) #Abc-def efg
print(C.title()) #Abc-Def Efg</code></pre>
<h2 id="find">find()</h2>
<ul>
<li>string.find(찾을 문자)<pre><code class="language-python">str = &quot;findletter&quot;
print(str.find(&quot;d&quot;))
#
#결과
3 </code></pre>
</li>
<li>string.find(찾을 문자, 시작 Index)</li>
<li>string.find(찾을 문자, 시작 Index, 끝 Index)</li>
</ul>
<p>find 함수 첫번째 인자- 찾을 문자열 혹은 찾을 문자</p>
<p>find 함수 두번째 인자 (생략가능)- 문자를 찾을때 어디서 부터 찾을지 시작 index. 생략시 0</p>
<p>find 함수 세번째인자 (생략가능)- 문자를 찾을때 어디 까지 찾을지 끝 index, 생략시 문자열 맨 마지막 index</p>
<p><strong>만약 찾는 단어가 없다면 -1 출력</strong></p>
<pre><code class="language-python">str = &quot;findletter&quot;
print(str.find(&quot;s&quot;))
#
#결과
-1</code></pre>
<p><strong>중복된 단어가 있다면 가장 첫번째 인덱스를 출력</strong></p>
<pre><code class="language-python">str = &quot;findletter&quot;
print(str.find(&quot;t&quot;))
#
#결과
6 # 7 인덱스에도 t가 있으나 6만 확인 가능</code></pre>
<h2 id="in">in</h2>
<ul>
<li>문자열 내에 찾고자 하는 문자열이 있는지 여부를 boolean값으로 반환</li>
</ul>
<pre><code class="language-python">str = &quot;findletter&quot;
print(&quot;d&quot; in str)
print(&quot;s&quot; in str)
#
#결과
True
False</code></pre>
<hr>
<h1 id="아스키코드">아스키코드</h1>
<p>&#39;A&#39; = 65
&#39;a&#39; = 97</p>
<h1 id="16진수-변환">16진수 변환</h1>
<p>0
1
2
3
4
5
6
7
8
9
A = 10
B = 11
C = 12
D = 13
E = 14
F = 15</p>
<p>알아두기 혹시 모르니</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Heap Stack 차이점]]></title>
            <link>https://velog.io/@yoon_bly/Heap-Stack-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@yoon_bly/Heap-Stack-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Thu, 04 Jan 2024 01:04:36 GMT</pubDate>
            <description><![CDATA[<h1 id="heap">Heap</h1>
<ul>
<li>자바의 힙 공간은 객체와 JRE 클래스들에게 메모리를 할당할 때 사용 된다.</li>
<li>객체가 생성되면 힙 공간에 할당되며, 이 객체에 대한 참조가 스택 메모리에 저장된다.</li>
<li>가비지컬렉션은 힙 메모리에 더이상 참조하지 않는 객체들을 정리 한다.</li>
<li>힙에 만들어진 객체는 어디서든 접근할 수 있고, 어플리케이션 어디에서나 참조될 수 있다.</li>
</ul>
<h1 id="heap-특징">Heap 특징</h1>
<ul>
<li>힙 영역이 가득차면 OutOfMemoryError가 발생</li>
<li>스택 영역에 비해 엑세스 속도가 조금 느리다.</li>
<li>스택 영역과 달리 메모리 해제가 자동으로 되지 않는다.</li>
<li>메모리 사용의 효율성을 위해 GC가 필요하다.(Garbage Collection)</li>
<li>Thread-safe하지 않기 때문에 적절히 synchronizing을 해야한다.</li>
</ul>
<h1 id="stack">Stack</h1>
<ul>
<li>정적 메모리 할당 및 쓰레드 실행을 위해 사용되는 영역</li>
<li>메소드에 고유한 기본 값과 메소드에서 참조하는 힘 개체에 대한 참조값을 포함하고 있다.</li>
<li>LIFO(Last-In-First-Out) 순서로 동작된다.</li>
<li>새 메소드가 호출될 때마다 스택 상단에 해당 메소드에 대한 새블록이 생성된다.</li>
<li>메소드 실행이 완료되면 해당 스택에서 pop되고, 호출한 메소드로 흐름이 돌아가게 된다.</li>
</ul>
<h1 id="stack-특징">Stack 특징</h1>
<ul>
<li>스택 내부의 변수는 변수를 생성한 메소드가 실행되는 동안만 존대한다.</li>
<li>메소드 실행에 따라 자동으로 할당되고 해제된다.</li>
<li>해당 메모리 영역이 가득차면 StackOverFlowError가 발생.</li>
<li>힙 메모리에 비해 엑세스 속도가 빠르다.</li>
<li>쓰레드마다 고유의 스택 영역을 가지고 있으므로, Threa-safe하다.</li>
</ul>
<h1 id="heap과-stack-차이점">Heap과 Stack 차이점</h1>
<ul>
<li>힙 메모리는 어플리케이션의 모든 부분에서 사용된다.</li>
<li>스택 메모리는 하나의 쓰레드가 실행 될 때 사용된다.</li>
<li>객체가 생성되면 항상 힙 공간에 저장된다.</li>
<li>스택 메모리는 힙 공간에 있는 객체를 참조만 한다.</li>
<li>스택 메모리는 primitive 타입의 지역변수와 힙 공간에 있는 객체 참조 변수만 갖고 있다.</li>
<li>힙 공간에 저장된 객체는 어디서든 접근이 가능하다.</li>
<li>스택 메모리는 다른 쓰레드가 접간할 수 없다. (Heap보다 어느정도 보안성이 있다.)</li>
<li>스택 메모리의 생명주기는 매우 짧다.</li>
<li>힙 메모리는 어플리케이션의 시작부터 끝까지 남아있다.</li>
<li>스택 메모리가 가득차면 java.lang.StackOverFlowError 발생</li>
<li>힙 메모리가 가득차면 java.lang.OutOfMemoryError 발생</li>
<li>스택 메모리의 사이즈는 힙 메모리와 비교시 매우 적다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(배포)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Fri, 11 Aug 2023 05:43:18 GMT</pubDate>
            <description><![CDATA[<p>js및 뷰단 타임리프작업까지 마무리가 되었다.</p>
<p>이번 회고록은 나의 배포 준비단계를 작성해보려고 한다.</p>
<p>가장 먼저 나는 docker를 이용하여 aws에 올릴 생각이었다.</p>
<p>docker에 나의 이미지를 올리고 aws인스턴스에 docker를 다운받아 해당 서버를 올릴 생각이었다.</p>
<p>그래서 3일간 열심히 찾아보고 도전해보았다.</p>
<p>docker-compose 파일을 이용하여 내 프로젝트와 db를 연결하려고 했지만, 자꾸만 오류를 나타내었다.</p>
<p>그래서 결국 aws만을 사용해서 배포하기로 결정하였다.</p>
<p>하지만, 여러 걸림돌이 걸렸었다.</p>
<h2 id="프리티어">프리티어</h2>
<p>아직 돈을 벌지 못하는 일개 대학생으로써 프리티어 버전으로 인스턴스를 만들고, 해당 인스턴스에 내 프로젝트파일을 옮기고 jar파일을 만드려하는데, 10분이 지나도 완료가 도저히 안되었다.</p>
<p>그래서 구글링 결과.</p>
<p>프리티어 버전의 인스턴스는 RAM이 1기가 밖에 안되기 때문에 파일이 크면 오래 걸리기도 안되기도 한다고 한다.</p>
<p>그래서 swap 파일을 사용하여 RAM 용량을 늘려 build하였다.</p>
<p>이전보다 금방 되는 모습을 확인할 수 있었다.</p>
<h2 id="db">DB</h2>
<p>자꾸 서버에서 패킷을 못받았다는 오류를 받았다.</p>
<p>아직 무지한 상태인 나이기에 부끄럽지만 적어본다.</p>
<p>사실 나는 원래 aws 서버에 내 프로젝트를 올리고, 내 컴퓨터에 있는 mysql db를 사용하려 했다.</p>
<p>그러나 계속 jar 파일을 실행해도, db쪽에서 에러가 났다.</p>
<p>그래서 aws의 rds를 사용하여 내 프로젝트에 해당 db를 연결한 후 서버를 실행하니 정상적으로 잘 작동 되었다.</p>
<p>내 로컬 pc에 있는 db를 사용해서 서버를 배포하는게 되는지 안되는지 사실 잘 모르겠다.</p>
<p>이것도 나중에 공부를 해볼 것이다.</p>
<hr>
<p>결국 이 프로젝트의 마무리를 지었다.</p>
<p>배포 과정과 마무리 회고록은 나중에 쓰도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(JS, Thymeleaf 작업)4]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%854</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%854</guid>
            <pubDate>Sun, 06 Aug 2023 11:12:48 GMT</pubDate>
            <description><![CDATA[<p>마지막으로 main페이지를 진행하였다.</p>
<p>나머지 자잘자잘한 것들은 간단하여 생략하였다.</p>
<h1 id="주요기능">주요기능</h1>
<ol>
<li><p>로그인시 nav바 변경</p>
</li>
<li><p>nav바 프로필 사진 변경</p>
</li>
<li><p>주소 설정 select box</p>
</li>
</ol>
<h2 id="로그인-시-nav바-변경">로그인 시 nav바 변경</h2>
<p>사실 이 프로젝트를 하기 전에 하다가 중단된 프로젝트가 있었다.</p>
<p>나를 포함한 팀원들 모두 아무것도 모르는 상태에서 하려다 중단되었었는데,</p>
<p>그 프로젝트에서도 로그인 했을 때 nav바를 변경하는 방법을 몰랐다.</p>
<p>그래서 이번 프로젝트에서도 이 부분이 가장 해결하기 어렵다 생각했던 부분 중 하나이다.</p>
<p>하지만, 지금의 나는 session공부를 함으로써 어떤 방식으로 nav바를 변경해야 하는지 알게 되었고, 정말 간단한 방법으로 nav바를 변경할 수 있었다.</p>
<p>만약  로그인을 했다면, 서버에서 해당 사용자의 session을 생성했다.</p>
<p>이 session을 사용하여  nav바를 변경하였다.</p>
<p>가장 먼저 로그인 했을 때의 nav바 파일과 로그인이 안됐을 때의 nav바 파일을 따로 생성한 후 타임리프를 이용하여 nav바를 교체하는 방식으로 진행하였다.</p>
<pre><code class="language-java">    if(userIndex != null){
            Optional&lt;ProfileEntity&gt; userProfile = profileService.findProfileEntity(userIndex);
            // session이 있을 때 true 반환해 login된 header사용
            model.addAttribute(&quot;alreadyHaveSession&quot;, &quot;true&quot;);
            if(userProfile.isPresent()){
                model.addAttribute(&quot;storeFileName&quot;, userProfile.get().getStoreFileName());
            }
        }
        else if(userIndex == null){
            // session 없을 때 false반환해 기본 header사용
            model.addAttribute(&quot;alreadyHaveSession&quot;, &quot;false&quot;);
        }
    }</code></pre>
<p>위의 코드는 메인 페이지 메서드중 하나이다.</p>
<p>위의 코드처럼 유저 index가 있을 때 alreadyHaveSession이라는 모델을 가져오도록 한다.</p>
<pre><code class="language-html">&lt;div th:if=&quot;${alreadyHaveSession == &#39;true&#39;}&quot;&gt;
        &lt;div th:replace=&quot;newHeader.html&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div th:if=&quot;${alreadyHaveSession == &#39;false&#39;}&quot;&gt;
        &lt;div th:replace=&quot;existingHeader.html&quot;&gt;&lt;/div&gt;
      &lt;/div&gt;</code></pre>
<p>만약 alreadyHaveSession이 true이면, 해당 사용자가 로그인을 했을 상태이기 때문에, 로그인이 된 nav바를 대체하도록 하고 false이면 로그인이 안된 상태의 nav바를 반환하도록 하였다.</p>
<h2 id="nav바-프로필-사진-변경">nav바 프로필 사진 변경</h2>
<p>이 부분은 로그인한 유저의 프로필 사진이 변경되었을 때, 프로필 사진을 가져와 nav바에 사진을 걸어 두는 것이다.</p>
<p>위의 코드에서 보듯 storeFileName이라는 모델로 해당 사용자의 프로필 사진을 가져온다.</p>
<pre><code class="language-html">        &lt;img
          class=&quot;picture&quot;
          onerror=&quot;this.src=&#39;/img/default_profile.png&#39;&quot;
          th:src=&quot;@{/upload/{uploadfile}(uploadfile=${storeFileName})}&quot;
        /&gt;</code></pre>
<p>타임리프를 사용하여 해당 사진의 주소를 받아와 보여준다.</p>
<p>가장 처음 회원가입을 했을 때에는 프로필 사진이 없기 때문에 만약 해당 프로필 사진이 없으면 기본 프로필 사진을 받아 올 수 있도록 onerror 속성을 사용하여  기본 프로필 사진을 가져오도록 하였다.</p>
<h2 id="주소-설정-select-box">주소 설정 select box</h2>
<p>이 부분이 이 프로젝트에 있어서 가장 어려웠고 시간을 가장 많이 쓴 문제였다.</p>
<p>우선 나는 프론트엔드 개발자가 아니다. js를 잘 모르는 상태로 시도해서 시간을 많이 쓴 것 같기도 하다.</p>
<p>그냥 어느정도 어떤 식으로 js가 돌아가는지만 아는 그런 수준인 상태였다.</p>
<p>나는 당근마켓 웹페이지의 주소 select와 비슷하게 만들고 싶었다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/c565d1d6-6ee5-4435-8828-e66f2fe3d38f/image.png" alt=""></p>
<p>가장 중요한 부분은 해당 구/군을 선택하면 해당 구/군에 있는 동들이 나와야 한다.</p>
<p>그리고 해당 시 / 군 / 구 를 선택할 때 해당 지역을 선택한 게시글들이 나오게 만드는 것이다.</p>
<p>예를 들면, 경기도 용인시 수지구까지만 선택했다면, 경기도 용인시 수지구를 선택했던 게시글들만 나오게 하는 것이다.</p>
<p>우선 이 select box의 option 값들이 너무 많아 프로토타입으로 서울특별시의 지역만 option값으로 넣어두었다.</p>
<p>여러가지 시도를 해보았다.</p>
<p>option값을 selected 속성을 사용하여 만들어, js로 url주소를 만들어 이동하는 방식으로도 해보았지만, 페이지가 넘어가면서 해당 주소를 selected한 option값을 못 받아 왔다.</p>
<p>이런 여러가지 방식으로 시도해왔지만, 번번히 실패했다.</p>
<p>결국 오랜 구글링 끝에 sessionStorage를 사용하여 해당 selected값을 가져오는 방식으로 도전해보았다.</p>
<p>이 방법은 이전에 같이 프로젝트를 해온 프론트엔드 팀원이 해보다 실패했다고 회의하면서 들었어서, 반신반의하며 도전하였다.</p>
<p>방식은 이렇다.</p>
<p>select box에서 해당 지역을 클릭하면 해당 option의 value 값을 받아와 sessionStorage에 저장한 후 새로운 페이지에서 해당 지역의 게시글을 받아오고, selected 한 지역은 sessionStorage에서 받아와 select box에 넣어주는 방식이다.</p>
<p>이전에 프론트를 담당한 팀원이 select box를 해놓았는데 그걸 이용하여 코드를 진행해보았다.</p>
<p>원래는 다음페이지까지 이동은 했지만, selected가 되지않아 다음 지역 selectbox가 선택되지 않았다.</p>
<p>팀원이 짠 코드를 console.log를 찍어가며 어떤 식으로 코드진행이 되는지를 알아가며 차근차근 천천히 풀어나가니 결국에 내가 원하는 기능을 제대로 실행할 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(JS, Thymeleaf 작업)3]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%853</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%853</guid>
            <pubDate>Sun, 06 Aug 2023 06:05:30 GMT</pubDate>
            <description><![CDATA[<p>프로필 수정 및 user정보를 수정하는 부분을 작업하였다.</p>
<h1 id="주요-기능">주요 기능</h1>
<ol>
<li><p>프로필 수정</p>
</li>
<li><p>회원 탈퇴 및 비밀번호 변경</p>
</li>
</ol>
<h2 id="1-프로필-수정">1. 프로필 수정</h2>
<p>사실 이 부분은 js작업 보다 서버쪽 코드 수정이 주였다.</p>
<p>사실 프로필 수정 자체는 form 태그의 multipart타입으로 만들면 쉽게 만들어진다.</p>
<p>말그대로 프로필 사진과 이름, 닉네임, 전화번호도 쉽게 수정되었다.</p>
<p>한가지 문제점을 내가 놓친 것이 있었는데, 만약 사용자가 게시글을 하나 이상 작성했거나, 댓글을 쓴 경우가 있는 상태에서 프로필을 바꾸면 해당 게시글과 댓글 프로필사진 및 닉네임도 변경이되어야 하는데, 그 부분을 생각하지 못했다.</p>
<h3 id="게시글-댓글-사용자-닉네임-수정">게시글, 댓글, 사용자 닉네임 수정</h3>
<pre><code class="language-java">    public Optional&lt;UserEntity&gt; changeUserInfo(Long userIndex, UserEntity userInfo) {
        Optional&lt;UserEntity&gt; user = userRepository.findById(userIndex);

        List&lt;PostEntity&gt; userPost = postRepository.findByUserIndex(userIndex);
        List&lt;ReplyEntity&gt; userReply = replyRepository.findByReplyUserIndex(userIndex);

        for(PostEntity post : userPost){
            post.setUserNickName(userInfo.getUserNickName());

            postRepository.save(post);
        }

        for(ReplyEntity reply : userReply){
            reply.setUserNickName(userInfo.getUserNickName());

            replyRepository.save(reply);
        }

        return user.map(p -&gt; {
            user.get().setUserName(userInfo.getUserName());
            user.get().setUserNickName(userInfo.getUserNickName());
            user.get().setUserPhoneNumber(userInfo.getUserPhoneNumber());

            return p;
        })
        .map(p -&gt; userRepository.save(p));

    }</code></pre>
<p>나는 user 엔티티와 프로필 엔티티를 따로 두어 userIndex를 통해 새로운 프로필을 만들거나 수정하는 방식으로 만들었다. 그래서 위 코드에는 프로필 사진 수정에 대한 코드는 없다.(아래에서 설명할 것이다.)</p>
<blockquote>
<p>return user.map(p -&gt; {
            user.get().setUserName(userInfo.getUserName());
            user.get().setUserNickName(userInfo.getUserNickName());
            user.get().setUserPhoneNumber(userInfo.getUserPhoneNumber());
            return p;
        })
        .map(p -&gt; userRepository.save(p));</p>
</blockquote>
<p>가장먼저 바뀐 사용자 정보를 원래 가지고 있던 유저 정보에서 수정한다.</p>
<blockquote>
<p>List&lt;PostEntity&gt; userPost = postRepository.findByUserIndex(userIndex);
        List&lt;ReplyEntity&gt; userReply = replyRepository.findByReplyUserIndex(userIndex);</p>
</blockquote>
<p>해당 코드는 사용자가 쓴 댓글과 게시글 전체를 가져오는 것이다.</p>
<p>그 다음 각각의 게시글과 댓글의 닉네임을 수정하여 저장한다.</p>
<h3 id="게시글-댓글-프로필-사진-수정">게시글, 댓글 프로필 사진 수정</h3>
<pre><code class="language-java">    public Optional&lt;ProfileEntity&gt; saveProfile(MultipartFile originFileName, Long userIndex) throws IOException {
        Optional&lt;ProfileEntity&gt; storeFile = fileStore.storeFile(originFileName, userIndex);

        List&lt;PostEntity&gt; userPost = postRepository.findByUserIndex(userIndex);
        List&lt;ReplyEntity&gt; userReply = replyRepository.findByReplyUserIndex(userIndex);

        for(PostEntity post : userPost){
            post.setStoreFileName(storeFile.get().getStoreFileName());

            postRepository.save(post);
        }

        for(ReplyEntity reply : userReply){
            reply.setStoreFileName(storeFile.get().getStoreFileName());

            replyRepository.save(reply);
        }
        return storeFile;
    }</code></pre>
<p>만약 사용자가 프로필 사진을  바꾼다면 실행되는 메서드이다.</p>
<blockquote>
<p>Optional&lt;ProfileEntity&gt; storeFile = fileStore.storeFile(originFileName, userIndex);</p>
</blockquote>
<p>가장 먼저 사용자의 프로필 사진을 저장한다.</p>
<p>이후 위의  게시글, 댓글의 닉네임을 변경하듯이, 프로필 사진을 각각의 게시글과 댓글에 수정하여 저장시킨다.</p>
<h3 id="문제점">문제점</h3>
<p>사실 이 방법이 맞는건지 잘 모르겠다.</p>
<p>내가 생각한 문제점은 데이터베이스적인 문제인것 같다.</p>
<p>사실 나의 무지함에서 일어난 문제같다.</p>
<p>사용자와 댓글, 게시글은 일대다 연관관계로, jpa를 사용한다면, getUserProfile() 이런식으로 게시글에서 사용자 프로필을가져올 수 있을 것이다.</p>
<p>하지만, 나는 닉네임과 프로필 사진을 게시글과 댓글에 새로운 컬럼으로 넣어두었고, 그렇게 해서 타임리프를 통해 뷰단에 뿌릴수 있다고 생각했다.</p>
<p>만약 게시글과 댓글에 사용자의 index만 가지고 뷰단에 뿌리게 된다면, 타임리프를 통해 어떤 방식으로 사용자의  닉네임과 프로필 사진을 가져오는 방법을 모르기 때문에 위의 방식으로 프로젝트를 진행하였다.</p>
<p>이게 맞는 것인지 틀린 것인지도 잘 모르겠다.</p>
<p>우선 배포과정까지 마무리한 후 다시 한번 손봐야 할 것 같다.</p>
<h2 id="회원-탈퇴-및-비밀번호-변경">회원 탈퇴 및 비밀번호 변경</h2>
<p>해당 로직은 아주 쉽게 끝났다.</p>
<pre><code class="language-javascript">$(&quot;#withdrawBtn&quot;).click(() =&gt; {
    const confirmed = confirm(&quot;회원탈퇴를 하시겠습니까?&quot;);

    if (confirmed) {
      $.ajax({
        url: &quot;/user/delete&quot;,
        type: &quot;DELETE&quot;,
        success: function () {
          alert(&quot;회원탈퇴가 완료되었습니다.&quot;);

          window.location.href = &quot;/&quot;;
        },
      });
    }
    // alert에서 취소 누르면 원상복귀
  });</code></pre>
<p>말 그대로 회원 탈퇴 방법을 ajax를  사용하여 회원탈퇴 버튼을  누르면 탈퇴를 할것인지 alert를 띄우고, 취소를 누르면 취소가 되고, 확인 버튼을 누르면 회원탈퇴가 되도록 만들었다.</p>
<p>비밀번호 변경도 쉽게 만들었다.</p>
<pre><code class="language-javascript">$(&quot;#submitBtn&quot;).click(() =&gt; {
    const newPassword = $(&quot;#inputNewpw&quot;).val();

    // 비밀번호 변경 input 이 비어있을 경우
    if (newPassword.trim() === &quot;&quot;) {
      alert(&quot;변경사항이 없습니다.&quot;);
      return;
    }

    // 새로운 비밀번호를 입력한 경우
    const confirmed = confirm(&quot;비밀번호를 변경하시겠습니까?&quot;);

    if (confirmed) {
      $.ajax({
        url: &quot;/change/pw&quot;,
        type: &quot;POST&quot;,
        data: { newPassword: newPassword },

        success: function (response) {
          alert(&quot;비밀번호를 변경하였습니다!&quot;);
        },
        error: function (xhr, status, error) {
          alert(&quot;에러가 발생했습니다.&quot;);
        },
      });
      // alert에서 취소 누르면 원상복귀
    }
  });</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(JS, Thymeleaf 작업)2]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%852</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%852</guid>
            <pubDate>Sun, 06 Aug 2023 04:30:03 GMT</pubDate>
            <description><![CDATA[<p>회원가입/ 로그인 기능을 끝마친 후 나는 게시글 상세화면에 대한 js작업과 thymeleaf작업을 진행하였다.</p>
<p>게시글 쓰는 페이지는 다른 팀원이 하기로 하고 나는 메인페이지에서 게시글을 클릭했을 때 나오는 게시글 상세화면을 만들었다.</p>
<h1 id="주요-기능">주요 기능</h1>
<ol>
<li><p>해당 게시글이 자신이 쓴 게시글이라면 게시글 수정 및 삭제 버튼 활성화</p>
</li>
<li><p>해당 게시글 thymeleaf 작업</p>
</li>
<li><p>해당 게시글의 댓글 thymeleaf 작업</p>
</li>
</ol>
<h2 id="1-해당-게시글이-자신이-쓴-게시글이라면-수정-및-삭제-버튼-활성화">1. 해당 게시글이 자신이 쓴 게시글이라면 수정 및 삭제 버튼 활성화</h2>
<p>이 부분이 사실 게시글 상세화면 부분에서 가장 어려운 부분이라고 생각한다.</p>
<p>우선 게시글 상세화면은 로그인세션이 있는 상태, 로그인이 되어있지 않은 상태 둘다 해당 페이지를 들어올 수 있기 때문에 게시글 수정 및 삭제 버튼은 게시글의 주인이 로그인하지 않는이상 활성화 되면 안된다.</p>
<p>이 부분을 어떻게 하면 좋을까 곰곰히 생각해보았다.</p>
<p>해당 게시글의 index와 로그인된 유저의 index가 같으면 해당 버튼들이 활성화 되어야 한다.</p>
<p>그래서 게시글을 가져올때의 메서드에 thymeleaf값 하나를 가져와서 만들기로 생각했다.(나중에 좋은 방법이 생각났다.)</p>
<pre><code class="language-java">@GetMapping(&quot;/post/{postId}&quot;)
    public String read(HttpSession session, @PathVariable(name = &quot;postId&quot;) Long postId, Model model) {
        Long userIndex = (Long) session.getAttribute(&quot;userIndex&quot;);
        model.addAttribute(&quot;userIndex&quot;, userIndex);

        // 게시글 model
        Optional&lt;PostEntity&gt; post = postService.read(postId);
        post.ifPresent(o -&gt; model.addAttribute(&quot;post&quot;, o));

        // 세션을 가져와서 만약 게시글, 댓글 userIndex와 같다면
        // 1을 출력해 수정 및 삭제 버튼 활성화 
        // 로그인 했지만, 다르면 0 
        // 로그인하지 않았다면, 2 출력
        // 수정 필요
        Integer checkedUserIndexForPost = 0;
        if(userIndex != null){
            if(userIndex == post.get().getUserIndex()){
                // 게시글, 댓글, 세션이 같으면
                checkedUserIndexForPost = 1;
                model.addAttribute(&quot;checkIndexForPost&quot;, checkedUserIndexForPost);
            } else if(userIndex != post.get().getUserIndex()){
                checkedUserIndexForPost = 2;
                model.addAttribute(&quot;checkIndexForPost&quot;, checkedUserIndexForPost);
            }
        }
        else if(userIndex == null){
            model.addAttribute(&quot;checkIndexForPost&quot;, checkedUserIndexForPost);
        }

        return &quot;post_detail&quot;;
    }</code></pre>
<p>가장 먼저 해당 게시글을 가져와 model화 해준다.</p>
<p>그 후 userIndex가 있을 때 없을 때를 비교하고 없다면 2,
로그인하여 userIndex가 있지만, 게시글 index와 다르다면 0,
해당 게시글이 로그인한 사용자의 게시글이면 1을 반환하여 model에 넣어주었다.</p>
<p>이 값을 js에서 받아서 확인하는 방식으로 진행하였다.</p>
<pre><code class="language-javascript">$(document).ready(() =&gt; {
  var index_button = $(&quot;input[name=index_button]&quot;).val();
  if (index_button == 1) {
    console.log(index_button);
    $(&quot;.deleteEdit&quot;).css(&quot;display&quot;, &quot;flex&quot;);
  }
});</code></pre>
<blockquote>
<p>$(&quot;input[name=index_button]&quot;).val();</p>
</blockquote>
<p>이 부분은 해당 타임리프 값을 input hidden 값으로 html에 집어넣어 해당 value를 가져오게 만든 것이다.</p>
<pre><code class="language-html">&lt;input type=&quot;hidden&quot; name=&quot;index_button&quot; th:value=&quot;${checkIndexForPost}&quot;/&gt;</code></pre>
<h3 id="개선사항">개선사항</h3>
<p>사실 어짜피 로그인세션의 userIndex를 model화 한다면, 위의 방법보다, userIndex의 value값을 가져오고, thymeleaf로 가져온 게시글 model에서 userIndex를 뽑아와 그 둘을 비교시켜도 되었을 것이다.</p>
<p>그것이 유지보수에 좀 더 도움이 되었을 것이다.</p>
<p>나중에 해당 프로젝트를 리펙토링하게 된다면 이 부분을 고쳐주고 싶다.</p>
<h2 id="2-해당-게시글-thymeleaf-작업">2. 해당 게시글 thymeleaf 작업</h2>
<p>해당 thymeleaf 작업은 너무 쉬워서 넘어가겠다.</p>
<h2 id="3-해당-게시글의-댓글-js-thymeleaf-작업">3. 해당 게시글의 댓글 js, thymeleaf 작업</h2>
<p>댓글 작성은 form 태그를 이용하여 구현하였다.</p>
<p>여기서 댓글을 작성한 후 해당 댓글을 보게 되면 만약 로그인한 유저가 해당 댓글을 썼다면, 수정 및 삭제 버튼이 생긴다.</p>
<p>이 부분을 위에 게시글 수정 삭제 버튼의 개선사항으로 만들어 보았다.</p>
<p>thymeleaf의 th:style을 이용하여 만약 같다면 해당 display를 활성화 시키는 방법으로 만들었다.</p>
<pre><code class="language-html">&lt;div
  class=&quot;deleteEditC&quot;
  th:style=&quot;${reply.replyUserIndex == userIndex ? &#39;display:flex&#39; : &#39;display:none&#39;}&quot;
&gt;
    &lt;button class=&quot;editC&quot;&gt;수정&lt;/button&gt;
    &lt;form
         th:action=&quot;@{/reply/delete/{replyIndex}(replyIndex=${reply.replyIndex})}&quot;
         method=&quot;post&quot;
    &gt;
    &lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;delete&quot; /&gt;
    &lt;button class=&quot;deleteC&quot;&gt;삭제&lt;/button&gt;
    &lt;/form&gt;
&lt;/div&gt;</code></pre>
<p>각각의 댓글의 userIndex를 가져와 만약 로그인세션의 userIndex와 같다면 해당 버튼들을 활성화 하고 다르면 비활성화 하게 만들었다.</p>
<p>댓글 수정은 ajax를 이용하여 비동기 방식으로 구현하였다.</p>
<pre><code class="language-javascript">document.addEventListener(&quot;DOMContentLoaded&quot;, function () {
  // Addeventlisteners &quot;수정&quot; 버튼
  function handleEditClick(event) {
    const commentBox = event.target.closest(&quot;.commentBox&quot;);
    const commentText = commentBox.querySelector(&quot;.comment&quot;);
    const commentContent = commentText.innerText;

    const textarea = document.createElement(&quot;textarea&quot;);
    textarea.classList.add(&quot;editTextarea&quot;);
    textarea.name = &quot;editedComment&quot;;
    textarea.value = commentContent;

    const saveButton = document.createElement(&quot;button&quot;);
    saveButton.classList.add(&quot;saveC&quot;);
    saveButton.innerText = &quot;저장&quot;;

    const editDeleteContainer = commentBox.querySelector(&quot;.deleteEditC&quot;);
    editDeleteContainer.innerHTML = &quot;&quot;;
    commentText.innerHTML = &quot;&quot;;
    commentText.appendChild(textarea);
    editDeleteContainer.appendChild(saveButton);

    //기능 삭제
    event.target.removeEventListener(&quot;click&quot;, handleEditClick);

    saveButton.addEventListener(&quot;click&quot;, function () {
      const editedComment = textarea.value;
      var reply_index = $(&quot;input[name=reply_index]&quot;).val();

      $.ajax({
        url: &quot;/reply/edit/&quot; + reply_index,
        type: &quot;POST&quot;,
        data: {
          replyDescription: editedComment,
        },
        success: function (response) {
          console.log(&quot;업데이트에 성공했습니다.&quot;);

          commentText.innerText = editedComment;
          editDeleteContainer.innerHTML = &quot;&quot;;

          const editButton = document.createElement(&quot;button&quot;);
          editButton.classList.add(&quot;editC&quot;);
          editButton.innerText = &quot;수정&quot;;

          const deleteButton = document.createElement(&quot;button&quot;);
          deleteButton.classList.add(&quot;deleteC&quot;);
          deleteButton.innerText = &quot;삭제&quot;;

          editDeleteContainer.appendChild(editButton);
          editDeleteContainer.appendChild(deleteButton);

          //다시 적용
          editButton.addEventListener(&quot;click&quot;, handleEditClick);
        },
      });
    });
  }

  const editButtons = document.querySelectorAll(&quot;.editC&quot;);
  editButtons.forEach((button) =&gt; {
    button.addEventListener(&quot;click&quot;, handleEditClick);
  });
});</code></pre>
<p>수정 버튼을 눌렀을 때, handleEditClick함수가 실행되어 textarea와 저장 버튼을  추가 시킨뒤 저장 버튼을 눌렀을 때 ajax로 수정이 가능하게 만들었다.</p>
<p>삭제 버튼은 form 태그를 이용하여 바로 삭제 가능하게 구현하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(JS, Thymeleaf 작업)1]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%851</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9DJS-Thymeleaf-%EC%9E%91%EC%97%851</guid>
            <pubDate>Fri, 04 Aug 2023 12:47:13 GMT</pubDate>
            <description><![CDATA[<p>내 프로젝트에서는 로그인을 한 뒤에 글을 쓰거나 댓글을 쓰는등 가장 우선시 되는 것이 로그인 기능이다.</p>
<p>그래서 가장 첫번째로 건든 부분이 회원가입/로그인 부분이다.</p>
<p><strong>signup.html</strong></p>
<h1 id="회원-가입-기능">회원 가입 기능</h1>
<p>가장 회원 가입이 되기 위한 조건은</p>
<ol>
<li><p>사용 가능한 ID (사용중인 ID 사용 불가)</p>
</li>
<li><p>비밀번호 확인 (비밀번호가 같아야 함)</p>
</li>
<li><p>이름과 닉네임이 같으면 안된다.(필요할 것 같아서 넣음)</p>
</li>
<li><p>회원가입</p>
</li>
</ol>
<p>이렇게 3가지가 필요하다.</p>
<h2 id="1-사용-가능한-id">1. 사용 가능한 ID</h2>
<p>사실 기본 회원가입과 비슷하게 ID를 입력 후 중복체크하는 버튼을 만드려고 했다.</p>
<p>하지만, 우리가 만든 디자인과는 중복체크 버튼은 어울리지 않았고, 결국 keyup 기능을 이용하여 중복체크를 하기로 했다.</p>
<pre><code class="language-javascript">$(&#39;.idInput&#39;).on(&quot;propertychange change keyup paste input&quot;, function(){
  // console.log(&quot;keyup 테스트&quot;);
  var userId = $(&quot;.idInput&quot;).val();
  var data = {userId : userId}

  $.ajax({
    type: &quot;post&quot;,
    url: &quot;/check/id&quot;,
    data : data,
    success : function(result){
      //사용 가능
      if(result === 0){
        $(&quot;.condition1&quot;).css(&quot;display&quot;, &quot;flex&quot;);
        $(&quot;.id_input_re_1&quot;).css(&quot;display&quot;,&quot;inline-block&quot;);
                $(&quot;.id_input_re_2&quot;).css(&quot;display&quot;, &quot;none&quot;);
        console.log(&quot;사용 가능&quot;);
      }
      // 아이디 중복
      if(result === 1){
        $(&quot;.condition1&quot;).css(&quot;display&quot;, &quot;flex&quot;);
        $(&quot;.id_input_re_1&quot;).css(&quot;display&quot;,&quot;none&quot;);
                $(&quot;.id_input_re_2&quot;).css(&quot;display&quot;, &quot;inline-block&quot;);
        console.log(&quot;아이디 중복!&quot;);
      }
    }
  });
});</code></pre>
<p>propertychange change keyup paste input을 이용하여 input값에 입력되는 값을 실시간으로 처리할 수 있도록 만들었다.</p>
<p>이후 ajax를 이용하여 ID 중복체크 메서드를 실행시켜 사용 가능한지 중복인지 판별하는 기능을 만들었다.</p>
<p><strong>service 부분</strong></p>
<pre><code class="language-java">public int checkDuplicateNickName(String userNickName) {
        Optional&lt;UserEntity&gt; find = userRepository.findByUserNickName(userNickName);
        if(find.isEmpty()){
            System.out.println(&quot;possible NickName!!&quot;);
            return 0;
        }
        else{
            System.out.println(&quot;duplicate NickName!!&quot;);
            return 1;
        }
    }</code></pre>
<p>keyup 된 ID를 가져와서 만약 해당 ID를 갖고 있는 사용자가 있다면, 1을 반환하고, 사용가능한 아이디라면 0을 반환하도록 만들었다.</p>
<h2 id="2-비밀번호-체크">2. 비밀번호 체크</h2>
<p>대부분 회원가입 페이지를 보면 비밀번호를 누르고 한번더 비밀번호를 입력하라고 한다.</p>
<p>그 부분을 만들었다.</p>
<p>이 부분은 회원가입을 누를 때 체크하는 기능으로, 비밀번호와 한번더 입력한 비밀번호 값이 같지 않다면 회원가입이 되지 않게 만들었다.</p>
<pre><code class="language-javascript">$(document).ready(() =&gt; {
  $(&quot;button&quot;).click((event) =&gt; {
    var password = $(&quot;.passwordInput&quot;).val();
    var passwordCheck = $(&quot;.passwordCheckInput&quot;).val();

    if(password !== passwordCheck) {
      $(&quot;.condition2&quot;).css(&quot;display&quot;, &quot;&quot;);
      event.preventDefault();
    }
    if(password === passwordCheck) {
      $(&quot;.condition2&quot;).css(&quot;display&quot;, &quot;none&quot;);
    }
  })
})</code></pre>
<p>해당 input 값들을 가져와 같은지 판별하였다.</p>
<h2 id="3-이름과-닉네임이-같으면-안된다">3. 이름과 닉네임이 같으면 안된다.</h2>
<p>이 부분도 2번과 똑같이 두 input 값을 가져와 비교하여 판별하였다.</p>
<pre><code class="language-javascript">$(document).ready(() =&gt; {
  $(&quot;button&quot;).click((event) =&gt; {
    var nameVal = $(&quot;.nameInput&quot;).val();
    var nickNameVal = $(&quot;.nickNameInput&quot;).val();
    if (nameVal === nickNameVal) {
      $(&quot;.condition&quot;).css(&quot;display&quot;, &quot;&quot;);
      event.preventDefault();
    }
    if (nameVal !== nickNameVal) {
      $(&quot;.condition&quot;).css(&quot;display&quot;, &quot;none&quot;);
      event.preventDefault();
    }
  });
});</code></pre>
<h2 id="4-회원-가입">4. 회원 가입</h2>
<p>모든 유효성 검사에 가능해지면 회원가입을 할 수 있다.</p>
<p>회원 가입은 ajax를 통해 회원가입이 이루어지게 만들었다.</p>
<pre><code class="language-javascript">$(document).ready(() =&gt; {
  $(&quot;.join_button&quot;).click((event) =&gt; {
    var nameVal = $(&quot;.nameInput&quot;).val();
    var nickNameVal = $(&quot;.nickNameInput&quot;).val();
    var password = $(&quot;.passwordInput&quot;).val();
    var passwordCheck = $(&quot;.passwordCheckInput&quot;).val();

    var userId = $(&quot;.idInput&quot;).val();
    var data = {
      userId : $(&quot;.idInput&quot;).val(),
      userNickName : $(&quot;.nickNameInput&quot;).val()
    };

    $.ajax({
      type: &quot;post&quot;,
      url: &quot;/check&quot;,
      data : data,
      success : function(result){
        if(nameVal !== nickNameVal){
          if(password === passwordCheck){
            if(result === 3){
              alert(&quot;회원가입되었습니다.&quot;);
              document.getElementById(&quot;signup&quot;).submit();
            }
            else if(result === 2){
              alert(&quot;닉네임이 중복됩니다.&quot;);
            }
          }
        }
      }
    })
  })
})</code></pre>
<p>회원가입에 필요한 input값들을 data 로 만들어 ajax로 서버에 보낸다.</p>
<p>그후 js로만 만든 유효성 검사를 if문으로 판별한 후 서버에서 받아온 값을 또 확인 하여 회원가입 시켰다.</p>
<p>이때 서버에서 받아오는 값은 아이디 닉네임이 중복되는지 안되는지를 판별하는 것이다.</p>
<p>회원가입은 form태그를 이용하여 설정하였다.</p>
<pre><code class="language-java"> @PostMapping(&quot;/check&quot;)
    public ResponseEntity&lt;Integer&gt; checkIdAndNick(@RequestParam String userId, @RequestParam String userNickName){
        int checkId = userService.checkDuplicateId(userId);
        int checkNick = userService.checkDuplicateNickName(userNickName);
        //아이디 닉네임 둘다 중복될 때
        if(checkId == 1 &amp;&amp; checkNick == 1){
            return new ResponseEntity&lt;&gt;(0, HttpStatus.BAD_REQUEST);
        }
        //아이디 중복될 때
        if(checkId == 1 &amp;&amp; checkNick == 0){
            return new ResponseEntity&lt;&gt;(1, HttpStatus.BAD_REQUEST);
        }
        //닉네임 중복될 때
        if(checkId == 0 &amp;&amp; checkNick == 1){
            return new ResponseEntity&lt;&gt;(2, HttpStatus.OK);

        }
        //아이디 닉네임 둘다 사용 가능할 때
        if(checkId == 0 &amp;&amp; checkNick == 0){
            return new ResponseEntity&lt;&gt;(3, HttpStatus.OK);   
        }
        else
            return new ResponseEntity&lt;&gt;(null, HttpStatus.BAD_GATEWAY);

    }</code></pre>
<p>이렇게 회원가입 부분을 만들었다.</p>
<h1 id="로그인-기능">로그인 기능</h1>
<p>로그인 기능은 아주 쉽게 만들었다.</p>
<pre><code class="language-javascript">function login() {
  var idInput = document.querySelector(&quot;.idInput&quot;).value;
  var passwordInput = document.querySelector(&quot;.passwordInput&quot;).value;

  let data = {
    userId: $(&quot;.idInput&quot;).val(),
    userPassword: $(&quot;.passwordInput&quot;).val(),
  };
  $.ajax({
    type: &quot;POST&quot;,
    url: &quot;/login&quot;,
    data: JSON.stringify(data),
    contentType: &quot;application/json; charset=utf-8&quot;,
    dataType: &quot;json&quot;,
    success: function (response) {
      alert(&quot;로그인 되었습니다.&quot;);
      window.location.replace(&quot;/&quot;); // Redirect to the main page
    },
    error: function () {
      alert(&quot;존재하지 않는 정보입니다.&quot;);
      window.location.href = &quot;/login&quot;;
    },
  });
}</code></pre>
<p>ID와 비밀번호 input 값을 가져와 서버에 반환해 로그인 시키는 기능이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-08-04)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-08-04</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-08-04</guid>
            <pubDate>Fri, 04 Aug 2023 07:14:24 GMT</pubDate>
            <description><![CDATA[<p>마지막으로 회고록을 작성한 후 많은 시간이 지났다.</p>
<p>핑계라고 생각하면 핑계이겠지만, 마지막으로 회고록을 쓴 후, 한동안 이 프로젝트를 하지 못했다.</p>
<p>가장 큰 이유로, 이 프로젝트는 나 혼자 하는 것이 아닌, 여러명이서 함께하는 팀 프로젝트이다.</p>
<p>서버 개발과 페이지 퍼블리싱이 끝난 상태였지만, 기말고사 기간이라 바로 js 작업 및 타임리프 작업에 들어가지 못했다.</p>
<p>기말고사가 끝난 후 종강을 하며 방학 기간에 돌입하면서, 회의를 진행했었다.</p>
<p>시간이 많이 지체 되었다. 사실상 3개월이면 끝날 수 있는 프로젝트를 질질 끌고 있었으니..</p>
<p>물론 나의 잘못도 있다.</p>
<p>사실상 학교 커리큘럼에 프로젝트를 진행하는 것도 없었고, 나도 중요하지만, 후배들도 프로젝트를 진행해보면서, 자신의 실력도 늘릴 수 있는 쉽게 말해 개발 공부를 제대로 하고 싶은 사람들을 도와 함께 진행 하고 싶었다.</p>
<p>그렇다 보니, 이제 처음 프로젝트를 접한 팀원들과 할라니 시간이 지체되었던 것 같다.</p>
<p>그리고, 사실상 PM인 내가 기말고사 일정 때문에 지체될것을 몰랐고, 그로인해 일정 조율등을 잘하지 못한 나의 잘못도 있다.</p>
<p>그래서 종강 후 첫 회의에서 나는 단도직입적으로 요번 7월달까지 마무리했으면 좋겠다고 말했다.</p>
<p>대학교 4학년인 나는 사실 마음이 조급했다.</p>
<p>같이 프로젝트를 진행하는 팀원(후배)도 나의 마음을 이해했는지, 최대한 노력해서 끝내보자고 얘기를 하였고, 오늘 8월 4일 드디어 잔 수정을 남겨놓고 모두 끝마쳤다.</p>
<p>사실 그냥 서버쪽만 신경쓰려했던 나는 조급한 마음 때문에 프론트 부분도 함께 진행하였다.</p>
<p>7월 한달동안 이 프로젝트를 끝내기 위해 열심히 달려왔다.</p>
<p>회고록을 쓰지 못할 정도로 시간 투자를 많이 해서 끝내왔다.</p>
<p>하지만 또 이렇게 복습을 하며 회고록을 쓰지 않는다면, 결국 7월 한달동안 내가 했던 것이 물거품이 될 수도 있기 때문에, 회고록을 다시 작성하려고 한다.</p>
<p>이제 배포 준비를 해야하기 때문에 또 바쁠 예정이다. 하지만 이렇게 바쁘게 살다보니, 내가 살아있는 느낌이 든다.</p>
<p>역시 나는 뭔갈 해야되는 사람인가 보다.</p>
<p>아무튼 다음 회고록부터 내가 프로젝트를 진행하면서, 해왔던 작업이나, 문제, 내가 겪었던 생각들을 적어볼 생각이다.</p>
<p>얼른 배포까지 마무리하여 내가 만든 첫 프로젝트를 세상에 내보내고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ServletUriComponentsBuilder과 ResponseEntity]]></title>
            <link>https://velog.io/@yoon_bly/ServletUriComponentsBuilder%EA%B3%BC-ResponseEntity</link>
            <guid>https://velog.io/@yoon_bly/ServletUriComponentsBuilder%EA%B3%BC-ResponseEntity</guid>
            <pubDate>Thu, 06 Jul 2023 05:04:18 GMT</pubDate>
            <description><![CDATA[<p>REST API에 대하여 공부하는 중, ServletUriComponentsBuilder를 사용하여 필요한 URI를 만들어 반환하는 것에 대해 궁금증이 생겨 정리해 보았다.</p>
<hr>
<h1 id="uricomponentsbuilder">UriComponentsBuilder</h1>
<p>인터넷상에 존재하는 모든 자원(Resource)은 URI를 이용하여 그 위치를 나타내게 된다.</p>
<p>따라서, 웹 애플리케이션을 개발할 때 어떤 데이터를 리턴해주기 위해서 내부적으로 URI를 이용해 요청을 전송하게 된다.</p>
<p>UriComponentsBuilder는 이러한 URI를 작성할 때 실수를 범하거나 유지보수 측에서 유리하게 작성하기 위해 사용하는 클래스이다.</p>
<p>UriComponentsBuilder는 UriComponents들을 쉽게 작성하기위해 만들어진 builder 클래스이다.</p>
<hr>
<p>Static Factory Method 중에 하나를 이용하여 UriComponentsBuilder 객체를 생성한다.</p>
<ol>
<li><p>Static Factory Method의 종류는 아래와 같다. </p>
<ul>
<li><p>newInstance()</p>
</li>
<li><p>fromPath(String)</p>
</li>
<li><p>fromUri (URI)</p>
</li>
<li><p>fromUriString (String)</p>
</li>
<li><p>fromHttpUrl (String)</p>
</li>
<li><p>fromHttpRequest (HttpRequest)</p>
</li>
<li><p>fromOriginHeader (String)</p>
</li>
</ul>
</li>
</ol>
<hr>
<ol start="2">
<li><p>대응되는 각 메서드를 이용하여 URI 구성요소를 설정하거나 인코딩을 설정한다.</p>
<ul>
<li><p>scheme(String) </p>
</li>
<li><p>userInfo(String)</p>
</li>
<li><p>host(String) </p>
</li>
<li><p>port(String or int)</p>
</li>
<li><p>path(String)</p>
</li>
<li><p>queryParam(String, Object...)</p>
</li>
<li><p>queryParams(MultiValueMap&lt;String, String&gt;)</p>
</li>
<li><p>fragment(String) </p>
</li>
<li><p>encode(void or Charset) : void일 경우 UTF-8 로 인코딩</p>
</li>
<li><p>expand(Map&lt;String, ?&gt; or Object... or UriTemplateVariables) : URI 템플릿 변수 값을 지정</p>
</li>
</ul>
</li>
</ol>
<hr>
<ol start="3">
<li><p>build() 메서드를 이용하여 UriComponents 인스턴스를 Build 한다.</p>
<p>또는 buildAndExpand() 메서드를 이용하여 URI 템플릿 변수를 설정한 후 Build한다.</p>
<p>이후 toUri()를 통하여 URI 객체로 변환 시킨다.</p>
</li>
</ol>
<pre><code class="language-java">// 1. 간단한 링크 생성
UriComponents uriComponents1 = UriComponentsBuilder.newInstance()
    .scheme(&quot;https&quot;).host(&quot;blog.naver.com/aservmz&quot;).path(&quot;/222313864092&quot;).build(); 
    // https://blog.naver.com/aservmz/222313864092

//  2. URI 인코딩
UriComponents uriComponents2 = UriComponentsBuilder.newInstance()
    .scheme(&quot;http&quot;).host(&quot;www.example.com&quot;).path(&quot;/encodeTest test&quot;).build().encode();
    // http://www.example.com/encodeTest%20test

//  3. path 내에 템플릿 변수설정 방법 =&gt; path() 내에 {}로 변수명 지정 후 buildAndExpand()메서드에서 값 설정
URI uriComponents3 = UriComponentsBuilder.newInstance()
    .scheme(&quot;http&quot;).host(&quot;www.example.com&quot;).path(&quot;/{키워드는}/{아무렇게나 지정해도 됩니다.}&quot;)
    .buildAndExpand(&quot;UriTemplate&quot;, &quot;setting&quot;)
    .toUri();
    // http://www.example.com/UriTemplate/setting

// 4. Query Parameter가 포함된 URI 생성
URI uriComponents4 = UriComponentsBuilder.newInstance()
    .scheme(&quot;http&quot;).host(&quot;www.example.com&quot;).path(&quot;/{경로}&quot;)
    .query(&quot;q={queryValue1}&quot;).query(&quot;p={queryValue2}&quot;).buildAndExpand(&quot;testPath&quot;, &quot;value1&quot;, &quot;value2&quot;)
    .toUri();
    // http://www.example.com/testPath?q=value1&amp;p=value2

// 5. toUriString()을 이용한 방법
UriComponents uriComponents5 = UriComponentsBuilder
    .fromUriString(&quot;https://example.com/test/{testVariable}&quot;)
    .queryParam(&quot;q&quot;, &quot;{q}&quot;).encode().buildAndExpand(&quot;test&quot;, &quot;12345&quot;);
    // https://example.com/test/test?q=12345</code></pre>
<p>이후 인코딩 시에는 항상 UriComponentsBuilder안에서 인코딩을 해야한다.</p>
<hr>
<h1 id="servleturicomponentsbuilder">ServletUriComponentsBuilder</h1>
<p>ServletUriComponentsBuilder 클래스는 UriComponentsBuilder클래스를 상속하고 있다.</p>
<p>UriComponentsBuilder는 직접 URI를 생성 및 인코딩 한다면, ServletUriComponentsBuilder는이전 요청의 URI를 재사용하여 보다 편리하게 URI를 사용할 수 있도록 하는 클래스이다.</p>
<hr>
<ul>
<li><p>fromContextPath(HttpServletRequest)</p>
</li>
<li><p>fromServletMapping(HttpServletRequest)</p>
</li>
<li><p>fromRequestUri(HttpServletRequest)</p>
</li>
<li><p>fromRequest(HttpServletRequest)</p>
</li>
</ul>
<p>// 이 메서드들은 요청 객체를 RequestContextHolder로부터 얻는다는 점을 제외하고 위의 메서드들과 동일합니다.</p>
<ul>
<li><p>fromCurrentContextPath()</p>
</li>
<li><p>fromCurrentServletMapping()</p>
</li>
<li><p>fromCurrentRequestUri()</p>
</li>
<li><p>fromCurrentRequest()</p>
</li>
</ul>
<hr>
<pre><code class="language-java">@PostMapping(&quot;/users&quot;)
    public ResponseEntity&lt;User&gt; createUser(@RequestBody User user) {
        User savedUser = service.save(user);

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path(&quot;/{id}&quot;)
                .buildAndExpand(savedUser.getId())
                .toUri();

        return ResponseEntity.created(location).build();
    }</code></pre>
<blockquote>
<p>fromCurrentRequest()</p>
</blockquote>
<ul>
<li>현재 요청된 Request 값을 가져옴</li>
<li>ex) localhost:8080/users</li>
</ul>
<blockquote>
<p>path(&quot;/{id}&quot;)</p>
</blockquote>
<ul>
<li>이후 URI 경로 설정</li>
</ul>
<blockquote>
<p>buildAndExpand(savedUser.getId())</p>
</blockquote>
<ul>
<li>위에서 결정한 파라미터 값 생성</li>
</ul>
<blockquote>
<p>toUri()</p>
</blockquote>
<ul>
<li>URI로 변경</li>
</ul>
<p>이렇게 URI를 만들 수 있다.</p>
<p>여기서 URI를 생성해서 반환하는 이유는</p>
<p>하나의 데이터를 생성한 후 데이터의 상세정보를 볼 때 id값을 다시한번 서버에 물어봐야 한다.</p>
<p>하지만 해당 URI를 반환시킨다면, 다시 물어보는 만큼의 네트워크 트래픽을 줄일 수 있기 때문에 사용하는 것이다.</p>
<hr>
<h1 id="responseentity">ResponseEntity</h1>
<p>RestAPI에서 상태코드와 함께 반환할 때 ResponseEntity를 사용한다.</p>
<p>ResponseEntity에서는 URI를 반환할 때, 필요한 상태코드들을 메서드로 제공한다.</p>
<p>created(201) 뿐만 아니라, accepted(202), noContent(204), badRequest(400), internalServerError(500), notFound(404) 등 자주 쓰이는 상태코드를 가독성 좋은 메소드로 제공한다.</p>
<p>따라서 반환 값을</p>
<pre><code class="language-java">return ResponseEntity.created(location).build();</code></pre>
<p>로 만든 것이다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/25fa3dc4-87ad-4595-b2b2-cb0c71f9f714/image.png" alt=""></p>
<p>이렇게 상태코드를 200번의 상태코드만이 아닌 다양한 상태코드를 사용하여 상태를 반환할 수 있다.</p>
<hr>
<p>참고
<a href="https://velog.io/@jakeseo_me/Spring-Boot%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-RESTful-Web-Services-%EA%B0%9C%EB%B0%9C-14-HTTP-Status-Code-%EC%A0%9C%EC%96%B4">https://velog.io/@jakeseo_me/Spring-Boot%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-RESTful-Web-Services-%EA%B0%9C%EB%B0%9C-14-HTTP-Status-Code-%EC%A0%9C%EC%96%B4</a>
<a href="https://blog.naver.com/PostView.naver?blogId=aservmz&amp;logNo=222322019981">https://blog.naver.com/PostView.naver?blogId=aservmz&amp;logNo=222322019981</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker]]></title>
            <link>https://velog.io/@yoon_bly/Docker</link>
            <guid>https://velog.io/@yoon_bly/Docker</guid>
            <pubDate>Tue, 20 Jun 2023 14:50:23 GMT</pubDate>
            <description><![CDATA[<p>도커란,  컨테이너 기술을 사용하여 애플리케이션에 필요한 환경을 신속하게 구축하고 테스트 및 배포를 할 수 있게 해주는 일종의 가상화 플랫폼이다.</p>
<p>가상화란 물리적 자원인 하드웨어를 효율적으로 활용하기 위해서 하드웨어 공간 위에 가상의 머신을 만드는 기술이고, 컨테이너란 컨테이너가 실행되고 있는 호스트 os의 기능을 그대로 사용하면서 프로세스를 격리해 독립된 환경을 만드는 기술을 뜻한다.</p>
<p>사실, 도커가 세상에 나오기 전 컨테이너 기술은 이미 있었다.</p>
<hr>
<p><strong>가상머신과 도커 비교</strong>
<img src="https://velog.velcdn.com/images/yoon_bly/post/423688e9-a9cb-42d6-b553-76f4a25a95fa/image.png" alt=""></p>
<hr>
<h1 id="가상머신virtual-machines">가상머신(Virtual Machines)</h1>
<p>기존의 가상화 기술인 가상머신(Virtual Machines)은 하이퍼바이저(Hypervisor)를 이용해 여러개의 운영체제를 하나의 호스트에서 생성해서 사용하는 방식이었다.</p>
<p>하이퍼바이저는 호스트 하드웨어에 설치되어 호스트와 게스트를 나누는 역할을 하고, 각각의 게스트는 하이퍼바이저에 의해 관리되며 시스템 자원을 할당받게 된다.</p>
<p>이 때 하이퍼바이저에 의해 생성된 게스트는 호스트나 다른 게스트와 상호 간섭하지 않고 완전히 분리된 환경에서 구동된다.</p>
<p>하이퍼바이저를 활용하면 마치 하드웨어가 여러 개인 것처럼 하나의 서버를 여러 명이 나눠 쓸 수도 있고, 컴퓨터 한 대에서 서로 다른 OS를 동시에 사용할 수도 있다.</p>
<p>이러한 가상화 방식을 사용하는 툴은 VirtualBox, VMware 등이 있다.</p>
<p>하지만, 이러한 가상화 방식은 기존OS 위에 OS를 실행하는 것이므로 리소스(CPU, 메모리 등)을 할당하는데 작업이 필요하고, 게스트 운영체제를 사용하기 위한 라이브러리, 커널 등을 전부 포함하기 때문에 가상 머신을 배포하기 위한 이미지로 만들었을 때 이미지의 크기 또한 커진다.</p>
<p>즉, 가상 머신은 완벽한 운영체제를 생성할 수 있다는 장점은 있지만 일반 호스트에 비해 성능 손실이 있으며, 수 기가바이트에 달하는 가상 머신 이미지를 애플리케이션으로 배포하기는 부담스럽다는 단점이 있다.</p>
<p><strong>정리</strong></p>
<ul>
<li><p>가상머신은 Hypervisor를 통해 여러개의 운영체제를 생성되고 관리됨. (Guest OS)</p>
</li>
<li><p>시스템 자원을 가상화하고 독립된 공간을 생성하는 작업은 HyperVisor를 거치므로 -&gt; 성능 손실이 큼 ↑</p>
</li>
<li><p>가상머신은 Guest OS를 사용하기 위한 라이브러리, 커널 등을 포함하므로 -&gt; 배포할 때 용량이 큼 ↑</p>
</li>
</ul>
<p>이러한 기술을 대체한 것이 바로 도커이다.</p>
<hr>
<h1 id="msamicro-seivice-architecture">MSA(Micro Seivice Architecture)</h1>
<p>사실 요즘 MSA(Micro Service Architecture)가 대세로 떠오르면서 MSA의 단점인 관리의 복잡성을 해결해주기 위해 서비스의 컨테이너화가 이루어지고, 이 과정에서 Docker와 Kubernetes가 많이 도입되었다.</p>
<p>MSA(Micro Service Architectur)란 간단하게 설명하자면, 서비스간의 의존성을 없애고 기능을 쪼개는 것을 중점적으로 설계한 아키텍처이다.</p>
<p>하나의 통합된 프로그램이 아닌 비슷한 기능을 가진 것들을 잘게 쪼개서 만든 것이다.</p>
<p>이런 방식의 아키텍처는 새로운 서비스 개발 시(기능 업데이트) 필요한 기능 부분만 수정하여 비교적 간편하게 작업을 경량화 시킬 수 있다.</p>
<p>전체적인 그림에서 서비스간 결합도(Coupling)를 줄이고 응집도(Cohesion)를 높이는 효과를 볼 수 있다.</p>
<p>MSA에 이런 장점들이 있는 반면에, 서비스들을 관리하기 복잡하다는 단점이 있다. </p>
<p>도커를 사용하면 이런 문제를 쉽게 해결할 수 있다.</p>
<hr>
<h1 id="도커docker">도커(Docker)</h1>
<p>도커 컨테이너는 가상화된 공간을 생성하기 위해 리눅스 자체 기능인 chroot, 네임스페이스(namespace), cgroup을 사용함으로써 프로세스 단위의 격리 환경을 만들기 때문에 성능 손실이 거의 없다.</p>
<p>컨테이너에 필요한 커널을 공유해서 사용하고, 컨테이너 안에는 어플리케이션을 구동하는 데 필요한 라이브러리 및 실행 파일만 존재하기 때문에 컨테이너를 이미지로 만들었을 때 이미지의 용량 또한 가상 머신에 비해 대폭 줄어든다.</p>
<p>따라서 컨테이너를 이미지로 만들어 배포하는 시간이 가상 머신에 비해 빠르며, 가상화된 공간을 사용할 때의 성능 손실도 거의 없다는 장점이 있다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/330d4375-882d-4a9f-8dc9-1a896260e17f/image.png" alt=""></p>
<ul>
<li><p>도커 컨테이너는 가상화된 공간을 생성할 때 리눅스 자체 기능을 사용하여 프로세스 단위의 격리 환경을 만드므로 -&gt; 성능 손실 없음 無</p>
</li>
<li><p>가상머신과 달리 커널을 공유해서 사용하므로, 컨테이너에는 라이브러리 및 실행파일만 있으므로 -&gt; 용량이 작음 ↓</p>
</li>
<li><p>위의 이유로 -&gt; 컨테이너를 이미지로 만들었을 때</p>
<p>배포하는 시간이 가상 머신에 비해 빠르며 ↑, 사용할 때의 성능 손실 또한 거의 없음 ↓</p>
</li>
</ul>
<hr>
<h1 id="도커를-사용하는-이유">도커를 사용하는 이유</h1>
<p>가상머신에 비해 월등히 뛰어나다, 도커는 단점이 없다. 이런 뜻이 아니다. 때에 따라 경우에 따라 사용하는 환경이 다를 뿐이다.</p>
<p>어떤 때에 도커를 사용하는 것일까?</p>
<p>가장 첫번째로 위에 말했듯이 MSA환경을 구축할 때 도커를 주로 이용한다.</p>
<p>도커를 사용하게 되면 IaC(Infrastructure as Code)가 가능 하다.</p>
<p>Docker의 진짜 큰 힘은 인프라를 코드화해서 관리할 수 있다는 점이다.</p>
<p>Dockerfile이나 docker-compose를 만져보면 이전에는 직접 시스템을 세팅해줘야 하는 부분들을 코드로 편리하게 관리할 수 있는 것을 볼 수 있다.</p>
<p>두번째로 도커는 테스트, 스테이징, 배포 등 여러 용도로도 사용 가능하다.</p>
<p>테스트 환경, 스테이징 환경, 배포 환경등이 개발환경과 다를 수 있다.
<img src="https://velog.velcdn.com/images/yoon_bly/post/30c0ead9-d0a2-4561-a7a4-fe154d2c03b0/image.png" alt=""></p>
<p>각각의 환경들을 하나의 레거시 환경에서 돌리게 된다면, 수많은 오류를 마주할 수 있다.</p>
<p>결국 도커를 사용하여 여러 환경에 맞추어 애플리케이션을 컨테이너화 시키면, 개발과 배포의 흐름이 매우 간단해져서 빠르게 진행될 수 있다. </p>
<p>서버의 운영체제나 미들웨어의 의존성에 대해 걱정할 필요없이 Docker만 설치되어 있다면 모든 것이 해결된다.</p>
<p>또한 배포 후, 애플리케이션 사용자 수가 늘어나면 해당 인프라를 확장할 시 scale-out에 매우 큰 도움이 될 수 있다.</p>
<p>결국 우리는 이러한 개발환경에 익숙해져야 하고 잘 쓸줄 알아야 한다.</p>
<p>곧 개발단계가 끝나가는 프로젝트가 있는데, 도커와 aws를 통해 배포해볼 예정이다. </p>
<p>해당 프로젝트를 배포하기전에 docker와 aws에 대해 자세히 알아볼 예정이다.</p>
<hr>
<p>References
<a href="https://myjamong.tistory.com/297">https://myjamong.tistory.com/297</a>
<a href="https://velog.io/@markany/%EB%8F%84%EC%BB%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EC%96%B4%EB%96%A4-%EA%B2%83-1.-%EB%8F%84%EC%BB%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80">https://velog.io/@markany/%EB%8F%84%EC%BB%A4%EC%97%90-%EB%8C%80%ED%95%9C-%EC%96%B4%EB%96%A4-%EA%B2%83-1.-%EB%8F%84%EC%BB%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</a>
<a href="https://seosh817.tistory.com/345#Virtual%20Machine(%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0)%20vs%20Docker%20Container(%EB%8F%84%EC%BB%A4%20%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88)-1">https://seosh817.tistory.com/345#Virtual%20Machine(%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0)%20vs%20Docker%20Container(%EB%8F%84%EC%BB%A4%20%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88)-1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 연관관계 매핑 2]]></title>
            <link>https://velog.io/@yoon_bly/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-2</link>
            <guid>https://velog.io/@yoon_bly/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91-2</guid>
            <pubDate>Tue, 20 Jun 2023 03:40:01 GMT</pubDate>
            <description><![CDATA[<h1 id="일대다-단방향-연관관계">일대다 단방향 연관관계</h1>
<p>일대다 관계는 위에서 봤던 @OneToMany다. 하지만 만약 @OneToMany로 단방향 관계를 맺는다면 어떻게 될까?</p>
<p>다시 말해 @OneToMany 어노테이션이 있는 필드에 @JoinColumn을 아래 코드처럼 건다면 어떻게 될까?</p>
<pre><code class="language-java">@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String teamName;
    @OneToMany
    @JoinColumn(name = &quot;TEAM_ID&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}</code></pre>
<p>해당 엔티티는 일대다 단방향 연관관계 매핑을 한 것이다.</p>
<p>하지만, 해당 외래키는 다쪽인 Member에 생긴다.</p>
<p>애당초 @OneToMany 어노테이션은 외래키 생성을 할 수 없다.</p>
<p>그래서 일대다 단방향 연관관계는 권장되지 않는다.</p>
<p>왜냐하면 연관관계 주인 엔티티에서 외래키를 관리하지않고 반대편 엔티티에서 외래키를 관리하기 때문에(데이터베이스 설계에서 1:M관계에서 외래키는 M에 존재하기 때문에) 관리가 부담스럽다.</p>
<hr>
<h1 id="일대다-양방향-연관관계">일대다 양방향 연관관계</h1>
<p>일대다 양방향 연관관계는 애초에 다대일 양방향 연관관계와 같기 때문에 존재하지 않는다.</p>
<hr>
<h1 id="일대일-단방향-연관관계">일대일 단방향 연관관계</h1>
<p>일대일 관계는 양쪽이 서로 하나의 관계만 가지는 관계이다.</p>
<p>일대일 관계에서는 외래키가 어디에 있든 상관이 없다.</p>
<p>Member 엔티티와 Locker 엔티티가 있고, 1:1 관계라 하자.</p>
<p>외래키는 Member 또는 Locker 엔티티에 존재할 수 있다.</p>
<p>연관관계 주인을 Member로 한 1:1 단방향 관계 코드를 보자.</p>
<pre><code class="language-java">@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToOne
    @JoinColumn(name = &quot;LOCKER_ID&quot;)
    private Locker locker;
}</code></pre>
<p>일대일 단방향 관계도 다대일 단방향과 비슷하다</p>
<p>Locker를 단방향 관계 주인으로 하고 싶으면 @OneToOne을 Locker에 붙여주면 된다.</p>
<p>단지 일대일 단방향 관계는 어디든 주인이 될 수 있다는 점이 특징이다.</p>
<p>반대로 Locker가 연관관계의 주인이 될 수 있다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;
}

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String lockerName;

    @OneToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;
}</code></pre>
<hr>
<h1 id="일대일-양방향-연관관계">일대일 양방향 연관관계</h1>
<p>일대일 양방향 관계도 다대일 양방향 관계랑 비슷하다. 바로 코드를 보자.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToOne(mappedBy = &quot;member&quot;)
    private Locker locker;
}

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String lockerName;

    @OneToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;
}</code></pre>
<p>만약 Member 객체가 연관관계의 주인으로 설정하고 싶다면 mappedBy 속성을 변경해주면 된다.</p>
<hr>
<h1 id="다대일-단방향-연관관계">다대일 단방향 연관관계</h1>
<p>RDBMS에서는 다대다 관계를 2개의 테이블을 이용해 표현이 불가능하다.</p>
<p>그래서 중간에 관계 테이블을 두어 일대다, 다대일 관계로 표현한다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/2aaf1e9b-4704-45ed-ae44-0c14789cc172/image.png" alt=""></p>
<p>하지만 객체는 테이블과 다르게 객체 2개로 다대다 연관관계를 만들 수 있다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String userName;

    @ManyToMany
    @JoinTable(name = &quot;MEMBER_PRODUCT&quot;,
    joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;PRODUCT_ID&quot;))
    private List&lt;Product&gt; product = new ArrayList&lt;&gt;();
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
}</code></pre>
<blockquote>
<p>@ManyToMany</p>
</blockquote>
<ul>
<li>다대다 매핑을 위한 어노테이션</li>
</ul>
<blockquote>
<p>@JoinTable</p>
</blockquote>
<ul>
<li>연결 테이블을 매핑하기 위한 어노테이션</li>
</ul>
<p>&lt;@JoinColumn 속성&gt;</p>
<ul>
<li>@JoinTable.name: 연결 테이블을 지정한다.</li>
<li>@JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정</li>
<li>@JoinTable.inverseJoinColumns: 반대방향인 Product와 매핑할 조인 컬럼 정보 지정</li>
</ul>
<p>MEMBER_PRODUCT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결 테이블일 뿐이기 때문에, 다대다 관계를 사용할 때는 이 연결 테이블을 신경쓰지 않아도 된다.</p>
<hr>
<h1 id="다대다-양방향-연관관계">다대다 양방향 연관관계</h1>
<p>위의 매핑들 처럼 반대 객체에 @ManyToMany(mappedBy = &quot;&quot;)을 사용해 양방향 연관관계를 시킨다.</p>
<pre><code class="language-java">@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String productName;
    @ManyToMany(mappedBy = &quot;product&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @ManyToMany
    @JoinTable(name = &quot;MEMBER_PRODUCT&quot;,
    joinColumns = @JoinColumn(name = &quot;MEMBER_ID&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;PRODUCT_ID&quot;))
    private List&lt;Product&gt; product = new ArrayList&lt;&gt;();
}</code></pre>
<p>역시나 mappedBy 속성이 없는 부분이 연관관계의 주인이다.</p>
<p>양방향으로 연관관계를 매핑했으니 객체 그래프 탐색을 이용해 product.getMembers()를 사용해 역방향으로도 조회할 수 있다.(단방향 관계시 member.getProducts()만 사용해 객체 그래프 탐색 가능)</p>
<hr>
<h1 id="다대다-한계">다대다: 한계</h1>
<p>위의 예제처럼 다대다 연관관계를 사용하면 좋겠지만, 실무에서는 다대다 연관관계를 사용하기에는 한계가 있다.</p>
<p>만약 회원이 상품을 주문할 때 주문 수량이나, 날짜같은 컬럼이 추가가 된다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/55c5e95d-59f3-4de5-b76b-4c0679c554d3/image.png" alt=""></p>
<p>이렇게 컬럼이 추가 되면 더이상 @ManyToMany를 사용할 수 없다.</p>
<p>결국 연결 테이블과의 일대다, 다대일 연관관계를 이용해 매핑해야 한다.</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToMany(mappedBy = &quot;member&quot;)
    private List&lt;MemberProduct&gt; memberProduct = new ArrayList&lt;&gt;();
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
}</code></pre>
<p>Member 객체와 MemberProduct 객체와는 양방향 관계지만, Product 객체와 MemberProduct는 단방향 관계이다.</p>
<p>상품 엔티티에서 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단되었기 때문에 단방향으로 연관관계를 맺었다.</p>
<p>가장 중요한 회원상품 엔티티를 보자.</p>
<h2 id="복합-기본키-사용">복합 기본키 사용</h2>
<p><strong>회원상품 엔티티</strong></p>
<pre><code class="language-java">@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
    @Id
    @ManyToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;

    @Id
    @ManyToOne
    @JoinColumn(name = &quot;PRODUCT_ID&quot;)
    private Product product;

    private String orderAmount;
}</code></pre>
<p><strong>회원상품 식별자 클래스</strong></p>
<pre><code class="language-java">public class MemberProductId implements Serializable {
    private Long member;
    private Long product;

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return super.equals(obj);
    }
}</code></pre>
<p>회원상품 엔티티를 보면 기본키를 매핑하는 @Id와 외래키를 매핑하는 @JoinColumn을 동시에 사용해 기본 키 + 외래 키를 한번에 매핑했다.</p>
<p>그리고 @IdClass 어노테이션을 사용해 복합 기본 키를 매핑했다.</p>
<p>&lt;복합 기본키&gt;</p>
<ul>
<li>복합 기본키를 사용하려면 별도의 식별자 클래스가 필요하다.</li>
<li>Serializabel을 구현해야 한다.</li>
<li>equals와 hashCode 메소드를 구현해야 한다.</li>
<li>기본 생성자가 있어야 한다.</li>
<li>식별자 클래스는 public 이어야 한다.</li>
<li>@IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.</li>
</ul>
<p>이렇게 회원과 상품의 기본 키를 받아 자신의 기본키로 사용하고 외래 키로도 사용하는 것을 식별 관계라고 한다.</p>
<p>하지만 복합 기본 키를 사용하는 방법은 복잡하기 때문에 새로운 키를 사용해 복합 기본 키를 생성하지 않고 매핑하는 것이다.</p>
<p>마치 연결테이블이 새로운 테이블을 만드는 것처럼 사용하는 것이다.</p>
<h2 id="새로운-기본-키-사용">새로운 기본 키 사용</h2>
<p>DB에서 자도으로 생성해주는 대리 키를 Long 값으로 사용헤 기본키를 설정 하는 것이다.</p>
<p>해당 방법은 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다.</p>
<p>또한 ORM매핑 시 복합키를 만들지 않아도 되므로 간단히 매핑을 완성 할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/de867199-8322-4780-ac90-ffea00b9ad07/image.png" alt=""></p>
<p>새로운 ORDER_ID라는 기본키를 만들어 다시 매핑 해보자.</p>
<pre><code class="language-java">@Entity
public class Order {

    @Id
    @GeneratedValue
    @Column(name = &quot;ORDER_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;MEMBER_ID&quot;)
    private Member member;

    @ManyToOne
    @JoinColumn(name = &quot;PRODUCT_ID&quot;)
    private Product product;

    private int orderAmount;
}</code></pre>
<hr>
<h1 id="개인적인-나의-의견">개인적인 나의 의견</h1>
<p>가장 처음 JPA 공부를 할 때, 궁금한 점이 있었다.</p>
<p>만약 회원(1)과 주문(N)간의 매핑을 할 때, 회원의 주문 목록을 들여오기 위해서는 양방향 연관관계를 써야 member.getOrders()로 객체 그래프 탐색을 이용해 주문 목록을 받아 올 수 있다고 생각을 했다.</p>
<p>하지만, 실무에서는 다대일 단방향 연관관계를 사용해도 해당 회원의 주문 목록을 가져올 수 있다는 것이다.</p>
<p>나는 회원의 주문 목록을 가져오기 위해서는 테이블에서 뿐만 아니라, 객체에서도 객체 그래프 탐색을 이용하여 받아와야지만 가능한 줄 알았다.</p>
<p>하지만, 객체 그래프 탐색을 이용하지 않고, 테이블의 양방향 연관관계로 회원Id를 이용해 가져올 수 있다고 알게 되었다.</p>
<p>처음에 나는 객체도 양방향으로 연관관계를 매핑하지 않으면 객체지향의 특징을 잃어 버리는 것인 줄 알았다.</p>
<p>구글링 해본 결과, 양방향으로 매핑하는 이유는 따로 있었다.</p>
<h2 id="양방향-매핑이-필요한-이유">양방향 매핑이 필요한 이유</h2>
<p>&lt;양방향 매핑이 필요한 이유&gt;</p>
<ul>
<li><p>양방향 매핑이 필요한 경우는 단방향 매핑된 관계를 역방향으로 조회할 때다.</p>
</li>
<li><p>데이터에 대한 일관성을 유지할 때 사용된다.</p>
</li>
<li><p>개발을 하다보면 단방향으로 매핑된 연관 관계를 역순으로 조회해야 하는 경우가 발생한다.</p>
</li>
<li><p>이처럼 필요한 경우에 한하여, 양방향 매핑을 추가적으로 구현하는 것을 권장한다.</p>
</li>
</ul>
<p>결국 객체에서의 연관관계를 역순으로 조회할 때 양방향 매핑이 필요한 것이었다.</p>
<p>그게 아니면 단방향으로도 사용 가능하다는 것을 알게 되었다.</p>
<p>그래서 처음 설계시 대부분 단방향으로 사용하고 양방향으로 구현하지 않는 것이 좋다.</p>
<p>이렇게 하나 또 알게 되면서 지식이 늘어나는 것이 행복하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 연관관계 매핑 1]]></title>
            <link>https://velog.io/@yoon_bly/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@yoon_bly/JPA-%EC%97%B0%EA%B4%80%EA%B4%80%EA%B3%84-%EB%A7%A4%ED%95%91</guid>
            <pubDate>Sat, 17 Jun 2023 12:30:30 GMT</pubDate>
            <description><![CDATA[<p>가장 먼저 JPA를 쓰는 이유부터 알아야 한다.</p>
<p>현재 우리는 SQL Mapper나 ORM 기술들을 이용하여, DB와 연결하여 사용하고 있다.</p>
<p>이중 JPA는 자바 진영의 ORM 기술 표준으로, 과거 JDBC API를 사용해 매핑을 해주던 것을 보다 간결하게 만들어주는 프레임워크이다.</p>
<p>뭐 이전보다 SQL문을 덜 쓴다는 것에 대해 장점을 가져서 JPA를 사용하는 것도 있지만, 가장 큰 문제는 따로있다.</p>
<p>바로 <strong>객체와 DB와의 패러다임의 불일치 문제를 해결하기 위해</strong> 사용하는 것이다.</p>
<hr>
<h1 id="객체와-db와의-패러다임-불일치">객체와 DB와의 패러다임 불일치</h1>
<p>자바같은 객체 지향 언어는 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 다양한 장치들을 제공한다.</p>
<p>특히 객체들 간에는 참조를 통해 다른 객체와 연관관계를 가지고, 참조에 접근해 연관된 객체를 조회한다.</p>
<p>반면, 테이블은 외래키(foreign key)를 이용해 다른 테이블과 연관관계를 가지고 조인을 사용해 연관된 테이블을 조회한다.</p>
<p>객체들은 참조가 있는 방향으로만 조회할 수 있다. 하지만 DB의 외래키들은 양방향으로 조회가 가능하다.</p>
<p>결국 우리는 FK, 외래키를 이용한 테이블에 맞춘 객체 모델을 사용해서, DB 테이블을 모델링했다.</p>
<pre><code class="language-java">class Member{

    String id;        // MEMBER_ID 컬럼 사용
    Long teamId;    // TEAM_ID FK 컬럼 사용
    String username;
}

class Team{

    Long id;        // TEAM_ID PK 사용
    String name;
}</code></pre>
<p>이렇게 객체를 테이블에 맞추어 모델링하면 객체를 테이블에 저장하거나 조회할 때는 편하지만, 객체에서는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다.</p>
<p>이렇게 되면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.</p>
<p>결국 객체지향의 특징을 잃지 않고 테이블을 모델링 하기위해 JPA라는 것이 필요한 것이다.</p>
<p><strong>참조를 사용하는 객체 모델</strong></p>
<pre><code class="language-java">class Member{

    String id;        // MEMBER_ID 컬럼 사용
    Team team;        // 참조로 연관관계를 맺는다.
    String username;

    Team getTeam() {
        return team;
    }
}

class Team{

    Long id;        // TEAM_ID PK 사용
    String name;
}</code></pre>
<p>이처럼 참조를 사용하는 객체모델을 사용하면 객체를 테이블에 저장하거나 조회하기 쉽지 않다.</p>
<p>Member 객체는 team 필드로 연관관계를 맺고 Member 테이블은 TEAM_ID로 연관관계를 맺기 때문인데, 객체 모델은 외래키가 필요 없고 단지 참조만 있으면 된다.</p>
<p>반면에 테이블은 참조가 필요없고 외래키만 있으면 된다.</p>
<p>결국, 우리는 JPA를 이용해서 변환역활을 해야 한다.</p>
<hr>
<h1 id="연관관계-매핑">연관관계 매핑</h1>
<p>객체는 참조를 사용해서 관계를 맺고, 테이블은 외래키를 사용해 관계를 맺는다.</p>
<p>이러한 연관관계들을 매핑하는 것이 JPA의 역할이다.</p>
<hr>
<h2 id="키워드">키워드</h2>
<p>연관관계의 매핑을 이해하기 위해 세가지의 키워드가 있다.</p>
<p><strong>방향</strong></p>
<ul>
<li>[단방향, 양방향]</li>
<li>회원과 팀이 관계가 있을 때, 회원 -&gt; 팀, 팀 -&gt; 회원 둘중 한 쪽만 참조하는 것을 단방향관계, 양쪽 모두 서로 참조하는 것을 양방향 관계라 한다.</li>
<li>방향은 객체관계에만 존재, 테이블 관계는 항상 양방향이다.</li>
</ul>
<p><strong>다중성</strong></p>
<ul>
<li>[다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)]</li>
<li>회원과 팀이 관계가 있을 때, 여러 회원은 한팀에 속한다. 따라서 회원과 팀은 다대일 관계다.</li>
<li>반대로 팀과 회원은 일대다 관계이다.</li>
</ul>
<p><strong>연관관계의 주인</strong></p>
<ul>
<li>객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.</li>
</ul>
<hr>
<h2 id="단방향-양방향-연관관계">단방향, 양방향 연관관계</h2>
<h3 id="단방향">단방향</h3>
<p>이 전에 말했듯, 객체는 단방향으로 관계를 맺고, 테이블은 양방향으로 관계를 맺는다.</p>
<p>테이블은 항상 양방향으로 관계를 맺기 때문에, JPA에서 나오는 연관관계의 방향을 객체들의 방향을 의미한다.</p>
<p>예를 들어 Member와 Team 객체가 있다고 가정하자.</p>
<ul>
<li>회원은 하나의 팀에만 소속될 수 있다.</li>
<li>회원과 팀은 다대일 관계이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/781e4cb7-9a61-447b-8909-1cf3d45d1953/image.png" alt=""></p>
<ul>
<li>객체 연관관계<ul>
<li>회원 객체는 Member.team 필드로 팀객체와 연관관계를 맺는다.</li>
<li>따라서 현재 Member와 team 객체는 단방향 관계이다.</li>
<li>단방향 관계이므로, member.getTeam()으로 조회가 가능하지만, team.getMembers()는 불가능하다.</li>
</ul>
</li>
</ul>
<ul>
<li>테이블 연관관계<ul>
<li>회원 테이블은 TEAM_ID 외래 키로 Team 테이블과 연관관계를 맺는다.</li>
<li>테이블은 항상 양방향 관계이다. 따라서 Member Join Team과 Team Join Member 둘다 가능하다.</li>
</ul>
</li>
</ul>
<p>따라서 객체 그래프 탐색으로 Member 에서 Team을 조회할 수 있지만, Team에서는 Member를 조회할 수 없다.</p>
<h3 id="양방향">양방향</h3>
<p>그렇다면 객체 연관관계도 양방향으로 만들면 되는 것이 아닐까라는 생각을 할 것이다.</p>
<p>테이블도 외래키로의 연관관계를 이용해 양방향으로 만드니 객체에서도 양방향으로 만들면 되는거 아니야? 싶을 것이다. </p>
<p>가능하다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/170965a9-d988-4038-87c9-97fa5e4182d6/image.png" alt=""></p>
<p>팀에서 회원을 조회할 수 있게 양방향 연관관계로 만들 수 있다.</p>
<p>테이블은 양방향 연관관계이다. 하지만 객체에서의 연관관계는 양방향처럼 보일지라도, 결국 단방향인 것이 2개인 것이다.</p>
<p>엄밀히 말하면 객체에는 양방향 연관관계라는 것이 없다.</p>
<p>이렇게 단방향과 양방향의 차이를 알아보았다.</p>
<p>이제는 다중성과 방향을 동시에 알아보자.</p>
<hr>
<h1 id="다대일-단방향-연관관계">다대일 단방향 연관관계</h1>
<p>가장 먼저 코드를 통해 확인해보자</p>
<pre><code class="language-java">@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String MemberName;
    @ManyToOne
    @JoinColumn(name=&quot;TEAM_ID&quot;)
    private Team team;
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String teamName;
}</code></pre>
<ul>
<li>객체 연관관계: 회원 객체의 Member.Team 필드 사용</li>
<li>테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용</li>
</ul>
<p>단방향 연관관계에서의 객체와 테이블이 사용할 수 있는 연관관계를 나타내었다.</p>
<p>객체 연관관계는 단방향으로 되어있기 때문에 Team.Member 필드를 사용할 수 없다.</p>
<p>반면에 테이블은 외래키를 이용해 양방향으로 조회를 할 수 있다.</p>
<blockquote>
<p>@ManyToOne</p>
</blockquote>
<ul>
<li>다대일 연관관계를 확인 시키는 어노테이션</li>
<li>현재 Member 객체가 다 쪽이기 때문에 다대일 연관관계 어노테이션을 사용한 것이다.</li>
</ul>
<blockquote>
<p>@JoinColumn(name=&quot;TEAM_ID&quot;)</p>
</blockquote>
<ul>
<li>해당 필드를 외래키로 설정하겠다는 어노테이션이다.</li>
</ul>
<h1 id="다대일-양방향-연관관계">다대일 양방향 연관관계</h1>
<p>양방향 연관관계 매핑 코드는 다음과 같다.</p>
<pre><code class="language-java">@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String teamName;
    //양방향 매핑을 위해 추가
    @OneToMany(mappedBy = &quot;team&quot;)
    private List&lt;Member&gt; members = new ArrayList&lt;&gt;();
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String MemberName;
    @ManyToOne
    @JoinColumn(name=&quot;TEAM_ID&quot;)
    private Team team;
}</code></pre>
<p>단방향 연관관계와 다르게 Team필드에 Member를 저장할 수 있는 필드가 생성된 것을 볼 수 있다.</p>
<p>해당 필드로 인해 Team에서 Member를 객체 그래프 탐색을 통해 조회할 수 있게 된다.</p>
<blockquote>
<p>@OneToMany(mappedBy = &quot;team&quot;)</p>
</blockquote>
<ul>
<li><p>객체에서의 양방향 관계는 단방향인 관계가 양쪽으로 있다고 설명했다. 따라서 다대일의 반대는 일대다 이므로 OneToMany어노테이션을 사용한다.</p>
</li>
<li><p>mappedBy 속성은 양방향 매핑일 때 사용된다.</p>
</li>
</ul>
<p>여기서 연관관계의 주인에 대해 나온다.</p>
<p>객체에는 양방향 관계는 없고 단방향인 연관관계가 두개 있다는 것인데, 그렇다면 외래키도 두개여야 할 것이라는 생각이 들 것이다.</p>
<p>하지만 테이블 특성상 외래키는 한부분에 있고, 한 곳에서 양방향 연관관계를 맺는다.</p>
<p>이 때문에 JPA는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리 해야 한다.</p>
<p>이것을 연관관계의 주인이라고 한다.</p>
<h3 id="양방향-매핑의-규칙">양방향 매핑의 규칙</h3>
<p>연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)를 할 수 있다.반면에 주인이 아닌 쪽은 읽기만 할 수 있다.</p>
<p>그래서 연관관계의 주인은 내맘대로 설정하는 것인가? </p>
<p>그것도 아니다. 대부분 외래키가 있는 곳이 연관관계의 주인이 된다.</p>
<p>왜냐하면, 그것이  더 관리하기 편하기 때문이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-06-08)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-06-08</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-06-08</guid>
            <pubDate>Thu, 08 Jun 2023 13:23:41 GMT</pubDate>
            <description><![CDATA[<p>오늘은 메인페이지에서 제공하는 여러 게시글들에 대한 페이지네이션 기능들을 구현해보았다.</p>
<p>우선 페이지네이션이란, 필요한 데이터들을 페이지 별로 나타내는 것으로,</p>
<p>한 페이지에 수백만개에 해당하는 게시글 데이터들을 조회해 화면에 렌더링하는 경우, 클라이언트가 브라우저 혹은 모바일 기기로 이를 한 눈에 보기 어려움을 겪을 공산이 크다.</p>
<p>또한 클라이언트가 보지도 않을 데이터까지 DB에서 조회하여 네트워크를 통해 전달하기 때문에, 서버의 리소스가 불필요하게 낭비된다.</p>
<p>그래서 한 페이지에서는 N개의 데이터만 보여주고, 다음 페이지로 이동하라는 클라이언트의 추가 요청이 있을 때 마다 다음 순번의 N개의 데이터를 보여준다면 UX 및 리소스 측면의 단점을 보완할 수 있다.</p>
<p>그렇기에 페이지네이션 처리를 하는 것이다.</p>
<p>사실 무한 스크롤방식을 이용해 데이터 렌더링을 하려 했으나, 백엔드 파트에서 조금 더 공부하고자 페이지네이션 처리를 했다.</p>
<p>사실 이 전에 게시판을 만들어 보았을 때 페이지네이션 기능을 구현해 보았으나, 구현하기 많은 코드와 그 당시 코딩을 공부하기 초반이라 이해하지 못했다.</p>
<p>하지만 구글링을 하다가 Spring Data JPA를 이용해 페이지네이션 기능을 구현할 수 있는 것을 보았고, 이 방법이 나에게 맞을 것 같아 공부해 보았다.</p>
<hr>
<p><strong>PageController</strong></p>
<pre><code class="language-java">// 메인페이지 게시글 리스트
    @GetMapping(&quot;/&quot;)
    public ResponseEntity&lt;Page&lt;PostEntity&gt;&gt; postList(Model model, @PageableDefault(page = 0,size = 20,sort = &quot;postIndex&quot;,direction = Sort.Direction.DESC) Pageable pageable){
        // 로그인 세션 환경 설정

        Page&lt;PostEntity&gt; readAll = postService.readAll(pageable);

        // 게시글 최신순으로 정렬
        // 페이징 처리
        int nowPage = readAll.getPageable().getPageNumber()+1; //pageable이 갖고 있는 페이지는 0부터 시작하기 때문에
        int startPage = Math.max(nowPage -4,1);
        int endPage = Math.min(nowPage +5,readAll.getTotalPages());


        model.addAttribute(&quot;posts&quot;, readAll);
        model.addAttribute(&quot;nowPage&quot;, nowPage);
        model.addAttribute(&quot;startPage&quot;,startPage);
        model.addAttribute(&quot;endPage&quot;,endPage);



        return new ResponseEntity&lt;&gt;(readAll, HttpStatus.OK);
    }</code></pre>
<p>Spring Data JPA 기능 중 JpaRepository가 있다.</p>
<p>현재 나는 이 레파지토리 인터페이스를 이용해 CRUD 및 기타 기능을 이용하고 있다.</p>
<p>JpaRepository의 부모 인터페이스로 PagingAndSortingRepository 에서 페이징과 소팅이라는 기능을 제공한다.</p>
<p>나는 이 PagingAndSortingRepository를 이용해 페이지네이션 처리를 할 것이다.</p>
<p>PagingAndSortingRepository를 이용하려면 Pageable이라는 인터페이스를 파라미터로 받아야 한다.</p>
<blockquote>
<p>@PageableDefault</p>
</blockquote>
<ul>
<li>Pageable의 기본정보를 입력할 수 있다.</li>
<li>page : default 페이지</li>
<li>size : 한 페이지 게시글 수</li>
<li>sort : 정렬 기준 컬럼(여기에서는 글 번호 id)</li>
<li>direction : 정렬 순서 (보통 게시판은 최근 글이 위로 올라와 있기 때문에 역순으로 설정함.)</li>
</ul>
<p>받아온 파라미터를 service 부분에 넘긴다.</p>
<hr>
<p><strong>PostService</strong></p>
<pre><code class="language-java">    // 게시글 리스트 가져오기
    // 페이징 처리
    public Page&lt;PostEntity&gt; readAll(Pageable pageable){
        Page&lt;PostEntity&gt; list = postRepository.findAll(pageable);

        return list;
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/a3592243-4bb6-454c-9a31-cea17fe6d5d5/image.png" alt=""></p>
<p>JpaRepository 부모 인터 페이스인 PagingAndSortingRepository를 보면 findAll메서드에 Pageable인자가 들어가 있는 것을 확인 할 수 있다.</p>
<p>findAll메서드로 DB에서 게시글들을 Page형태로 받아와 반환한다.</p>
<hr>
<p><strong>PageController</strong></p>
<pre><code class="language-java">        int nowPage = readAll.getPageable().getPageNumber()+1; //pageable이 갖고 있는 페이지는 0부터 시작하기 때문에
        int startPage = Math.max(nowPage -4,1);
        int endPage = Math.min(nowPage +5,readAll.getTotalPages());


        model.addAttribute(&quot;posts&quot;, readAll);
        model.addAttribute(&quot;nowPage&quot;, nowPage);
        model.addAttribute(&quot;startPage&quot;,startPage);
        model.addAttribute(&quot;endPage&quot;,endPage);</code></pre>
<p>여기서 현재 나의 페이지, 첫 페이지, 그리고 마지막 페이지를 지정해     view단으로 데이터를 넘기면 자연스럽게 페이지네이션 처리가 된다.</p>
<hr>
<p>이렇게 메인 페이지의 게시글들을 페이징 하였지만, 또 다른 문제가 있었다.</p>
<p>내가 기획한 이 프로젝트는 지역별로 게시글들을 나눌 수 있게 만들어 놓았다.</p>
<p>따라서 메인페이지 뿐만아니라 지역별로 필터링한 게시글들도 페이징 처리를 해야했다.</p>
<p>사실 어떤 방식으로 시작해야 할지 잘 몰랐다.</p>
<p>Pageable 인자를 받으면서 해당 데이터를 필터링해 페이징 처리를 해야 하는 것이기 때문에,</p>
<p>service 부분에서 필터링할 데이터와 Pageable 값을 같이 보내야 했다.</p>
<p>그러다 이전 게시글에서 확인할 수 있는 검색기능의 Containing이 생각이 났다.</p>
<p>분명 PagingAndSortingRepository에서는 findAll(Pageable pageable)메서드만 가능 했지만, 내가 만들어서 사용할 수 있지 않을까? 라는 생각을 했다.</p>
<p>그래서 바로 실행에 옮겼다.</p>
<hr>
<p><strong>PageController</strong></p>
<pre><code class="language-java">    // 시/도 select box만 설정했을 경우
    @GetMapping(&quot;/{postSido}&quot;)
    public ResponseEntity&lt;Page&lt;PostEntity&gt;&gt; searchSido(@PathVariable(&quot;postSido&quot;) String postSido,
                                                            @PageableDefault(page = 0,size = 20,sort = &quot;postIndex&quot;,direction = Sort.Direction.DESC) Pageable pageable,
                                                            Model model){
        Page&lt;PostEntity&gt; sidoList = postService.findSido(postSido, pageable);

        // 페이징 처리
        int nowPage = sidoList.getPageable().getPageNumber()+1; //pageable이 갖고 있는 페이지는 0부터 시작하기 때문에
        int startPage = Math.max(nowPage -4,1);
        int endPage = Math.min(nowPage +5,sidoList.getTotalPages());

        model.addAttribute(&quot;sidoList&quot;, sidoList);
        model.addAttribute(&quot;nowPage&quot;, nowPage);
        model.addAttribute(&quot;startPage&quot;,startPage);
        model.addAttribute(&quot;endPage&quot;,endPage);


        return new ResponseEntity&lt;&gt;(sidoList, HttpStatus.OK);
    }</code></pre>
<p>메인 페이지 메서드와 다른 것은 service api로 넘어갈 때 postSido를 추가로 파라미터에 넣어 보낸다는 것이다.</p>
<hr>
<p><strong>PostService</strong></p>
<pre><code class="language-java">    // 시도 select box 찾기
    public Page&lt;PostEntity&gt; findSido(String postSido, Pageable pageable) {
        Page&lt;PostEntity&gt; sidoList = postRepository.findByPostSidoContaining(postSido, pageable);

        return sidoList;
    }</code></pre>
<p>findByPostSidoContaining메서드에 Pageable 인자를 추가로 넣어 Page 타입으로 데이터를 가져오게 하였다.</p>
<pre><code class="language-java">Page&lt;PostEntity&gt; findByPostSidoContaining(String postSido, Pageable pagealbe);</code></pre>
<p>이 때, 가져오는 데이터는 postSido에 postSido라는 데이터를 포함하는 데이터만 가져오도록 하였다.</p>
<p>그 후, 가져온 데이터를 반환하는 것이었다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/dd0819e4-b53c-44fb-9c06-2689fe1b3039/image.png" alt="">
해당 테스트는 시/도 설정을 용인시로 설정한 페이지의 마지막 페이지이다.
(기존 size는 20이지만 테스트를 위해 5로 변경)</p>
<p>보다시피 페이지에 따른 데이터들이 나왔다.</p>
<p>이렇게 필터링에 따른 데이터 페이징도 완성했다.</p>
<hr>
<p>드디어 모든 기능들을 구현하였다.</p>
<p>이제 진짜 얼마 남지 않았다.</p>
<p>필터링에 따른 페이징 처리에 대해 애를 많이 썼는데, 혼자서 생각하여 문제를 해결한 것에 대해 정말 행복하고, 실력이 늘고 있구나에 대한 생각이 들었다.</p>
<p>남은 프로젝트도 열심히 하여 끝마치고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-06-06)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-06-06</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-06-06</guid>
            <pubDate>Tue, 06 Jun 2023 14:33:45 GMT</pubDate>
            <description><![CDATA[<p>오늘은 게시글 검색 기능 구현을 해보았다.</p>
<p>대부분의 기능을 거의 다 만들어 놓아서 최근 JPA공부를 했었다.</p>
<p>프론트 부분도 js부분을 뺀 나머지 부분은 거의 다 완성되어 가서 다시 프로젝트를 진행하였다.</p>
<p>원래 게시글 제목과 내용에서의 키워드를 검색하면 해당 게시글들을 반환하도록 하려 했지만,</p>
<p>아직 JPA를 잘 다루지 못함 + JPQL을 잘 사용하지 못하여 우선 JpaRepository를 사용하여 검색 기능을 만들기로 하였다.</p>
<p>검색 기능은 프로젝트가 끝나기 전까지 JPA를 이용해서 검색기능을 리펙토링 하던지, JpaRepository를 이용해서 게시글 내용에서도 키워드를 찾을 수 있도록 하던지 할 예정이다.</p>
<p><strong>PageController</strong></p>
<pre><code class="language-java">    @GetMapping(&quot;/post/search&quot;)
    public ResponseEntity&lt;List&lt;PostEntity&gt;&gt; search(@RequestParam(&quot;keyword&quot;) String keyword, Model model){
        List&lt;PostEntity&gt; searchList = postService.search(keyword);
        model.addAttribute(&quot;searchList&quot;, searchList);

        return new ResponseEntity&lt;&gt;(searchList, HttpStatus.OK);
    }</code></pre>
<p>HTML에서의 input 태그로 해당 키워드를 가져오도록 만들었다.</p>
<p>키워드는 RequestParam 어노테이션으로 가져온다.</p>
<p>가져온 키워드를 postService의 search 메서드로 넘겨 해당 키워드를 가진 제목의 게시글들을 가져온다.</p>
<hr>
<p><strong>PostService</strong></p>
<pre><code class="language-java">    public List&lt;PostEntity&gt; search(String keyword) {

        List&lt;PostEntity&gt; search = postRepository.findByTitleContaining(keyword);
        List&lt;PostEntity&gt; searchList = new ArrayList&lt;&gt;();

        if(search.isEmpty()) return search;

        for(PostEntity post : search) {
            searchList.add(post);
        }
        return searchList;
    }   </code></pre>
<p>해당 키워드를 postRepository의 findByTitleContaining이라는 메서드로 보낸다.</p>
<p>사실 여기서 가장 중요한 부분은 postRepository의 메서드라고 생각한다.</p>
<pre><code class="language-java">@Repository
public interface PostRepository extends JpaRepository&lt;PostEntity, Long&gt;{
    List&lt;PostEntity&gt; findByTitleContaining(String keyword);
}</code></pre>
<p>기본적으로 JpaRepository에서 메소드명의 By 이후는 SQL의 where 조건 절에 대응된다.</p>
<p>findByTitleContaining은 title에서 들어있는 내용을 찾는 Like검색과 동일하다고 생각하면 된다.</p>
<p>가져온 PostEntity를 for문을 이용해 추가한 뒤, 반환한다.</p>
<hr>
<p>검색 기능에 대해 postman으로 테스트를 진행하려는데 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/87225eed-5df1-4d64-b2c4-f1d8e8aa1865/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/5ab2eff0-0795-4ee1-a4e5-ed3b9124b257/image.png" alt="">
키워드라는 파라미터가 존재하지 않는다는 오류가 나왔다.</p>
<p>분명 다른 메서드들과 똑같이 RequestParam을 사용해서 form 태그로 데이터를 보냈는데도, 계속 오류가 났다.</p>
<p>그래서 오타가 있는지도 오랬동안 확인해 보았지만, 오타는 없고, 계속 오류가 났다.</p>
<p>한참동안 이 문제를 가지고 고전했다.</p>
<p>분명 다른 메서드와 다른건 매핑 방법(Get, Post)밖에 없는데 왜 안되는 거지 라는 생각을 했다.</p>
<p>근데 문득 공부한 것이 생각이 났다.</p>
<p>서버로 데이터를 보내는 방법중 Get방식으로 쿼리 파라미터를 보내는 것과, Post방식으로 form태그를 이용해 보내는 방법이 있다는 것이 기억이 났다.</p>
<p>그래서 내가 velog에 복습 겸 써놓았던 것들을 확인했다.</p>
<p><a href="https://velog.io/@yoon_bly/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0%EC%84%9C%EB%B8%94%EB%A6%BF#%EC%A4%91%EC%9A%943-http-%EC%9A%94%EC%B2%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0---%EA%B0%9C%EC%9A%94">https://velog.io/@yoon_bly/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1%ED%8E%B8-%EB%B0%B1%EC%97%94%EB%93%9C-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EC%88%A0%EC%84%9C%EB%B8%94%EB%A6%BF#%EC%A4%91%EC%9A%943-http-%EC%9A%94%EC%B2%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0---%EA%B0%9C%EC%9A%94</a></p>
<p>이 게시글에서 데이터 요청 방법을 내가 복습해 놓았던 것이다.</p>
<p>GetMapping으로 파라미터를 보내는 방법은 url 뒤에 파라미터로 보내는 것이고,
PostMapping으로 파라미터를 보낼 때는 form태그를 이용해 HTTP Body에 파라미터를 넣어 보내는 것이다.</p>
<p>그래서 url뒤에 파라미터를 넣어서 테스트 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/d6e5acde-1ed3-4c8b-bbf2-40bf3497478d/image.png" alt=""></p>
<p>사진에서 보다시피 url뒤에 &quot;?keyword=가나다&quot;라는 파라미터를 넣어서 데이터를 보냈더니 정상적으로 데이터가 보내져 &quot;가나다&quot;라는 keyword를 가진 게시글을 정상적으로 받아왔다.</p>
<p>다행히도 해당 문제를 해결해서 다행이다.</p>
<hr>
<p>오늘은 내가 여태까지 공부했던 것들을 복습해 둔것을 정말 다행이라고 생각하고 장하다고 생각된 하루였다.</p>
<p>만약 내가 공부한 것을 복습해두지 않았더라면 오늘 이 문제에 더 많은 시간을 투자했을 것이다.</p>
<p>이렇게 다시한번 복습이 얼마나 중요한지 알게 되었던 하루였다.</p>
<p>물론 복습을 했지만, 기억을 못한 나에게 조금 실망스러웠지만, 그래도 복습을 했기 때문에 시간을 별로 쓰지 않았다고 생각한다.</p>
<p>앞으로 남은 프로젝트 일들도 빠르게 마무리 하여서 완성된 페이지를 얼른 만들어 보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-31)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-31</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-31</guid>
            <pubDate>Wed, 31 May 2023 10:55:54 GMT</pubDate>
            <description><![CDATA[<p>오늘은 내가 짠 코드에 대한 검토를 진행하였다.</p>
<p>서비스를 배포했을 때, 코드들의 진행에 따라 테스트를 해보았다.</p>
<p>예를 들면,</p>
<p>1) 회원 가입 -&gt; 로그인 -&gt; 게시글 작성</p>
<p>2) 여러 사용자들이 게시글 여러개 작성 -&gt; 시/군/동 select box에 따른 게시글 필터링 -&gt; 내 게시글 페이지</p>
<p>3) 로그인 -&gt; 댓글 작성 -&gt; 수정 및 삭제</p>
<p>등등 여러가지 상황을 고려해 사용자가 해당 서비스를 이용할 때 진행되는 구조에 대한 테스트를 진행해보았다.</p>
<p>결과는 모두 성공. </p>
<p>내가 원하는 대로 결과 값들이 나왔다.</p>
<p>대부분의 기능들을 완성하고 오늘처럼 테스트를 하고나니, 정말 내가 무언가 만들었다는 생각에 기분이 좋아졌다.</p>
<p>이런 성취감이 드는 일이 있을 때마다, 내가 이 개발 공부를 하는 것을 잘했다고 생각하게 된다.</p>
<p>아직 게시글 검색 기능, 프론트 작업을 마친 후 프론트단에 따른 백엔드 코드 수정, 리턴값 변경, 프론트단과 백단간의 데이터 통신 작업, 성능 최적화 등등 할일이 많다.</p>
<p>하지만 걱정이 들지는 않다.</p>
<p>이미 이 프로젝트에 대한 애정이 깊어져 어떻게든 완성을 하겠다는 생각이 계속 든다.</p>
<p>앞으로 다른 프로젝트도 진행하게 될 것이고, 공부도 더 열심히 해서 내가 개발에 대한 이해도가 높아지면 리펙토링을 통해 업데이트도 진행 할 것이다.</p>
<p>그렇기엔 이 공부를 포기하지 않을 것이다.</p>
<p>별로 길지 않은 인생을 살았지만, 이 정도로 공부가 재밌는 것이 처음이다.</p>
<p>얼른 더 많은 것을 배우고 싶다.</p>
<p>그렇기에 더욱 열심히 하고, 열심히 복습해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-30)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-30</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-30</guid>
            <pubDate>Tue, 30 May 2023 09:18:04 GMT</pubDate>
            <description><![CDATA[<p>오늘은 댓글 기능을 구현해 보았다.</p>
<p>가장 먼저 댓글이 보여지는 화면은 게시글 상세페이지에 들어가면 보이기 때문에 postController에 replyService 메서드를 기입했다.</p>
<hr>
<p><strong>PostController</strong></p>
<pre><code class="language-java">    // 게시글 상세보기
    @GetMapping(&quot;/post/{postId}&quot;)
    public ResponseEntity&lt;Integer&gt; read(HttpSession session, @PathVariable(name = &quot;postId&quot;) Long postId, Model model) {
        Long userIndex = (Long) session.getAttribute(&quot;userIndex&quot;);

        // 게시글 model
        Optional&lt;PostEntity&gt; post = postService.read(postId);
        model.addAttribute(&quot;post&quot;, post);

        // 댓글 model
        List&lt;ReplyEntity&gt; replyList = replyService.read(postId);
        model.addAttribute(&quot;reply&quot;, replyList);


        // 세션을 가져와서 만약 게시글, 댓글 userIndex와 같다면, 1을 출력해 수정 및 삭제 버튼 활성화 / 다르면 0 or 2 출력
        // 수정 필요
        Integer checkedUserIndex = 0;
        if(userIndex != null){
            if(userIndex == post.get().getUserIndex()){
                // 게시글, 댓글, 세션이 같으면
                checkedUserIndex = 1;
                model.addAttribute(&quot;checkIndex&quot;, checkedUserIndex);
            } else if(userIndex != post.get().getUserIndex()){
                checkedUserIndex = 2;
                model.addAttribute(&quot;checkIndex&quot;, checkedUserIndex);
            } else {
                model.addAttribute(&quot;checkIndex&quot;, checkedUserIndex);
            }
        }


        return new ResponseEntity&lt;&gt;(checkedUserIndex, HttpStatus.OK);
        // return &quot;post_detail&quot;;
    }</code></pre>
<p>해당 게시글의 index를 가져와서 댓글들을 가져오게 만들었다.</p>
<p>여기서 댓글도 만약 로그인한 유저가 단 댓글일 때, 수정 및 삭제 버튼이 생성되어야 한다.</p>
<p>만약 게시글 userIndex가 같다면, 게시글이 자신의 것이기 때문에 댓글도 자신의 것일 것이다.</p>
<p>그래서 댓글에 대한 수정 및 삭제 버튼도 게시글 수정 및 삭제 버튼과 같이 조건문으로 값을 가져와 버튼 display를 활성화/비활성화 시키면 된다.</p>
<p><strong>ReplyService</strong></p>
<pre><code class="language-java">@Service
public class ReplyService {

    @Autowired
    private ReplyRepository replyRepository;

    // 댓글 저장
    @Transactional
    public ReplyEntity save(ReplyDto replyDto, Long userIndex, Long postIndex) {
        ReplyEntity replyEntity = replyDto.toEntity(userIndex, postIndex);

        replyRepository.save(replyEntity);

        return replyEntity;
    }

    // 게시글 읽어올 때 해당 게시글 댓글 불러오기
    public List&lt;ReplyEntity&gt; read(Long postIndex) {
        List&lt;ReplyEntity&gt; replyList = replyRepository.findByReplyPostIndex(postIndex);

        return replyList;
    }

    // 댓글 수정
    @Transactional
    public Optional&lt;ReplyEntity&gt; edit(Long replyIndex, ReplyDto replyDto) {
        Optional&lt;ReplyEntity&gt; reply = replyRepository.findById(replyIndex);

        return reply.map(r -&gt; {
            reply.get().setReplyDescription(replyDto.getReplyDescription());

            return r;
        })

            .map(r -&gt; replyRepository.save(r));
    }

    // 댓글 삭제
    public void delete(Long replyIndex) {
        replyRepository.deleteById(replyIndex);
    }
}</code></pre>
<p>해당 메서드들은 게시글 메서드와 거의 비슷한 형태를 띄고있다.</p>
<hr>
<p>이제 백엔드는 거의 마무리 단계에 돌입했다.</p>
<p>이제 내가 짠 코드를 검토해보고, 메모리 최적화, return 값 변경, thymeleaf 설정등이 남았다.</p>
<p>아직 성능 최적화, 메모리 최적화를 해본적이 없어서 좀 더 구글링 해보고 찾아봐야한다.</p>
<p>아직 프로젝트가 완성된 것은 아니지만, 무언가 내가 생각해낸 것을 만들어 냈다는 생각에 기분이 좋아진다.</p>
<p>아직 부족한 부분이 많지만, 좀 더 성능 개선에 힘쓰고, 공부를 더 해야겠다.</p>
<p>얼른 프론트 부분이 마무리 지어져 내가 만든 것들을 postman이 아닌 화면으로 테스트 해보고 싶다.</p>
<p>얼마 안남은 프로젝트 확실하게 마무리 하고 싶은 마음이 더 커지는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-25)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-25</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-25</guid>
            <pubDate>Fri, 26 May 2023 05:31:14 GMT</pubDate>
            <description><![CDATA[<p>오늘은 게시글 CRUD 부분에 대한 수정을 하였다.</p>
<p>이전의 회고록 마지막 부분에 써놨던, 해당 게시글이 로그인한 유저의 게시글인지 확인하는 방법에 대하여 고민하고 코드를 작성해보았다.</p>
<p>해당 고민은 자신의 게시글일 경우 수정 및 삭제를 할 수 있도록 해야하므로 수정 및 삭제 버튼을 활성화/ 비활성화 시킬 방법을 모색한 것이다.</p>
<p>로그인 부분을 혼자 공부하기도 했고, 기능구현도 처음이다 보니 이게 정확한 방법, 제대로된 방법은 아닐 것이다.</p>
<p>하지만, 나만의 생각으로 고민하고, 기능을 구현하는 것은 분명 나중에 도움이 될것이라고 생각된다.</p>
<hr>
<p>로그인한 유저의 게시글인지 확인하려면 필요한 것은 로그인한 유저의 세션에 저장된 userIndex와 게시글의 userIndex가 같은지 확인하면 된다.</p>
<p>만약 두 userIndex가 같다면 자신의 글인 것이니까.</p>
<p>postController를 보면 해당 게시글을 찾아 View단으로 뿌리는 로직이다.</p>
<pre><code class="language-java">    // 게시글 상세보기
    @GetMapping(&quot;/post/{postId}&quot;)
    public ResponseEntity&lt;Integer&gt; read(HttpSession session, @PathVariable(name = &quot;postId&quot;) Long postId) {

        Optional&lt;PostEntity&gt; post = postService.read(postId);
        model.addAttribute(&quot;post&quot;, post);

        return new ResponseEntity&lt;&gt;(post, HttpStatus.OK);
        // return &quot;post_detail&quot;;
    }</code></pre>
<p>해당 url로 데이터를 보내려면 우선 게시글 정보는 필수로 view단에 넘겨야 한다.</p>
<p>그래서 나는 생각했다.</p>
<p>이름이 다른 model을 생성해서 하나는 게시글 정보를, 다른 하나는 로그인 세션userIndex가 게시글 userIndex와 같은지 체크하는 정보를.</p>
<p>그래서 혹시 model 값을 js로 가져와 index가 같으면 버튼 display를 활성화 시키고 index가 다르면 display를 비활성화 시킬수 있나 라는 생각을 하게 되었고, 구글링을 해보았다.</p>
<blockquote>
<p><a href="https://mollangpiu.tistory.com/331">https://mollangpiu.tistory.com/331</a></p>
</blockquote>
<p>해당 게시물을 보면 타임리프로 받아온 데이터를 js에서도 사용할 수 있는 것을 확인하였고, 바로 로직을 구현해 보았다.</p>
<hr>
<pre><code class="language-java">    // 게시글 상세보기
    @GetMapping(&quot;/post/{postId}&quot;)
    public ResponseEntity&lt;Integer&gt; read(HttpSession session, @PathVariable(name = &quot;postId&quot;) Long postId, Model model) {
        Long userIndex = (Long) session.getAttribute(&quot;userIndex&quot;);

        Optional&lt;PostEntity&gt; post = postService.read(postId);
        model.addAttribute(&quot;post&quot;, post);


        // 세션을 가져와서 만약 게시글 userIndex와 같다면, 1을 출력해 수정 및 삭제 버튼 활성화
        Integer checkedUserIndex = 0;
        if(userIndex != null){
            if(userIndex == post.get().getUserIndex()){
                model.addAttribute(&quot;correct&quot;, &quot;1&quot;);
                checkedUserIndex = 1;
            } else if(userIndex != post.get().getUserIndex()){
                model.addAttribute(&quot;incorrect&quot;, &quot;0&quot;);
                checkedUserIndex = 2;
            }
        }

        return new ResponseEntity&lt;&gt;(checkedUserIndex, HttpStatus.OK);
        // return &quot;post_detail&quot;;
    }</code></pre>
<p>해당 로직을 보면, 우선 세션을 통해 로그인한 유저의 index를 가져온다.</p>
<p>그리고, 게시글의 userIndex를 가지고 올 수 있도록 먼저 게시글을 찾아온다.</p>
<p>그 후, 사용자 userIndex와 게시글 userIndex가 같으면 1, 다르면 2, 로그인이 되어있지 않은 상태에서 게시글을 보게되면 0을 출력하게 만들었다.</p>
<p>나의 생각은 이렇다.</p>
<p><strong>model로 받은 게시글 데이터는 바로 보여주고, 자신의 게시글인지 체크하는 model은 js로 받아 0,2를 받아오면 수정 및 삭제 버튼 비활성화, 1을 받아오면 수정 및 삭제 버튼 활성화</strong></p>
<p>이렇게 만들어보았다.</p>
<p>이 이후의 수정 페이지 들어가는 로직, 수정 로직, 삭제 로직은 방금 설명한 상세 게시글 들어갈 때 이미 자신의 게시글인지 확인할 수 있기 때문에 자신의 게시글인지 체크할 이유는 없을 것 같다.</p>
<hr>
<p>혼자 공부하면서 이렇게 내가 혼자 생각해서 만드는 로직이 참 재미있는 것 같다.</p>
<p>뭔가 내가 직접 만들었다는 생각이 드니 기분이 좋아진다.</p>
<p>이런 경험도 성장하는데 일부분을 차지하는 것 일지는 모르겠지만, 이렇게 계속 꾸준히 공부하고, 생각하면 언젠간 도움이 될거라 생각한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-23)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-23</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-23</guid>
            <pubDate>Tue, 23 May 2023 07:41:49 GMT</pubDate>
            <description><![CDATA[<p>오늘은 간단하게 게시글 사용시 로그인 부분과 게시글의 사용언어 entity를 구현을 했다.</p>
<p>또, 회원가입 시 아이디 및 닉네임 중복 체크 버튼 없이 중복 체크를 하는 방법에 대해 찾아보았다.</p>
<p>jpa를 잘 다루지 못하는 나이기에 postEntity에 userIndex를 추가하여 해당 게시글이 사용자의 게시글인 것을 알 수 있게 해야했다.</p>
<p>그래서 가장 먼저 PostEntity를 수정하였다.</p>
<h2 id="엔티티-수정">엔티티 수정</h2>
<pre><code class="language-java">@Entity(name = &quot;post&quot;)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table
public class PostEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long postIndex;

    @Column(length = 1000)
    @NotNull
    private String title;

    @Column(length = 1000)
    @NotNull
    private String description;

    @CreationTimestamp //INSERT 쿼리가 발생할 때, 현재 시간을 값으로 채워서 쿼리를 생성한다.
    @Column
    private LocalDateTime postRegDate;

    @Column(length = 1000)
    @NotNull
    private String postSido;

    @Column(length = 1000)
    @NotNull
    private String postGugun;

    @Column(length = 1000)
    @NotNull
    private String postDong;

    @Column(length = 1000)
    private String postLanguage1;

    @Column
    private String postLanguage2;

    @Column
    private String postLanguage3;

    // 유저 인덱스
    @Column
    @NotNull
    private Long userIndex;

    public PostDto toDto() {
        PostDto postDto = PostDto.builder()
        .title(title)
        .description(description)
        .postRegDate(postRegDate)
        .postSido(postSido)
        .postGugun(postGugun)
        .postDong(postDong)
        .postLanguage1(postLanguage1)
        .postLanguage2(postLanguage2)
        .postLanguage3(postLanguage3)
        .userIndex(userIndex)
        .build();

        return postDto;
    }
}</code></pre>
<p>이전의 PostEntity와 다른 점은 사용언어인 postLanguage를 3개로 늘렸다.</p>
<p>원래는 하나의 postLanguage 컬럼을 만들어 여러 데이터를 집어 넣으려했지만, 방법도 모르겠고, 찾아보니 관계형 DB에서는 한 컬럼에 여러 데이터를 넣는 것은 좋지 않다는 것을 알게 되었다.</p>
<p>그래서 그냥 컬럼을 여러개 만들고 Null이어도 되게 만들었다.</p>
<p>또한 userIndex를 컬럼으로 가져왔다.</p>
<hr>
<h2 id="게시글-작성-수정">게시글 작성 수정</h2>
<h3 id="controller">Controller</h3>
<p>다음으로 Controller의 게시글 작성 메서드를 수정하였다.</p>
<p>게시글을 쓰기 위해선 로그인이 필수이다.</p>
<p>하지만 로그인이 안되어있는 상태에서는 게시글 작성 url을 html에 넣어두지 않을 것이기 때문에, 로그인 체크를 따로 안해도 된다.</p>
<pre><code class="language-java">    @PostMapping(&quot;/post/save&quot;)
    public ResponseEntity&lt;PostEntity&gt; save(HttpSession session, @ModelAttribute(&quot;post&quot;) PostDto postDto){
        Long userIndex = (Long) session.getAttribute(&quot;userIndex&quot;);
        PostEntity post = postService.save(postDto, userIndex);    // 변경해야함( 테스트위해서 변경 )
        // postService.save(postDto, userIndex);

        return new ResponseEntity&lt;&gt;(post, HttpStatus.OK);
        // return &quot;redirect:/post_detail.html&quot;;
    }</code></pre>
<blockquote>
<p>Long userIndex = (Long) Session.getAttribute(&quot;userIndex&quot;);</p>
</blockquote>
<p>가장 먼저 게시글 저장 버튼을 누르려면 기본적으로 로그인 세션이 있는 상태일 것이다.</p>
<p>그래서 세션에서 사용자의 index를 가져온다.</p>
<blockquote>
<p>PostEntity post = postService.save(postDto, userIndex);</p>
</blockquote>
<p>그후 postService의 save 메서드로 작성된 게시글 정보와 userIndex를 파라미터로 넘긴다.</p>
<h3 id="service">service</h3>
<pre><code class="language-java">    public PostEntity save(PostDto postDto, Long userIndex) {
        PostEntity postEntity = postDto.toEntity(userIndex);
        PostEntity saved = postRepository.save(postEntity);

        return saved;
    }</code></pre>
<p>파라미터로 받아온 postDto와 userIndex를 저장해야한다.</p>
<p>하지만 여기서 문제는 postDto와 userIndex는 따로 데이터가 온다는 것이다.</p>
<p>게시글을 userIndex를 추가해 저장하기 위해서는 userIndex를 postEntity에 추가해 저장해야 한다.</p>
<p>그래서 혼자 생각을 해보았다.</p>
<p>controller에서 service로 넘어갈때 Dto로 넘어가는데 repository에서는 해당 dto를 entity로 변경해 저장해야 한다.</p>
<p>그러면 dto에서 entity로 넘어갈 때 userIndex를 추가할 수는 없는건가라는 생각을 했다.</p>
<p>여기서 dto와 entity를 따로두는 이유는 모두가 알 것이다.</p>
<h4 id="dto와-entity를-분리하는-이유">DTO와 Entity를 분리하는 이유</h4>
<blockquote>
<p>DTO(Data Transfer Object)는 Entity 객체와 달리 각 계층끼리 주고받는 우편물이나 상자의 개념입니다. 순수하게 데이터를 담고 있다는 점에서 Entity 객체와 유사하지만, 목적 자체가 전달이므로 읽고, 쓰는 것이 모두 가능하고, 일회성으로 사용되는 성격이 강합니다.</p>
</blockquote>
<p>그래서 dto를 entity로, entity를 dto로 변경하는 메서드가 필요하다.</p>
<p><strong>PostEntity</strong></p>
<pre><code class="language-java">public PostDto toDto() {
        PostDto postDto = PostDto.builder()
        .title(title)
        .description(description)
        .postRegDate(postRegDate)
        .postSido(postSido)
        .postGugun(postGugun)
        .postDong(postDong)
        .postLanguage1(postLanguage1)
        .postLanguage2(postLanguage2)
        .postLanguage3(postLanguage3)
        .userIndex(userIndex)
        .build();

        return postDto;
    }</code></pre>
<p><strong>PostDto</strong></p>
<pre><code class="language-java">public PostEntity toEntity() {
        PostEntity postEntity = PostEntity.builder()
        .title(title)
        .description(description)
        .postRegDate(postRegDate)
        .postSido(postSido)
        .postGugun(postGugun)
        .postDong(postDong)
        .postLanguage1(postLanguage1)
        .postLanguage2(postLanguage2)
        .postLanguage3(postLanguage3)
        .userIndex(userIndex)
        .build();

        return postEntity;
    }</code></pre>
<p>만약  dto에서 entity로 변경할 때 파라미터로 userIndex를 넣으면 해당 userIndex를 엔티티에 추가할 수 있지 않을까 라는 생각으로 toEntity 메서드를 수정해보았다.</p>
<pre><code class="language-java">public PostEntity toEntity(Long userIndex) {
        PostEntity postEntity = PostEntity.builder()
        .title(title)
        .description(description)
        .postRegDate(postRegDate)
        .postSido(postSido)
        .postGugun(postGugun)
        .postDong(postDong)
        .postLanguage1(postLanguage1)
        .postLanguage2(postLanguage2)
        .postLanguage3(postLanguage3)
        .userIndex(userIndex)
        .build();

        return postEntity;
    }</code></pre>
<p>toEntity메서드를 수정한 후, 서버를 돌려 postman으로 테스트를 돌려보았는데, userIndex가 잘 들어가있는 것을 확인할 수 있었다.</p>
<hr>
<h2 id="아이디-및-닉네임-중복-체크">아이디 및 닉네임 중복 체크</h2>
<p>이미 아이디 및 닉네임의 중복 체크 기능은 구현해두었다.</p>
<p>하지만 프론트 디자인 중, 회원가입 페이지에서 중복 체크 버튼이 없는 것이 더 디자인이 괜찮다는 얘기가 나와,  중복 체크 버튼 없이 중복 체크를 할 수 있는지 찾아보았다.</p>
<p><a href="https://kimvampa.tistory.com/90">https://kimvampa.tistory.com/90</a></p>
<p>구글링 중 해당 페이지에서 해답을 찾을 수 있었다.</p>
<blockquote>
<p>propertychange change keyup paste input</p>
</blockquote>
<p>해당 js 기능은 html의 input 태그의 값을 실시간으로 처리할 수 있도록 하는 기능이다.</p>
<p>이 기능을 사용해서 ajax로 input 값을 보내 해당 아이디나 닉네임을 중복체크를 하게 한다.</p>
<p>만약 중복이라면 1을, 사용가능한 아이디나 닉네임이면 0을 반환해 사용가능을 확인하게 한다.</p>
<hr>
<p>오늘은 생각보다 중요한 작업을 한것 같다.</p>
<p>확실히 로그인 구현은 여러가지를 생각하게 한다.</p>
<p>아직 중요한 작업들이 많이 남았다.</p>
<p>공부하다 생각한 것인데, 자기가 작성한 게시글이면 수정 및 삭제 버튼이 있어야하고, 자기가 작성한 게시글이 아니라면, 수정 및 삭제 버튼이 없어야 한다.</p>
<p>해당 게시글이 자신이 작성한 게시글인지 아닌지를 어떻게 판단해야 하는가에 대한 고민이었다.</p>
<p>이 고민은 차차 생각해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-17)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-17</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-17</guid>
            <pubDate>Wed, 17 May 2023 11:09:31 GMT</pubDate>
            <description><![CDATA[<p>오늘은 이미지 저장 로직에 대한 테스트를 진행해 보았다.</p>
<p>사실 TDD 방식으로 백엔드 작업을 진행하여 공부했던 CRUD까지는 테스트가 가능했지만, 로그인 기능 구현, 이미지 파일 저장 기능 구현 등 모르는 것을 배우려다보니 TDD 방식으로 하기 까다로웠다. </p>
<p>그래서 코드를 작성하면서 그때 그때 postman으로 테스트를 진행하는 방식으로 하였다.</p>
<p>사실 아직 공부를 많이 하지 못한 나의 잘못이라고 판단이 된다. 앞으로 공부를 더 열심히 해야겠다는 생각이 코드를 작성하면서 느꼈다.</p>
<hr>
<p>전날 했던 회원 정보 수정 기능에 대해 postman으로 테스트했다.</p>
<h2 id="진행-순서">진행 순서</h2>
<p>개인정보를 수정하기 위해서는 회원정보가 필요하여 회원가입이 먼저 필요했고, 프로필을 저장하기 위해서는 로그인이 필요하다.</p>
<p>그래서 테스트 진행 순서는 이전에 만들었던 회원가입 로직, 그 다음 로그인을 한 후 회원 정보를 수정하는 순서로 진행 하였다.</p>
<p>회원가입 -&gt; 로그인 -&gt; 회원정보 수정</p>
<h3 id="회원가입">회원가입</h3>
<p><strong>Controller</strong></p>
<pre><code class="language-java">    @PostMapping(&quot;/signup&quot;)
    public ResponseEntity&lt;UserEntity&gt; signup(UserDto userDto){
        UserEntity user = userService.save(userDto);

        return new ResponseEntity&lt;&gt;(user, HttpStatus.OK);
    }</code></pre>
<p><strong>Service</strong></p>
<pre><code class="language-java">    public UserEntity save(UserDto userDto) {
        UserEntity entity = userDto.toEntity();
        userRepository.save(entity);

        return entity;
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/4c9f4c25-8a5e-4586-9074-0efd66915bed/image.png" alt=""></p>
<p>회원 가입에 필요한 데이터들을 form태그를 이용해 데이터를 보낼것이기 때문에 x-www-form-urlencoded 방식으로 데이터를 기입해 보냈다.</p>
<p>반환값은 ResponseEntity를 이용하여 테스트 하였다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/ebaaa941-41dc-422e-83e8-4c5afc5fc39a/image.png" alt=""></p>
<p>회원가입 완성!</p>
<hr>
<h3 id="로그인">로그인</h3>
<p><strong>Controller</strong></p>
<pre><code class="language-java">    @PostMapping(&quot;/login&quot;)
    public ResponseEntity&lt;String&gt; login(@RequestParam(&quot;userId&quot;) String userId,@RequestParam(&quot;userPassword&quot;) String userPassword, HttpSession session){
        Long userIndex = userService.login(userId, userPassword);
        if(userIndex == null){
            return new ResponseEntity&lt;&gt;(&quot;no userData&quot;, HttpStatus.BAD_REQUEST);
            // return &quot;redirect:/login&quot;;
        }
        session.setAttribute(&quot;userIndex&quot;, userIndex);
        log.info(session.getAttribute(&quot;userIndex&quot;).toString());

        return new ResponseEntity&lt;&gt;(userIndex.toString(), HttpStatus.OK);
        // return &quot;redirect:/&quot;;
    }</code></pre>
<p>사용자의 아이디와 비밀번호를 받아와 로그인을 한다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/55763cbd-3e0e-4c0c-99ab-f47665678226/image.png" alt=""></p>
<p>반환값으로 해당 아이디 비밀번호로 로그인 했을 때 세션을 저장하고, 해당 사용자의 index를 반환했다.</p>
<p>지금 회원정보는 1명이기 때문에 1을 반환한다.
<img src="https://velog.velcdn.com/images/yoon_bly/post/b82dda6f-4652-42d2-b2ec-9feaa2e3205a/image.png" alt=""></p>
<h3 id="회원정보-수정문제-및-해결">회원정보 수정(문제 및 해결)</h3>
<p>오늘의 가장 메인 파트이다.</p>
<p>사실 저번에 썼던 코드로 테스트를 진행해 보았는데 파일이 저장이 안되고, 프로필 엔티티도 생성되지 않았다.</p>
<p>맨 처음 테스트를 진행했을 때, 엔티티는 생성되었지만, 파일이 로컬 컴퓨터에 저장이 안되는 것으로 알았다.</p>
<p>그래서 파일 경로를 찾아 보았다.</p>
<p>찾아보던 와중 생각해보니</p>
<pre><code class="language-java">    public Optional&lt;ProfileEntity&gt; storeFile(MultipartFile multipartFile, Long userIndex) throws IOException {
        if(multipartFile.isEmpty()){
            return null;
        }

        String originFileName = multipartFile.getOriginalFilename();
        String storeFileName = createStoreFileName(originFileName);
        multipartFile.transferTo(new File(getFullPath(storeFileName)));

        ProfileEntity entity = ProfileEntity.builder()
                                    .originFileName(originFileName)
                                    .storeFileName(storeFileName)
                                    .userIndex(userIndex)
                                    .build();

        Optional&lt;ProfileEntity&gt; profileEntity = profileRepository.findByUserIndex(userIndex);

        // 만약 유저 인덱스를 가진 프로필엔티티가 있으면 해당 프로필 엔티티를 수정
        if(profileEntity != null){
            profileEntity.map(p -&gt; {
                profileEntity.get().setOriginFileName(originFileName);
                profileEntity.get().setStoreFileName(storeFileName);
                profileEntity.get().setUserIndex(userIndex);

                return p;
            })
            .map(p -&gt; profileRepository.save(p));

            return profileEntity;
        }
        // 없다면 프로필 엔티티를 생성해서 저장
        else{
            ProfileEntity saved = profileRepository.save(entity);
            Optional&lt;ProfileEntity&gt; find =profileRepository.findById(userIndex);
            return find;
        }
    }</code></pre>
<p>profileEntity는 Optional로 감싼 타입인데, if문에 null값을 넣어 조건문을 만들었던 것이다.</p>
<p>그래서 테스트를 해보며 DB를 확인 했는데 역시나 생성되어있지도 않았다.(애초에 조건문이 성립되지 않았기 때문에 else문도 안되었다.)</p>
<pre><code class="language-java">    public Optional&lt;ProfileEntity&gt; storeFile(MultipartFile multipartFile, Long userIndex) throws IOException {
        if(multipartFile.isEmpty()){
            return null;
        }

        String originFileName = multipartFile.getOriginalFilename();
        String storeFileName = createStoreFileName(originFileName);

        try{
            multipartFile.transferTo(new File(getFullPath(storeFileName)));
        }catch(Exception e){
            System.out.println(&quot;can&#39;t upload file&quot;);
        }

        ProfileEntity entity = ProfileEntity.builder()
                                    .originFileName(originFileName)
                                    .storeFileName(storeFileName)
                                    .userIndex(userIndex)
                                    .build();

        Optional&lt;ProfileEntity&gt; profileEntity = profileRepository.findByUserIndex(userIndex);

        // 만약 유저 인덱스를 가진 프로필엔티티가 있으면 해당 프로필 엔티티를 수정
        if(!profileEntity.isEmpty()){
            profileEntity.map(p -&gt; {
                profileEntity.get().setOriginFileName(originFileName);
                profileEntity.get().setStoreFileName(storeFileName);
                profileEntity.get().setUserIndex(userIndex);

                return p;
            })
            .map(p -&gt; profileRepository.save(p));

            return profileEntity;
        }
        // 없다면 프로필 엔티티를 생성해서 저장
        else{
            profileRepository.save(entity);
            Optional&lt;ProfileEntity&gt; find =profileRepository.findById(userIndex);
            return find;
        }
    }</code></pre>
<p>그래서 if문을 isEmpty 메서드를 사용하여 <strong>&quot;비어있지 않다면&quot;</strong> 이라는 조건으로 변경하였다.</p>
<p>변경 후 테스트를 돌려보니 해당 엔티티는 생성이 되었다.</p>
<p>하지만 여전히 파일이 저장은 안되었다.</p>
<p>그래서 FileStore클래스의 모든 코드를 확인해보았다.</p>
<p>하지만 모두 맞는 코드였다.</p>
<p>결국 경로 설정이 잘못되었던 것인가라는 결론이 나왔다.</p>
<p>properties로 경로를 설정하려 했는데 구글에도 잘 나오지 않았던 터라 일단 넘어갔었다.</p>
<p>그래서 구글링을 해본 결과</p>
<pre><code class="language-java">    // 파일 경로
    @Value(&quot;${file.dir}&quot;)
    private String fileDir;</code></pre>
<p>경로설정을 위한 어노테이션인 @Value를 사용하기 위해 properties에서 경로를 지정해주어야한다.</p>
<p>나는 file.dir이라는 이름으로 주소를 설정할 것이기 때문에 properties에</p>
<blockquote>
<p>file.dir = 파일을 저장할 폴더 위치</p>
</blockquote>
<p>이런 식으로 설정을 해야한다.</p>
<p>하지만 주소를 설정할 때 주의해야 할 점은 주소 마지막에 / 를 넣어줘야 한다는 점이다.</p>
<p>그래서 나는 주소를 넣은 뒤에 테스트를 돌려보았다.</p>
<blockquote>
<p>file.dir = file.dir = C:\Users\yhw01\talkcoding\src\main\resources\static\upload\</p>
</blockquote>
<p>하지만 갑자기 잘되던 서버가 오류가 나버렸다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/06820202-42ca-4e07-b3c3-5fb584b2c5e8/image.png" alt=""></p>
<p>... 갑자기 오류가 나서 멘붕이 왔다.</p>
<p>인코딩이 잘못되었다는 오류인것 같아서 고민을 해보다 파일 저장 경로 이름이 <img src="https://velog.velcdn.com/images/yoon_bly/post/8ad99b39-2db3-49bf-96b9-6cf08d0bd739/image.png" alt=""></p>
<p>이 슬래시로 되어있어서 설마하고 반대 슬래시로 고친 후 돌려보았더니 해결이 되었다.</p>
<p>정말 다행이었다.</p>
<p>이렇게 하고 테스트를 진행해 보았다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/2d005244-5533-43bd-8505-e48a8c9aad88/image.png" alt=""></p>
<p>결국 엔티티는 생성이 되었고 내가 원하는 파일도 저장이 된 것을 확인할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/yoon_bly/post/4c342edb-cb6d-448f-b76a-8c44749fc691/image.png" alt=""></p>
<hr>
<p>오늘 나는 이 이미지 저장 기능의 구현을 끝마쳐서 너무 행복하다.</p>
<p>오랫동안 고민하며 공부했던 것이기에 성공할 수 있어서 정말 뿌듯한 하루였던 것 같다.</p>
<p>앞으로도 더욱 열심히 해서 프로젝트를 성공적으로 마무리 해야겠다.</p>
<p>내일의 나도 파이팅 하자!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TACO 프로젝트 회고록(2023-05-16)]]></title>
            <link>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-16</link>
            <guid>https://velog.io/@yoon_bly/TACO-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0%EB%A1%9D2023-05-16</guid>
            <pubDate>Wed, 17 May 2023 02:52:28 GMT</pubDate>
            <description><![CDATA[<p>오늘은 사용자 프로필 저장 기능을 구현해보았다.</p>
<h2 id="엔티티">엔티티</h2>
<p>사용자는 하나의 프로필을 설정할 수 있기 때문에 1대1관계라 생각하고 ProfileEntity를 따로 하나 더 생성하였다.</p>
<p><strong>ProfileEntity</strong></p>
<pre><code class="language-java">public class ProfileEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long profileIndex;

    @Column
    private Long userIndex;

    @Column
    private String originFileName;

    @Column
    private String storeFileName;

    public ProfileDto toDto(){
        ProfileDto profileDto = ProfileDto.builder()
        .userIndex(userIndex)
        .originFileName(originFileName)
        .storeFileName(storeFileName)
        .build();

        return profileDto;
    }
}</code></pre>
<blockquote>
<p>private String originFileName;</p>
</blockquote>
<ul>
<li>사용자가 업로드한 원본 이름</li>
</ul>
<blockquote>
<p>private String storeFileName;</p>
</blockquote>
<ul>
<li>업로드된 파일의 이름이 겹칠 수 있기 때문에 저장한 파일 이름을 다르게 했다.</li>
</ul>
<blockquote>
<p>private Long userIndex;</p>
</blockquote>
<ul>
<li>사용자 인덱스의 FK</li>
</ul>
<hr>
<h2 id="filestore-class">FileStore Class</h2>
<p>파일을 저장하는데 있어서 특화된 메소드들을 따로 클래스로 구성하여 이를 분리해두었다.</p>
<p><strong>FileStore</strong></p>
<pre><code class="language-java">public class FileStore {

    @Autowired
    private ProfileRepository profileRepository;

    // 파일 경로
    @Value(&quot;${file.dir}&quot;)
    private String fileDir;

    // 파일 경로 구성
    public String getFullPath(String storeFileName) {
        return fileDir + storeFileName;
    }

    // 프로필을 저장 -  이미 유저인덱스로 찾아 엔티티가 있을 경우 수정 / 없으면 저장
    public Optional&lt;ProfileEntity&gt; storeFile(MultipartFile multipartFile, Long userIndex) throws IOException {
        if(multipartFile.isEmpty()){
            return null;
        }

        String originFileName = multipartFile.getOriginalFilename();
        String storeFileName = createStoreFileName(originFileName);
        multipartFile.transferTo(new File(getFullPath(storeFileName)));

        ProfileEntity entity = ProfileEntity.builder()
                                    .originFileName(originFileName)
                                    .storeFileName(storeFileName)
                                    .userIndex(userIndex)
                                    .build();

        Optional&lt;ProfileEntity&gt; profileEntity = profileRepository.findByUserIndex(userIndex);

        // 만약 유저 인덱스를 가진 프로필엔티티가 있으면 해당 프로필 엔티티를 수정
        if(profileEntity != null){
            profileEntity.map(p -&gt; {
                profileEntity.get().setOriginFileName(originFileName);
                profileEntity.get().setStoreFileName(storeFileName);
                profileEntity.get().setUserIndex(userIndex);

                return p;
            })
            .map(p -&gt; profileRepository.save(p));

            return profileEntity;
        }
        // 없다면 프로필 엔티티를 생성해서 저장
        else{
            ProfileEntity saved = profileRepository.save(entity);
            Optional&lt;ProfileEntity&gt; find =profileRepository.findById(userIndex);
            return find;
        }
    }

    // storeFile 이름 구성
    private String createStoreFileName(String originFileName) {
        String uuid = UUID.randomUUID().toString();
        String ext = extractExt(originFileName);
        String storeFileName = uuid + ext;

        return storeFileName;
    }

    // 확장자 추출
    private String extractExt(String originFileName) {
        int idx = originFileName.lastIndexOf(&quot;.&quot;);
        String ext = originFileName.substring(idx);
        return ext;
    }
}</code></pre>
<blockquote>
<p>@Value(&quot;${file.dir}&quot;)
private String fileDir;</p>
</blockquote>
<ul>
<li>파일을 실제로 저장해둘 경로를 지정</li>
</ul>
<blockquote>
<p>private String extractExt(String originFileName){...}</p>
</blockquote>
<ul>
<li>파일의 확장자를 추출하는 메서드</li>
</ul>
<blockquote>
<p>private String createStoreFileName(String originFileName) {...}</p>
</blockquote>
<ul>
<li>저장할 파일 이름 구성</li>
<li>UUID를 통해 동일한 파일이름 저장 방지</li>
</ul>
<blockquote>
<p>public String getFullPath(String storeFileName) {...}</p>
</blockquote>
<ul>
<li>storeFileName에 들어갈 파일 경로 구성</li>
</ul>
<blockquote>
<p>public Optional&lt;ProfileEntity&gt; storeFile(MultipartFile multipartFile, Long userIndex) throws IOException {...}</p>
</blockquote>
<ul>
<li>원본 파일 이름과 저장할 파일 이름을 반환하게 해주는 메서드</li>
</ul>
<hr>
<p>이 storeFile 메서드가 가장 중요한 부분이다.</p>
<p>파일을 저장시켜주는 메서드로써, originFileName과 storeFileName의 값을 반환해야 한다.</p>
<p>파라미터를 multipartFile과 회원정보를 수정하고 있는 유저의 인덱스를 받아와</p>
<p>multipartFile들을 String타입으로 변환 후 새로운 ProfileEntity를 생성한다.</p>
<p><strong>하지만, 프로필은 하나만 있어야 하므로, 만약 동일한 userIndex를 갖고 있는 ProfileEntity가 있는 경우 해당 Entity에 덮어씌우게 하고, 동일한 userIndex가 없다면 Entity를 생성하는 방식으로 만들었다.</strong></p>
<hr>
<h2 id="컨트롤러">컨트롤러</h2>
<p>프로필 변경 작업은 회원정보 변경 페이지에서 진행되기 때문에 UserController에 로직을 생성하였다.</p>
<p>이전에 프로필이 아닌 이름, 닉네임, 전화번호를 변경하는 로직에 프로필 변경 로직을 추가하였다.</p>
<pre><code class="language-java">    // 회원정보 수정 버튼 기능
    @PostMapping(&quot;/user/edit&quot;)
    public ResponseEntity&lt;Optional&lt;UserEntity&gt;&gt; userEdit(HttpSession session,@RequestParam(&quot;originFileName&quot;) MultipartFile originFileName, @ModelAttribute UserDto userDto) throws IOException{
        Long userIndex = (Long) session.getAttribute(&quot;userIndex&quot;);

        UserEntity user = userDto.toEntity();

        // 회원 정보 수정
        Optional&lt;UserEntity&gt; userInfo = userService.changeUserInfo(userIndex, user);

        // 프로필 수정
        profileService.saveProfile(originFileName, userIndex);

        return new ResponseEntity&lt;&gt;(userInfo, HttpStatus.OK);
    }</code></pre>
<blockquote>
<p>Optional&lt;UserEntity&gt; userInfo = userService.changeUserInfo(userIndex, user);</p>
</blockquote>
<ul>
<li>해당 페이지의 세션을 받아와 사용자의 userIndex를 받아오고, 수정된 이름, 닉네임, 전화번호를 dto로 받아와 수정시켜준다.</li>
</ul>
<p>여기까지는 이전에 있던 로직이다.</p>
<blockquote>
<p>profileService.saveProfile(originFileName, userIndex);</p>
</blockquote>
<ul>
<li>프로필이 변경된 파일을 MultipartFile로 받아와 회원정보 수정 메서드와 같이 userIndex와 함께 서비스 부분으로 넘겨준다.</li>
</ul>
<hr>
<h2 id="서비스">서비스</h2>
<pre><code class="language-java">    public ProfileEntity saveProfile(MultipartFile originFileName, Long userIndex) throws IOException {
        fileStore.storeFile(originFileName, userIndex);
        return null;
    }</code></pre>
<p>컨트롤러에서 받아온 파일과 userIndex를 StoreFile메서드로 넘겨 ProfileEntity를 생성하고, 파일을 저장한다.</p>
<hr>
<p>아직 로직을 전부 만들지는 않았지만, 파일과 이미지 업로드 기능에 대하여 여러가지 공부를 하였다.</p>
<p>이 이미지 저장 로직에 대해 이해가 잘 안되었어서 오래걸리긴 했지만, 혼자 공부하는 것 치고 꽤 빠르게 이해한 것 같다.</p>
<p>또 이렇게 기술 회고를 작성하며 복습할 수 있어서 참 다행인것 같다.</p>
<p>만약 복습을 하지 않았다면, 이 프로젝트가 끝난 뒤에 나중에 또 이미지 파일을 저장할 때 다시 공부를 해야겠지만, 복습을 통해서 완전히 나의 것으로 만들었다.</p>
<p>오늘은 안풀리던 이미지 저장 로직을 이해해서 참 기분 좋았다.</p>
<p>앞으로 남은 로직과 프론트와의 데이터 렌더링을 잘 마무리 하고, 프로젝트를 성공적으로 끝낼 수 있었으면 좋겠다.</p>
]]></description>
        </item>
    </channel>
</rss>