<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>HwangDo</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 26 Jan 2025 07:36:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. HwangDo. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/im_h_jo" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[민감한 yml 파일을 카톡 대신 동적으로 관리해보자]]></title>
            <link>https://velog.io/@im_h_jo/%EB%AF%BC%EA%B0%90%ED%95%9C-yml-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%B9%B4%ED%86%A1-%EB%8C%80%EC%8B%A0-%EB%8F%99%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@im_h_jo/%EB%AF%BC%EA%B0%90%ED%95%9C-yml-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EC%B9%B4%ED%86%A1-%EB%8C%80%EC%8B%A0-%EB%8F%99%EC%A0%81%EC%9C%BC%EB%A1%9C-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 26 Jan 2025 07:36:49 GMT</pubDate>
            <description><![CDATA[<p><code>database.yml, .env</code> 업데이트 되었으니 이 파일로 갈아끼워 사용 해 주세요!
라는 카톡을 보내본 적이 있는가? 
github actions에서 빌드를 위해 파일이 바뀔 때 마다 base64 인코딩 이후 secrets에 넣어본 경험이 있는가?</p>
<p>나는 굉장히 잦다.. 민감한 파일은 .gitignore 시켜야하고, 버전관리를 할 수 없으니 매번 메신저로 파일을 주고받았다. 그러다보면 멸종되는 파일도 생기고, 귀찮은게 이만 저만이 아니다.</p>
<h3 id="뭐가-불편할까">뭐가 불편할까?</h3>
<ol>
<li>메신저로 변경된 파일을 보내준 뒤, 해당 파일로 일일히 갈아끼우지 않으면 빌드가 안 돌거나 의도치 않은 동작이 된다.</li>
<li>CI/CD를 github actions로 구축해놨다면, yml 파일의 전문을 secrets에 등록해놔야한다. 게다가 base64 인코딩까지 해야한다..!! </li>
<li>파일의 관리를 모두가 각각 하다보니, 개발 환경이 이전되면 작업을 이어 하기가 어렵다. (파일을 모두 하나하나 파밍해야 한다)</li>
</ol>
<h3 id="아이디어">아이디어</h3>
<p><em>그냥 스토리지에 올리면 되는거 아닌가?</em>
라는 생각이 들었다. 이를 위해 S3등의 서비스를 쓸 수도 있겠지만, 나는 <strong>WebDAV</strong>를 도입해봤다.</p>
<h2 id="webdav">WebDAV</h2>
<blockquote>
<p>WebDAV(Web Distributed Authoring and Versioning)는 웹 사이트를 편집할 수 있는 인터넷 기반 프로토콜입니다. 하이퍼텍스트 전송 프로토콜(HTTP)의 확장으로, HTTPS를 통해 데이터를 업로드하고 다운로드할 수 있습니다. </p>
</blockquote>
<p>쉽게 말해, HTTP 프로토콜을 지원하는 웹기반 저장소다. POST 하면 저장, GET 하면 조회, DELETE하면 삭제.. 등등 익숙한 인터페이스다.</p>
<h3 id="왜-webdav">왜 WebDAV?</h3>
<p>WebDAV는 HTTP를 지원한다. 따라서, curl같은 커맨드로 아주 단순히 파일 연산을 할 수 있다.
<code>curl -X GET https://my-webdav-url/server/database.yml -o database.yml</code>
이 한 줄 만으로 바로 데이터를 가져올 수 있다!</p>
<h3 id="구축해보자">구축해보자</h3>
<p>WebDAV는 apache 또는 nginx를 띄워 구축할 수도 있고, 방법은 다양하다.
나는 <strong>Docker</strong>를 이용해 아주 간편하게 구축해봤다.
나는 1M+의 다운로드가 기록된 <a href="https://hub.docker.com/r/ugeek/webdav">ugeek/webdav</a>를 사용했다. 내부적으로는 nginx를 이용해 webdav를 구축해놨다.</p>
<pre><code class="language-yml">  webdav:
    image: ugeek/webdav:arm  # ARM용 이미지
    container_name: webdav-container
    restart: always
    expose:
      - &quot;80&quot;  # 컨테이너가 내부적으로 80 포트 사용
    volumes:
      - $HOME/docker/webdav:/media  # WebDAV 데이터 저장 경로
    environment:
      - USERNAME=!WEBDAV_USERNAME_SECRET!
      - PASSWORD=!WEBDAV_PASSWORD_SECRET!
      - UID=1000
      - GID=1000
      - TZ=Asia/Seoul</code></pre>
<p>docker-compose에 위와 같은 구문을 추가했다. 내 webdav 서버는 M4 실리콘 맥 위에서 돌기에, arm 이미지를 가져왔다.
<code>volumes</code>에는 webdav의 데이터가 저장될 경로를 설정해주면 되고, env에서 username과 password를 잘 설정해주자. 저 Credential을 가지고 접근 권한을 설정해 줄 것이다.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/38cbe9b5-0bad-40a1-bd4a-90ac0bf8b2d2/image.png" alt="">
컨테이너를 띄우면 로그인 화면이 뜨고, 위처럼 접속됨을 알 수 있다.</p>
<h3 id="어떻게-쓰나요">어떻게 쓰나요?</h3>
<p>우선 위에서 Username, password를 설정해준 값은 항상 들어가야한다.
예시 몇 가지를 첨부하겠다.</p>
<blockquote>
<p>조회
<code>curl -X GET https://my-webdav-url/server/database.yml -o database.yml -u WEBDAV_USERNAME_SECRET:WEBDAV_PASSWORD_SECRET</code> 
추가
<code>curl -T  ./database.yml https://my-webdav-url/server/database.yml -u WEBDAV_USERNAME_SECRET:WEBDAV_PASSWORD_SECRET</code> 
수정(덮어쓰기)
<code>curl -T PUT ./database.yml https://my-webdav-url/server/database.yml -u WEBDAV_USERNAME_SECRET:WEBDAV_PASSWORD_SECRET</code> 
삭제
<code>curl -X DELETE https://my-webdav-url/server/database.yml -u WEBDAV_USERNAME_SECRET:WEBDAV_PASSWORD_SECRET</code> </p>
</blockquote>
<p>이게 기본적으로 cURL을 이용해 연산하는 방법이다.
그렇다면 이제 개발 환경에서는 처음 제시한 문제들이 해결되었을까?</p>
<h2 id="스프링-프로젝트에-적용">스프링 프로젝트에 적용</h2>
<p>먼저, 개발자들에게 개발 환경 세팅으로 환경 변수를 설정하게 했다.
<code>~/.zshrc</code>에 다음과 같이 두개의 환경 변수를 설정한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/4b03fe53-31f6-4779-8ac2-62f9aa599969/image.png" alt="">
CREDENTIAL_NAME은 위에서 설정한 username, CREDENTIAL_PW는 password로 하면 된다.</p>
<h3 id="prepare-buildsh-추가">prepare-build.sh 추가</h3>
<pre><code class="language-bash">#!/bin/bash
set -e

webdav_url=&quot;https://my-webdav-url/server&quot;
resources_path=&quot;./src/main/resources&quot;


# 환경 변수 읽기
is_empty_or_null() {
  [ -z &quot;$1&quot; ] || [ &quot;$1&quot; = &quot;null&quot; ]
}

if is_empty_or_null &quot;$CREDENTIAL_NAME&quot; || is_empty_or_null &quot;$CREDENTIAL_PW&quot;; then
  echo &quot;필요한 환경변수(CREDENTIAL_NAME, CREDENTIAL_PW) 중 하나 이상이 설정되어 있지 않거나 null입니다.&quot;
  exit 1
fi
download_yml() {
  local file_name=$1
  echo &quot;Downloading ${file_name}&quot;
  curl -Ss -f -X GET &quot;${webdav_url}/${file_name}&quot;  --user &quot;${CREDENTIAL_NAME}:${CREDENTIAL_PW}&quot; -o &quot;${resources_path}/${file_name}&quot; &amp;
}

download_yml &quot;aws-dev.yml&quot;
download_yml &quot;aws-prod.yml&quot;
download_yml &quot;jwt.yml&quot;
download_yml &quot;mysql-dev.yml&quot;
download_yml &quot;mysql-prod.yml&quot;
download_yml &quot;webhook.yml&quot;
download_yml &quot;github.yml&quot;

wait</code></pre>
<p>그 다음, 위와 같은 sh 스크립트를 만들었다.</p>
<p>download_yml이라는 함수를 선언해두고, 호출해가며 yml을 서버에서 동적으로 받아온다.</p>
<h3 id="빌드-이전-무조건-실행되게-하기">빌드 이전 무조건 실행되게 하기</h3>
<p>저 스크립트는 빌드 또는 실행 이전에 무조건 실행되어야 파일의 최신 상태를 보장한다.
이에 대해 세팅해보자.</p>
<p>먼저, build.gradle에서</p>
<pre><code class="language-gradle">tasks.register(&quot;preScript&quot;, Exec) {
    environment &quot;CREDENTIAL_NAME&quot;, System.getenv(&quot;CREDENTIAL_NAME&quot;)
    environment &quot;CREDENTIAL_PW&quot;, System.getenv(&quot;CREDENTIAL_PW&quot;)

    if (System.getProperty(&#39;os.name&#39;).toLowerCase().contains(&#39;win&#39;)) {
        // For windows
        commandLine &#39;C:\\Program Files\\Git\\bin\\bash.exe&#39;, &#39;-c&#39;, &#39;./prepare-build.sh&#39;
    } else {
        commandLine &quot;sh&quot;, &quot;prepare-build.sh&quot;
    }
}

tasks.named(&quot;processResources&quot;) {
    dependsOn(&quot;preScript&quot;)
}

tasks.named(&quot;bootRun&quot;){
    dependsOn(&quot;preScript&quot;)
}

tasks.named(&quot;build&quot;) {
    dependsOn(&quot;preScript&quot;)

}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
    dependsOn(&quot;preScript&quot;)
}</code></pre>
<p>위처럼 태스크를 등록해준다. 우리 서버 개발자중에 윈도우 환경이 있어서, git bash로 실행되도록 라인이 추가되어 있다.</p>
<p>다만 <strong>IntelliJ를 쓴다면, gradle을 거쳐 실행하지 않는다.</strong>
따라서 추가로 세팅해줘야 한다.</p>
<h3 id="intellij-세팅">IntelliJ 세팅</h3>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/70a14def-eaf7-4b73-8fad-568f2569af50/image.png" alt="">
&quot;구성 편집&quot;을 선택한 뒤 </p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/976c9777-e00e-4f29-b0f0-39f82b056bd9/image.png" alt=""></p>
<ol>
<li>프로젝트 파일로 저장을 체크한다. 이 실행 구성을 git으로 버전관리해, 모든 개발자가 공유할 수 있도록 하는 설정이다.</li>
<li>Gradle 작업 실행을 선택한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/926d6906-8e0a-4302-931c-03cf835316ed/image.png" alt="">
그리고 작업에서 위에서 만든 preScript를 선택해주면 완료된다.</p>
<h2 id="문제가-해결되었을까">문제가 해결되었을까?</h2>
<ol>
<li>메신저로 파일을 보내지 않고, 단순히 수정한 사람이 curl을 통해 webdav에서 업데이트 해주면 된다. 그러면 변경 사항이 자동으로 빌드시에 적용된다.</li>
<li>새 파일이 추가되면, webdav에 올리고 prepare-build.sh에 한 줄 추가해주면 된다. 이 prepare 스크립트는 git으로 버전관리가 되기에, 커밋 버전에 맞게 받을지 말지 정해진다.</li>
<li>github actions에서도 yml을 모두 들고있을 필요 없이, CREDENTIAL_NAME과 CREDENTIAL_PW만 secrets에 설정해주면 된다.</li>
<li>개발 환경이 이전되더라도, ID와 PW만 알면 바로 작업을 진행할 수 있다.</li>
</ol>
<p>이를 통해 더이상 카톡으로 하나하나 민감 파일을 공유하지 않을 수 있게 되었다 :) </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker + Nginx에 Https 제대로 적용하기]]></title>
            <link>https://velog.io/@im_h_jo/Docker-Nginx%EC%97%90-Https-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@im_h_jo/Docker-Nginx%EC%97%90-Https-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 18 Jan 2025 20:13:25 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@im_h_jo/Cloudflare%EC%9D%98-Https%ED%94%84%EB%A1%9D%EC%8B%B1%EC%97%90%EB%8A%94-%ED%95%A8%EC%A0%95%EC%9D%B4-%EC%9E%88%EB%8B%A4">지난 편</a>에서, Cloudflare https 프록싱이 정말 느리다는걸 알았다.
이번엔, 제대로 인증서를 발급받아 직접 적용해보자.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/d184c4d2-eb54-40e3-ac52-e43e1c19b0e1/image.png" alt="">
먼저 내 환경은 Docker + Nginx다. 도커 위에 여러 서비스가 있고, 이를 Nginx가 리버스 프록싱해준다.
그러니, Nginx단에다가 https를 적용하면 된다. 
이를 위해 CloudFlare API와 Let&#39;s encrypt를 이용해보자.
혹시 인터넷에서 <code>init_letsencrypt.sh</code> 를 이용하는 방법을 봤다면, 그거는 서브도메인 (abc.domain.com)이 적용이 안 되니 이 방법을 써야한다. </p>
<h3 id="도메인-사오세요">도메인 사오세요</h3>
<p>혹시 몰라 하는 말이지만, IP에는 HTTPS를 적용 할 수 없으니 도메인을 사오자.
HTTPS을 위한 SSL 인증서는 보통 유료거나, 제약있는 무료가 많은데
Let&#39;s encrypt는 무료고, 서브도메인도 무제한이다. 다만 3개월마다 갱신해야 하지만, 이정도면 감동적인 수준.</p>
<h2 id="나야-cloudflare">나야, Cloudflare</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/f8ed5fe6-6a50-44db-b34d-a0feea957778/image.png" alt="">
지난 시간에 Cloudflare를 신나게 욕했지만, 미워도 다시한번 이용해보자.
다만, 이번에는 프록싱을 이용하지 않을것이다.
CloudFlare의 API를 이용해 간편하게 HTTPS 인증서를 발급받자.</p>
<h2 id="도메인-등록하기">도메인 등록하기</h2>
<p>먼저, Cloudflare에서 여러분의 도메인을 등록해주자.
타사 (가비아, 호스팅케이알, route53...)에 등록이 되어있더라도, cloudflare가 스캔해서 dns들을 옮겨주니 편하다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/aabc5f72-52a8-4d0e-a857-03cfa2113575/image.png" alt=""></p>
<p>계속을 눌러 진행하면, 어렵지 않게 등록이 완료된다. 네임서버 이전 잊지말자!</p>
<h2 id="api-token-받기">API Token 받기</h2>
<p><a href="https://dash.cloudflare.com/profile/api-tokens">https://dash.cloudflare.com/profile/api-tokens</a> 링크로 이동해,  API 토큰을 만들어야 한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/4cb6d0f2-c1bf-4864-9844-f2fa0bca1466/image.png" alt="">
** [ 토큰  생성 ] -&gt; [ 영역 DNS 편집 ]**을 눌러주고, API 토큰을 발급받자.</p>
<p>발급받은 토큰은 다시 보여주지 않으니 잘 가지고 있자.</p>
<h2 id="token-저장하기">Token 저장하기</h2>
<p>Docker-compose가 있는 디렉토리로 오자.
그 다음, <code>cloudflare.ini</code> 파일을 만들고 안에 다음과 같이 적어주자.</p>
<pre><code>dns_cloudflare_api_token = &lt;발급받은_API_TOKEN&gt;</code></pre><p>이후에 터미널에서 권한을 600으로 맞춰주자. 권한 높으면 실행시 오류가 발생한다!  (보안 이슈)</p>
<pre><code class="language-shell">chmod 600 cloudflare.ini</code></pre>
<h2 id="docker-compose-작성">docker-compose 작성</h2>
<pre><code class="language-yml">  certbot:
    container_name: certbot
    image: certbot/dns-cloudflare:latest
    volumes:
      - ./certs:/etc/letsencrypt
      - ./letsencrypt-var:/var/lib/letsencrypt
      - ./cloudflare.ini:/etc/letsencrypt/cloudflare.ini:ro
    environment:
      - TZ=Asia/Seoul
    command: &gt;
      certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini
      --email &lt;YOUR_EMAIL_HERE&gt; --agree-tos --no-eff-email
      -d *.domain.com -d domain.com
      --preferred-challenges dns-01 --expand
      --dns-cloudflare-propagation-seconds 120</code></pre>
<p>이제 docker-compose 파일에 certbot 컨테이너를 정의해주자.
하나하나 설명해보겠다.</p>
<ol>
<li>certbot/dns-cloudflare</li>
</ol>
<ul>
<li>우리가 cloudflare api를 통해 인증서를 발급받을거라, 그에 맞는 이미지다.</li>
</ul>
<p>2 . volumes</p>
<ul>
<li><code>./certs</code> 인증서가 저장될 디렉토리다. 직접 만들지 않아도 된다.</li>
<li><code>/letsencrypt-var</code> let&#39;s encrypt 관련 변수들이 스쳐 지나갈 곳.. 직접 만들지 않아도 된다.</li>
<li><code>/cloudflare.ini</code> 바로 위에서 만든 ini 파일 경로를 지정해주면 된다.</li>
</ul>
<p>그리고 커맨드에 
<code>--preferred-challenges dns-01</code> 라는 문구가 있다. dns챌린징인데, 특정 TXT 레코드를 추가하게 요구하는 방식이다. 이 dns 챌린징 방식을 써야 서브도메인도 일괄  인증받을 수 있다.</p>
<p>만약 도메인에 특정 경로 (./well-known/acme-challenge)에 파일을 업로드하는 방식이면 Webroot고, 서브도메인 인증 안된다. 각각 해야한다.</p>
<h3 id="바꿀-점">바꿀 점</h3>
<p>commands에서는 딱 두 가지만 바꾸면 된다.</p>
<ol>
<li>email</li>
<li>도메인</li>
</ol>
<p><YOUR_EMAIL_HERE>에 당신의 이메일을 적고, <code>-d *.domain.com -d domain.com</code>에서 당신의 도메인을 적절히 넣어준다.
<code>*.domain.com</code>으로 하면 모든 서브 도메인이 등록된다. 
그런데 당황스러운게 루트 도메인은 포함이 안된다. 그래서 뒤에 따로 <code>domain.com</code>으로 포함시켜준다.</p>
<h3 id="실행">실행</h3>
<p><code>docker-compose up certbot</code>으로 돌려보자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/ed47e99f-0c79-4ba2-a4aa-89a06db7ddd5/image.png" alt="">
docker 로그를 보면 120초를 기다린다. 2분동안 겸허하게 기다리자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/61b8d224-fa83-4021-abc2-67c69166f2a4/image.png" alt="">
2분 뒤에 다음처럼 Success가 나왔다면, 성공했다!</p>
<h3 id="갱신">갱신?</h3>
<p>갱신은 command를 다음처럼 그냥 renew로 바꾸면 자동으로 이루어진다.
<code>command: renew</code>
이를 Crontab등 이용해 자동 갱신되게 하면 된다.</p>
<h2 id="nginx-설정">Nginx 설정</h2>
<p>이제 Nginx conf를 수정하자.</p>
<p>http 블록에 인증서를 import해주자.</p>
<pre><code>http {
    ssl_certificate /etc/letsencrypt/live/hwangdo.kr/fullchain.pem; 
    ssl_certificate_key /etc/letsencrypt/live/hwangdo.kr/privkey.pem; 
    ...</code></pre><p>그 다음, server 블록에서 443 포트를 받아주자.</p>
<pre><code>    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        ...</code></pre><p>마지막으로, nginx compose에서 443 포트를 열어주자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/8aa33125-0c8b-4cc8-9a5c-f82104fdf77e/image.png" alt="">
이러면 설정이 완료되었다 :) </p>
<h2 id="적용-확인">적용 확인</h2>
<p><a href="https://www.ssllabs.com/ssltest/%EB%A1%9C">https://www.ssllabs.com/ssltest/로</a> 이동해서, 도메인을 넣고 테스트해보자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/6291dd72-38ae-43ef-98c0-62aca37665af/image.png" alt="">
A등급을 받을 수 있다!</p>
<h2 id="해치웠나">해치웠나?</h2>
<p>CloudFlare 프록시 기능으로 HTTPS 기능 사용할 때
<img src="https://velog.velcdn.com/images/im_h_jo/post/d13f30e0-0990-4852-beed-189afea57be8/image.png" alt="">
426ms이 걸렸다. 25kb짜리 이미지인데, 절망적인 수치다.
그렇다면 직접 적용한 뒤엔?
<img src="https://velog.velcdn.com/images/im_h_jo/post/70c5d063-06d6-45aa-9cf4-5dd7fb9eab51/image.png" alt="">
14ms. <strong>3042%가 빠르다..!!!</strong></p>
<p>클라우드 플레어의 프록시는 나중에 돈 많이 벌면 유료로 쓰자. (돈내면 서울리전 서버로 연결해준다..)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cloudflare의 Https(프록싱)에는 함정이 있다]]></title>
            <link>https://velog.io/@im_h_jo/Cloudflare%EC%9D%98-Https%ED%94%84%EB%A1%9D%EC%8B%B1%EC%97%90%EB%8A%94-%ED%95%A8%EC%A0%95%EC%9D%B4-%EC%9E%88%EB%8B%A4</link>
            <guid>https://velog.io/@im_h_jo/Cloudflare%EC%9D%98-Https%ED%94%84%EB%A1%9D%EC%8B%B1%EC%97%90%EB%8A%94-%ED%95%A8%EC%A0%95%EC%9D%B4-%EC%9E%88%EB%8B%A4</guid>
            <pubDate>Sat, 18 Jan 2025 19:43:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/im_h_jo/post/6fa82cb0-88e9-477a-b3c1-dae26c2a24e0/image.png" alt=""></p>
<h1 id="편히-살고-싶었어요">편히 살고 싶었어요</h1>
<p>내 서버가 생겼다. 
아아피로 접속하는것도 싫어 도메인도 샀다.
그러나 한 가지 문제가 있었으니..</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/cc99cfdd-4d6a-4efc-b4a3-c157257d0aac/image.png" alt="">
<strong>HTTPS를 적용하지 않았다!</strong> 
접속 할 때마다 저런 경고가 발생하는 서비스는 사람들이 그닥 반가워하지 않을것이다..
따라서 HTTPS를 적용하고자 하다가 CloudFlare라는 서비스를 찾았다.</p>
<h1 id="cloudflare">Cloudflare</h1>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/537e6817-9fcc-4376-9a0c-375856b55577/image.png" alt="">
다들 인터넷을 돌아다니며 이런 화면을 한번 쯤 봤을 것이다. 묘하게 기분이 나빠지는 문구로, 봇이 아닌지 확인하는  기능이다.
DNS를 비롯해, 이런 DDos나 방어 솔루션을 제공하는게 CloudFlare다.</p>
<h2 id="날먹-성공">날먹 성..공?</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/df3285c4-45b8-4a55-95da-c211b8858d33/image.png" alt=""></p>
<p>Cloudflare는 dns만 등록해도 https를 자동으로 적용해준다. 진짜 말도 안되는 편한 서비스처럼 보인다!
그 비밀은 프록싱에 있는데..</p>
<h2 id="프록시">프록시</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/37db9d9a-026b-469a-81a2-d05b8baccd22/image.png" alt="">
클라우드 플레어는 접속을 중개한다. 즉, domain.com에 접속하면 바로 연결되는것이 아닌, cloudflare를 한 번 거친다.</p>
<h3 id="특징">특징</h3>
<p>이러면 장점이 많아보인다!</p>
<ol>
<li>원본  서버의 아이피가 노출되지 않는다.</li>
<li><code>유저&lt;-&gt;cloudflare</code>는 https로 통신하고, <code>cloudflare&lt;-&gt;서버</code> 는 http로 통신한다. 이렇게만 해도 https를 적용한 효과를 꽤나 볼 수 있다!</li>
<li>cloudflare가 cdn 역할도 해준다.</li>
</ol>
<p><em><strong>그러나............</strong></em>
무료 플랜은 접속하는 cloudflare 서버가 서울에 없다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/059d03da-35bf-4412-81ee-967b68db7599/image.png" alt=""></p>
<h3 id="느려">느려</h3>
<p>처음엔 무슨 차이인지 이해가 안 갈수도 있다.
만약 cloudflare 중개 서버가 서울에 있다면
유저(국내) &lt;-&gt;  cloudflare(국내) &lt;-&gt; 서버(국내)로 통신이 가능한데, </p>
<p>미국에 있다면</p>
<p>유저(국내) &lt;-&gt; cloudflare(미국) &lt;-&gt; 서버(국내)로 통신한다..!!!
<img src="https://velog.velcdn.com/images/im_h_jo/post/9dbd1f0d-7467-4e3f-a4f7-e5983844f4c1/image.png" alt="">
그말인 즉슨.. 집 앞 편의점을 가려고 비행기로 한바퀴 돌고 오는것과 다를 바가 없었다.
에이~ 데이터는 광케이블을 타고 광속으로 움직이는데, 해외여봤자 <strong>얼마나 차이가 나겠어?</strong>
<img src="https://velog.velcdn.com/images/im_h_jo/post/19ff6c69-3553-4951-921c-568a94fc964e/image.png" alt=""></p>
<p>6번 hop이 cloudflare인데, 저기 미국이다.
갑자기 128ms나 시간이 소요된걸 보니, 아주 유의미한 차이다.
충격은 받았지만 돈은 내기 싫고, https는 적용하고싶은 나를 위해 다음 글에서 적용법을 알아보자.
마지막으로는 적용 이후 응답 속도를 미리 보여주며 마친다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/bb33d643-308f-4fb6-bea2-f8e10aa7fa1c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security에서, 현재 로그인한 엔티티를 바로 불러오자 ( + null )]]></title>
            <link>https://velog.io/@im_h_jo/Spring-Security%EC%97%90%EC%84%9C-%ED%98%84%EC%9E%AC-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%9C-%EC%97%94%ED%8B%B0%ED%8B%B0%EB%A5%BC-%EB%B0%94%EB%A1%9C-%EB%B6%88%EB%9F%AC%EC%98%A4%EC%9E%90-null</link>
            <guid>https://velog.io/@im_h_jo/Spring-Security%EC%97%90%EC%84%9C-%ED%98%84%EC%9E%AC-%EB%A1%9C%EA%B7%B8%EC%9D%B8%ED%95%9C-%EC%97%94%ED%8B%B0%ED%8B%B0%EB%A5%BC-%EB%B0%94%EB%A1%9C-%EB%B6%88%EB%9F%AC%EC%98%A4%EC%9E%90-null</guid>
            <pubDate>Mon, 05 Feb 2024 16:37:41 GMT</pubDate>
            <description><![CDATA[<p>Spring Security를 쓸 때, 현재 로그인한 유저 엔티티가 필요한 상황은 많다.
당장 로그인 자체를 구현하지 않았다면, <a href="https://velog.io/@im_h_jo/Spring-Security%EB%A1%9C-%EC%84%B8%EC%85%98-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84">해당 기능부터 구현</a>해보자.</p>
<pre><code class="language-java">UserDetails principal = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal()
username = principal.getUsername()
User usr = repository.findByUsername(username);</code></pre>
<p>엔티티를 로드하기 위해서는 항상 위 같은 코드를 쓸 수 있는데, 모든 권한이 필요한 작업을 할 때 마다 반복적으로 위 코드를 호출하는것은 꽤나 마음에 들지 않는다. 추가로, username을 기반으로 다시 DB에 쿼리를 날려서 객체를 가져오는 것 또한 불필요한 작업이다.</p>
<p>이를 위해 @AuthenticationPrincipal을 많이 사용하는데, 이부터 확인해보자.</p>
<h2 id="1-authenticationprincipal-적용해보기">1. @AuthenticationPrincipal 적용해보기</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/4316c7e0-7dac-42a4-9424-e2ff76da79a3/image.png" alt="">
먼저, Security의 흐름은 위와 같다. 여기서 UserDetailsService에서 사용자의 username을 기반으로 객체를 로드하고, 해당 객체와 password를 비교하는 과정을 거친다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/34f54b97-b516-44cd-b0b1-cb653fbee173/image.png" alt="">
UserDetailsService는 Interface인데, 해당 작업을 위한 메서드는 loadUserByUsername이 존재함을 알 수 있다.
우리는 이 UserDetailsService를 Implement하는 커스텀 클래스를 구현할 것이다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserRepository repository;
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Optional&lt;User&gt; user = repository.findByEmail(email);
        return user.map(SecurityUser::new).orElseThrow(() -&gt; new UsernameNotFoundException(email));
    }
}</code></pre>
<p>나는 username 대신 email을 사용했기에, repository에 email을 기반으로 쿼리를 날린다.
Interface를 구현하는 클래스를 만들고, return을 보면 SecurityUser라는 클래스로 매핑한다. 이 또한 내가 구현한, 위에 있는 UserDetails의 구현체다. 해당 코드를 보자.</p>
<pre><code class="language-java">public class SecurityUser extends User {

    private com.tripbros.server.user.domain.User user;

    public SecurityUser(com.tripbros.server.user.domain.User user) {
        super(user.getEmail(), user.getPassword(), AuthorityUtils.createAuthorityList(user.getRole().toString()));
        this.user = user;

    }
}</code></pre>
<p>UserDetails의 구현체인, User를 상속한다. 내가 유저의 엔티티 이름을 user로 지었고, Security에서도 user라는 이름이 사용되어 패키지 경로까지 작성 된 점은 양해 바란다.
보면 SecuriyUser는 필드로 <strong>내가 만든 User</strong> 객체를 필드로 가진다. 이후 생성자에서 this.user = user;로 이를 할당해준다.</p>
<p>이후 Security의 config 파일로 가서, 내가 방금 만든 UserDetailsService를 사용함을 선언하자.</p>
<pre><code class="language-java">private final UserDetailsServiceImpl userDetailsService; //의존성 주입받음.

...(생략
.userDetailsService(userDetailsService);</code></pre>
<p>이를 거쳐서, 우리는 UserDetails를 재정의 했다.
이제 컨트롤러에서 @AuthenticationPrincipal을 사용하면 되는데...
이 어노테이션은 어떻게 작동할까?
해당 어노테이션을 구현?한 클래스인 <strong>AuthenticationPrincipalArgumentResolver</strong> 를 보자.</p>
<pre><code class="language-java">@Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
        if (authentication == null) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        AuthenticationPrincipal annotation = findMethodAnnotation(AuthenticationPrincipal.class, parameter);
        String expressionToParse = annotation.expression();
        if (StringUtils.hasLength(expressionToParse)) {
            StandardEvaluationContext context = new StandardEvaluationContext();
            context.setRootObject(principal);
            context.setVariable(&quot;this&quot;, principal);
            context.setBeanResolver(this.beanResolver);
            Expression expression = this.parser.parseExpression(expressionToParse);
            principal = expression.getValue(context);
        }
        if (principal != null &amp;&amp; !ClassUtils.isAssignable(parameter.getParameterType(), principal.getClass())) {
            if (annotation.errorOnInvalidType()) {
                throw new ClassCastException(principal + &quot; is not assignable to &quot; + parameter.getParameterType());
            }
            return null;
        }
        return principal;
    }</code></pre>
<p>중간에 parse 과정등은 넘어가면,  SecurityContextHolder.getContext().getAuthentication().getPrincipal()의 과정을 대신 처리해주고 return해줌을 알 수 있다.
따라서 ,우리는 컨트롤러에서 @AuthenticationPrincipal 뒤에 Userdetails 타입 객체를 주입받을 수 있다.</p>
<pre><code class="language-java">@GetMapping
public String test(@AuthenticationPrincipal SecurityUser user) {
        if (user == null) throw new SomeException();
        return user.getUser().getNickname();

    }</code></pre>
<p>이런식으로 접근하면, DB에 추가 쿼리 없이 유저 객체를 바로 접근 할 수 있고, 꺼내오는 과정도 편하다.
그런데, 만약 로그인하지 않고 해당 API를 호출하면 어떻게 될까?</p>
<p>바로 user에 null이 담겨 날아온다. 그러면 NPE를 피하기 위해 항상 if (user != null) 코드를 추가해 줘야 하는데, 난 이 부분도 자동화 하고 싶었다. 추가로, @AuthenticationPrincipal보다 더 짧은 이름을 짓고싶었던 마음도 있었다.</p>
<h2 id="2-null-check도-하는-커스텀-어노테이션-선언">2. Null Check도 하는 커스텀 어노테이션 선언</h2>
<p>먼저, Null check를 대행할 유틸 클래스를 하나 만들어주자.</p>
<pre><code class="language-java">public class SecurityUtils {
    public static Object checkAuthenticationPrincipal(Object principal) {
        if (&quot;anonymousUser&quot;.equals(principal)) {
            throw new UnauthorizedAccessException(&quot;인증에 실패하였습니다.&quot;);
        }
        return principal;
    }
}</code></pre>
<p>여기서 갑자기 String으로 &quot;anonymousUser&quot;는 어디서 튀어나온걸까?
만약 인증되지 않았을 경우, 스프링 시큐리티는 principal에 &quot;anonymousUser&quot;라는 <strong>String</strong>을 담아 보낸다.
@AuthenticationPrincipal에서, UserDetails 클래스는 당연히 저 String과 호환이 안되니까, null이 담겨 오는것이다.
그리고 저기서 던지는 Exception은 내가 지정한 Custom Exception이다.
이를 받아줄 @ControllerAdvice와, @ExcpetionHandler를 이용해 예외를 처리해주는 과정은 생략하겠다.</p>
<p>이제, 유틸 클래스를 만들었으면 어노테이션을 만들자.</p>
<pre><code class="language-java">@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = &quot;T(com.tripbros.server.security.SecurityUtils).checkAuthenticationPrincipal(#this)&quot;)
public @interface AuthUser {
}</code></pre>
<p>RetentionPolicy.RUNTIME 은 런타임에도 참조가 가능함을 뜻하고
ElementType.PARAMETER는 사용처가 파라미터임을 뜻한다.
그리고, @AuthenticationPrincipal을 넣어 해당 어노테이션을 감싸는 래퍼 어노테이션처럼 사용 할 것이다.
그런데, expression은 무엇일까?
이는 <strong>SpeL</strong> 이라고 부르는, 스프링의 문법이다.
<a href="https://devwithpug.github.io/spring/spring-spel/">잘 정리된 글</a>을 참고해보자. 런타임에 객체를 접근 및 조작 할 수 있는 문법이다.
여기서, 우리가 만든 유틸 클래스의 해당 메소드를 호출한다. 이를 통해, 널 검사가 이루어진다.</p>
<pre><code class="language-java">@GetMapping
public String test(@AuthUser SecurityUser user) {
        return user.getUser().getNickname();

    }</code></pre>
<p>이제 우리는 Null 검사 또한 할 필요가 없이, 깔끔하게 접근 할 수 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Security 쓰는데 필터가 두번 호출되는 이슈]]></title>
            <link>https://velog.io/@im_h_jo/Spring-Security-%EC%93%B0%EB%8A%94%EB%8D%B0-%ED%95%84%ED%84%B0%EA%B0%80-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@im_h_jo/Spring-Security-%EC%93%B0%EB%8A%94%EB%8D%B0-%ED%95%84%ED%84%B0%EA%B0%80-%EB%91%90%EB%B2%88-%ED%98%B8%EC%B6%9C%EB%90%98%EB%8A%94-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Wed, 31 Jan 2024 19:36:16 GMT</pubDate>
            <description><![CDATA[<p>Spring Security로 JWT 기반 로그인을 구현중이다.
이 때, 요청이 들어오면 필터에서 토큰을 읽고, 인가하는 로직이 존재한다. 이는 다음과 같다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    public static final String AUTHORIZATION_HEADER = &quot;Authorization&quot;;
    public static final String BEARER_PREFIX = &quot;Bearer&quot;;

    private final TokenProvider tokenProvider;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
        String token = getToken(request);
        if (token != null &amp;&amp; tokenProvider.validateToken(token)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
    ...(생략)</code></pre>
<p>tokenProvider.getAuthentication(token);에서 DB에 접근하는 코드가 있다. 그러나 매 요청마다, 쿼리가 두 번씩 나갔다!
처음에는 해당 부분 로직 문제인줄 알았는데, 알고보니 Filter 자체가 두번 호출되는 것이였다.</p>
<p><strong>응? OncePerReqeustFilter는 한번만 실행 보장 아닌가??</strong>
라고 하면 맞는말이긴 하다!
여기서 문제는, 내가 저 커스텀 필터를 Spring Security의 config에서 다음같이 등록해뒀다.</p>
<pre><code class="language-java">.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);</code></pre>
<p>스프링에서는, <strong>@Bean</strong>으로 등록된 것중 필터가 있다면 자동으로 필터체인에 등록한다.
그런데, 내가 추가로 Config를 통해 필터체인의 특정 부분에 추가로 등록하였기 때문에 두 번 호출되는 것이다.</p>
<h3 id="해결">해결</h3>
<p>단순하다. 그냥 <strong>@Component</strong>를 제거해서 스프링 빈으로 등록하지 않으면 된다. 습관성 빈 등록을 주의하자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] DTO를 Class에서 Record로 변경해보자]]></title>
            <link>https://velog.io/@im_h_jo/Spring-DTO%EB%A5%BC-Class%EC%97%90%EC%84%9C-Record%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@im_h_jo/Spring-DTO%EB%A5%BC-Class%EC%97%90%EC%84%9C-Record%EB%A1%9C-%EB%B3%80%EA%B2%BD%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 29 Jan 2024 18:44:24 GMT</pubDate>
            <description><![CDATA[<p>API를 만들다 보면, 정말 필수적으로 만들게 되는게 Request / Response 를 위한 객체이다.
이를 우린 흔히 &#39;DTO&#39; 라고 부르게 된다.
현재 진행중인 프로젝트의 Request의 일부를 가져와봤다.</p>
<pre><code class="language-java">import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

public class RegisterRequest {
    @NotBlank(message = &quot;이메일은 필수 항목입니다.&quot;)
    @Email
    private String email;

    @NotBlank(message = &quot;비밀번호는 필수 항목입니다.&quot;)
    private String password;

    @NotBlank(message = &quot;닉네임은 필수 항목입니다.&quot;)
    private String nickname;

    @NotNull(message = &quot;나이는 필수 항목입니다.&quot;)
    private Long age;

    // 생성자
    public RegisterRequest(String email, String password, String nickname, Long age) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.age = age;
    }

    // getter
    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }

    public String getNickname() {
        return nickname;
    }

    public Long getAge() {
        return age;
    }

    // setter
    public void setEmail(String email) {
        this.email = email;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public void setAge(Long age) {
        this.age = age;
    }

    // toString
    @Override
    public String toString() {
        return &quot;RegisterRequest{&quot; +
                &quot;email=&#39;&quot; + email + &#39;\&#39;&#39; +
                &quot;, password=&#39;&quot; + password + &#39;\&#39;&#39; +
                &quot;, nickname=&#39;&quot; + nickname + &#39;\&#39;&#39; +
                &quot;, age=&quot; + age +
                &#39;}&#39;;
    }

    public User toEntity() {
        return User.builder()
                ...
                .build();
    }
}
</code></pre>
<p>조금 극적인 차이를 보이기 위해, Lombok을 사용하지 않았다.
위 DTO의 아쉬운 점은 무엇이 있을까?</p>
<blockquote>
<ol>
<li>보일러플레이트 코드가 많다.
 보일러 플레이트 코드는, getter, setter, toString과 같은 코드들을 말한다. 수정을 거치지 않고 여러 부분에 사용되는 반복되는 코드들이다. 이런 코드는 가독성을 저해하고, 해당 클래스가 데이터 클래스임을 알아보기 어렵게 한다.<ol start="2">
<li>(만약) setter가 존재하지 않더라도, 해당 클래스 객체가 불변(immutable) 객체인지 바로 파악하기 어렵다.</li>
</ol>
</li>
</ol>
</blockquote>
<p>이런 점들을 보완하기 위해, Java14에서 처음 도입된 <strong>Record</strong>를 도입해보자.</p>
<h3 id="record란">Record란?</h3>
<p>record는 불변 객체를, 특히 데이터 객체를 쉽게 만들 수 있는 클래스의 신유형이다. JDK16부터 정식 지원한다고 한다.</p>
<p>특징은 다음과 같다.</p>
<blockquote>
<ol>
<li>getter, toString, equals, hashCode와 같은 보일러 플레이트 코드를 자동 생성해준다. 불변 객체기에 setter는 없다!</li>
<li>모든 필드는 자동 private final로 선언된다. 그게 아니라면, static이여야 한다.</li>
<li>생성자를 자동으로 만들어준다.</li>
<li>다른 클래스를 상속 할 수 없다. (레코드가 상속하거나, 다른 클래스가 레코드를 상속하는 행위)</li>
</ol>
</blockquote>
<h3 id="구조">구조</h3>
<p>위 DTO는 다음과 같은 Record로 정의 할 수 있다.</p>
<pre><code class="language-java">public record RegisterRequest(@NotBlank(message = &quot;이메일은 필수 항목입니다.&quot;) @Email String email,
                              @NotBlank(message = &quot;비밀번호는 필수 항목입니다.&quot;) String password,
                              @NotBlank(message = &quot;닉네임은 필수 항목입니다.&quot;) String nickname,
                              @NotNull(message = &quot;나이는 필수 항목입니다.&quot;) Long age, // 출생 년도임
                              ) {
        public User toEntity() {
            return User.builder()
                    ...
                    .build();
    }</code></pre>
<p>이게 끝이다!
getter, 생성자, toString과 같은 코드들은 자동 생성된다.
public record 레코드이름(필드들...){ } 의 꼴로 선언하면 된다.
Record를 사용하면 해당 클래스가 데이터 클래스이자, 불변 객체임을 보장한다. 추가적으로 코드의 가독성도 굉장히 많이 향상되었음을 알 수 있다.</p>
<h3 id="사용">사용</h3>
<pre><code class="language-java">RegisterRequest request = new RegisterRequest(&quot;email@exam.com&quot;, &quot;password&quot;, &quot;nickname&quot;, &quot;2024&quot;);</code></pre>
<p>의 꼴로 사용하면 된다. setter는 없으니, 생성자를 통해 값을 바인딩하면 된다.</p>
<p>헷갈릴 수 있는 점은, 기존에 getter가 있을때는 <strong>request.getEmail()</strong> 과 같은 형태로 getter를 통해 값을 받았지만
record의 경우엔 <strong>record.email();</strong> 과 같이  get이 빠진 형태로 getter가 구현된다. 해당 부분만 적응하면 record의 장점만 느껴지게 될 것이다.</p>
<h3 id="어-근데-setter-없이-request로-쓸-수-있나">어? 근데 Setter 없이 Request로 쓸 수 있나?</h3>
<p>이 부분은 스프링부트에 해당하는 내용으로 , 내가 처음에 헷갈렸던 부분이다.
우리가 Json 형태로 Request를 받고자 하면, <strong>@RequestBody</strong>를 사용한다. 근데 Record엔 Setter가 없는데, 스프링은 어떻게 값을 바인딩해주지?
처음엔 생성자를 쓰나? 라고 생각했지만, @RequestBody는 생성자를 사용하지 않는다.
정답은 @RequestBody는 <strong>Reflection</strong>을 이용해 값을 넣어준다.</p>
<p>(참고로, @ModelAttribute는 생성자를 통해 첫 바인딩을 하고, 바인딩 되지 않은 값들은 Setter를 통해 바인딩한다.)</p>
<h3 id="reflection">Reflection?</h3>
<p>Reflection은 클래스 타입같은걸 모르더라도, <strong>런타임</strong>에 클래스의 메서드, 필드등에 접근 가능하도록하는 자바의 API다.
즉, 런타임에 동적으로 클래스에 접근 및 수정 할 때 사용된다. 우리가 쓰는 Spring의 Annotation들 (@Component를 붙이면 컴포넌트 스캔이 이루어지는 등..)에서도 사용한다. 실제 코드 레벨에서 우리가 사용할 일은 거의 없는 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Controller에서 Lazy-load가 가능한 이유]]></title>
            <link>https://velog.io/@im_h_jo/Spring-Controller%EC%97%90%EC%84%9C-Lazy-load%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@im_h_jo/Spring-Controller%EC%97%90%EC%84%9C-Lazy-load%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sat, 06 Jan 2024 07:41:15 GMT</pubDate>
            <description><![CDATA[<p>스프링 이론을, 특히 트랜잭션과 관련한 내용을 살펴보면서 의문이 생겼다.</p>
<blockquote>
<ol>
<li>영속성 컨텍스트는 Transaction 단위로 관리된다. 즉, 트랜잭션이 종료되면 영속성 컨텍스트는 소멸한다.</li>
<li>만약 Service 계층에서, @Transactional을 붙인 메소드를 호출한다면 트랜잭션은 Service의 메소드가 종료 될 때 함께 종료된다.</li>
<li>그렇다면, 컨트롤러에서 Service 계층의 메소드를 호출해 엔티티를 받았다면, 그 때는 트랜잭션이 끝난 상태이다.</li>
<li>해당 엔티티는 &quot;Detached&quot;. 즉 영속성 컨텍스트의 관리를 받지 않는다</li>
<li>Detached 상태 엔티티는 DB와 연결 매개체가 없으므로, Lazy-Loading을 사용 할 수 없다.</li>
<li>그렇다면 컨트롤러에선 Lazy-Loading 필드에 접근 할 수 없..다?</li>
</ol>
</blockquote>
<p>위와 같은 흐름을 통해 의문이 생겼는데, 그러면 컨트롤러와 뷰는 Lazy-Load를 쓸 수 없는건가?
<img src="https://velog.velcdn.com/images/im_h_jo/post/f2e76351-5a2d-4a4b-9110-7f9aa7b77192/image.png" alt="">
AOP를 통해 메소드의 시작/종료에 로그를 찍게 했다. 보면 Service 및 Controller까지 종료되었고, 타임리프를 통해 뷰를 그렸다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/e346521c-2a4e-4aed-85ff-1b0a7a62c9d8/image.png" alt="">
이후 타임리프에서, Lazy-load로 설정된 필드를 접근했는데...!! (일부러 N+1 문제를 발생시켰다.)
쿼리가 나간다. </p>
<p>뭐지? 분명 트랜잭션도 끝났고 영속성 컨텍스트도 죽었을텐데..?
질문이 복잡해서 지피티한테 먼저 물어봤다
<img src="https://velog.velcdn.com/images/im_h_jo/post/c711addb-82d6-418d-bdc2-03bf4ac6a605/image.png" alt="">
1번은.. 정확하지 않은 내용같았다.
그런데 2번, 처음보는 개념인 OSIV가 등장했다!
<img src="https://velog.velcdn.com/images/im_h_jo/post/43c110bf-ce71-4289-a3af-c274fb149a2b/image.png" alt="">
구글에 같은 이미지가 많길래 주워왔다..
트랜잭션은 죽더라도, 영속성 컨텍스트의 라이프사이클은 Request에 맞춰주는 기능이라고 한다.
스프링에서는 <strong>기본적으로 활성화</strong> 되어있다.
단, 트랜잭션은 Service 계층부터 살아있으니 컨트롤러에서 영속성 객체의 값을 변경하더라도 <strong>flush하지 않는다.</strong></p>
<hr>
<h2 id="단점">단점</h2>
<p>단점으로는, 컨트롤러와 뷰에서도 DB와의 커넥션을 물고 있다는 점이다.
만약 컨트롤러에서 외부 API를 아주 열심히..정말..오래오래.. 호출하고 있다고 하자
이 때, DB와는 관계없는 일이지만 커넥션을 쭉 물고있다. 따라서 이용자가 많아지면 커넥션 풀 관리가 정말 힘들어진다.</p>
<hr>
<p>따라서, OSIV의 개념을 알고 로직에 따라 적절히 설정해 사용하는게 중요하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] Soft Delete vs Hard Delete]]></title>
            <link>https://velog.io/@im_h_jo/DB-Soft-Delete-vs-Hard-Delete</link>
            <guid>https://velog.io/@im_h_jo/DB-Soft-Delete-vs-Hard-Delete</guid>
            <pubDate>Sun, 31 Dec 2023 14:26:20 GMT</pubDate>
            <description><![CDATA[<p>이번에 새로 알게된 개념인 Soft Delete에 대해 같이 공부해보자.</p>
<p>먼저 가정해보자. 우리의 User가 하나 존재한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/9c12b815-7648-49ba-abb5-d4707e89e553/image.png" alt="">
유저가 잘 서비스를 사용하다가, 탈퇴를 하려고 한다. 이럴 때 어떻게 처리해야 할까?
당연히 그냥 delete를 통해 user를 지워버리면 된다. 이게 JPA의 기본 수행 방법이자, Hard Delete라고 부르는 방법이다.
그런데, 요구 사항이 추가되었다. 유저가 실수로 탈퇴 했을 수 있으니 복구 기능을 추가하고 싶다.
그렇다면 어떻게 해야할까?</p>
<p>다양한 사용 방법이 존재하지만, 이럴 때 사용 가능한 것이, Soft Delete다.</p>
<hr>
<h3 id="삭제한척하기">삭제..한척하기</h3>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/8f6df02f-39dc-4b01-a06e-6d1f9b4108e0/image.png" alt="">
삭제 여부를 체크하는 칼럼을 추가해서, 만약 탈퇴할 경우 user를 지우는 것이 아닌 delete_flag만 1로 업데이트 해주는 방식이다.
이러면 영구적으로 삭제되지 않고, delete_flag의 값을 where 절에 넣어서 탈퇴한 유저들의 값을 모아 볼 수도 있다.</p>
<hr>
<h3 id="장점">장점</h3>
<ol>
<li>복구 가능성
당연하게도, 실수로 delete한 튜플을 되돌리는 법은 구글에 검색해도 더 확고한 절망을 가지게 된다. 그러나 Soft delete의 경우, 실제 튜플을 삭제하지 않아 복구 할 수 있다. </li>
<li>성능
update는 delete보다 빠르다. 
라고 찾았는데, 얼마나 빠를지 궁금해서 직접 테스트를 구성했다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/edf9f499-dd03-40a7-b133-7471f69a39b1/image.png" alt="">
먼저 각 테스트 수행 전, 10만개의 엔티티를 삽입했다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/019749ad-62a8-4439-b32b-d8cf4dbcd8d2/image.png" alt="">
이후 각 메소드는 업데이트와 삭제를 진행하며 수행 시간을 측정한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/94232743-114d-4828-af79-2bf9c7d1cdf8/image.png" alt="">
<img src="https://velog.velcdn.com/images/im_h_jo/post/7efe03f7-c9d8-442a-b35f-38b6563e4b9c/image.png" alt="">
단위는 ms다. 약 1.5초가량 Update가 더 빠른 모습을 보였다. 7%가량 빠른 결과다.</li>
</ol>
<hr>
<h3 id="단점">단점</h3>
<p>그렇지만 위 장점만 있었으면 Soft Delete가 표준이 되었을 것이다..</p>
<ol>
<li>공간 낭비
soft delete는 튜플을 삭제하지 않고, 칼럼값만 바꾼 뒤 DB에 냅둔다. 따라서 삭제연산한 튜플이 DB에 지속적으로 남아있으니 공간을 많이 잡아먹게 된다. 그 말은 즉 조회 쿼리 성능에도 영향을 줄 수 있다.</li>
<li>복잡성
이 복잡성은 개발자 입장이다. Soft Delete를 채용 할 경우, 모든 개발자가 Soft Delete를 수행함을 명확히 인지하고 있어야 한다. 만약 실수로라도 update가 아닌 delete 쿼리를 써버린다면.. 결과는 처참하다. 이를 어느정도 막기 위해 JPA는 <code>@SQLDelete</code>라는 어노테이션을 통해, 삭제 쿼리 수행시 대체할 쿼리를 엔티티 레벨에서 제한 할 수 있다.</li>
</ol>
<hr>
<h3 id="개인적인-모호함">개인적인 모호함</h3>
<p>위 단점 말고도, 나는 칼럼 중 Unique Constraint가 걸린 경우에 대해 의문이 생겼다.
만약 Nickname이라는 테이블이 있고, 닉네임은 중복일 수 없다.
이 테이블에 대해 Soft delete를 적용하면, 삭제로 표기된 튜플이더라도 DB에는 존재하기 떄문에, 한번 쓰인 닉네임은 다시는 쓸 수 없게 되어버린다.
이에 대해 찾아봤다.</p>
<pre><code class="language-sql">ALTER TABLE users
ADD not_archived BOOLEAN
GENERATED ALWAYS AS (IF(deleted_at IS NULL, 1, NULL)) VIRTUAL;</code></pre>
<p>MySQL 기준, deleted_at이 NULL이면 1이고, 아니면 NULL이 되는 Virtual 칼럼을 선언하고</p>
<pre><code class="language-sql">ALTER TABLE users
ADD CONSTRAINT UNIQUE (email, not_archived);</code></pre>
<p>그 두개를 같이 UNIQUE로 묶어버리는 방법을 사용했다.</p>
<p>나도 Virtual Column에 대해 처음 들어봤는데, 이는 실제 칼럼처럼 데이터는 추가되지 않기에 db 용량에는 영향을 주지 않는다. 하지만 select 구문이 실행 될 때 마다 AS 뒤에 있는 트리거가 실행되는 형태로 작동한다고 한다.</p>
<hr>
<h3 id="개인적인-결론">개인적인 결론</h3>
<p>사실 나는 Soft Delete를 알고 나니까, 잘 써야할 큰 메리트를 모르겠다. 정말 삭제후 일정 기간동안 복구를 해야할 유예 기간을 준다던가 하는 경우정도가 아니면, 보통 Hard Delete가 더 좋을 것 같다. UPDATE로 인한 성능 개선보다는, 잃는 조회 성능이 더 뼈아플 것 같다.
만약 유예기간이 필요한 경우에는, 별도의 테이블에 데이터를 임시로 이관하는 방법도 떠오르기도 한다. 마지막으로 StackOverFlow에 있는  </p>
<blockquote>
<p>&quot;Soft Delete는 좋은 아이디어인가요?&quot; </p>
</blockquote>
<p>라는 질문 게시글에서, 100개의 추천을 받은 답변의 첫 문장을 첨부하며 마무리한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/324f0480-e95f-4c9a-95b5-ec3e0c240a8e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] OrphanRemoval의 불편한 성능]]></title>
            <link>https://velog.io/@im_h_jo/Spring-OrphanRemoval%EC%9D%98-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%84%B1%EB%8A%A5</link>
            <guid>https://velog.io/@im_h_jo/Spring-OrphanRemoval%EC%9D%98-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%84%B1%EB%8A%A5</guid>
            <pubDate>Thu, 28 Dec 2023 15:39:05 GMT</pubDate>
            <description><![CDATA[<p>JPA 쓸 때 편한 기능이 있다.
바로 orphanRemoval이다.</p>
<pre><code class="language-java">@OneToMany(mappedBy = &quot;folder&quot;, orphanRemoval = true)
    List&lt;Link&gt; links = new ArrayList&lt;&gt;();</code></pre>
<p>One측에서 orphanRemoval을 걸어두면, one측 엔티티를 지울 때 매핑된 Many들을 모두 같이 지워버릴 수 있다.
마치 게시글과 댓글의 관계를 생각해보면 편하다.
그렇다면, 쿼리는.. 어떻게 나갈까?
먼저, 이해를 돕기 위해 나는 
FOLDER - LINK가 1:N으로 매핑되어 있다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/9171368b-4c49-4e01-a346-b349c41f5d18/image.png" alt="">
FOLDER를 삭제 할 경우 제일 먼저, FOLDER와 매핑된 LINK를 모두 조회한다.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/d8807934-c98b-462a-a585-f3ebbc22ccb5/image.png" alt="">
그리고 매핑 된 링크가 N개라면, N개를 ID를 기반으로 하나하나 지운다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/a166bcc0-a0fa-4c0d-8068-75010973c8f2/image.png" alt="">
마지막에 폴더를 지워준다.</p>
<p>아무리봐도 성능상 그냥 큰 손해같았다. 뭔가 머리속에 N+1 문제가 떠오르면서, 그거보다 손해같았다..
내가 코드 몇 줄 덜 짜겠다고 쿼리 한두번이 아닌, N번이 더나가는 문제는 수정해야 했다.</p>
<hr>
<p>벌크 연산을 사용해보자.</p>
<pre><code class="language-java">    @Query(&quot;delete from Link l where l.folder = :folder&quot;)
    @Modifying(clearAutomatically = true, flushAutomatically = true)
    void deleteAllByFolder(@Param(&quot;folder&quot;) Folder folder);</code></pre>
<p>LinkRepository에 직접 벌크 삭제 쿼리를 작성하자. @Modyfying 애노테이션을 붙여야, 더티체킹 패스하고 바로 우리가 원하는 쿼리가 날아간다.
그렇게 된다면, 영속성 Context는 이 사실을 알지 못한다. 따라서 clear &amp;&amp; flush를 해줘 캐시를 초기화해주자.</p>
<hr>
<p>변경 후엔 잘 작동 할까?
<img src="https://velog.velcdn.com/images/im_h_jo/post/3909e5e9-804c-43d8-98bc-279df4aee269/image.png" alt="">
Repository에 쿼리 하나, Service에서 호출 하나 추가해서 아주 단순하게 고쳐졌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 한 Service는 여러 Repository를 의존해도 될까?]]></title>
            <link>https://velog.io/@im_h_jo/Spring-%ED%95%9C-Service%EB%8A%94-%EC%97%AC%EB%9F%AC-Repository%EB%A5%BC-%EC%9D%98%EC%A1%B4%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@im_h_jo/Spring-%ED%95%9C-Service%EB%8A%94-%EC%97%AC%EB%9F%AC-Repository%EB%A5%BC-%EC%9D%98%EC%A1%B4%ED%95%B4%EB%8F%84-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Wed, 27 Dec 2023 09:49:00 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트를 하다가 깊게 고민할 거리가 생겼다.
세상에서 가장 단순한 게시판을 하나 생각해보자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/54fd7bce-eb1f-410f-832c-e02dc2aa0d18/image.png" alt=""></p>
<p>하나의 게시글은, 여러 댓글을 가질 수 있다. 따라서 
게시글 : 댓글은 1:N으로 매핑되어 있다.</p>
<p>만약 MVC에 맞춰 코드를 작성 할 때, 댓글을 추가하는 기능을 작성하고자 한다.
댓글을 추가하고자 한다면, Post의 정보를  가져와야 한다. 그렇다면 CommentService에서, PostRepository를 참조하게 된다.
예시 코드를 보자.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class CommentService{
    private final CommentRepository commentRepository;
    private final PostRepository postRepository;

    public void addNewPost(String content, Long postId){
        Comment comment = new Comment();
        comment.setContent(content);
        comment.setPost(postRepository.getById(postId));
        commentRepository.save(comment);
        }
}</code></pre>
<p>CommentService에서 PostRepository도 의존하고 있다.
 당장은 정말 단순한 코드여서 문제가 없어보이지만, 규모가 커지며 한 서비스가 5개,6개가 넘어가는 리포지토리를 의존하게 된다면
너무 복잡해지지 않을까? 라는 고민을 갖게 되었다.</p>
<hr>
<h2 id="stackoverflow">StackOverFlow</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/3f4e2cfc-825b-4ba1-8baf-c15ef6591d55/image.png" alt="">
StackOverFlow에서 나온 비슷한 고민의 경우엔, &quot;괜찮다&quot; 의견이였다. 비즈니스 로직이 없다면 Repository를, 비즈니스 로직이 필요하다면 코드 중복 제거를 위해 Service를 의존해 사용해도 괜찮다였다.</p>
<hr>
<h2 id="인프런">인프런</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/312b67c4-8e2a-4428-989a-a595f4b23cb5/image.png" alt="">
이곳에서도 &quot;괜찮다&quot; 라는 의견이였다. 조금 놀라웠던건 컨트롤러에서 Repository를 직접적으로 참조해 사용한다는 경우였다. 그러면 계층 관계가 복잡해지지 않을까? 라는 고민이 또 생기지만, 아키텍쳐를 크게 해치는 구조가 아니라면 괜찮다고 간주하는 것 같다.</p>
<hr>
<h2 id="퍼사드-패턴">퍼사드 패턴</h2>
<p>아키텍쳐적으로, Repository와 Service를 1:1로 매핑하고자 할 때, 찾아보니까 퍼사드 패턴 (Facade Pattern)을 적용 할 수 있을 것 같다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/faf1164c-bc56-4fa1-bc94-678d84f0f730/image.png" alt="">
이미지 출처 : <a href="https://www.happykoo.net/@happykoo/posts/266">https://www.happykoo.net/@happykoo/posts/266</a></p>
<p> 퍼사드 패턴은, 클라이언트는 복잡하게 얽혀있는 서브시스템은 모른 채 Facade 객체에만 의존하는것이다.
 우리 예시로 따지면, 하나의 서비스가 의존할 여러 Repository를 가지고 우리가 만들 Service에서 필요한 메소드를 가지게 되는, 일종의 조합 전용 객체를 만드는 방법이다.</p>
<p> 따라서 서비스는 Facade 객체만 의존하게 되고, Facade 객체는 사용할 Repository들을 주입받는 형태로 구성된다.</p>
<hr>
<p> 코드를 짜는 시간보다는, 이러한 고민에 대한 답을 찾는 시간이 정말 긴 것 같다. 앞으로도 해나갈 고민들을 잘 정리해가자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security로 세션 기반 로그인 구현]]></title>
            <link>https://velog.io/@im_h_jo/Spring-Security%EB%A1%9C-%EC%84%B8%EC%85%98-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@im_h_jo/Spring-Security%EB%A1%9C-%EC%84%B8%EC%85%98-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 19 Dec 2023 13:40:02 GMT</pubDate>
            <description><![CDATA[<p>Spring을 사용하면서 로그인등의 기능이 필요한 경우가 아주 많다.
정말 처음 배울 때 처럼, Plain text로서 PW를 저장한다던가 하는건 불법이고 당연히 배포할 프로젝트에서 보안적으로 절대 사용해선 안된다.</p>
<p>따라서, 우리가 편하게 기능들을 사용 할 수 있도록 도와주는 Spring Security를 사용해보자.</p>
<hr>
<p>Spring Security 구조
Spring Security는 진입 장벽을 가진 Reference doc을 가지고 있다.
<a href="https://docs.spring.io/spring-security/reference/index.html">https://docs.spring.io/spring-security/reference/index.html</a></p>
<p>이 글에서,같이 Refernce doc을 읽으며 세션 기반 로그인을 구현해보자.</p>
<pre><code class="language-java">@Entity @Getter
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private String nickname;
    private String password;
    private LocalDate registeredAt;

    @Enumerated(EnumType.STRING)
    private Role role;
}</code></pre>
<p>먼저, User에 대한 Entity를 선언하자. Role의 경우엔 필수라고 할 수는 없지만, 그래도 Reference 문서에서 많이 활용하는 것 같기에 추가했다.</p>
<pre><code class="language-java">@Getter
public enum Role {
    ROLE_ADMIN(&quot;admin&quot;), ROLE_USER(&quot;user&quot;);
    private final String description;
    Role(String description) {
        this.description = description;
    }
}</code></pre>
<p>Role은 Enumerate로 선언해주자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/f8eacac6-ce42-43f6-802e-783fa5bd3f3c/image.png" alt="">
출처 : <a href="https://mangkyu.tistory.com/">https://mangkyu.tistory.com/</a></p>
<p>Spring Security는 코드를 작성 하기 전, 구조를 잘 파악해둬야 한다. 코드 짜는 시간보다는, 구조 파악하는 시간이 더 길다..</p>
<blockquote>
<ol>
<li>사용자가 로그인 요청(Request)를 보낸다.</li>
<li>AuthenticationFilter가 이를 캐치한다. 이후 UsernamePasswordAuthenticationToken 객체를 생성한다. 이 토큰은 JWT같은 토큰이 아니다!</li>
<li>AuthenticatinManager는 인터페이스기에, 이를 구현한 ProviderManager에게 방금 생성한 객체를 넘긴다.</li>
<li>ProviderManager는 AuthenticationProvider (여러 개 일 수 있음)을 통해 인증을 요청한다</li>
<li>Provider(s)는 우리 DB에서 사용자 정보를 가져올 Service인 UserDetailsService에 정보를 넘긴다.</li>
<li>Service를 통해, UserDetails 객체 (return받은 사용자 정보)를 얻는다.</li>
<li>AuthenticationProvider(s)는 객체의 정보를 가지고 올바른지 인증한다.</li>
<li>성공시, Authentication 객체를 만들어 반환한다.</li>
<li>필터로 해당 객체를 넘긴다.</li>
<li>이 Authentication 객체를, SecurityContext에 넣는다. 이는 곧 세션이다.</li>
</ol>
</blockquote>
<p>대략적인 흐름만 생각하면서, 코드를 작성해 공부해보자.</p>
<hr>
<p><a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html">https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html</a>
위 문서를 기반으로 코드를 작성해보자.</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity
            .authorizeHttpRequests((authorize) -&gt; authorize
                .requestMatchers(&quot;/test&quot;).authenticated()
                .requestMatchers(&quot;/admin&quot;).hasRole(&quot;ADMIN&quot;)
                .anyRequest().permitAll())
            .formLogin(form -&gt; form
                .loginPage(&quot;/login&quot;)
                .permitAll());
        return httpSecurity.build();
    }
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}</code></pre>
<p>먼저 Config를 하나 만들어주자.
&quot;/test&quot;에 대해선 인증이 필요하고 (로그인 필요)
&quot;/admin&quot;에 대해선 ROLE_ADMIN이,
그 외 페이지에 대해선 누구나 접근 가능하게 설정했다.</p>
<p>이후, &quot;/login&quot;은 로그인 페이지로서 지정했다 로그인 페이지 또한 당연하게도 permitAll()로 누구나 접근 가능하게 해준다.
나는 <a href="https://velog.io/@im_h_jo/SpringBoot-Bcrypt-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%A7%A4%EB%B2%88-%EA%B2%B0%EA%B3%BC%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A7%88-%EB%95%8C">BCryptEncoder</a> 를 비밀번호 Encoder로서 사용할것이기에, Bean으로 등록해줬다.</p>
<hr>
<h2 id="회원가입">회원가입</h2>
<p>먼저 ,회원가입부터 구현하자. 회원가입은 Spring Security를 거치지 않고 자체 구현하면 된다. Spring Security는 인증과 인가, 권한을 관리하기에 회원 가입과 큰 연관은 없다.</p>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

}</code></pre>
<p>먼저 리포지토리를 만들어주고 </p>
<pre><code class="language-java">@SpringBootTest
@Transactional
class MemberRepositoryTest {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private BCryptPasswordEncoder encoder;
    @Test
    @Commit
    void register(){
        Member member = Member.builder()
            .username(&quot;testid&quot;)
            .password(encoder.encode(&quot;password&quot;))
            .nickname(&quot;testNick&quot;)
            .registeredAt(LocalDate.now())
            .role(Role.ROLE_ADMIN)
            .build();
        Member saved = memberRepository.save(member);
        Assertions.assertThat(saved).isEqualTo(member);
    }
}</code></pre>
<p>테스트 코드를 통해 DB에 테스트 데이터를 하나 넣자. @Commit을 붙이면 @Transactional이 붙어있어도 롤백되지 않는다.
아까 빈으로 등록한 encoder도 불러와서, PW는 Encode해서 넣어준다!
<img src="https://velog.velcdn.com/images/im_h_jo/post/d53d3c81-5e0e-461e-bd00-3b88329e445e/image.png" alt="">
행복
<img src="https://velog.velcdn.com/images/im_h_jo/post/399c50c1-01cf-4146-bdcf-021ff67eef4d/image.png" alt="">
데이터도 잘 들어갔다. 이제 다시 로그인을 구현해보자.</p>
<hr>
<h2 id="로그인">로그인</h2>
<p>간단하게만 구현 할 경우, 반드시 추가로 설정해야 할 것은 UserDetailsService와 Users다.
UserDetailsService에는 우리가 디비에서 Username을 기반으로 정보 (id, pw, role..)를 가져오기 위한 인터페이스고
Users는 Security에 있는 클래스를 우리 엔티티인 Member로서 사용해야 한다.</p>
<pre><code class="language-java">@Getter @Setter
public class CustomUser extends User {
    private Member member;

    public CustomUser(Member member) {
        super(member.getUsername(), member.getPassword(), AuthorityUtils.createAuthorityList(member.getRole().toString()));
        this.member = member;
    }
}</code></pre>
<p>위와 같이 만들어준다. 어려운 코드는 아니다. 해당 커스텀 유저는 Bean으로서 등록하지 않아도 된다 (주입받는 클래스는 존재하지 않는다.)</p>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Optional&lt;Member&gt; findByUsername(String userId);
}</code></pre>
<p>그리고 리포지토리에 findByUsername을 만들어주자.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    private final MemberRepository memberRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional&lt;Member&gt; member = memberRepository.findByUsername(username);
        return member.map(CustomUser::new).orElseThrow(() -&gt; new UsernameNotFoundException(username));
    }
}</code></pre>
<p>이후 UserDetailsService를 구현하는 클래스를 만들자. UserDetailsService는 인터페이스다!
loadUserByUsername을 Override하면 된다.
여기서 우리 디비에서 아이디를 기반으로 엔티티를 찾아오고, 위에서 만든 커스텀 유저에 매핑해서 던져버리면 된다. 람다식 최고</p>
<hr>
<p>이제, POST로 (form 데이터로!!)username, password를 작성해  /login에 던져버리면 처리가 된다.
하지만 난 이 과정에서 로그인이 되지 않는 문제를 발견했고, 디버깅을 시작했다.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/c125ca2c-a471-4c81-90c3-937e695d4955/image.png" alt="">
시큐리티 config에서, failure handler를 등록한다.</p>
<pre><code class="language-java">@Slf4j
public class LoginFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
        AuthenticationException exception) throws IOException, ServletException {

        log.info(&quot;fail handler&quot;);
        log.info(exception.getMessage());

        super.onAuthenticationFailure(request, response, exception);
    }
}</code></pre>
<p>핸들러 클래스를 만들어서, 단순하게 로그만 찍어봤다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/3c1055c7-909e-497a-a763-74abf3ae1f50/image.png" alt="">
로그인 실패랑 같은뜻인데, 정말 많이 찾아보니까
username, password는 파라미터 이름이 엔티티에도 동일해야한다. 수정된 상태로 업로드 하지만, 이 글을 처음 쓸 때는 username이 아니라 userId로 했었다. 
만약 엔티티 이름을 바꿀수 없는 사람들은 
<img src="https://velog.velcdn.com/images/im_h_jo/post/60ccddfc-8c62-459c-895b-c8e429bb176d/image.png" alt="">
Config에서 parameter 이름을 바꿔주자. 이거 몰라서 거의 반나절이 날아갔다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Linker] 1인 사이드 프로젝트 기획]]></title>
            <link>https://velog.io/@im_h_jo/Linker-1%EC%9D%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@im_h_jo/Linker-1%EC%9D%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Tue, 19 Dec 2023 07:59:00 GMT</pubDate>
            <description><![CDATA[<p>스프링의 아주 기초는 어느정도 익혔다고 생각해
이제 무언가 내 손으로 만들고 싶어졌다.</p>
<p>따라서, 간단한 사이드 프로젝트를 진행해보고자 한다</p>
<hr>
<h2 id="1--프로젝트-개요">1.  프로젝트 개요</h2>
<p>&#39;Linker&#39;는, 북마크와 비슷하게 웹사이트의 링크를 모아놓고, 공유할 수 있는 서비스다.</p>
<ul>
<li>이름 : Linker</li>
<li>주요 기능 : 링크 관리, 링크 공유, 링크 업로드 (게시판)</li>
<li>개발 프레임워크 : Spring Boot 3.2.0 / Gradle / Spring Security + JWT / Spring Data JPA / QueryDSL </li>
<li>자바 버전 : 17 
<img src="https://velog.velcdn.com/images/im_h_jo/post/f016eccc-d28c-4ec4-97f0-7028e0767f71/image.png" alt=""></li>
</ul>
<hr>
<h2 id="2-요구사항-분석서">2. 요구사항 분석서</h2>
<ol>
<li>회원 가입</li>
</ol>
<ul>
<li>유효성검사
ID는 lower-case 알파벳과 숫자만 허용
닉네임은 특수문자 제외
모든 필드는 NonBlank
비밀번호는 8자 이상, 대문자+소문자+숫자+특수문자로 구성</li>
<li>중복 검사
ID와 닉네임에 대하여 중복 검사 수행</li>
</ul>
<ol start="2">
<li>로그인
모든 필드는 NonBlank
같은 아이디에 대해 너무 많은 로그인 시도시 접속 차단</li>
</ol>
<p>+) 추후, 소셜로그인 도입 고려해보기</p>
<ol start="3">
<li>링크 관리</li>
</ol>
<ul>
<li>링크 추가의 경우, 같은 사용자에게 중복되었는지 확인</li>
<li>링크 추가 과정에서 폴더 선택 가능하도록 하기 </li>
<li>링크 추가 필드는 NonBlank</li>
</ul>
<ol start="4">
<li>링크 공유<ul>
<li>링크 폴더 공유시, URL로 추출하기</li>
<li>사용자의 아이디를 기반으로 공유한다면, 이외 사용자는 접근 권한 검사하기</li>
</ul>
</li>
<li>게시글
링크 폴더를 게시글에 첨부해 작성 할 수 있음
모든 필드는 NonBlank
삭제/수정 권한 검사</li>
<li>댓글
게시글 삭제시 Cascade로 삭제됨
NonBlank
권한 검사</li>
</ol>
<hr>
<h2 id="3erd">3.ERD</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/ea12a854-2a05-414a-a267-bf3954b67b40/image.png" alt="">
프로토타입 뷰는 타임리프와 부트스트랩만 이용해 간단하게 구성할 예정이기에 제외했다.
구현 과정은 시리즈를 통해 업로드 할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MySQL] Member 테이블이 만들어지지 않을 때]]></title>
            <link>https://velog.io/@im_h_jo/MySql-Member-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</link>
            <guid>https://velog.io/@im_h_jo/MySql-Member-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%B4-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EC%9D%84-%EB%95%8C</guid>
            <pubDate>Mon, 18 Dec 2023 11:11:59 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 위해  JPA와 MYSQL을 연동해서 테이블을 만들고 있었다.
유저 엔티티의 이름을 Member로 했는데, 계속해서 테이블이 만들어지지 않았다.. 이전엔 이상이 없었고, 필드 이름중 예약어(update, like같은..)가 존재하나 확인했는데도 없었다.</p>
<blockquote>
<p>2023-12-18T19:53:38.412+09:00  WARN 13392 --- [           main] o.h.t.s.i.ExceptionHandlerLoggedImpl     : GenerationTarget encountered exception accepting command : Error executing DDL &quot;
    create table member (
        id bigint not null,
        nickname varchar(255),
        password varchar(255),
        registered_at date,
        user_id varchar(255),
        primary key (id)
    ) engine=InnoDB&quot; via JDBC [You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &#39;member (
        id bigint not null,
        nickname varchar(255),
        p&#39; at line 1]</p>
</blockquote>
<p>   도저히 이유를 모르겠어서 여기저기 찾아봤다.
   <img src="https://velog.velcdn.com/images/im_h_jo/post/2cc14d8f-cfc4-4c98-9684-e194493a311d/image.png" alt="">
짜잔~ member는 8.0.17에서 예약어였다가 8.0.19에서 사라졌습니다~
<img src="https://velog.velcdn.com/images/im_h_jo/post/6fe54f90-950a-4a42-ad68-0a83955fb592/image.png" alt="">
기가 막히게도, 전공 과목에서 반강제한 MySql 버전이 8.0.18이였다.
삭제 한 뒤
<a href="https://dev.mysql.com/downloads/installer/">https://dev.mysql.com/downloads/installer/</a>
해당 링크에서 최신 버전을 설치해서 해결하자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] QueryDSL을 적용해보자]]></title>
            <link>https://velog.io/@im_h_jo/Spring-QueryDSL%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@im_h_jo/Spring-QueryDSL%EC%9D%84-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 15 Dec 2023 18:38:37 GMT</pubDate>
            <description><![CDATA[<p>몇 달 전, 스프링을 처음 사용 할 때 했던 프로젝트를 열어보았다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/590a19a3-64c2-42f7-8083-c7cde3e02458/image.png" alt=""></p>
<p>동적 쿼리를 위해 StringBuilder를 쓰다보니, 쿼리를 봤을때 뭐하는 쿼리인지 도저히 알아 볼 수 없었다.
따라서 QueryDSL을 도입해보자 한다.</p>
<p>추가로 QueryDSL 적용시 컴파일 타임 검사가 들어가 Type 체크나, 오타등의 여지도 걸러 줄 수 있다. </p>
<h3 id="설정">설정</h3>
<p>내 생각에 QueryDSL에서 가장 어려운건 세팅이다. </p>
<pre><code class="language-java">buildscript {
    ext {
        queryDslVersion = &quot;5.0.0&quot;
    }
}
plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;2.7.13&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.0.15.RELEASE&#39;
    id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot; //추가 된 부분
}

...

    implementation &quot;com.querydsl:querydsl-jpa:${queryDslVersion}&quot;
    implementation &quot;com.querydsl:querydsl-apt:${queryDslVersion}&quot;

...
tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}

def querydslDir = &quot;$buildDir/generated/querydsl&quot;

querydsl { 
    jpa = true
    querydslSourcesDir = querydslDir
}

sourceSets { 
    main.java.srcDir querydslDir
}

configurations { 
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl { 
    options.annotationProcessorPath = configurations.querydsl
}
</code></pre>
<p>먼저, build.gradle로 가서 implementation을 추가해주자.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/9943f4b7-59ba-4b53-b4f4-a3b7dc5b0f9e/image.png" alt=""></p>
<p>이후, Gradle-other 탭에 가서 compileQuerydsl을 실행하자.
<img src="https://velog.velcdn.com/images/im_h_jo/post/f0c3cb90-42a2-45ab-aa4c-52b288445413/image.png" alt=""></p>
<p>그러면 build 디렉토리 안에 이렇게 엔티티마다 Q 클래스가 생성됨을 볼 수 있다. </p>
<pre><code class="language-java">@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}
</code></pre>
<p>이제 config 파일을 하나 만들어주자. JPAQueryFactory를 빈으로 등록해서, 레포지토리에서 주입받아 쓰자.</p>
<p>@PersistenceContext는, EntityManager를 주입받을 때 사용해야 한다는데...?</p>
<p>의존성 주입은 보통 @Autowired를 사용하지 않는가?
<img src="https://velog.velcdn.com/images/im_h_jo/post/8860a4fc-57f8-4512-9b62-98275f476617/image.png" alt=""></p>
<p>스택 오버플로우를 찾아보니, EntityManager는 thread-safe 하지 않는다고 한다. 
즉, 어떤 호출마다 스레드가 생성되고, 그 스레드는 각각의 entitymanager를 가져야 한다. 근데 만약 일반 빈처럼 하나의 EntityManager를 공유하게 된다면.. 다른 사용자들이 하나의 엔티티를 공유하는 대 참사가 발생한다. 따라서 추가된 JPA 표준 Annotation이다. </p>
<h2 id="적용">적용</h2>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/e8f2b0be-825b-44cc-aa72-c1baeadd93b9/image.png" alt="">
출처 : <a href="https://jojoldu.tistory.com/372">https://jojoldu.tistory.com/372</a></p>
<p>만약 스프링 데이터 JPA를쓰고 있었다면, 위 링크를 참조하길 바란다. 나의 경우엔 모든 Repository를 JPQL만으로 썼다.</p>
<h3 id="searchrepositorycustom">SearchRepositoryCustom</h3>
<pre><code class="language-java">public interface SearchRepositoryCustom {
    List&lt;SearchDTO&gt; findResultByLowPrice(SearchForm searchForm);
}</code></pre>
<p>그래도 인터페이스 형태로 미리 구성해둬 나중의 편의를 챙기기로 했다.
먼저,  Custom 인터페이스를 만들었다. QueryDSL을 쓰기 위해선 강제로 인터페이스를 적극 활용하게 되니 좋다,,</p>
<h3 id="searchrepositorycustomimpl">SearchRepositoryCustomImpl</h3>
<p>네이밍은 무조건 Impl로 해야 JPA가 인식한다.</p>
<pre><code class="language-java">@Override
    public List&lt;SearchDTO&gt; findResultByLowPrice(SearchForm searchForm) {
        return queryFactory.select(
                Projections.constructor(
                    SearchDTO.class,
                    p.placeName,
                    p.placeAddress,
                    p.placeRating,
                    p.placeLink,
                    p.placeDistance,
                    p.school,
                    m.id,
                    m.menuName,
                    m.menuPrice,
                    m.menuImg,
                    new CaseBuilder().when(b.menu.isNull()).then(false).otherwise(true)
                )
           )
            .from(m)
            .join(m.place, p)
            .leftJoin(b).on(b.menu.eq(m).
                and(b.userId.eq(searchForm.getUserId())))
            .where(p.school.eq(searchForm.getSchool())
                .and(m.menuPrice.between(searchForm.getMinimumPrice(), searchForm.getMaximumPrice())),
                searchKeyword(searchForm))
            .orderBy(orderMethod(searchForm))
            .offset((searchForm.getPage() - 1) * elementCountInPage)
            .limit(elementCountInPage)
            .fetch();
    }</code></pre>
<p> 위 함수가 QueryDSL을 적용한 버전이다.
 Projections.constructor로 DTO로 바로 매핑해줬다. 
 그리고 가장 큰 발전은 다음 부분이라고 생각한다.</p>
<pre><code class="language-java">      StringBuilder jpqlBuilder = new StringBuilder();
        for (int i = 0; i &lt; searchForm.getSearchKeywordList().size(); i++) {
            if (i &gt; 0) {
                jpqlBuilder.append(&quot; OR &quot;);
            }
            jpqlBuilder.append(&quot;m.menuName LIKE :searchString&quot;).append(i);
        }</code></pre>
<p>기존에는 사용자가 입력한 키워드에 대해 검색을 하려면, 위와 같이 string을 이어 붙였어야 한다.  굉장히 직관적이지 않다. 이런 코드는 조금만 길어지더라도 고치는 것보다 새로 작성하는게 빠를 것 같다.  그리고 이렇게 더하는 방법을 쓰기 위해, 검색어가 있을 경우의 쿼리를 따로 만들어야 해서 관리도 힘들었다.</p>
<pre><code class="language-java"> private BooleanBuilder searchKeyword(SearchForm searchForm){
        if (searchForm.getSearchKeywordList() == null || searchForm.getSearchKeywordList().size() == 0)
            return null;
        BooleanBuilder builder = new BooleanBuilder();
        for(String keyword : searchForm.getSearchKeywordList()){
            builder.or(m.menuName.contains(keyword));
        }
        return builder;
    }</code></pre>
<p>그러나 위 코드를 통해, 최소한 위 StringBuilder보단 유지 관리와 가독성이 향상되었다.</p>
<pre><code class="language-sql"> and m.menuPrice between :minValue and :maxValue order by + searchform().makeSortResult() ... </code></pre>
<p>또한 기존에는 동적으로 정렬 방법을 정하기 위해, 이를 입력 받는 DTO에 위임시켰었다. 추후 유지 보수를 하려면, 직접 저 클래스까지 찾아가서 함수를 봐야 할 것이다.</p>
<pre><code class="language-java">private OrderSpecifier&lt;Integer&gt; orderMethod(SearchForm searchForm){
        switch (searchForm.getSortMethod()) {
            case &quot;lowPrice&quot;:
                return new OrderSpecifier&lt;&gt;(Order.ASC, m.menuPrice);
            case &quot;highPrice&quot;:
                return new OrderSpecifier&lt;&gt;(Order.DESC, m.menuPrice);
            case &quot;distance&quot;:
                return new OrderSpecifier&lt;&gt;(Order.ASC, p.placeDistance);
        }
        return null;
    }</code></pre>
<p>저 부분을 OrderSpecifier를 통해 코드의 형태로 바꿨다. 
참고로, OrderSpecifier의 타입은 정렬 기준의 타입이다. 내가 정렬할 타입인 Price, Distance는 모두 Integer였다. 문자열이면 String, 날짜 형태면 LocalDateTime등을 넣어주면 된다.
만약 이도 저도 아니라면, 그냥 OrderSpecifier&lt;?&gt; 의 꼴로 와일드 카드를 넣자.</p>
<hr>
<h2 id="정리">정리</h2>
<p>기존 프로젝트에 QueryDSL을 도입해보며, 확실히 가독성을 정말 많이 챙긴 것 같다. String 형태의 JPQL에서, 코드를 기반으로 하도록 바뀌었으니 수정하기도 편할 듯 하다. 복잡했던 동적 쿼리를 더 낫게 고친 것 같아 좋다.
아직 문법이 익숙하지 않지만, 사용하면서 QueryDSL의 장점을 더 살려보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 생성자 주입 안하면 NPE????]]></title>
            <link>https://velog.io/@im_h_jo/Spring-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A3%BC%EC%9E%85-%EC%95%88%ED%95%98%EB%A9%B4-%EC%95%88</link>
            <guid>https://velog.io/@im_h_jo/Spring-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%A3%BC%EC%9E%85-%EC%95%88%ED%95%98%EB%A9%B4-%EC%95%88</guid>
            <pubDate>Sun, 10 Dec 2023 16:25:13 GMT</pubDate>
            <description><![CDATA[<p>구글에 생성자 주입이라고 검색하면 수많은 게시글이 나온다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/68c2d345-d745-4107-ac15-ee990152732b/image.png" alt=""></p>
<p>내용은 대부분 비슷하게, </p>
<h3 id="생성자-주입-하고-필드-주입은-하지-마세요">생성자 주입 하고, 필드 주입은 하지 마세요!</h3>
<p>정도다. 이유로는 컴파일 타임 순환 참조 방지, 객체의 불변성 등등,,, 구구절절 맞는 이야기들이 나온다</p>
<p>그 중에서 , 테스트 코드 작성시 NPE가 발생한다는 점에 대해 곰곰히 생각해봤다.</p>
<hr>
<h3 id="when">When?</h3>
<pre><code class="language-java">@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private MemberService memberService;

    public void register(String name) {
        userRepository.add(name);
    }
}</code></pre>
<p> </p>
<pre><code class="language-java">public class UserServiceTest {

    @Test
    public void addTest() {
        UserService userService = new UserService();
        userService.register(&quot;Name&quot;);
    }

}</code></pre>
<p>코드 출처: <a href="https://mangkyu.tistory.com/125">https://mangkyu.tistory.com/125</a></p>
<p>위 테스트 코드를 보자. 이 코드는 실행하면 NPE가 발생한다. 왜일까?</p>
<p>이를 알기 위해선 <code>생성자 주입 시점</code>에 대해 알아야한다.</p>
<p>스프링에서, 객체 (빈)은 언제 만들어질까? 
<img src="https://velog.velcdn.com/images/im_h_jo/post/4fe43d16-8ebf-4293-86fb-2a32f795df3a/image.png" alt=""></p>
<p>스프링에서 따로 설정하지 않았다면, Bean은 Singleton으로 만들어진다. 
이 Bean의 생성 시점은 , Application 시작 -&gt; 스프링 컨테이너 시작 -&gt; 컨테이너에서 모든 빈을 싱글톤으로 생성 &amp;&amp; 의존성 주입
과정을 거칠 때 생성된다!</p>
<p>즉, 우리가 <code>@Component</code>등으로 지정해 빈으로 설정 했을 경우 <strong>스프링 컨테이너</strong>에서 한번만 만들고, 주입까지 해준다는 뜻이다.
다시 위 코드를  바라보자.</p>
<pre><code class="language-java">UserService userService = new UserService(); </code></pre>
<p>테스트 클래스에서, <strong>new</strong>를 통해 서비스 클래스를 생성하고 있다.</p>
<blockquote>
<ol>
<li>해당 클래스는 <code>@SpringBootTest</code> 애노테이션이 붙지 않아, 순수 자바 코드로 돌아간다.</li>
<li>@SpringBootTest 애노테이션을 붙여 스프링으로 실행하더라도, 컨테이너에서 만든 객체가 아닌 새로운 객체가 생성된다.</li>
<li>이들은 직접 생성한 클래스니까 당연히 의존성 주입이 되어있지 않다.</li>
</ol>
</blockquote>
<p>따라서 new로 만든 객체에서, 의존하는 클래스의 메소드가 간접적으로 호출되면 NPE가 발생한다.</p>
<hr>
<h2 id="fix-it">Fix it!</h2>
<p>그럼 생성자 주입으로 바꿔서 해결하는 방법은 뭘까?</p>
<pre><code class="language-java">@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository){
        this.userRepository = userRepository;
    }

    public void register(String name) {
        userRepository.add(name);
    }
}</code></pre>
<p> </p>
<pre><code class="language-java">public class UserServiceTest {

    @Test
    public void addTest() {
        UserService userService = new UserService(new UserRepository());
        userService.register(&quot;Name&quot;);
    }

}</code></pre>
<p>이렇게 주입 대상 객체를 같이 new를 통해 생성자로 넘겨버리면 된다. 
이러면 스프링 부트 어플리케이션과 실행과 무관하게 순수 자바 코드에서도 문제없이 돌아가서, 테스트를 진행 할 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ArrayList의 get은 어떻게 O(1)에 작동할까]]></title>
            <link>https://velog.io/@im_h_jo/ArrayList%EC%9D%98-get%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-O1%EC%97%90-%EC%9E%91%EB%8F%99%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@im_h_jo/ArrayList%EC%9D%98-get%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-O1%EC%97%90-%EC%9E%91%EB%8F%99%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 06 Nov 2023 03:59:04 GMT</pubDate>
            <description><![CDATA[<p>자료구조를 C로 하나하나 만들다 보면, 자연스럽게 동적 크기의 리스트도 만들게 된다.
내가 리스트를 구현 할 때는, Linked List를 사용하였고, 특정 인덱스의 값을 얻기 위해서는 다음같은 코드가 필요했다.</p>
<pre><code class="language-c">int i=0;
for(myList = head;, i&lt;N; i++, myList = myList-&gt;next);
printf(&quot;%d\n&quot;, myList-&gt;value);</code></pre>
<p>단순 코드지만, 위와 같은 방법으로 만든 get 함수는 시간복잡도가 <code>O(N)</code>이 되게 된다. 
같은 동적 리스트 역할을 하는 Java의 ArrayList나 Python의 List의 시간 복잡도를 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/im_h_jo/post/d84d6677-9526-438f-8478-574cb1d03a12/image.png" alt="">
Java 기준인데, 명확히 Linked List와 ArrayList는 구분돼있다. 그렇다면 ArrayList는, 우리가 아는 LinkedList로 구현되지 않았다는 뜻이다. 그렇다면 ArrayList는 어떻게 구현되어 있을까?</p>
<p>보통 배열의 인덱스 접근이 <code>O(1)</code>에 이루어진다. 이것이 가능한 이유는 배열에 들어가는 자료형이 동일하고, 선언시에 크기를 지정하여 스택에 연속적인 공간을 할당하기에 가능하다.</p>
<p>ArrayList는 동적으로 원소를 추가하기 때문에,LinkedList를 생각한다면 연속적인 메모리 할당을 보장하기 힘들다고 생각했다.</p>
<hr>
<p>그 배경에는 &quot;배열&quot;이 있었다. ArrayList는 명백히 배열 기반 자료구조다.
ArrayList 내부에는 <code>Object[]</code> 배열이 존재하고, 이 안에 원소를 저장한다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/d214f425-b5d0-408d-8ac2-5177671f01f3/image.png" alt="">
배열에 차곡차곡 원소를 넣다가, 배열의 크기가 꽉 차게 된다면 다음 과정을 수행한다.</p>
<ol>
<li>배열의 크기가 기존의 2배인 배열을 만든다.</li>
<li>새로 만든 배열에 기존 데이터를 복사해 옮긴다</li>
</ol>
<p>따라서 배열 확장시, K개의 원소가 존재한다면 K개의 데이터를 모두 복사해야 하기 떄문에 지연이 발생한다.</p>
<p>삭제의 경우엔, 중간에 있는 원소를 삭제한다면 그 뒷 원소들을 모두 한칸씩 앞으로 당겨줘야 하므로 지연이 발생한다.</p>
<p>따라서 연속적인 공간을 가지는 배열을 사용하기에 O(1) 시간으로 데이터를 조회할수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ] 피자 (Small)]]></title>
            <link>https://velog.io/@im_h_jo/BOJ-%ED%94%BC%EC%9E%90-Small</link>
            <guid>https://velog.io/@im_h_jo/BOJ-%ED%94%BC%EC%9E%90-Small</guid>
            <pubDate>Thu, 21 Sep 2023 10:24:13 GMT</pubDate>
            <description><![CDATA[<p>DP를 너무 못하는 관계로 쉬운 DP부터 차근차근 익숙해지려고 한다.
<a href="https://www.acmicpc.net/problem/14606">피자(Small)</a>
<img src="https://velog.velcdn.com/images/im_h_jo/post/5ff28c04-cb16-431d-b5d3-62b98183d7e2/image.png" alt=""></p>
<hr>
<p>해당 문제는, $$N$$ 높이를 가진 피자 탑을 쪼개면서 최대 즐거움을 찾는 문제이다.
두 가지로 풀었는데, 하나는 완전 탐색과 하나는 DP이다.
먼저 처음 접근한 DP 방법이다.</p>
<hr>
<p>$N=1$일때 즐거움은 <code>0</code>으로 문제에서 제공했다.
그렇다면, $N=2$일때는, 하나를 떼어내면 높이 1짜리 타워 두개가 완성된다. 이 때 즐거움은 $1 \times 1 +0+0=1$이다. 1짜리 두개로 떼어냈으니 1x1, 높이1짜리의 기존 즐거움이 더해진 값이다.
$N=3$일때는, <code>1, 2</code>로 떼어 낼 수 있다. 이때 즐거움은 $1\times2+0+1$ (떼어낸 두 크기인 2x1, 각각 1과 2의 즐거움인 0 + 1)이다. 
이를 반복해서 쓰다보면... 같은 계산값을 반복해서 사용하게 됨을 눈치 챌 수 있다. <code>N=3</code>일때만 보더라도, <code>N=2, N=1</code>일때 계산값이 반복해서 필요해지게 된다. 따라서 DP로 접근할 수 있다.</p>
<p>먼저, N이 짝수일땐 무조건 반으로 쪼개는게 가장 크다.  (N과 즐거움은 비례한다.)
따라서 다음 점화식을 따른다.
$$dp[N] = ({N\over2})^2 + 2\times dp[{N\over2}] 
$$
만약 N이 홀수라면, N을 2로 나눈 몫과, 그에 1을 더한값으로 나누는게 가장 크다. 
$$
dp[N] = \lfloor{N\over2}\rfloor\times (\lfloor{N\over2}\rfloor+1)+dp[\lfloor{N\over2}\rfloor] + dp[\lfloor{N\over2}\rfloor+1]
$$
이를 코드로 옮겨주면 된다.</p>
<pre><code class="language-python">arr=[0, 0]
for i in range(2, 10 +1):
    if i%2 == 0:
        arr.append((i//2)**2+arr[i//2]*2)
    else:
        arr.append(i//2 * (i//2+1) + arr[i//2] + arr[i//2+1])
print(arr[int(input())])</code></pre>
<hr>
<p>완전 탐색은, N이 10밖에 안되기 때문에 충분히 시간내에 풀 수 있는 선택지다.
2중 for문 변수를 <code>i,j</code>라고 둘 때 $i+j=N$인 케이스에서, 
$dp[N] = i\times j+dp[i]+dp[j] (1&lt;=i&lt;= {N\over2}, i&lt;=j&lt;=N)$
를 적용해주면 된다.</p>
<pre><code class="language-python">arr=[0] * 11
for i in range(2, 10 + 1):
    for j in range(1,i//2+1):
        for k in range(j, i+1):
            if j+k &gt; i:
                break
            if j+k == i:
                arr[i] = j*k + arr[j]+arr[k]
print(arr[int(input())])</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SpringBoot] Bcrypt 사용시 매번 결과가 달라질 때]]></title>
            <link>https://velog.io/@im_h_jo/SpringBoot-Bcrypt-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%A7%A4%EB%B2%88-%EA%B2%B0%EA%B3%BC%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A7%88-%EB%95%8C</link>
            <guid>https://velog.io/@im_h_jo/SpringBoot-Bcrypt-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EB%A7%A4%EB%B2%88-%EA%B2%B0%EA%B3%BC%EA%B0%80-%EB%8B%AC%EB%9D%BC%EC%A7%88-%EB%95%8C</guid>
            <pubDate>Sun, 17 Sep 2023 09:43:02 GMT</pubDate>
            <description><![CDATA[<p>API 서버를 만들면서, 비밀번호는 평문으로 저장하지 않고 <code>BcryptEncoder</code>를 적용했다.
회원가입 과정에서도 비밀번호를 db에 encode에서 저장하고, 유저인증이 필요할때마다 엔티티 조회를 위해 다음같은 코드를 썼다.</p>
<pre><code class="language-kotlin">userRepository.findByEmailAndPassword(request.email, encoder.encode(request.password))</code></pre>
<p>그런데, 분명 password와 email에 정상적으로 옳은 평문을 넣었는데도 계속 유저를 찾지 못했다!</p>
<p>이게 무슨일이지 싶어서 디버거를 돌려봤다.
테스트용으로 쓴 비밀번호는 평문으로 &quot;password&quot;였는데, 인코드 결과 다음과 같이 저장되어있다.
<img src="https://velog.velcdn.com/images/im_h_jo/post/00b31100-4e58-4665-b9dd-fc921977f57d/image.png" alt="">
이제 디버거를 통해 같은 &quot;password&quot;를 encode한 결과를 보면
<img src="https://velog.velcdn.com/images/im_h_jo/post/95c2f433-010d-463b-aff4-6e63d1a6e654/image.png" alt="">
다르다.. 7번째부터 확연히 다르다. 난 분명 같은 평문을 넣었는데 이런 일이 왜 생겼을까?
이유는 <a href="https://velog.io/@yoosj97/%EC%95%94%ED%98%B8%ED%99%94-%EA%B8%B0%EB%B2%95-%ED%95%B4%EC%8B%9CHash-%EC%86%94%ED%8A%B8Salt#%EB%B3%B4%EC%99%84-%EB%B0%A9%EB%B2%95">솔트(저자 : yoosj97님)</a>때문이다. 솔트란 간단히 말해 평문에 임의의 문자열을 붙여 암호화 하는 보완 방법이다.</p>
<p>따라서 encode마다 임의로 문자열이 붙어서 인코딩되니, 같을 수가 없었다.</p>
<h3 id="해결법은">해결법은?</h3>
<p>내가 사용하는 Spring Security의 PasswordEncoder는, <code>.matches</code>라는 함수를 제공한다. 두 평문 하나와 인코드된 문자열을 넘기면, 같은건지 체크해준다.
로직은 굉장히 복잡한것으로 보여서.. 따로 공부하거나 다루진 않겠다. 오버헤드가 얼마나 될지정도는 궁금하긴 하다.
<code>passwordEncoder.matches(password, storedEncodedPassword)</code></p>
<h3 id="저는-jpa-쓰는데-그럼-엔티티를-어떻게-가져오죠">저는 JPA 쓰는데 그럼 엔티티를 어떻게 가져오죠?</h3>
<p>나는 맨위에 첨부한 코드처럼, 평문을 쓸땐 그냥 findByEmailAndPassword를 썼다.
근데 이제 db에 있는 Password를 가져와 matches로 비교를 해야하기에, 과정을 쪼개야한다.</p>
<blockquote>
<ol>
<li>findByEmail 등으로, 아이디등의 정보만 가지고 엔티티를 조회한다. 이 떄, Email등은 당연히 Unique해야한다.</li>
<li>방금 가져온 엔티티를 user라고 하자. 이후 passwordEncoder.matches를 통해 user.password(암호화된, db에 저장된것)와 평문을 비교한다.</li>
<li>matches의 결과가 True면 인증 성공, False거나 user 자체가 null이 반환되었다면 인증 실패로 처리한다.</li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] Data Class 간략 정리]]></title>
            <link>https://velog.io/@im_h_jo/Kotlin-Data-Class-%EA%B0%84%EB%9E%B5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@im_h_jo/Kotlin-Data-Class-%EA%B0%84%EB%9E%B5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sat, 16 Sep 2023 19:32:36 GMT</pubDate>
            <description><![CDATA[<p>난 죽어도 쓰지 않을것같던 코틀린을 배우고있다.
자바와 Lombok이 친숙한 사람들은 스프링 부트에서 엔티티를 다음처럼 짤것이다.</p>
<pre><code class="language-java">@Entity
@Getter @Setter
public class Menu {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;place_id&quot;)
    private Place place;

    private String menuName;
    private int menuPrice;

    @Column(columnDefinition = &quot;text&quot;, nullable = true)
    private String menuImg;
}
</code></pre>
<p><a href="https://velog.io/@im_h_jo/JPA-Entity%EC%97%90-%EC%A0%95%EB%A7%90-Setter%EB%A5%BC-%EC%93%B0%EB%A9%B4-%EC%95%88%EB%90%A0%EA%B9%8C">Entity에 Setter를 지양해야 하는 이유</a>같은 글을 써놓고 @Setter를 붙인건 당장은 무시하고, 수많은 Getter, Setter가 우리 코드를 침범하는걸 막기 위해 Lombok을 통해 많이들 구현한다.
디버깅이나, 로깅등을 위해 toString, hashcode등을 인텔리제이 시켜서 작성도 간간히 한다.
코틀린은 나름 최신 언어답게 이런 불편함을 제거해준다.</p>
<h3 id="data-class">Data Class</h3>
<pre><code class="language-kotlin">@Entity
data class Article(
    @Id
    @GeneratedValue
    val articleId: Long,
    var createdAt: LocalDateTime,
    var updatedAt: LocalDateTime,
    var content: String,
    var title: String,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;)
    val user: User,
)</code></pre>
<p>엔티티 구성은 조금 다르지만.. class 앞에 data 한마디 붙여준것만으로
생성자, getter, setter, toString, hashCode, equals, copy(deep copy임.)를 지원시켜준다. 자바와 달리 여러분의 alt+insert키의 수명을 10년은 늘려줄것이다.
위 메서드들을 지원하게 하고싶은 필드를, data class의 생성자 위치에 써주면 된다.</p>
<pre><code class="language-kotlin">class Myclass(필드1, 필드2, 필드3...)</code></pre>
<p>참고로, 코틀린은 생성자를 위와 같이 클래스에 괄호로 붙여서 선언한다.</p>
<h2 id="제약-사항">제약 사항</h2>
<p>data class는 이런 편의사항이 있지만, 제약도 있다.
abstract와 open(해당 클래스를 상속하는것을 허용하는 키워드)이 불가능하다. Data Class는 <code>immutable</code>을 목적으로 만들어진 클래스기 때문이다.
추가적으로, 필드가 <code>var</code>이면 Setter를 사용가능하고, <code>val</code>이면 사용 불가능하다. 개인적으로 무지성 val을 쓴다음 필요 할 때 var로 바꿔주는 방법을 선호한다.</p>
<h2 id="스프링부트-사용시-주의-사항">스프링부트 사용시 주의 사항</h2>
<p>양방향 매핑(@ManyToOne 이후 @OneToMany(mappedBy=...) )를 사용할때 아주 주의해야한다.
자바로 코드를 짤때도, 양방향 매핑시 <strong>toString()</strong> 메서드는 주의해서 override 했어야했다.
<code>순환  참조</code> 때문인데, 다음 예시를 보자. PK는 생략한다.</p>
<pre><code class="language-java">@Entity
public class Parent {
    private String name;

    @OneToMany(mappedBy=&quot;parent&quot;)
    private List&lt;Child&gt; children = new ArrayList();

    @Override
    public String toString() {
        return &quot;Menu{&quot; +
            &quot;id=&quot; + id +
            &quot;, children=&quot; + children +
            &#39;}&#39;;
    }
}

@Entity
public class Child {
    private String name;

    @ManyToOne
    @JoinColiumn(name = &quot;parent_id&quot;)
    private Parent parent;

    @Override
    public String toString() {
        return &quot;Menu{&quot; +
            &quot;name=&quot; + name +
            &quot;, Parent=&quot; + Parent +
            &#39;}&#39;;
    }
}</code></pre>
<p>위 엔티티가 있다고 할때, 개발중 log등에서 <code>log.info(&quot;child = {}&quot;, child)</code>를 써버렸다고 하자.
그러면 child에서 toString이 호출되고, 그 안에 있는 parent때문에 parent의 toString이 호출되고, 또 그 안에 있는 children때문에 child의 toString이 호출되고...</p>
<p>함수 호출이 무한히 이루어진다. 재귀함수를 배웠다면 미래가 보일것이다. <code>StackOverFlow</code> Exception을 무조건 발생시킨다.</p>
<p>그런데, 코틀린의 <code>data class</code>가 지원하는 메서드중 toString()도 존재한다. 그래서 무심코 생성자 자리에 양방향 매핑을 시켜버리면, 로그를 찍지 않더라도 사용 과정에서 내부 로직등에 의해 반드시 순환 참조가 발생한다. 진짜 무조건 발생한다.</p>
<p>근데 스프링부트와 JVM이 어떻게든 관리하고 버텨내서 당장 티가 안나는거지만, 이론상 함수 CALL 한번마다 스택 오버플로우가 한번씩 터져서 쓰레드가 하나씩 사망할것이다.</p>
<p>쓰레드는 스택공간을 공유하지 않기때문에 다행인지 아닌지 겉으로 크게 티는 안나지만, 실제 서비스 단계에서 발생한 일이면 끔찍한 미래가 확정이다.. </p>
<p>따라서, Data Class에서 @OneToMany는 생성자가 아닌, 클래스 내부에 집어넣자. 다음처럼.</p>
<pre><code class="language-kotlin">@Entity
data class Article(
    @Id
    @GeneratedValue
    val articleId: Long,
    var createdAt: LocalDateTime,
    var updatedAt: LocalDateTime,
    var content: String,
    var title: String,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;)
    val user: User,
) {
    @OneToMany(mappedBy = &quot;article&quot;, cascade = [CascadeType.REMOVE], orphanRemoval = true)
    val comments: MutableList&lt;Comment&gt; = mutableListOf()
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 1차 캐시 작동 조건]]></title>
            <link>https://velog.io/@im_h_jo/JPA-1%EC%B0%A8-%EC%BA%90%EC%8B%9C-%EC%9E%91%EB%8F%99-%EC%A1%B0%EA%B1%B4</link>
            <guid>https://velog.io/@im_h_jo/JPA-1%EC%B0%A8-%EC%BA%90%EC%8B%9C-%EC%9E%91%EB%8F%99-%EC%A1%B0%EA%B1%B4</guid>
            <pubDate>Sat, 16 Sep 2023 19:07:36 GMT</pubDate>
            <description><![CDATA[<p>지금까지 굉장히 잘못된 지식을 가지고 있었다.
이 글을 한 줄로 요약하자면</p>
<blockquote>
<p><strong>1차 캐시는</strong> PK가 아니면 작동하지 않는다!!!!</p>
</blockquote>
<h3 id="jpa와-1차-캐시">JPA와 1차 캐시</h3>
<p>JPA는 캐시가 존재한다. 캐시를 두는게, DB에 쿼리를 날리고, 다시 받아오는 오버헤드보다 훨 낫기 때문이다.
나는 지금까지 <strong><em>한번 조회한 엔티티는 트랜잭션이 끝나기 전까지 무조건 1차 캐시에 남고, 어떤 쿼리의 조회 대상이 캐시에 존재하면 쿼리없이 가져온다</em></strong> 라고생각했다
반은 맞고 반은 틀렸다. 엔티티를 조회하면 트랜잭션이 종료되기 전까지 1차 캐시에 남는건 맞다. 그렇지만 , <strong>PK를 기반으로 조회</strong>하지 않는다면 <strong>무조건 쿼리가 날아간다</strong></p>
<h3 id="1차-캐시-작동-조건">1차 캐시 작동 조건</h3>
<p>@EntityGraph, @Cacheable은 제외하고 서술하겠다.</p>
<blockquote>
<ol>
<li>캐시에 저장되는건 커스텀 쿼리(JPQL)을 사용해도 문제 없이 1차 캐시에 저장된다.</li>
<li>단, 캐시에서 값을 가져오는건 PK를 기반으로한 find (entity manager를 쓸때 find() 메서드 혹은 data-jpa의 findById)에서만 작동한다.</li>
<li>select c from Comment c where c.id = :paramId 와 같은 PK기반 JPQL도 무조건 쿼리가 날아간다. (em.createQuery 가 호출된다. em.find가 아니라!)</li>
<li>트랜잭션이 종료되거나, flush()될 경우 캐시는 초기화된다. (메서드나 서비스 클래스 자체에 <code>@Transactional</code>을 붙임을 생각해보자.)</li>
<li>당연한 이야기지만 PK 기반 find 조회를 하더라도, 처음 불러오는 엔티티면 쿼리가 날아간다.</li>
</ol>
</blockquote>
<p>2번,3번 조건을 모르고 있어서 세시간이 날아갔다. <code>join fetch</code>써서 로드 한번에 한 엔티티인데 계속 쿼리가 두번 날아가더라. 너무 고치고싶어서 디버깅 하다가 다른곳에서 발생한 순환 참조만 찾아서 고쳤다.
미래의 나는 공식문서를 제발 읽고 쓰길..</p>
]]></description>
        </item>
    </channel>
</rss>