<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>555</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 05 Jul 2023 16:05:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 555. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yeopju_5" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[SQL Error: 1054, SQLState: 42S22]]></title>
            <link>https://velog.io/@yeopju_5/SQL-Error-1054-SQLState-42S22</link>
            <guid>https://velog.io/@yeopju_5/SQL-Error-1054-SQLState-42S22</guid>
            <pubDate>Wed, 05 Jul 2023 16:05:46 GMT</pubDate>
            <description><![CDATA[<p>SQL문에서 사용한 컬럼이 DB에 존재하지 않을 때 발생하는 에러</p>
<ul>
<li><em><strong>서버에 배포한 후 Entity를 변경이 있으면 꼭 ALTER TABLE 해주기!!</strong></em></li>
<li>스네이크 케이스 주의...<ul>
<li>DB에는 스네이크 케이스로 작성해야되는데 카멜 케이스로 작성해서 한 시간...ㅎ<img src="https://velog.velcdn.com/images/yeopju_5/post/e6671127-65e2-4f66-a678-057bcbb30cfa/image.png" width=600>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Error: 1451, SQLState: 23000]]></title>
            <link>https://velog.io/@yeopju_5/SQL-Error-1451-SQLState-23000</link>
            <guid>https://velog.io/@yeopju_5/SQL-Error-1451-SQLState-23000</guid>
            <pubDate>Wed, 05 Jul 2023 14:35:12 GMT</pubDate>
            <description><![CDATA[<h3 id="회원-탈퇴-시-발생한-에러">회원 탈퇴 시 발생한 에러</h3>
<ul>
<li><p>User Entity 상태 업데이트 시, User Entity에 <code>ManyToOne</code>(One이 User)으로 매핑되어 있는 객체들을 처리하는 규칙이 정해져 있지 않아 발생하는 에러이다.</p>
</li>
<li><p>이런 경우 User 객체를 삭제하기 전에 먼저 관련 객체들을 모두 삭제해야 한다.</p>
<pre><code class="language-java">  @Override
  public void withdrawalUser(User user) {
      try {
          userLectureRepository.deleteAllByUser(user);            |
          userNotificationRepository.deleteAllByUser(user);        &gt; 추가
          tokenRepository.deleteAllByUser(user);                  |

          userRepository.deleteById(user.getId());
      } catch (Exception e){
          throw new BaseException(Code.WITHDRAWAL_FAILED);
      }
  }</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[리눅스] 서버 로그 관리]]></title>
            <link>https://velog.io/@yeopju_5/%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@yeopju_5/%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%84%9C%EB%B2%84-%EB%A1%9C%EA%B7%B8-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Wed, 05 Jul 2023 14:28:08 GMT</pubDate>
            <description><![CDATA[<h1 id="스프링-로그">스프링 로그</h1>
<p>스프링에서는 로그를 관리하기 위해서 <code>application.properties</code> 와 <code>logback-spring.xml</code> 파일을 사용한다. 전자의 경우 로그 레벨 설정과 같은 간단한 설정밖에 하지 못하기 때문에 특정 비즈니스 로직별, 일자별과 같이 자세한 설정을 위해서는 <code>logback-spring.xml</code>을 사용해야 한다.</p>
<h2 id="logback">Logback</h2>
<p><code>Logback</code>은 <code>log4j</code>를 발전시킨 Logging Framework로 Spring Boot는 <code>spring-boot-starter-web</code> -&gt; <code>spring-boot-starter-logging</code>에 logback 구현체가 포함되어 있다.
그래서 의존성을 따로 추가하지 않아도 logback이 기본 로깅 구현체로 자동 적용된다.</p>
<p>이렇게 적용된 로깅 구현체는 <strong>SLF4J(Simple Logging Framework for JAVA : interface)와 compile시에 바인딩된다.</strong></p>
<h3 id="log-level">Log Level</h3>
<p>로그 레벨은 5가지로 구성되며 오른쪽으로 갈수록 단계가 높아진다.</p>
<blockquote>
<p>Trace ➡ Debug ➡ Info ➡ Warn ➡ Error</p>
</blockquote>
<h1 id="리눅스-서버-로그-관리">리눅스 서버 로그 관리</h1>
<p>먼저 로그 관리의 기본인 로테이션이라는 용어부터 정리해보자</p>
<h3 id="로테이션">로테이션</h3>
<ul>
<li><p>로그 로테이션의 경우, 시스템이나 애플리케이션에서 생성되는 로그 파일이 일정 크기에 도달하거나 일정 시간이 지나면 새로운 로그 파일을 생성하고 오래된 로그 파일을 압축, 아카이빙, 또는 삭제하는 프로세스를 말합니다.</p>
</li>
<li><p>예를 들어, 로그 파일이 일주일마다 로테이션이 설정되어 있다면, 매주 새로운 로그 파일이 생성되고 이전 주의 로그 파일은 아카이빙되거나 삭제될 것이다. 이렇게 함으로써 로그 파일이 무한정 커지는 것을 방지하고, 디스크 공간을 효율적으로 활용하며, 로그 데이터를 쉽게 관리할 수 있다.</p>
</li>
</ul>
<hr>
<p>현재 나의 EC2 서버는 <strong>Nginx, Docker(WAS:Tomcat)</strong>으로 이루어져 있다.</p>
<h2 id="nginx">Nginx</h2>
<h3 id="etcnginxnginxconf"><code>/etc/nginx/nginx.conf</code></h3>
<ul>
<li><p>로그 파일의 저장 경로와 로그 출력 형식을 지정할 수 있다.</p>
</li>
<li><p>로그 출력 형식 Default</p>
<pre><code>log_format  main  &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#39;
                      &#39;$status $body_bytes_sent &quot;$http_referer&quot; &#39;
                      &#39;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#39;;</code></pre><p><code>192.0.2.1 - john [10/Jul/2023:14:02:39 +0900] &quot;GET /index.html HTTP/1.1&quot; 200 555 &quot;http://example.com/&quot; &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36&quot; &quot;-&quot;</code></p>
<ul>
<li><code>$remote_addr</code>: 클라이언트의 IP 주소를 나타낸다.</li>
<li><code>$remote_user</code>: 인증된 사용자의 이름을 나타낸다.</li>
<li><code>$time_local</code>: 로컬 시간을 나타냅니다. 이는 접속 시간을 나타낸다.</li>
<li><code>$request</code>: 클라이언트의 원래 요청 문자열을 나타낸다. 이는 사용자가 요청한 URL 및 HTTP 메서드(GET, POST 등)을 나타낸다.</li>
<li><code>$status</code>: 요청에 대한 응답의 HTTP 상태 코드를 나타낸다.</li>
<li><code>$body_bytes_sent</code>: 응답 본문의 바이트 크기를 나타낸다.</li>
<li><code>$http_referer</code>: 클라이언트 HTTP 헤더의 Referer 필드를 나타낸다. 이는 사용자가 해당 요청을 하기 이전에 방문했던 웹페이지의 URL을 나타낸다.</li>
<li><code>$http_user_agent</code>: 클라이언트 HTTP 헤더의 User-Agent 필드를 나타낸다. 이는 클라이언트의 브라우저, 운영 체제 등의 정보를 나타낸다.</li>
<li><code>$http_x_forwarded_for</code>: 클라이언트 HTTP 헤더의 X-Forwarded-For 필드를 나타낸다. 이는 프록시나 로드 밸런서를 통해 서버에 도달한 요청의 경우, 원래 클라이언트의 IP 주소를 나타낸다.</li>
</ul>
</li>
</ul>
<h3 id="etclogroateconf"><code>/etc/logroate.conf</code></h3>
<ul>
<li><p>Linux 시스템에서 로그 파일 로테이션(자동 로그 관리)을 제어하는 설정 파일이다. 이 파일의 설정에 따라, 로그 파일의 크기가 일정 크기에 도달하거나 일정 시간이 경과하면 새로운 로그 파일을 생성하고, 오래된 로그 파일을 삭제하거나 압축하는 작업을 수행한다.</p>
</li>
<li><p><code>/etc/logrotate.conf</code> 설정 파일에는 전역 설정을 포함하고, <code>/etc/logrotate.d/</code> 디렉토리에는 개별 서비스(예: apache, nginx 등)에 대한 로그 로테이션 설정 파일이 위치해 있다.</p>
</li>
<li><p><code>/etc/logroate.conf/nginx</code></p>
<ul>
<li><p>Default 값</p>
<pre><code>/var/log/nginx/*.log {
create 0640 nginx root
daily
rotate 10
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
  /bin/kill -USR1 `cat /run/nginx.pid 2&gt;/dev/null` 2&gt;/dev/null || true
endscript
}</code></pre><p><code>/var/log/nginx/*.log { ... }</code>: 이 설정이 적용될 로그 파일을 지정합니다./var/log/nginx/ 디렉토리 내의 모든 .log 파일에 이 설정이 적용됩니다.</p>
<p><code>create 0640 nginx root</code>: 로그 파일을 로테이션한 후 새 로그 파일을 생성합니다. 생성될 파일의 권한은 0640이며, 소유자는 &#39;nginx&#39;, 그룹은 &#39;root&#39;입니다.</p>
<p><code>daily</code>: 로그 파일을 매일 로테이션합니다.</p>
<p><code>rotate 10</code>: 로그 파일을 최대 10개까지 유지합니다. 10개가 넘으면 가장 오래된 로그 파일부터 삭제됩니다.</p>
<p><code>missingok</code>: 로그 파일이 없을 경우 에러를 무시하고 넘어갑니다.</p>
<p><code>notifempty</code>: 로그 파일이 비어있는 경우 로테이션하지 않습니다.</p>
<p><code>compress</code>: 로테이션된 로그 파일을 gzip으로 압축합니다.</p>
<p><code>delaycompress</code>: 로테이션된 이전 로그 파일의 압축을 지연합니다. 이는 compress 옵션과 함께 사용될 때 유용하며, 프로그램이 로그 파일을 쓰는 중에 발생하는 에러를 방지해줍니다.</p>
<p><code>sharedscripts</code>: 로그 파일 모두에 대해 postrotate와 endscript 사이의 스크립트를 한 번만 실행합니다.</p>
<p><code>postrotate ... endscript</code>: 로그 파일을 로테이션한 후에 실행할 쉘 스크립트를 지정합니다. 이 스크립트는 Nginx에 USR1 시그널을 보내 로그 파일을 재오픈하라는 명령을 내립니다. 이는 Nginx가 현재 로그 파일을 계속 사용하는 것을 방지하며, 새로운 로그 파일에 로그를 기록하도록 합니다.</p>
</li>
</ul>
</li>
</ul>
<h3 id="logrotate">Logrotate</h3>
<p>logrotate는 일반적으로 cron이라는 리눅스의 작업 스케줄러를 통해 주기적으로 실행된다. </p>
<p>리눅스 배포판에 따라 다르지만, 일반적으로 /etc/cron.daily 또는 /etc/cron.weekly 등의 디렉토리에 logrotate를 주기적으로 실행하는 스크립트가 포함되어 있다. 이 스크립트는 시스템이 부팅될 때 cron에 의해 자동으로 스케줄링된다.</p>
<ul>
<li><code>logrotate -f</code>와 같은 명령은 로그 파일의 로테이션을 즉시 강제로 실행할 때 사용되며, 일반적으로 디버깅이나 테스트, 또는 디스크 공간을 즉시 확보해야 할 경우에 사용된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DORO 운영 서버 수정사항]]></title>
            <link>https://velog.io/@yeopju_5/DORO-%EC%9A%B4%EC%98%81-%EC%84%9C%EB%B2%84-%EC%88%98%EC%A0%95%EC%82%AC%ED%95%AD</link>
            <guid>https://velog.io/@yeopju_5/DORO-%EC%9A%B4%EC%98%81-%EC%84%9C%EB%B2%84-%EC%88%98%EC%A0%95%EC%82%AC%ED%95%AD</guid>
            <pubDate>Sat, 24 Jun 2023 07:18:19 GMT</pubDate>
            <description><![CDATA[<h2 id="💠-osiv-성능최적화">💠 OSIV 성능최적화</h2>
<ul>
<li>실시간 트래픽이 중요한 서비스가 아니고 DB 커넥션이 부족할 만큼 트래픽이 많지 않기 때문에 아직은 OSIV를 켜두는 것으로 결정<h2 id="💠-application-properties-분리">💠 Application Properties 분리</h2>
<h3 id="운영-서버--개발-서버">운영 서버 / 개발 서버</h3>
</li>
<li>DDL: NONE / CREATE </li>
<li>DataSource: RDS / Local MySQL</li>
<li>SQL LOG: False / True</li>
<li>Log Level: INFO / DEBUG<h2 id="💠-db-pk-테이블별-auto-increment-적용">💠 DB PK 테이블별 Auto Increment 적용</h2>
</li>
<li>GeneratedValue 전략 설정<h2 id="💠-시큐리티-컨텍스트-홀더-초기화-aop-적용">💠 시큐리티 컨텍스트 홀더 초기화 AOP 적용</h2>
</li>
</ul>
<h3 id="적용-대상">적용 대상</h3>
<ul>
<li>Domain 패키지의 하위 패키지의 모든 *Api 클래스</li>
<li>ClearSecurityContext Annotation 적용 메서드</li>
</ul>
<hr>
<h2 id="💠-보안-강화---해킹-시도-차단">💠 보안 강화 - 해킹 시도 차단</h2>
<ul>
<li>WAF 도입<ul>
<li><a href="https://techblog.woowahan.com/2699/">https://techblog.woowahan.com/2699/</a><h2 id="💠-핀포인트-도입">💠 핀포인트 도입</h2>
<h2 id="💠-api-성능-테스트---쿼리-최적화">💠 API 성능 테스트 - 쿼리 최적화</h2>
<h2 id="💠-테스트-코드-작성">💠 테스트 코드 작성</h2>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="💠-회원가입-시-학교-학과-검색-선택">💠 회원가입 시 학교, 학과 검색 선택</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] Error: Request timed out]]></title>
            <link>https://velog.io/@yeopju_5/EC2-Error-getaddrinfo-ENOTFOUND</link>
            <guid>https://velog.io/@yeopju_5/EC2-Error-getaddrinfo-ENOTFOUND</guid>
            <pubDate>Sat, 10 Jun 2023 16:38:32 GMT</pubDate>
            <description><![CDATA[<p>EC2에 올려놓은 서버에 Request가 보내지지 않는다...</p>
<h3 id="에러-로그">에러 로그</h3>
<ul>
<li>Error: Request timed out</li>
<li>Error: getaddrinfo ENOTFOUND</li>
</ul>
<h2 id="체크-사항">체크 사항</h2>
<ul>
<li>로드 밸런서 ✔️</li>
<li>타겟 그룹 ✔️</li>
<li>EC2 ✔️</li>
<li>Route53 - 로드 밸런서 연결 ✔️</li>
<li>네트워크 구성(VPC) ✔️</li>
<li>Nginx ✔️</li>
<li>Docker network ✔️</li>
</ul>
<h3 id="aws">AWS</h3>
<p>AWS에서 사용 중인 서비스들의 상태를 확인해봤지만 모든 요소가 정상 입력 되어있다.
상태 또한 Healthy로 정상 작동 중임을 확인했다.</p>
<h3 id="nginx">Nginx</h3>
<p>이러한 에러가 났을 때 내가 가장 먼저 취하는 액션은 로그 확인, WARNING 로그 제거이다.
WARNING으로 인해서 서버가 작동 되지않는 경우는 드물지만 눈에 거슬리는 로그를 제거함과 동시에 에러의 범위를 좁혀 나갈 수 있는 좋은 기회라고 생각하기 때문이다.</p>
<ul>
<li><p>WARNING - 80포트가 이미 사용 중입니다</p>
<ul>
<li>Nginx의 포트 번호와 타겟 그룹의 포트 번호를 변경하는 방법도 있지만 80번 포트를 사용하는 것이 직관적이기 때문에 포트 번호 변경 없이, 기존에 80포트를 사용 중인 프로세스를 중단시킨 후 Nginx를 재시작했다 --&gt; WARNING 해결</li>
</ul>
</li>
<li><p>엑세스 로그와 에러 로그가 찍히지 않는다. </p>
<ul>
<li><p>Nginx는 별도의 설정을 하지 않는다면 nginx.conf.default 파일을 기본 설정으로 실행된다. default 파일의 기본값에서 로그를 저장하는 부분은 수정하지 않았기 때문에 로그가 정상적으로 작동하지 않을 이유가 없었고 로그가 저장되는 원리를 공부해 잘못된 명령어가 있는지 확인했지만 전혀 문제가 없었다.</p>
<p>결국 nginx를 삭제 후 재설치했지만 Default 패키지들이 재설치 되지 않는 문제 발생했다.
기존의 설정 파일들이 남아있다면 패키지들이 설치되지 않을 가능성이 있어 모든 패키지를 삭제하고 메타데이터 또한 삭제 후 <code>which</code> 명령어 와 <code>nginx -v</code> 을 통해 남은 nginx 패키지가 있는지 확인 후 재설치를 진행했지만 여전히 파일이 생성되지 않았다.</p>
</li>
<li><p>주변 사람들에게 자문을 구해봤지만 원인을 찾지 못했고 구글링, ChatGpt로도 그 원인을 찾지 못했다. 결국 <strong>EC2 인스턴스 포맷</strong>을 진행했다.</p>
</li>
<li><p><strong><em>엑세스 로그에 HealthChecker 로그가 계속해서 찍혀 정상적인 로그 확인에 어려움을 느낀 나는 이번 기회에 엑세스 로그를 정상적인 Https 요청 로그와 Http 리다이렉트 로그로 분리했고 에러 로그와 HealthChecker 로그 파일을 별도로 관리할 수 있도록 설정했다.</em></strong></p>
</li>
</ul>
</li>
</ul>
<h3 id="docker">Docker</h3>
<p>Nginx까지도 Request가 들어오지 못한다는 것을 확인했기에 도커에서 에러가 발생했을 가능성은 0에 가까웠지만 WARNING을 해결하기로 결정했다.</p>
<ul>
<li>Docker network - Docker Container를 Build 할 때 Docker network Warning이 발생했다.network를 삭제하고 ReBuild 함으로써 Warning 해결</li>
</ul>
<h3 id="해결">해결</h3>
<p>종단간 테스트를 진행하고 모든 로그를 확인했음에도 원인을 찾지 못했다 
EC2 내부 까지 Request가 도달하지 못한다는 사실은 확실했고, 에러의 원인을 찾지 못한 이유는 방대한 AWS 에코시스템 내부에 내가 인지하지 못한 네트워크 요소들이 있기 때문일 가능성이 크다고 생각했다.<br>여기서 발동한 승부욕..니가 이기나 내가 이기나 해보자라는 생각으로 AWS Document를 정독해나갔다. 네트워크 (VPC, 서브넷, 게이트웨이 등), 로드 밸런서, 타겟 그룹, 그리고 Route53을 읽어 나가던 중 도메인을 구매한 후 15일 내에 이메일을 통한 인증이 이루어지지 않으면 도메인 사용이 중지된다는 내용을 발견했다. 그 후 이메일 인증을 진행했더니 정상작동되었다...ㅎ</p>
<p>원래는 이 내용에 대해 도메인 페이지에서 경고 메시지를 띄워준다고 한다. 하지만 내가 수차례 확인한 도메인 페이지에는 경고 메시지가 뜨지 않았고 콘솔창에도 관련 내용이 전혀 표시되지 않았다. 
심지어 호스팅 영역에서 레코드 테스트를 진행했을 때도 정상적으로 응답이 수신되었다.
<code>알아보니 Route53에서 제공하는 레코드 테스트는 실제 DNS로 쿼리를 보내는 것이 아니라 호스팅 영역의 레코드 설정에 따라 응답할 뿐, 호스팅 영역이 현재 도메인의 트래픽을 라우팅하는 데 사용되고 있는지 여부와 관계없이 동일한 정보를 반환한다고 한다..</code></p>
<p>레코드 테스트 결과를 믿고 나와 같은 삽질을 하는 사람들이 없었으면...🤣</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📂 스프링 DB 2]]></title>
            <link>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-DB-2</link>
            <guid>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-DB-2</guid>
            <pubDate>Sun, 04 Jun 2023 12:59:30 GMT</pubDate>
            <description><![CDATA[<h1 id="✔️-데이터-접근-기술---베이스">✔️ 데이터 접근 기술 - 베이스</h1>
<h2 id="용어-정리">용어 정리</h2>
<h3 id="sql-mapper">SQL Mapper</h3>
<ul>
<li>JdbcTemplate, MyBatis</li>
<li>개발자는 SQL만 작성하면 해당 S<strong>QL의 결과를 객체로 편리하게 매핑해준다.</strong></li>
<li>JdbcTemplate은 여러가지 중복코드들을 제거해준다.</li>
</ul>
<h3 id="orm">ORM</h3>
<ul>
<li>JPA, Hibernate, 스프링 데이터 JPA, Querydsl</li>
<li>기본적인 SQL은 JPA가 대신 작성하고 처리해준다.</li>
<li>JPA는 자바 진영의 ORM 표준이고 Hibernate는 JPA에서 가장 많이 사용하는 구현체이다.</li>
</ul>
<h3 id="dto">DTO</h3>
<ul>
<li>Data Transfer Object</li>
<li>기능은 없고 데이터를 전달하는 용도로 사용되는 객체</li>
<li>DTO가 사용되는 엔드포인트에 선언하면된다.</li>
</ul>
<h3 id="eventlistener">@EventListener</h3>
<ul>
<li><p><code>@EventListener(ApplicationReadyEvent.class)</code> : 스프링 컨테이너가 완전히 초기화를 다 끝내고 실행 준비가 되었을 떄 발생하는 이벤트이다.</p>
<ul>
<li><p>이 기능 대신 <code>@PostContruct</code> 를 사용할 경우 AOP 같은 부분이 다 처리되지 않은 시점에 호출될 수 있기 때문에, 문제가 발생할 수 있다.</p>
<pre><code class="language-java">@Slf4j
@RequiredArgsConstructor
public class TestDataInit {
private final ItemRepository itemRepository;
/**
확인용 초기 데이터 추가
*/
@EventListener(ApplicationReadyEvent.class)
public void initData() {
   itemRepository.save(new Item(&quot;itemA&quot;, 10000, 10));
   itemRepository.save(new Item(&quot;itemB&quot;, 20000, 20));
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="itemserviceapplication-설정--프로필">ItemServiceApplication 설정 + 프로필</h2>
<pre><code class="language-java">@Import(MemoryConfig.class)
@SpringBootApplication(scanBasePackages = &quot;hello.itemservice.web&quot;)
public class ItemServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ItemServiceApplication.class, args);
    }

    @Bean
    @Profile(&quot;local&quot;)
    public TestDataInit testDataInit(ItemRepository itemRepository) {
        return new TestDataInit(itemRepository);
    }

}</code></pre>
<ul>
<li><code>@Import(MemoryConfig.class)</code> : 앞서 설정한 <code>MemoryConfig</code> 를 설정 파일로 사용한다.</li>
<li><code>scanBasePackages = &quot;hello.itemservice.web&quot;</code> : 컴포넌트 스캔 경로 - 해당 패키지부터 하위 모든 클래스</li>
</ul>
<h3 id="프로필">프로필</h3>
<ul>
<li><code>@Profile(&quot;local&quot;)</code> : 특정 프로필의 경우에만 스프링 빈으로 등록한다.</li>
<li>스프링은 로딩 시점에 application.properties의 <code>spring.profiles.active</code> 속성을 읽어서 프로필로 사용한다.</li>
<li>프로필은 로컬, 운영 환경, 테스트 실행 등 다양한 환경에 따라서 다른 설정을 할 때 사용한다.</li>
<li>설정하지 않는다면 <code>default</code> 라는 프로필로 실행</li>
</ul>
<h1 id="✔️-스프링-jdbctemplete">✔️ 스프링 JdbcTemplete</h1>
<h2 id="jdbctemplete-정리">JdbcTemplete 정리</h2>
<h3 id="장점">장점</h3>
<ul>
<li>설정의 편리함<ul>
<li>JdbcTemplate은 <code>spring-jdbc</code> 라이브러리에 포함되어 있는데 이 라이브러리는 스프링으로 JDBC를 사용할 때 기본으로 사용되는 라이브러리이다.</li>
</ul>
</li>
<li>반복 문제 해결<ul>
<li>템플릿 콜백 패턴을 통해 반복 작업을 대신 처리해준다.<ul>
<li>커넥션 흭득</li>
<li><code>statement</code> 준비, 실행</li>
<li>결과를 반복하도록 루프를 실행</li>
<li>커넥션 종료, <code>statement</code>, <code>resultset</code> 종료</li>
<li>트랜잭션을 다룩 위한 커넥션 동기화</li>
<li>예외 발생시 스프링 예외 변환기 실행</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>동적 SQL을 해결하기 어렵다.</li>
</ul>
<h2 id="구현---v1">구현 - V1</h2>
<h3 id="코드">코드</h3>
<pre><code class="language-java">@Slf4j
public class JdbcTemplateItemRepositoryV1 implements ItemRepository {

    private final JdbcTemplate template;

    public JdbcTemplateItemRepositoryV1(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    @Override
    public Item save(Item item) {
        String sql = &quot;insert into item (item_name, price, quantity) values (?, ?, ?)&quot;;
        KeyHolder keyHolder = new GeneratedKeyHolder();
        template.update(con -&gt; {
            PreparedStatement ps = con.prepareStatement(sql, new String[]{&quot;id&quot;});
            ps.setString(1, item.getItemName());
            ps.setInt(2, item.getPrice());
            ps.setInt(3, item.getQuantity());
            return ps;
        }, keyHolder);

        Long key = keyHolder.getKey().longValue();
        item.setId(key);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        String sql = &quot;update item set item_name=?, price=?, quantity=? where id=?&quot;;
        template.update(sql, updateParam.getItemName(), updateParam.getPrice(), updateParam.getQuantity(), itemId);
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        String sql = &quot;select id, item_name, price, quantity from item where id=?&quot;;
        try{
            Item item = template.queryForObject(sql, itemRowMapper(), id);
            return Optional.of(item);
        } catch (EmptyResultDataAccessException e){
            return Optional.empty();
        }
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();
        String sql = &quot;select id, item_name, price, quantity from item&quot;;
        //동적 쿼리
        if (StringUtils.hasText(itemName) || maxPrice != null) {
            sql += &quot; where&quot;;
        }
        boolean andFlag = false;
        List&lt;Object&gt; param = new ArrayList&lt;&gt;();
        if (StringUtils.hasText(itemName)) {
            sql += &quot; item_name like concat(&#39;%&#39;,?,&#39;%&#39;)&quot;;
            param.add(itemName);
            andFlag = true;
        }
        if (maxPrice != null) {
            if (andFlag) {
                sql += &quot; and&quot;;
            }
            sql += &quot; price &lt;= ?&quot;;
            param.add(maxPrice);
        }
        log.info(&quot;sql={}&quot;, sql);
        return template.query(sql, itemRowMapper(), param.toArray());
    }

    private RowMapper&lt;Item&gt; itemRowMapper() {
        return ((rs, rowNum) -&gt; {
            Item item = new Item();
            item.setId(rs.getLong(&quot;id&quot;));
            item.setItemName(rs.getString(&quot;item_name&quot;));
            item.setPrice(rs.getInt(&quot;price&quot;));
            item.setQuantity(rs.getInt(&quot;quantity&quot;));
            return item;
        });
    }
}</code></pre>
<h3 id="save-메서드">Save 메서드</h3>
<ul>
<li>데이터를 저장할 때 PK 생성에 <code>identity (auto increment)</code> 방식을 사용하기 때문에 PK인 값을 비워두고 저장해야 한다.<ul>
<li><code>KeyHolder</code> 와 <code>connection.prepareStatement(sql, new String[]{&quot;id})</code> 를 사용해서 id를 지정해주면 Insert 쿼리 실행 이후에 데이터베이스에서 생성된 ID값을 참조할 수 있다.</li>
<li>뒤에서 설명할 <code>SimpleJdbcTemplate</code> 을 사용하면 더 편리하다</li>
</ul>
</li>
</ul>
<h3 id="findbyid-메서드">FindById 메서드</h3>
<ul>
<li><code>template.queryForObject()</code><ul>
<li>결과 로우가 하나일 때 사용한다.</li>
<li>ResultSet을 객체로 변환하는 RowMapper를 인자로 넣어줘야한다.</li>
<li>결과가 없으면 <code>EmptyResultDataAccessException</code> 예외가 발생한다.</li>
<li>결과가 둘 이상이면 <code>IncorrectResultSizeDataAccessException</code> 예외가 발생한다.</li>
</ul>
</li>
</ul>
<p>※ JdbcTemplate은 dataSource를 사용하는데 스프링 부트는 application.properties에 설정 정보만 넣어 놓으면 해당 설정을 사용해서 커넥션 풀과 DataSource, 트랜잭션 매니저를 스프링 빈으로 자동 등록한다. → <strong>스프링 부트의 자동 리소스 등록</strong></p>
<h2 id="namedparameterjdbctemplate---v2"><code>NamedParameterJdbcTemplate</code> - V2</h2>
<ul>
<li>이름 지정 파라미터<ul>
<li>파라미터를 전달하려면 Map처럼 Key, Value 데이터 구조를 만들어서 전달해야 한다.</li>
<li>이름 지정 바인딩에서 자주 사용하는 파라미터의 종류는 아래의 3가지 이다.</li>
</ul>
</li>
<li>SQL 문에서 ?로 되어 있는 부분을 <code>:Key</code>값 형태로 바꿔준다.</li>
</ul>
<h3 id="1beanpropertysqlparamtersource">1.<code>BeanPropertySqlParamterSource</code></h3>
<ul>
<li>자바빈 프로퍼티 규약을 통해서 자동으로 파라미터 객체를 생성한다.<ul>
<li><code>getXxx() -&gt; xxx, getItemName() -&gt; itemName</code></li>
<li>SQL문의 인자로 들어가는 값 모두가 Dto 인자로 들어올 경우에만 사용가능</li>
</ul>
</li>
</ul>
<pre><code class="language-java">SqlParameterSource param = new BeanPropertySqlParameterSource(item);</code></pre>
<h3 id="2mapsqlparamtersource-3map">2.<code>MapSqlParamterSource</code>, 3.<code>Map</code></h3>
<pre><code class="language-java">SqlParameterSource param = new MapSqlParameterSource()
                .addValue(&quot;itemName&quot;, updateParam.getItemName())
                .addValue(&quot;price&quot;, updateParam.getPrice())
                .addValue(&quot;quantity&quot;, updateParam.getQuantity())
                .addValue(&quot;id&quot;, itemId);

Map&lt;String, Object&gt; param = Map.of(&quot;id&quot;, id);</code></pre>
<h3 id="beanpropertyrowmapper"><code>BeanPropertyRowMapper</code></h3>
<pre><code class="language-java">private RowMapper&lt;Item&gt; itemRowMapper() {
        return BeanPropertyRowMapper.newInstance(Item.class);
}</code></pre>
<h3 id="별칭">별칭</h3>
<ul>
<li><code>select item_name</code> 의 경우 <code>setItem_name()</code> 이라는 메서드 없기 때문에 Param을 받을 때 문제가 생긴다. 이런 경우 개발자가 SQL을 다음과 같이 <code>as</code> 를 사용해서 고치면 된다.<ul>
<li><code>select item_name as itemName</code></li>
<li>하지만 <code>BeanPropertyRowMapper</code> 는 언더스코어 표기법을 카멜 표기법으로 변환해주기 때문에 <strong>컬럼 이름과 객체 이름이 완전히 다른 경우에만 as를 사용하면 된다.</strong></li>
</ul>
</li>
</ul>
<h3 id="관례의-불일치">관례의 불일치</h3>
<ul>
<li>자바 객체는 카멜 표기법을 사용하고 관계형 데이터베이스에서는 주로 언더스코어 표기법을 사용한다.</li>
</ul>
<h2 id="simplejdbcinsert---v3">SimpleJdbcInsert - V3</h2>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  public class JdbcTemplateItemRepositoryV3 implements ItemRepository {

      private final NamedParameterJdbcTemplate template;
      private final SimpleJdbcInsert simpleJdbcInsert;

      public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
          this.template = new NamedParameterJdbcTemplate(dataSource);
          this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
                  .withTableName(&quot;item&quot;)
                  .usingGeneratedKeyColumns(&quot;id&quot;);
      }

      @Override
      public Item save(Item item) {
          SqlParameterSource param = new BeanPropertySqlParameterSource(item);
          Number key = simpleJdbcInsert.executeAndReturnKey(param);
          item.setId(key.longValue());
          return item;
      }
  }</code></pre>
</li>
<li><p><code>withTableName</code>: 데이터를 저장할 테이블 명을 지정한다.</p>
</li>
<li><p><code>usingGeneratedKeyColumns</code>: key를 생성하는 PK 컬럼 명을 지정한다.</p>
</li>
<li><p>SimpleJdbcInsert는 생성 시점에 데이터베이스 테이블의 메타 데이터를 조회한다. 따라서 어떤 칼럼이 있는지 확인 할 수 있으므로 <code>usingColumns</code>를 생략할 수 있다.</p>
</li>
</ul>
<h1 id="✔️-db관련-테스트">✔️ DB관련 테스트</h1>
<h2 id="db-연동">DB 연동</h2>
<ul>
<li>데스트 케이스는 <code>src/test</code> 에 있기 때문에 그 하위에 있는 <code>[application.properties](http://application.properties)</code> 파일이 우선순위를 가지고 실행된다.<ul>
<li>해당 파일에 DB 설정 정보를 추가한다.</li>
</ul>
</li>
</ul>
<h3 id="springboottest">SpringBootTest</h3>
<ul>
<li>@SpringBootTest Annotation은 @SpringBootApplication을 찾아서 설정으로 사용한다.<ul>
<li>현재 DB설정이 <code>@Import(JdbcTemplateV3Config.class)</code> 로 설정되어 있기 때문에 JdbcTemplate을 통해 데이터베이스를 호출하게 된다.</li>
</ul>
</li>
</ul>
<h2 id="db-분리">DB 분리</h2>
<ul>
<li>로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하는 경우 이미 존재하는 데이터들로인해 에러가 발생할 수 있다.</li>
<li>이렇게 DB를 분리했지만 테스트를 2번 실행하는 경우 데이터를 중복으로 인해 에러가 발생한다.</li>
</ul>
<h3 id="테스트의-중요-원칙">테스트의 중요 원칙</h3>
<ol>
<li><strong>테스트는 다른 테스트와 격리해야 한다.</strong></li>
<li><strong>테스트는 반복해서 실행할 수 있어야 한다.</strong></li>
</ol>
<h2 id="데이터-롤백">데이터 롤백</h2>
<ul>
<li>위의 문제점을 트랜잭션을 통해 해결할 수 있다.</li>
<li>테스트가 끝나고 나서 트랜잭션을 강제로 롤백하면 데이터가 제거된다.</li>
<li>중간에 테스트가 실패해서 롤백에 실패해도 커밋이 되지 않았기 때문에 데이터베이스에 데이터가 반영되지 않는다.</li>
</ul>
<h3 id="beforeeach-aftereach를-통한-직접-구현---코드">BeforeEach, AfterEach를 통한 직접 구현 - 코드</h3>
<pre><code class="language-java">@SpringBootTest
class ItemRepositoryTest {

    @Autowired
    ItemRepository itemRepository;

    @Autowired
    PlatformTransactionManager transactionManager;
    TransactionStatus status;

    @BeforeEach
    void beforeEach(){
        //트랜잭션 시작
        status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    }
    @AfterEach
    void afterEach() {
        //MemoryItemRepository 의 경우 제한적으로 사용
        if (itemRepository instanceof MemoryItemRepository) {
            ((MemoryItemRepository) itemRepository).clearStore();
        }
        //트랜잭션 롤백
        transactionManager.rollback(status);
    }
}</code></pre>
<ul>
<li>PlatformTransactionManager를 주입받아 각 테스트 전에 getTransaction을 통해 트랜잭션을 시작하고 테스트가 끝나면 RollBack 한다.</li>
</ul>
<h3 id="transactional-annotation을-통한-구현">Transactional Annotation을 통한 구현</h3>
<ul>
<li>클래스 또는 메서드에 <code>@Transactional</code>을 붙이면 된다.</li>
<li>원래 @Transactional은 로직이 성공적으로 수행되면 커밋되지만 테스트에서 사용할 경우에는 테스트를 트랜잭션 안에서 실행하고, 테스트가 끝나면 자동으로 롤백시킨다.</li>
</ul>
<p><del>※ 테스트와 애플리케이션 서버, 둘 다에서 @Transactional이 사용되는 경우에 대해서는 뒤에서 다룰 예정이다.</del> </p>
<ul>
<li>가끔 데이터베이스에 데이터가 잘 보관되었는지 결과를 눈으로 확인하고 싶을 경우<ul>
<li>해당 메서드에 <code>@Commit</code> 또는 <code>@Rollback(false)</code> 를 붙여주면 된다.</li>
</ul>
</li>
</ul>
<h2 id="임베디드-모드-db">임베디드 모드 DB</h2>
<h3 id="임베디드-모드">임베디드 모드</h3>
<ul>
<li>H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다. 그래서 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM메모리에 포함해서 함께 실행할 수 있다.</li>
<li>DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드라 한다.</li>
</ul>
<h3 id="직접-설정">직접 설정</h3>
<ul>
<li><p>ItemServiceApplication에 Bean Annotaion과 Test Profile을 통해 직접 등록한다.</p>
<pre><code class="language-java">      @Bean
      @Profile(&quot;test&quot;)
      public DataSource dataSource(){
          DriverManagerDataSource dataSource = new DriverManagerDataSource();
          dataSource.setDriverClassName(&quot;org.h2.Driver&quot;);
          dataSource.setUrl(&quot;jdbc:h2:mem:db;DB_CLOSE_DELAY=-1&quot;);
          dataSource.setUsername(&quot;sa&quot;);
          dataSource.setPassword(&quot;&quot;);
          return dataSource;
      }</code></pre>
</li>
<li><p>메모리 DB는 서버가 꺼질 때마다 완전히 다운되는 것이기 때문에 테스트를 실행하기 전에 항상 테이블을 먼저 생성해주어야 한다.</p>
<ul>
<li><p>JdbcTemplate을 직접 사용해서 테이블을 생성하는 DDL을 호출해도 되지만 너무 불편하다.</p>
</li>
<li><p>스프링 부트는 SQL 스크립트를 실행해서 애플리케이션 로딩 시점에 데이터베이스를 초기화하는 기능을 제공한다.</p>
</li>
<li><p><strong>src/test/resources/schema.sql</strong> 을 생성하고 코드를 작성한다.</p>
<pre><code class="language-java">  drop table if exists item CASCADE;
  create table item
  (
      id bigint generated by default as identity,
      item_name varchar(10),
      price integer,
      quantity integer,
      primary key (id)
  );</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="스프링-부트-사용">스프링 부트 사용</h3>
<ul>
<li>테스트 application.properties에 DB 설정정보를 삭제하고 ItemServiceApplication에 등록한 빈 또한 제거한 후 <strong>별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터소스를 만들어서 제공한다.</strong></li>
</ul>
<h1 id="✔️-mybatis">✔️ MyBatis</h1>
<h3 id="장점-1">장점</h3>
<ul>
<li>SQL 문을 xml 파일에 편하게 작성할 수 있다.<ul>
<li>JdbcTemplate의 경우 문장이 길어질 경우 +를 통해 붙여야하는 불편함이 있었음</li>
</ul>
</li>
<li>태그를 이용해서 동적 쿼리를 편하게 작성할 수 있다.</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>JdbcTemplate은 스프링에 내장된 기능이고, 별도의 설정없이 사용할 수 있지만 MyBatis는 약간의 설정이 필요하다.</li>
</ul>
<h2 id="설정">설정</h2>
<pre><code class="language-java">#application.properties  MyBatis
mybatis.type-aliases-package=hello.itemservice.domain
mybatis.configuration.map-underscore-to-camel-case=true
logging.level.hello.itemservice.repository.mybatis=trace</code></pre>
<ul>
<li><code>mybatis.type-aliases-package</code><ul>
<li>마이바티스에서 타입 정보를 사용할 때는 패키지 이름을 적어주어야 하는데, 여기에 명시하면 패키지 이름을 생략할 수 있다.</li>
<li>지정한 패키지와 그 하위 패키지가 자동으로 인식된다.</li>
<li>여러 위치를 지정하려면 , , ; 로 구분하면 된다.</li>
</ul>
</li>
<li><code>mybatis.configuration.map-underscore-to-camel-case</code><ul>
<li>JdbcTemplate의 BeanPropertyRowMapper 에서 처럼 언더바를 카멜로 자동 변경해주는 기능을 활성화 한다.</li>
</ul>
</li>
<li><code>logging.level.hello.itemservice.repository.mybatis=trace</code><ul>
<li>MyBatis에서 실행되는 쿼리 로그를 확인할 수 있다</li>
</ul>
</li>
</ul>
<h2 id="적용">적용</h2>
<h3 id="itemmapper-인터페이스">ItemMapper 인터페이스</h3>
<pre><code class="language-java">@Mapper
public interface ItemMapper {
    void save(Item item);
    void update(@Param(&quot;id&quot;) Long id, @Param(&quot;updateParam&quot;)ItemUpdateDto updateParam);
    Optional&lt;Item&gt; findById(Long id);
    List&lt;Item&gt; findAll(ItemSearchCond itemSearchCond);
}</code></pre>
<ul>
<li>마이바티스 매핑 XML을 호출해주는 매퍼 인터페이스를 선언해준다.</li>
<li>이 인터페이스에는 MyBatis가 인식할 수 있도록 @Mapper를 붙여준다.</li>
<li>이 인터페이스의 메서드를 호출하면 리소스 패키지 아래 같은 경로로 설정한 Xml 파일을 읽어서 해당 SQL을 실행하고 결과를 반환한다.</li>
</ul>
<h3 id="xml-파일">XML 파일</h3>
<pre><code class="language-sql">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt;
&lt;mapper namespace=&quot;hello.itemservice.repository.mybatis.ItemMapper&quot;&gt;
    &lt;insert id=&quot;save&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&gt;
        insert into item(item_name, price, quantity)
        values (#{itemName}, #{price}, #{quantity})
    &lt;/insert&gt;

    &lt;update id=&quot;update&quot;&gt;
        update item
        set item_name=#{updateParam.itemName},
            price=#{updateParam.price} ,
            quantity=#{updateParam.quantity}
        where id=#{id}
    &lt;/update&gt;

    &lt;select id=&quot;findById&quot; resultType=&quot;Item&quot;&gt;
        select id, item_name, price, quantity
        from item
        where id=#{id}
    &lt;/select&gt;

    &lt;select id=&quot;findAll&quot; resultType=&quot;Item&quot;&gt;
        select id, item_name, price, quantity
        from item
        &lt;where&gt;
            &lt;if test=&quot;itemName != null and itemName != &#39;&#39;&quot;&gt;
                and item_name like concat(&#39;%&#39;,#{itemName},&#39;%&#39;)
            &lt;/if&gt;
            &lt;if test=&quot;maxPrice !=null&quot;&gt;
                and price &amp;lt;= #{maxPrice}
            &lt;/if&gt;
        &lt;/where&gt;
    &lt;/select&gt;
&lt;/mapper&gt;</code></pre>
<ul>
<li><code>namespace</code> : 앞서 만든 메퍼 인터페이스를 지정해주면 된다.</li>
</ul>
<p>※ XML 파일의 경로를 수정하고 싶다면 application.properties에 <code>mybatis.mapper-location=classpath:mapper/**/*.xml</code> 을  설정해주면 된다.</p>
<ul>
<li>이렇게 하면 <code>resource/mapper</code> 를 포함한 하위 폴에 있는 XML을 XML 매핑 파일로 인식</li>
<li><strong>finById</strong><ul>
<li><code>resultType</code> 에는 반환 타입을 명시한다.<ul>
<li>application.properties에 <code>mybatis.type-aliases-package=hello.itemservice.domain</code> 속성을 지정한 덕분에 모든 패키지명을 다 적지 않아도 된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="특수문자-사용">특수문자 사용</h3>
<ul>
<li><p>xml에서 &lt;, &gt; 해당 문자는 태그에 해당되기 때문에 사용하기 위해서는 다른 방법을 사용해야한다.</p>
<ul>
<li><p>문자 사용</p>
<blockquote>
<p>&lt; : &lt;
: &gt;
  &amp; : &amp;</p>
</blockquote>
</li>
<li><p>CDATA 사용</p>
<pre><code class="language-sql">  &lt;![CDATA[
   and price &lt;= #{maxPrice}
   ]]&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="원리">원리</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/62eed98e-49a8-4fff-988d-01bd730e4f77/image.png" width="700">

<h3 id="매퍼-구현체">매퍼 구현체</h3>
<ul>
<li>매퍼 구현체는 예외 변환까지 처리해준다.<ul>
<li>MyBatis에서 발생한 예외를 스프링 예외 추상화인 <code>DataAccessExeption</code> 에 맞게 변환해서 반환해준다.</li>
</ul>
</li>
</ul>
<h2 id="기타기능">기타기능</h2>
<ul>
<li>PDF 참고</li>
</ul>
<h1 id="✔️-jpa">✔️ JPA</h1>
<h2 id="이론-정리">이론 정리</h2>
<ul>
<li>객체를 관계형 DB에 저장해야하는데 이 둘 사이에는 차이점이 존재한다.<ul>
<li>상속</li>
<li>연관관계</li>
<li>데이터 타입</li>
<li>데이터 식별 방법</li>
</ul>
</li>
<li>그렇기 때문에 객체답게 모델링 할수록 매핑 작업만 늘어난다.</li>
<li>객체를 자바 컬렉션에 저장 하듯이 DB에 저장하는 방법 → JPA(Java Persistence APIp)<ul>
<li>객체는 객체대로 설계</li>
<li>관계형DB는 관계형DB대로 설계</li>
</ul>
</li>
</ul>
<h3 id="jpa를-사용하는-이유">JPA를 사용하는 이유</h3>
<ul>
<li>SQL 중심적인 개발에서 객체 중심으로 개발</li>
<li>생산성, 유지보수</li>
<li>패러다임의 불일치 해결</li>
<li>성능<ol>
<li>1차 캐시와 동일성 보장<ul>
<li>같은 트랜잭션 안에서는 같은 엔티디를 반환</li>
<li>DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장</li>
</ul>
</li>
<li>트랜잭션을 지원하는 쓰기 지원<ul>
<li>트랜잭션을 커밋할 때까지 INSERT SQL을 모음</li>
<li>JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송</li>
<li>Update, Delete로 인한 로우락 시간 최소화</li>
</ul>
</li>
<li>지연 로딩</li>
</ol>
</li>
<li>데이터 접근 추상화와 벤더 독립성</li>
</ul>
<h2 id="적용-1">적용</h2>
<h3 id="item-class">Item Class</h3>
<pre><code class="language-java">@Data
@Entity
public class Item {

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

    @Column(name = &quot;item_name&quot;, length = 10)
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}</code></pre>
<ul>
<li><code>@Entity</code> : JPA가 사용하는 객체라는 뜻</li>
<li><code>@Id</code>: 테이블의 PK와 해당 필드를 매핑한다.</li>
<li><code>@Column</code> : 객체의 필드를 테이블의 컬럼과 매핑한다.<ul>
<li><code>name</code> 속성으로 테이블 컬럼이름을 지정할 수 있다.</li>
<li><strong>@Column을 생략하면 필드의 이름을 테이블 컴럼 이름으로 사용</strong>하는데 객체 필드의 <strong>카멜케이스를 테이블 컬럼의 언더스코어로 자동 변환</strong>해주기 때문에 itemName 같은 경우 @Column을 생략해도 된다.</li>
</ul>
</li>
<li>JPA는 public 또는 protected 기본 생성자가 필수이다.</li>
</ul>
<h3 id="jpaitemrepository-코드">JPAItemRepository 코드</h3>
<pre><code class="language-java">@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV1 implements ItemRepository {

    private final EntityManager em;

    @Override
    public Item save(Item item) {
        em.persist(item);
        return item;
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item item = em.find(Item.class, itemId);
        item.setItemName(updateParam.getItemName());
        item.setPrice(updateParam.getPrice());
        item.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional&lt;Item&gt; findById(Long id) {
        Item item = em.find(Item.class, id);
        return Optional.ofNullable(item);
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String jpql = &quot;select i from Item i&quot;;

        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if (StringUtils.hasText(itemName) || maxPrice != null) {
            jpql += &quot; where&quot;;
        }

        boolean andFlag = false;
        if (StringUtils.hasText(itemName)) {
            jpql += &quot; i.itemName like concat(&#39;%&#39;,:itemName,&#39;%&#39;)&quot;;
            andFlag = true;
        }
        if (maxPrice != null) {
            if (andFlag) {
                jpql += &quot; and&quot;;
            }
            jpql += &quot; i.price &lt;= :maxPrice&quot;;
        }
        TypedQuery&lt;Item&gt; query = em.createQuery(jpql, Item.class);
        if (StringUtils.hasText(itemName)) {
            query.setParameter(&quot;itemName&quot;, itemName);
        }
        if (maxPrice != null) {
            query.setParameter(&quot;maxPrice&quot;, maxPrice);
        }
        return query.getResultList();
    }
}</code></pre>
<ul>
<li>EntityManager 주입<ul>
<li>JPA는 모든 동작을 엔티티 매니저를 통해서 이루어진다.</li>
<li>엔티디 매니저는 내부에 데이터 소스를 가지고 있고, 데이터베이스에 접근할 수 있다.</li>
</ul>
</li>
<li><code>@Transactional</code><ul>
<li>JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다. 보통의 경우 Service 계층에서 Transactional을 걸어주는게 일반적이다. 이 프로젝트만 예외</li>
</ul>
</li>
</ul>
<p><strong>※</strong> JPA를 설정하려면 <code>EntityManagerFactory</code>, JPA 트랜잭션 매니저, 데이터소스 등등 다양한 설정을 해야 한다. 하지만 스프링 부트는 이 과정을 모두 자동화 해준다.</p>
<h2 id="예외-변환">예외 변환</h2>
<ul>
<li>EntityManager는 순수한 JPA 기술이고 ,스프링과는 관계가 없기 때문에 엔티티 매니저는 예외가 발생하면 JPA 관련 예외를 발생시킨다.</li>
<li>JPA는 <code>PersistenceException</code> 과 그 하위 예외 <code>IllegalStateException</code>, <code>IllegalArgumentException</code> 을 발생시킨다.<ul>
<li>이렇게 되면 런타임 에러인 해당 에러는 서비스 계층까지 던져져 <strong>서비스 계층이 JPA기술에 의존적이게 된다.</strong></li>
</ul>
</li>
</ul>
<h3 id="repository">@Repository</h3>
<ul>
<li>@Repository Annotation은 해당 클래스가 컴포넌트 스캔의 대상이라는 선언 기능과 예외 변환 AOP의 적용 대상이 되는 기능이 있다.</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/a8fae448-4024-4627-9edd-e3a3f9759e50/image.png" width="700">

<h1 id="✔️-스프링-데이터-jpa">✔️ 스프링 데이터 JPA</h1>
<h2 id="이론">이론</h2>
<ul>
<li>스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 라이브러리이다.</li>
<li>대표 기능<ul>
<li>공통 인터페이스 기능</li>
<li>쿼리 메서드 기능</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/06b49d93-1247-4b95-9afd-63c186b691dc/image.png" width="700">

<h2 id="적용---jparepository-interface">적용 - JpaRepository Interface</h2>
<h3 id="사용법">사용법</h3>
<ul>
<li>JpaRepository 인터페이스를 상속받고, 제네릭에 관리할 &lt;엔티티, 엔티티ID&gt;를 준다.<ul>
<li>그러면 JpaRepository가 제공하는 기본 CRUD 기능을 모두 사용할 수 있다.</li>
<li>스프링 데이터 JPA가 프록시 기술을 사용해서 해당 인터페이스의 구현체를 만들고 구현체의 인스턴스를 만들어서 스프링 빈으로 등록한다.<ul>
<li>따라서 개발자는 구현 클래스 없이 인터페이스만으로 CRUD 기능을 사용할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="쿼리-메서드-기능">쿼리 메서드 기능</h3>
<ul>
<li><p>기본 CRUD 외에 메서드를 사용하고 싶다면 쿼리 메서드 기능을 사용하면 된다.</p>
</li>
<li><p>스프링 데이터 JPA는 인터페이스에 메서드만 적어두면 메서드 이름을 분석해서 쿼리를 자동으로 만들고 실행해주는 기능을 제공한다.</p>
<p>  <a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation">쿼리 메서드 필터 조건 공식 문서</a></p>
</li>
</ul>
<h3 id="jpql-직접-작성">JPQL 직접 작성</h3>
<ul>
<li>메서드 이름이 너무 복잡하고 긴 경우 <code>@Query</code> 를 사용해 직접 JPQL을 작성할 수 있다.</li>
<li>스프링 데이터 JPA는 JPQL 뿐만 아니라 SQL로도 직접 작성할 수 있도록 해준다.</li>
</ul>
<pre><code class="language-java">public interface SpringDataJpaItemRepository extends JpaRepository&lt;Item, Long&gt; {

    List&lt;Item&gt; findByItemNameLike(String itemName);
    List&lt;Item&gt; findByPriceLessThanEqual(Integer price);

    //쿼리 메서드
    List&lt;Item&gt; findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);

    //쿼리 직접 실행
    @Query(&quot;select i from Item i where i.itemName like :itemName and i.price &lt;= :price&quot;)
    List&lt;Item&gt; findItems(@Param(&quot;itemName&quot;) String itemName,@Param(&quot;price&quot;) Integer price);
}</code></pre>
<h2 id="주의점과-플로우">주의점과 플로우</h2>
<h3 id="의존관계와-구조">의존관계와 구조</h3>
<ul>
<li>ItemService는 ItemRepository에 의존하기 때문에 ItemService에서 <code>SpringDataJpaItemRepository</code> 를 그대로 사용할 수 없다.<ul>
<li>JpaItemRepositoryV2를 만들어 <strong><code>ItemRepository</code> 와 <code>SpringDataJpaItemRepository</code> 사이를 맞추기 위한 어댑터</strong>처럼 사용한다.</li>
</ul>
</li>
</ul>
<h3 id="클래스-의존관계">클래스 의존관계</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/90e37431-2a82-483c-9f49-5a24454edee2/image.png" width="700">


<h3 id="런타임-객체-의존관계">런타임 객체 의존관계</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/7c931047-613d-49b8-95b8-5b1c150a9122/image.png" width="700">


<h3 id="예외-변환-1">예외 변환</h3>
<ul>
<li>스프링 데이터 JPA도 스프링 예외 추상화를 지원한다.</li>
<li>스프링 데이터 JPA가 만들어주는 프록시에서 이미 예외 변환을 처리하기 때문에 <code>@Repository</code> 에 관계없이 예외가 변환된다.</li>
</ul>
<h1 id="querydsl">Querydsl</h1>
<h2 id="설정-1">설정</h2>
<ul>
<li>dependecy 추가, build-clean 추가</li>
<li>Querydsl 사용을 위해서는 Q클래스를 생성해야한다.</li>
</ul>
<h3 id="빌드-툴에-따른-q클래스-생성-방법">빌드 툴에 따른 Q클래스 생성 방법</h3>
<ol>
<li><strong>Gradle</strong><ol>
<li><code>Gradle -&gt; Tasks -&gt; build -&gt; clean</code></li>
<li><code>Gradle -&gt; Tasks -&gt; other -&gt; compileJava</code></li>
</ol>
<ul>
<li>build → generated 하위 패키지에 확인해보면 QItem이 생성되어 있어야 한다</li>
<li>Q타입 삭제를 위해서는 gradle clean을 수행하면 build 폴더 자체가 삭제된다.</li>
</ul>
</li>
<li><strong>IntelliJ IDEA</strong><ol>
<li>Build → Build Project 또는 Build → Rebuild 또는 main() 실행 또는 테스트 실행</li>
</ol>
<ul>
<li>src → main → generated 하위에 QItem 생성</li>
</ul>
</li>
</ol>
<p><strong>※</strong> Q타입은 컴파일 시점에 자동 생성되므로 <strong>버전관리에 포함하지 않는 것이 좋다</strong>. <code>src/main/generated</code> 를 포함하지 않는다.</p>
<h2 id="적용-2">적용</h2>
<h3 id="querydslrepository-코드">QueryDslRepository 코드</h3>
<pre><code class="language-java">@Repository
@Transactional
public class JpaItemRepositoryV3 implements ItemRepository {

    private final EntityManager em;
    private final JPAQueryFactory query;

    public JpaItemRepositoryV3(EntityManager em) {
        this.em = em;
        this.query = new JPAQueryFactory(em);
    }

    @Override
    public List&lt;Item&gt; findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        return query.select(item)
                .from(item)
                .where(likeItemName(itemName), maxPrice(maxPrice))
                .fetch();
    }

    private BooleanExpression maxPrice(Integer maxPrice) {
        if(maxPrice != null){
            return item.price.loe(maxPrice);
        }
        return null;
    }

    private BooleanExpression likeItemName(String itemName) {
        if (StringUtils.hasText(itemName)){
            return item.itemName.like(&quot;%&quot; + itemName + &quot;%&quot;);
        }
        return null;
    }
}</code></pre>
<ul>
<li>JpaQueryFactory를 주입받아야 하며 JpaQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager가 필요하다.</li>
<li>JPAQueryFactory를 스프링 빈으로 등록해서 사용해도 된다.</li>
</ul>
<h3 id="예외-변환-2">예외 변환</h3>
<ul>
<li><code>Querydsl</code> 은 별도의 스프링 예외 추상화를 지원하지 않는다. 대신에 JPA에서 학습한 것처럼 <code>@Repository</code> 에서 스프링 예외 추상화를 처리해준다.</li>
</ul>
<h3 id="장점-2">장점</h3>
<ul>
<li>오타가 있어도 컴파일 시점에 오류를 막을 수 있다.</li>
<li>메서드 추출을 통해서 코드를 재사용할 수 있다.</li>
</ul>
<h1 id="✔️-데이터-접근-기술-활용-방안---트레이드-오프">✔️ 데이터 접근 기술 활용 방안 - 트레이드 오프</h1>
<h2 id="어댑터-도입-구조-유지---현재-상태">어댑터 도입, 구조 유지 - 현재 상태</h2>
<h3 id="클래스-의존관계-1">클래스 의존관계</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/e6f14da8-72d9-4575-83c8-02a40bad9b90/image.png" width="700">

<h3 id="런타임-객체-의존관계-1">런타임 객체 의존관계</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/18e14a36-4207-4850-bd72-cd93327fe6a7/image.png" width="700">

<ul>
<li>현재의 구조는 JpaItemRepositoryV2가 중간에서 어댑터 역할을 해준 덕분에 ItemService가 사용하는 ItemRepository 인터페이스를 그대로 유지할 수 있고 ItemService의 코드를 변경하지 않아도 되는 장점이 있다.<ul>
<li>DI, OCP 원칙을 지킬 수 있다.</li>
</ul>
</li>
<li>하지만 구조가 복잡하고 어댑터 코드와 실제 코드까지 함께 유지보수 해야 하는 어려움이 발생한다.</li>
</ul>
<h2 id="실용적인-구조">실용적인 구조</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/b5965779-edda-4478-8983-aad0adf4134c/image.png" width="700">

<ul>
<li><p><code>ItemRepositoryV2</code> : 스프링 데이터 JPA의 기능을 제공하는 리포지토리</p>
<pre><code class="language-java">  public interface ItemRepositoryV2 extends JpaRepository&lt;Item, Long&gt; {
  }</code></pre>
</li>
<li><p><code>ItemQueryRepositoryV2</code> : Querydsl을 사용해서 복잡한 쿼리 기능을 제공하는 리포지토리</p>
<pre><code class="language-java">  @Repository
  public class ItemQueryRepositoryV2 {
      private final JPAQueryFactory query;

      public ItemQueryRepositoryV2(EntityManager em) {
          this.query = new JPAQueryFactory(em);
      }

      public List&lt;Item&gt; findAll(ItemSearchCond cond) {
          String itemName = cond.getItemName();
          Integer maxPrice = cond.getMaxPrice();

          return query.select(item)
                  .from(item)
                  .where(likeItemName(itemName), maxPrice(maxPrice))
                  .fetch();
      }

      private BooleanExpression maxPrice(Integer maxPrice) {
          if(maxPrice != null){
              return item.price.loe(maxPrice);
          }
          return null;
      }

      private BooleanExpression likeItemName(String itemName) {
          if (StringUtils.hasText(itemName)){
              return item.itemName.like(&quot;%&quot; + itemName + &quot;%&quot;);
          }
          return null;
      }
  }</code></pre>
</li>
</ul>
<ul>
<li>Service 계층에서 두가지 레포지토리를 모두 주입받는다.<ul>
<li>두가지 모두 스프링 빈으로 등록</li>
<li>ItemRepositoryV2는 스프링에서 구현체를 생성해 인스턴스를 스프링 빈에 등록해준다.</li>
</ul>
</li>
</ul>
<h2 id="트레이드-오프">트레이드 오프</h2>
<ul>
<li>DI, OCP를 지키기 위해 어댑터를 도입하고, 더 많은 코드를 유지한다.</li>
<li>어댑터를 제거하고 구조를 단순하게 가져가지만, DI, OCP를 포기하고 <code>ItemService</code> 코드를 직접 변경한다.</li>
</ul>
<p><strong>→  구조의 안정성 VS 단순한 구조</strong></p>
<ul>
<li>추상화도 비용이 든다. - 유지 보수 관점에서의 비용<ul>
<li>이 추상화 비용을 넘어설 만큼 효과가 있을 때 추상화를 도입하는 것이 실용적이다.</li>
</ul>
</li>
<li>현재 상황에 맞는 선택을 하자 → <strong>프로젝트 규모와 변동성에 따라 선택</strong></li>
</ul>
<h2 id="다양한-데이터-접근-기술-조합">다양한 데이터 접근 기술 조합</h2>
<ul>
<li>실무에서 95% 정도는 JPA, 스프링 데이터 JPA, Querydsl 등으로 해결하고, 나머지 5%는 SQL을 직접 사용해야 하니 JdbcTemplate이나 MyBatis로 해결한다.</li>
</ul>
<h3 id="트랜잭션-매니저-선택">트랜잭션 매니저 선택</h3>
<ul>
<li>JPA, 스프링 데이터 JPA, Querydsl은 모두 JPA 기술을 사용하는 것이기 때문에 <code>JpaTransactionManager</code> 를 선택하면 된다. 해당 기술을 사용하면 스프링 부트는 자동으로 해당 매니저를 스프링 빈에 등록한다.</li>
<li>그런데 <code>JdbcTemplate</code>, <code>MyBatis</code> 와 같은 기술들은 내부에서 JDBC를 직접 사용하기 때문에 <code>DataSourceTransactionManager</code> 를 사용한다.</li>
<li>이렇게 트랜잭션 매니저가 달라지는 문제가 있지만 <code>**JpaTransactionManager</code> 는 <code>DataSourceTransactionManager</code> 가 제공하는 기능을 대부분 제공한다.**<ul>
<li><strong>JPA라는 기술도 결국 내부에서는 DataSource와 JDBC 커넥션을 사용하기 때문이다.</strong></li>
</ul>
</li>
</ul>
<h3 id="주의점">주의점</h3>
<ul>
<li>JPA와 JdbcTemplate을 함께 사용할 경우 JPA의 플러시 타이밍에 주의해야 한다.<ul>
<li>JPA 강의에서 다룰 예정</li>
</ul>
</li>
</ul>
<h1 id="✔️-스프링-트랜잭션">✔️ 스프링 트랜잭션</h1>
<h2 id="트랜잭션의-이해">트랜잭션의 이해</h2>
<ul>
<li>DB 1편 트랜잭션 참고</li>
</ul>
<h2 id="트랜잭션-적용-확인">트랜잭션 적용 확인</h2>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @SpringBootTest
  public class TxBasicTest {

      @Autowired BasicService basicService;

      @Test
      void proxyCheck(){
          log.info(&quot;aop class={}&quot;, basicService.getClass());
          Assertions.assertThat(AopUtils.isAopProxy(basicService)).isTrue();
      }

      @Test
      void txTest(){
          basicService.tx();
          basicService.nonTx();
      }

      @TestConfiguration
      static class TxApplyBasicConfig{

          @Bean
          BasicService basicService(){
              return new BasicService();
          }
      }

      @Slf4j
      static class BasicService{

          @Transactional
          void tx(){
              log.info(&quot;call tx&quot;);
              boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
              log.info(&quot;tx active={}&quot;, txActive);
          }

          void nonTx(){
              log.info(&quot;call nonTx&quot;);
              boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();
              log.info(&quot;tx active={}&quot;, txActive);
          }

      }
  }</code></pre>
</li>
<li><p><code>AopUtils.isAopProxy()</code> : 선언적 트랜잭션 방식에서 스프링 트랜잭션은 AOP를 기반으로 동작한다.</p>
</li>
</ul>
<h3 id="스프링-컨테이너에-트랜잭션-프록시-등록">스프링 컨테이너에 트랜잭션 프록시 등록</h3>
<ul>
<li><code>@Transactional</code> 을 클래스나 메서드에 붙이면 해당 객체는 트랜잭션 AOP의 대상이되고, 클래스내에 하나의 <code>@Transactional</code> 이라도 있으면 스프링 빈에 서비스 객체 대신에 서비스 프록시 객체가 등록된다.<ul>
<li>주입 받을 때 또한 프록시 객체가 주입된다.</li>
</ul>
</li>
<li>프록시는 <code>BasicService</code> 를 상속해서 만들어지기 때문에 다형성을 활용할 수 있다.<ul>
<li>따라서 BasicService 대신에 프록시인 <code>BasicService$$CGLIB</code> 을 주입할 수 있다.<ul>
<li>부모는 자식을 품을 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="트랜잭션-프록시-동작-방식">트랜잭션 프록시 동작 방식</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/ae073859-09aa-46c9-8d37-e7c748077485/image.png" width="700">

<ul>
<li><strong>basicService.tx 호출</strong><ul>
<li>프록시의 tx()가 호출된다. 프록시는 <code>tx()</code> 메서드가 트랜잭션을 사용할 수 있는지 확인해본다.</li>
<li>트랜잭션을 시작한 다음 실제 <code>basicService.tx()</code> 를 호출한다.</li>
<li>그리고 실제 basicService.tx()의 호출이 끝나서 프록시로 제어가 돌아오면 프록시는 트랜잭션을 커밋하거나 롤백해서 트랜잭션을 종료한다.</li>
</ul>
</li>
<li><strong>nonTx 호출</strong><ul>
<li>트랜잭션 대상이 아니라면 바로 <code>basicService.nonTx()</code> 를 참조해서 호출하고 종료한다.</li>
</ul>
</li>
<li><code>TransactionSynchronizationManager.isActualTransactionActive()</code><ul>
<li>현재 쓰레드에 트랜잭션이 적용되어 있는지 확인</li>
<li>Boolean값 반환</li>
</ul>
</li>
</ul>
<h2 id="트랜잭션-적용-위치---우선순위">트랜잭션 적용 위치 - 우선순위</h2>
<ul>
<li>스프링에서 우선순위는 항상 <strong>더 구체적이고 자세한 것이 높은 우선순위</strong>를 가진다.</li>
</ul>
<h3 id="인터페이스에-transactional-적용">인터페이스에 @Transactional 적용</h3>
<ul>
<li><strong>우선 순위</strong><ol>
<li>클래스의 메서드</li>
<li>클래스의 타입</li>
<li>인터페이스의 메서드</li>
<li>인터페이스의 타입</li>
</ol>
</li>
<li>그런데 인터페이스에 <code>@Transactional</code> 을 사용하는 것은 스프링 공식 메뉴얼에서 권장하지 않는 방법이다.<ul>
<li>AOP를 적용하는 방식에 따라서 인터페이스에 애노테이션을 두면 AOP가 적용 되지 않는 경우도 있기 때문이다.</li>
</ul>
</li>
</ul>
<h2 id="트랜잭션-aop-주의-사항---프록시-내부-호출">트랜잭션 AOP 주의 사항 - 프록시 내부 호출</h2>
<ul>
<li>트랜잭션 AOP는 기본적으로 프록시 방식의 AOP를 사용한다.</li>
<li><code>@Transactional</code> 을 적용하면 프록시 객체가 요청을 먼저 받아서 트랜잭션을 처리하고 실제 객체를 호출해준다.<ul>
<li>따라서 트랜잭션을 호출하려면 항상 프록시를 통해서 대상 객체를 호출해야 한다.</li>
</ul>
</li>
<li>만약 프록시를 거치지 않고 대상 객체를 직접 호출하게 되면 AOP가 적용되지 않고, 트랜잭션도 적용되지 않는다.</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/d40e4a4f-5f95-4d9a-9312-a37ffebf85a1/image.png" width="700">

<ul>
<li><strong>대상 객체의 내부에서 메서드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.</strong></li>
</ul>
<h3 id="프록시와-내부-호출">프록시와 내부 호출</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @SpringBootTest
  public class InternalCallV1Test {
      @Autowired
      CallService callService;
      @Test
      void printProxy() {
          log.info(&quot;callService class={}&quot;, callService.getClass());
      }
      @Test
      void internalCall() {
          callService.internal();
      }
      @Test
      void externalCall() {
          callService.external();
      }
      @TestConfiguration
      static class InternalCallV1Config {
          @Bean
          CallService callService() {
              return new CallService();
          }
      }
      @Slf4j
      static class CallService {
          public void external() {
              log.info(&quot;call external&quot;);
              printTxInfo();
              internal();
          }
          @Transactional
          public void internal() {
              log.info(&quot;call internal&quot;);
              printTxInfo();
          }
          private void printTxInfo() {
              boolean txActive =
                      TransactionSynchronizationManager.isActualTransactionActive();
              log.info(&quot;tx active={}&quot;, txActive);
          }
      }
  }</code></pre>
</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/b4b48f06-485a-4aa8-a812-ee97ab9dd4a0/image.png" width="700">

<ol>
<li>클라이언트인 테스트 코드는 트랜잭션 프록시에서 external()을 호출한다.</li>
<li>callService의 트랜잭션 프록시가 호출된다.</li>
<li>external() 메서드에는 <code>@Transactional</code>이 없다.</li>
<li>그러므로 실제 객체의 실제 callService의 external()이 호출된다.</li>
<li>external()은 내부에서 internal() 메서드를 호출한다. → <strong>여기서 문제가 발생한다.</strong></li>
</ol>
<h3 id="문제-원인">문제 원인</h3>
<ul>
<li>자바 언어에서 메서드 앞에 별도의 참조가 없으면 <code>this</code> 라는 뜻으로 자기 자신의 인스턴스를 가리킨다.</li>
<li><code>this.internal()</code> 에서 <code>this</code> 는 자기 자신, 실제 대상 객체의 인스터스를 뜻하므로 이러한 내부 호출은 프록시를 거치지 않는다.<ul>
<li>따라서 트랜잭션을 적용할 수 없다.</li>
</ul>
</li>
</ul>
<h3 id="가장-간단한-해결-방안---클래스-분리">가장 간단한 해결 방안 - 클래스 분리</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @SpringBootTest
  public class InternalCallV2Test {

      @Autowired CallService callService;

      @Test
      void externalCallV2(){
          callService.external();
      }

      @TestConfiguration
      static class InternalCallV1Config{
          @Bean
          CallService callService(){
              return new CallService(internalService());
          }

          @Bean
          InternalService internalService(){
              return new InternalService();
          }
      }

      @RequiredArgsConstructor
      static class CallService{

          private final InternalService internalService;

          public void external(){
              log.info(&quot;call external&quot;);
              printTxInfo();
              internalService.internal();
          }

          private void printTxInfo(){
              boolean active = TransactionSynchronizationManager.isActualTransactionActive();
              log.info(&quot;tx active={}&quot;, active);
              boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
              log.info(&quot;readOnly={}&quot;, readOnly);
          }
      }

      @Slf4j
      static class InternalService{

          @Transactional
          public void internal(){
              log.info(&quot;call internal&quot;);
              printTxInfo();
          }

          private void printTxInfo(){
              boolean active = TransactionSynchronizationManager.isActualTransactionActive();
              log.info(&quot;tx active={}&quot;, active);
              boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
              log.info(&quot;readOnly={}&quot;, readOnly);
          }

      }
  }</code></pre>
</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/92ab4a32-2d75-4f8d-afb4-49dbcc8b617f/image.png" width="700">

<h3 id="public-메서드">Public 메서드</h3>
<ul>
<li>스프링의 트랜잭션 AOP 기능은 <code>public</code> 메서드에만 트랜잭션을 적용하도록 기본 설정이 되어있다.<ul>
<li><code>protected</code>, <code>private</code>, <code>package-visible</code> 에는 트랜잭션이 적용되지 않는다.</li>
</ul>
</li>
</ul>
<h2 id="트랜잭션-aop-주의-사항---초기화-시점">트랜잭션 AOP 주의 사항 - 초기화 시점</h2>
<ul>
<li>스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있다.</li>
</ul>
<pre><code class="language-java">@PostConstruct
@Transactional
public void initV1(){
    boolean active = TransactionSynchronizationManager.isActualTransactionActive();
    log.info(&quot;Hello init @PostConstruct tx active={}&quot;, active);
}</code></pre>
<ul>
<li>다음과 같이 선언 시 초기화 코드가 먼저 호출 되고 그 다음에 트랜잭션 AOP가 적용되기 때문에 해당 메서드에서 트랜잭션을 흭득할 수 없다.</li>
</ul>
<h3 id="대안">대안</h3>
<pre><code class="language-java">@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initV2(){
    boolean active = TransactionSynchronizationManager.isActualTransactionActive();
    log.info(&quot;Hello init ApplicationReadyEvent tx active={}&quot;, active);
}</code></pre>
<ul>
<li>다음과 같이 선언 시 트랜잭션 AOP를 포함한 스프링이 컨테이너가 완전히 생성되고 난 다음에 이벤트가 붙은 메서드를 호출한다.</li>
</ul>
<h2 id="트랜잭션-옵션">트랜잭션 옵션</h2>
<ul>
<li>PDF 참고</li>
</ul>
<h2 id="예외와-트랜잭션-커밋-롤백">예외와 트랜잭션 커밋, 롤백</h2>
<ul>
<li><p>예외가 발생했는데 내부에서 예외를 처리하지 못하고 트랜잭션 범위(@Transactional이 적용된 AOP) 밖으로 예외를 던지면?</p>
  <img src="https://velog.velcdn.com/images/yeopju_5/post/c3fa0c5a-0607-4860-a783-30823e36200d/image.png" width="700">

<ul>
<li><strong>언체크 예외는 트랜잭션을 롤백한다.</strong></li>
<li><strong>체크 예외는 트랜잭션을 커밋한다.</strong></li>
<li><strong>이 정책이 디폴트이고 이 정책을 따르고 싶지 않다면 <code>rollbackFor</code> 이라는 옵션을 사용해서 체크 예외도 롤백시킬 수 있다.</strong></li>
</ul>
</li>
</ul>
<h3 id="체크-언체크에-따른-커밋-롤백-이유">체크, 언체크에 따른 커밋, 롤백 이유</h3>
<ul>
<li><p>체크 예외는 <strong>비즈니스상 의미가 있을 때 사용</strong>할 것이라고 생각하기 때문에 커밋한다. (서비스 계층이나 컨트롤러 계층에서 예외를 잡아서 비즈니스 로직 처리)</p>
</li>
<li><p>언체크 예외는 복구 불가능한 예외이기 때문에 롤백한다.</p>
</li>
<li><p>OrderService 코드</p>
<pre><code class="language-java">  @Slf4j
  @Service
  @RequiredArgsConstructor
  public class OrderService {

      private final OrderRepository orderRepository;

      @Transactional
      public void order(Order order) throws NotEnoughMoneyException {
          log.info(&quot;order 호출&quot;);
          orderRepository.save(order);

          log.info(&quot;결제 프로세스 호출&quot;);
          if (order.getUsername().equals(&quot;예외&quot;)){
              log.info(&quot;시스템 예외&quot;);
              throw new RuntimeException();
          } else if (order.getUsername().equals(&quot;잔고부족&quot;)){
              log.info(&quot;비즈니스 예외&quot;);
              order.setPayStatus(&quot;대기&quot;);
              throw new NotEnoughMoneyException(&quot;잔고가 부족합니다&quot;);
          } else{
              log.info(&quot;정상 승인&quot;);
              order.setPayStatus(&quot;완료&quot;);
          }
          log.info(&quot;결제 프로세스 완료&quot;);
      }
  }</code></pre>
</li>
<li><p>OrderServiceTest 코드</p>
<pre><code class="language-java">  @Slf4j
  @SpringBootTest
  public class OrderServiceTest {

      @Autowired
      OrderService orderService;
      @Autowired
      OrderRepository orderRepository;

      @Test
      void complete() throws NotEnoughMoneyException {
          Order order = new Order();
          order.setUsername(&quot;정상&quot;);

          orderService.order(order);

          Order findOrder = orderRepository.findById(order.getId()).get();
          Assertions.assertThat(findOrder.getPayStatus()).isEqualTo(&quot;완료&quot;);
      }
      @Test
      void runtimeException() {
          Order order = new Order();
          order.setUsername(&quot;예외&quot;);

          Assertions.assertThatThrownBy(() -&gt; orderService.order(order))
                  .isInstanceOf(RuntimeException.class);

          Optional&lt;Order&gt; byId = orderRepository.findById(order.getId());
          Assertions.assertThat(byId.isEmpty()).isTrue();
      }
      @Test
      void bizException(){
          Order order = new Order();
          order.setUsername(&quot;잔고부족&quot;);

          try{
              orderService.order(order);
              fail(&quot;잔고 부족 예외가 발생해야 합니다&quot;);
          }catch (NotEnoughMoneyException e){
              log.info(&quot;고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내&quot;);
          }

          Order findOrder = orderRepository.findById(order.getId()).get();
          Assertions.assertThat(findOrder.getPayStatus()).isEqualTo(&quot;대기&quot;);
      }
  }</code></pre>
</li>
</ul>
<h1 id="✔️-스프링-트랜잭션-전파-이론">✔️ 스프링 트랜잭션 전파 이론</h1>
<ul>
<li>트랜잭션이 둘 이상 있을 때 어떻게 동작하는지 알아본다.</li>
</ul>
<h3 id="별개의-트랜잭션-2번-사용">별개의 트랜잭션 2번 사용</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Test
      void double_commit(){
          log.info(&quot;트랜잭션1 시작&quot;);
          TransactionStatus status1 = txManager.getTransaction(new DefaultTransactionAttribute());
          log.info(&quot;트랜잭션1 커밋&quot;);
          txManager.commit(status1);

          log.info(&quot;트랜잭션2 시작&quot;);
          TransactionStatus status2 = txManager.getTransaction(new DefaultTransactionAttribute());
          log.info(&quot;트랜잭션2 커밋&quot;);
          txManager.commit(status2);
      }</code></pre>
</li>
<li><p>로그를 보면 트랜잭션1과 트랜잭션1는 같은 <code>conn0</code> 커넥션을 사용중이다.</p>
<ul>
<li>이 것은 Hikari 커넥션 풀을 사용하기 때문이다.</li>
</ul>
</li>
<li><p>이 둘을 구분하는 방법이 있다.</p>
<ul>
<li>히카리 커넥션 풀에서 커넥션을 흭득하면 실제 커넥션을 그대로 반환하는 것이 아니라 내부 관리를 위해 히카리 프록시 커넥션이라는 객체를 생성해서 반환한다.</li>
<li>이 객체의 주소를 확인하면 커넥션 풀에서 흭득한 커넥션을 구분할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="트랜잭션-전파-이론">트랜잭션 전파 이론</h2>
<ul>
<li>외부 트랜잭션이 수행 중인데, 내부 트랜잭션이 추가로 수행될 경우<ul>
<li>스프링이 외부 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어준다.</li>
<li><strong>내부 트랜잭션이 외부 트랜잭션에 참여하는 것이다.</strong></li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/e90a2b19-ece6-4d2b-88b7-e0286bd85cb0/image.png" width="700">

<h3 id="물리-트랜잭션과-논리-트랜잭션">물리 트랜잭션과 논리 트랜잭션</h3>
<ul>
<li>논리 트랜잭션은 하나의 물리 트랜잭션으로 묶인다.</li>
<li>물리 트랜잭션은 실제 데이터베이스에 적용되는 트랜잭션을 뜻한다.</li>
<li>논리 트랜잭션은 트랜잭션 매니저를 통해 트랜잭션을 사용하는 단위이다.</li>
</ul>
<p>→ 트랜잭션이 사용 중일 때 또 다른 트랜잭션이 내부에 사용되면 여러가지 복잡한 상황이 발생한다. 그렇기 때문에 다음과 같은 단순한 원칙을 만들기 위해 물리, 논리 트랜잭션이라는 개념을 도입하는 것이다.</p>
<blockquote>
<p>⚠️  <strong>원칙</strong></p>
</blockquote>
<ul>
<li>**모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.</li>
<li>하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.**</li>
</ul>
<h2 id="전파-예제-플로우">전파 예제, 플로우</h2>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Test
      void inner_commit(){
          log.info(&quot;외부 트랜잭션 시작&quot;);
          TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
          log.info(&quot;outer.isNewTransaction()={}&quot;, outer.isNewTransaction());

          log.info(&quot;내부 트랜잭션 시작&quot;);
          TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
          log.info(&quot;inner.isNewTransaction()={}&quot;, inner.isNewTransaction());
          log.info(&quot;내부 트랜잭션 커밋&quot;);
          txManager.commit(inner);

          log.info(&quot;외부 트랜잭션 커밋&quot;);
          txManager.commit(outer);
      }</code></pre>
</li>
</ul>
<h3 id="플로우">플로우</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/98bf826d-ab94-46d1-bee5-d3641df6e303/image.png" width="700">

<ul>
<li>트랜잭션 매니저는 트랜잭션을 생성한 결과를 <code>TransactionStatus</code> 에 담아서 반환하는데, 여기에 신규 트랜잭션의 여부가 담겨 있다.<ul>
<li><code>isNewTransaction</code> 을 통해 신규 트랜잭션 여부를 확인할 수 이따.</li>
</ul>
</li>
<li>트랜잭션 매니저는 트랜잭션을 시작할 때 항상 트랜잭션 동기화 매니저를 통해 기존 트랜잭션이 존재하는지 확인한다.</li>
<li>내부 트랜잭션 커밋 시 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않는다.</li>
</ul>
<h2 id="전파---외부-롤백">전파 - 외부 롤백</h2>
<ul>
<li>내부 트랜잭션이 커밋되어도 해당 트랜잭션은 신규 트랜잭션이 아니기 때문에 실제 커밋을 호출하지 않는다. 실제 커넥션에 커밋이나 롤백을 호출하면 물리 트랜잭션이 끝나버린다.</li>
</ul>
<h2 id="전파---내부-롤백">전파 - 내부 롤백</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/fc702371-d87d-44be-b977-c73b9f7bad88/image.png" width="700">

<ul>
<li><strong>내부 트랜잭션이 롤백을 했지만, 내부 트랜잭션은 물리 트랜잭션에 영향을 주지 않는다</strong>.<ul>
<li>물리 트랜잭션에 영향을 주지 않는 대신 커넥션 동기화 매니저에 <code>rollbackOnly=true</code> 라는 표시를 해둔다.</li>
</ul>
</li>
<li>외부 트랜잭션을 커밋하는 시점에 트랜잭션 동기화 매니저에 <code>rollbackOnly</code> 가 있는지 확인한다.<ul>
<li><strong>해당 표시가 있으면 물리 트랜잭션을 롤백하고 <code>UnexpectedRollbackException</code> 런타임 예외를 던진다.</strong></li>
</ul>
</li>
</ul>
<h2 id="전파---requires_new">전파 - REQUIRES_NEW</h2>
<ul>
<li>외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션을 사용하는 방법이다.</li>
<li>내부 트랜잭션에 해당 옵션을 설정하면 <code>conn0</code> 이 아닌 <code>conn1</code>, 즉 다른 커넥션을 사용하여 트랜잭션을 시작한다.<ul>
<li><code>isNewTransaction()=true</code> 가 된다.</li>
</ul>
</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/9c8608ed-0cca-41c0-9e4a-313e35ecb4b5/image.png" width="700">

<h2 id="다양한-전파-옵션">다양한 전파 옵션</h2>
<ul>
<li><code>isolation</code>, <code>timeout</code>, <code>readOnly</code> 는 트랜잭션이 처음 시작될 때만 적용된다.<ul>
<li>트랜잭션에 참여하는 경우에는 적용되지 않는다.</li>
</ul>
</li>
<li>실무에서는 대부분 <code>REQUIRED</code> - Default 옵션을 사용하고 아주 가끔 <code>REQUIRES_NEW</code> 를 사용하고 나머지는 거의 사용하지 않는다.<ul>
<li>나머지 옵션은 PDF 참고</li>
</ul>
</li>
</ul>
<h1 id="✔️-전파-활용">✔️ 전파 활용</h1>
<h3 id="코드-1">코드</h3>
<pre><code class="language-java">@Slf4j
@SpringBootTest
class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Autowired LogRepository logRepository;

    /**
     * MemberService    @Transactional:OFF
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:OFF
     */

    @Test
    void outerTxOff_success(){
        String username = &quot;outerTxOff_success&quot;;

        memberService.joinV1(username);

        Assertions.assertTrue(memberRepository.find(username).isPresent());
        Assertions.assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService    @Transactional:OFF
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:ON Exception
     */

    @Test
    void outerTxOff_fail(){
        String username = &quot;로그예외_outerTxOff_fail&quot;;
        assertThatThrownBy(() -&gt; memberService.joinV1(username))
                .isInstanceOf(RuntimeException.class);

        Assertions.assertTrue(memberRepository.find(username).isPresent());
        Assertions.assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService    @Transactional:ON
     * MemberRepository @Transactional:OFF
     * MemberRepository @Transactional:OFF
     */

    @Test
    void singleTx(){
        String username = &quot;singleTx&quot;;
        memberService.joinV1(username);

        Assertions.assertTrue(memberRepository.find(username).isPresent());
        Assertions.assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService    @Transactional:ON
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:ON
     */

    @Test
    void outerTxOn_success(){
        String username = &quot;outerTxOn_success&quot;;
        memberService.joinV1(username);

        Assertions.assertTrue(memberRepository.find(username).isPresent());
        Assertions.assertTrue(logRepository.find(username).isPresent());
    }

    /**
     * MemberService    @Transactional:ON
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:ON Exception
     */

    @Test
    void outerTxOn_fail(){
        String username = &quot;로그예외_outerTxOn_fail&quot;;
        assertThatThrownBy(() -&gt; memberService.joinV1(username))
                .isInstanceOf(RuntimeException.class);

        Assertions.assertTrue(memberRepository.find(username).isEmpty());
        Assertions.assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService    @Transactional:ON
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:ON Exception
     */

    @Test
    void recoverException_fail(){
        String username = &quot;로그예외_recoverException_fail&quot;;
        assertThatThrownBy(() -&gt; memberService.joinV2(username))
                .isInstanceOf(UnexpectedRollbackException.class);

        Assertions.assertTrue(memberRepository.find(username).isEmpty());
        Assertions.assertTrue(logRepository.find(username).isEmpty());
    }

    /**
     * MemberService    @Transactional:ON
     * MemberRepository @Transactional:ON
     * MemberRepository @Transactional:ON(REQUIRES_NEW) Exception
     */

    @Test
    void recoverException_success(){
        String username = &quot;로그예외_recoverException_success&quot;;
        memberService.joinV2(username);

        Assertions.assertTrue(memberRepository.find(username).isPresent());
        Assertions.assertTrue(logRepository.find(username).isEmpty());
    }

}</code></pre>
<h2 id="outertxon_fail">outerTxOn_fail</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/fed1c466-6ed1-4b92-8609-1a5ce9e8335c/image.png" width="700">

<ul>
<li>이 경우 트랜잭션C에서 rollbackOnly를 설정하지만 트랜잭션A까지 예외가 잡히지 않고 던져졌기 때문에 &lt;<strong>트랜잭션 매니저에 커밋을 요청한 후 rollBackOnly를 확인 후 롤백을 실행하는 것&gt;이 아니라 트랜잭션 매니저에 롤백을 요청한다.</strong><ul>
<li>이 경우에는 rollBackOnly 설정을 참고하지 않는다.</li>
</ul>
</li>
</ul>
<h2 id="recoverexception_fail">recoverException_fail</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/dda70e0d-7ab7-47e8-aed6-12cafa9e486e/image.png" width="700">

<ul>
<li>이 경우 MemberService에서 예외를 처리하기 때문에 정상 흐름으로 돌아간다.</li>
<li>논리 트랜잭션A는 트랜잭션 매니저에 커밋을 요청하고 트랜잭션 매니저는 rollbackOnly 설정을 참고한다.<ul>
<li>rollBackOnly가 True이기 때문에 롤백을 하고 <code>UnexpectedRollbackException</code> 을 클라이언트에게 던진다.</li>
</ul>
</li>
</ul>
<h2 id="recoverexception_success---requires_new">recoverException_success - REQUIRES_NEW</h2>
<ul>
<li>회원 가입을 시도한 로그를 남기는데 실패하더라도 회원 가입은 유지되어야 할 경우</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/691f8960-bcc8-4581-928b-2db0652734cd/image.png" width="700">

<ul>
<li>Propagation.REQUIRES_NEW를 설정할 경우 트랜잭션이 기존 물리 트랜잭션에 참여하는 것이 아니라 신규 물리 트랜잭션이 시작된다.<ul>
<li>그렇기 때문에 트랜잭션C에서 롤백이 되면 해당 트랜잭션은 트랙잭션A와는 별개로 종료된다.</li>
<li>트랜잭션은 분리시켰지만 발생한 런타임예외는 밖으로 던져진다.<ul>
<li>하지만 MemberService에서 해당 예외를 잡기 때문에 정상흐름으로 돌아온다.</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 배치와 스케줄러의 차이]]></title>
            <link>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98%EC%99%80-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98%EC%99%80-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%9F%AC%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Thu, 01 Jun 2023 00:57:24 GMT</pubDate>
            <description><![CDATA[<h1 id="✔️-배치란">✔️ 배치란?</h1>
<ul>
<li>사용자와의 상호작용 없이 여러 작업을 미리 정해진 순서에 따라 중단 없이 처리하는 것</li>
<li>배치의 특징<ul>
<li>대용량 데이터</li>
<li>자동화</li>
<li>견고성: 잘못된 데이터를 충돌/중단 없이 처리할 수 있어야 합니다.</li>
<li>신뢰성: 로깅, 알림 등을 통해 무엇이 잘못되었는지를 추적할 수 있어야 합니다.</li>
<li>성능: 지정한 시간 안에 처리를 완료하거나 동시에 실행되는 다른 프로그램을 방해하지 않아야 합니다.</li>
</ul>
</li>
</ul>
<h2 id="스프링-배치-구성">스프링 배치 구성</h2>
<h3 id="배치-플로우">배치 플로우</h3>
<ul>
<li>읽기(read) : 데이터 저장소(일반적으로 데이터베이스)에서 특정 데이터 레코드를 읽습니다.</li>
<li>처리(processing) : 원하는 방식으로 데이터 가공/처리 합니다.</li>
<li>쓰기(write) : 수정된 데이터를 다시 저장소(데이터베이스)에 저장합니다.</li>
</ul>
<blockquote>
<p>Job과 Step은 1:M
Step과 ItemReader, ItemProcessor, ItemWriter 1:1</p>
</blockquote>
<img src="https://velog.velcdn.com/images/yeopju_5/post/938fac52-4aba-417d-9eda-898c37868405/image.png" width=700>

<h3 id="jobinstance">JobInstance</h3>
<ul>
<li>JobInstance는 배치 처리에서 Job이 실행될 때 하나의 Job 실행 단위입니다.
각각의 JobInstance는 하나의 JobExecution을 갖는 것은 아닙니다. 오늘 Job이 실행 했는데 실패했다면 다음날 동일한 JobInstance를 가지고 또 실행합니다.
Job 실행이 실패하면 JobInstance가 끝난것으로 간주하지 않기 때문입니다. 그렇다면 JobInstance는 어제 실패한 JobExecution과 오늘의 성공한 JobExecution 두 개를 가지게 됩니다. 즉 JobExecution 는 여러 개 가질 수 있습니다.</li>
</ul>
<h3 id="jobexecution">JobExecution</h3>
<ul>
<li>JobExecution은 JobIstance에 대한 한 번의 실행을 나타내는 객체입니다.
JobExecution은 JobInstance, 배치 실행 상태, 시작 시간, 끝난 시간, 실패했을 때 메시지 등의 정보를 담고 있습니다.</li>
</ul>
<h3 id="jobparameters">JobParameters</h3>
<ul>
<li>JobParameters는 Job이 실행될 때 필요한 파라미터들은 Map 타입으로 지정하는 객체 입니다.
JobParameters는 JobInstance를 구분하는 기준이 되기도 합니다.
JobParameters와 JobInstance는 1:1 관계입니다.</li>
</ul>
<h3 id="step">Step</h3>
<ul>
<li>Step은 실직적인 배치 처리를 정의하고 제어 하는데 필요한 모든 정보가 있는 도메인 객체입니다. Job을 처리하는 실질적인 단위로 쓰입니다.
모든 Job에는 1개 이상의 Step이 있어야 합니다.</li>
</ul>
<h3 id="stepexecution">StepExecution</h3>
<ul>
<li>Job에 JobExecution Job실행 정보가 있다면 Step에는 StepExecution이라는 Step 실행 정보를 담는 객체가 있습니다.</li>
</ul>
<h3 id="jobrepository">JobRepository</h3>
<ul>
<li>JobRepository는 배치 처리 정보를 담고 있는 매커니즘입니다. 어떤 Job이 실행되었으면 몇 번 실행되었고 언제 끝났는지 등 배치 처리에 대한 메타데이터를 저장합니다.</li>
<li>예를들어 Job 하나가 실행되면 JobRepository에서는 배치 실행에 관련된 정보를 담고 있는 도메인 JobExecution을 생성합니다.</li>
<li>JobRepository는 Step의 실행 정보를 담고 있는 StepExecution도 저장소에 저장하여 전체 메타데이터를 저장/관리하는 역할을 수행합니다.</li>
</ul>
<h3 id="joblauncher">JobLauncher</h3>
<ul>
<li>JobLauncher는 Job. JobParamerters와 함께 배치를 실행하는 인터페이스입니다.</li>
</ul>
<h3 id="itemreader">ItemReader</h3>
<ul>
<li>ItemReader는 Step의 대상이 되는 배치 데이터를 읽어오는 인터페이스입니다. File, Xml Db등 여러 타입의 데이터를 읽어올 수 있습니다.</li>
</ul>
<h3 id="itemprocessor">ItemProcessor</h3>
<ul>
<li>ItemProcessor는 ItemReader로 읽어 온 배치 데이터를 변환하는 역할을 수행합니다. 이것을 분리하는 이유는 다음과 같습니다.</li>
<li>비즈니스 로직의 분리 : ItemWriter는 저장 수행하고, ItemProcessor는 로직 처리만 수행해 역할을 명확하게 분리합니다.
읽어온 배치 데이터와 씌여질 데이터의 타입이 다를 경우에 대응할 수 있기 때문입니다.</li>
</ul>
<h3 id="itemwriter">ItemWriter</h3>
<ul>
<li>ItemWriter는 배치 데이터를 저장합니다. 일반적으로 DB나 파일에 저장합니다.</li>
<li>ItemWriter도 ItemReader와 비슷한 방식을 구현합니다. 제네릭으로 원하는 타입을 받고 write() 메서드는 List를 사용해서 저장한 타입의 리스트를 매게변수로 받습니다.</li>
</ul>
<h1 id="✔️-스프링-배치를-이용한-휴먼-계정-처리">✔️ 스프링 배치를 이용한 휴먼 계정 처리</h1>
<pre><code class="language-java">@Configuration
@RequiredArgsConstructor
@Slf4j
public class InactiveUserJobConfiguration {

    private final UserRepository userRepository;
    private final StepBuilderFactory stepBuilderFactory;
    private final JobBuilderFactory jobBuilderFactory;

    @Bean
    public Job inactiveUserJob(JobBuilderFactory jobBuilderFactory, Step inactiveJobStep) {
        return jobBuilderFactory.get(&quot;inactiveUserJob&quot;)
            .preventRestart()
            .incrementer(new RunIdIncrementer())
            .start(inactiveJobStep)
            .build();
    }

    @Bean
    public Step inactiveJobStep(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get(&quot;inactiveUserStep&quot;)
            .&lt;User, User&gt; chunk(10)
            .reader(inactiveUserReader())
            .processor(inactiveUserProcessor())
            .writer(inactiveUserWriter())
            .build();
    }

    @Bean
    @StepScope
    public QueueItemReader&lt;User&gt; inactiveUserReader() {
        List&lt;User&gt; targetUsers =
            userRepository.findBylastModifiedAtBeforeAndStatusEquals(
                LocalDateTime.now().minusYears(1),
                true);
        log.info(&quot;InActive Target User Count: &quot; + targetUsers.size());

        return new QueueItemReader&lt;&gt;(targetUsers);
    }

    @Bean
    @StepScope
    public ItemProcessor&lt;User, User&gt; inactiveUserProcessor() {
        return User::toInActive;
    }

    @Bean
    @StepScope
    public ItemWriter&lt;User&gt; inactiveUserWriter() {
        return (userRepository::saveAll);
    }
}</code></pre>
<ul>
<li><p>스프링 배치를 사용하기 위해서는 DB에 배치 처리를 위한 테이블들이 필요합니다.</p>
</li>
<li><p>schema.mysql.sql 파일을 통해 테이블 생성 후 실행 가능
<a href="https://sic-dev.tistory.com/68">Spring Batch 개발기 ( 4. MetaData테이블 )</a></p>
</li>
<li><p>이후 스케줄러를 적용하여 ApplicationContext에 있는 Job bean을 가져와 JobLauncher로 실행시킬 수 있습니다. </p>
</li>
</ul>
<h1 id="✔️-스케쥴러란">✔️ 스케쥴러란?</h1>
<ul>
<li>특정한 시간에 등록한 작업을 자동으로 실행시키는 것</li>
<li>Spring Scheduler, Quartz</li>
</ul>
<h3 id="휴먼-계정-처리---스케쥴러-선택-이유">휴먼 계정 처리 - 스케쥴러 선택 이유</h3>
<ul>
<li>스케쥴러는 한 번에 많은 양의 데이터를 처리하려면 성능 이슈가 발생할 수 있기 때문에 대용량 데이터 처리에는 부적합합니다. 하지만 현재 서비스는 출시 직후 예상 유저가 약 200명이기 때문에 간단한 스케쥴링 작업에 적합합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌱 스프링 MVC 2]]></title>
            <link>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-2</link>
            <guid>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-2</guid>
            <pubDate>Wed, 31 May 2023 00:11:53 GMT</pubDate>
            <description><![CDATA[<h1 id="✔️-타임리프-기본기능">✔️ 타임리프 기본기능</h1>
<h3 id="컨텐츠안에-내용-바로-삽입하기---">컨텐츠안에 내용 바로 삽입하기 - [[…]]</h3>
<pre><code class="language-html">&lt;ul&gt;
  &lt;li&gt;th:text 사용 &lt;span th:text=&quot;${data}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
  &lt;li&gt;컨텐츠 안에서 직접 출력하기 = [[${data}]]&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<h3 id="이스케이프---thutext---">이스케이프 - th:utext  / [(…)]</h3>
<p>문자를 태그로 변환하지 않고 문자 그대로 출력</p>
<ul>
<li>th: text : 이스케이프   VS   th: utext: 언이스케이프</li>
<li>[[…]] : 이스케이프  VS   [(…)] : 언이스케이프</li>
</ul>
<p><strong>※</strong> 이스케이프를 기본으로 하고 꼭 필요할 때만 언이스케이프 사용</p>
<h3 id="지역변수---thwith">지역변수 - th:with</h3>
<pre><code class="language-html">&lt;div th:with=&quot;first=${users[0]}&quot;&gt;
  &lt;p&gt;처음 사람의 이름은 &lt;span th:text=&quot;${first.username}&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;</code></pre>
<h3 id="param-session-spring-bean">Param, Session, Spring Bean</h3>
<ul>
<li>Thymeleaf 에서 지원하는 이름으로 바로 꺼내쓸 수 있다.</li>
</ul>
<pre><code class="language-html">&lt;li&gt;Request Parameter = &lt;span th:text=&quot;${param.paramData}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;session = &lt;span th:text=&quot;${session.sessionData}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;spring bean = &lt;span th:text=&quot;${@helloBean.hello(&#39;Spring!&#39;)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;</code></pre>
<h3 id="url">URL</h3>
<pre><code class="language-html">&lt;li&gt;&lt;a th:href=&quot;@{/hello}&quot;&gt;basic url&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a th:href=&quot;@{/hello(param1=${param1}, param2=${param2})}&quot;&gt;hello query param&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a th:href=&quot;@{/hello/{param1}/{param2}(param1=${param1}, param2=${param2})}&quot;&gt;path variable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a th:href=&quot;@{/hello/{param1}(param1=${param1}, param2=${param2})}&quot;&gt;path variable + query parameter&lt;/a&gt;&lt;/li&gt;</code></pre>
<h3 id="리터럴---작은-따옴표">리터럴 - 작은 따옴표</h3>
<ul>
<li>소스코드 상의 고정된 값을 말하는 용어</li>
<li>타임리프에서 리터럴은 작은 따옴표로 감싸야한다.</li>
</ul>
<pre><code class="language-html">&lt;li&gt;&#39;hello&#39; + &#39; world!&#39; = &lt;span th:text=&quot;&#39;hello&#39; + &#39; world!&#39;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
 &lt;li&gt;&#39;hello world!&#39; = &lt;span th:text=&quot;&#39;hello world!&#39;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
 &lt;li&gt;&#39;hello &#39; + ${data} = &lt;span th:text=&quot;&#39;hello &#39; + ${data}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
 &lt;li&gt;리터럴 대체 |hello ${data}| = &lt;span th:text=&quot;|hello ${data}|&quot;&gt;&lt;/span&gt;&lt;/li&gt;</code></pre>
<h3 id="연산---no-operation">연산 - No-Operation</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-html">  &lt;body&gt;
  &lt;ul&gt;
    &lt;li&gt;산술 연산
      &lt;ul&gt;
        &lt;li&gt;10 + 2 = &lt;span th:text=&quot;10 + 2&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;10 % 2 == 0 = &lt;span th:text=&quot;10 % 2 == 0&quot;&gt;&lt;/span&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;비교 연산
      &lt;ul&gt;
        &lt;li&gt;1 &gt; 10 = &lt;span th:text=&quot;1 &amp;gt; 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;1 gt 10 = &lt;span th:text=&quot;1 gt 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;1 &gt;= 10 = &lt;span th:text=&quot;1 &gt;= 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;1 ge 10 = &lt;span th:text=&quot;1 ge 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;1 == 10 = &lt;span th:text=&quot;1 == 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;1 != 10 = &lt;span th:text=&quot;1 != 10&quot;&gt;&lt;/span&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;조건식
      &lt;ul&gt;
        &lt;li&gt;(10 % 2 == 0)? &#39;짝수&#39;:&#39;홀수&#39; = &lt;span th:text=&quot;(10 % 2 == 0)?
  &#39;짝수&#39;:&#39;홀수&#39;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;Elvis 연산자
      &lt;ul&gt;
        &lt;li&gt;${data}?: &#39;데이터가 없습니다.&#39; = &lt;span th:text=&quot;${data}?: &#39;데이터가없습니다.&#39;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;${nullData}?: &#39;데이터가 없습니다.&#39; = &lt;span th:text=&quot;${nullData}?: &#39;데이터가 없습니다.&#39;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;No-Operation
      &lt;ul&gt;
        &lt;li&gt;${data}?: _ = &lt;span th:text=&quot;${data}?: _&quot;&gt;데이터가 없습니다.&lt;/span&gt;&lt;/li&gt;
        &lt;li&gt;${nullData}?: _ = &lt;span th:text=&quot;${nullData}?: _&quot;&gt;데이터가없습니다.&lt;/span&gt;&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;/body&gt;</code></pre>
</li>
<li><p><strong>No-Operation</strong></p>
<ul>
<li>_ 인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다. 이것을 잘 사용하면 HTML
의 내용 그대로 활용할 수 있다. 마지막 예를 보면 데이터가 없습니다. 부분이 그대로 출력된다</li>
</ul>
</li>
</ul>
<h3 id="속성-값-설정---추가-checked">속성 값 설정 - 추가, Checked</h3>
<ul>
<li><p>속성 추가</p>
<pre><code class="language-html">  - th:attrappend = &lt;input type=&quot;text&quot; class=&quot;text&quot; th:attrappend=&quot;class=&#39;large&#39;&quot; /&gt;&lt;br/&gt;
  - th:attrprepend = &lt;input type=&quot;text&quot; class=&quot;text&quot; th:attrprepend=&quot;class=&#39;large &#39;&quot; /&gt;&lt;br/&gt;
  - th:classappend = &lt;input type=&quot;text&quot; class=&quot;text&quot; th:classappend=&quot;large&quot; /&gt;&lt;br/&gt;</code></pre>
</li>
<li><p>Checked 속성</p>
<ul>
<li><p>기존의 checked는 false로 설정해도 무조건 체크가 되어져서 렌더링된다. 하지만 th:ckecked를 사용하면 false로 설정할 경우 checked 설정 자체를 없애준다.</p>
<p>```html</p>
</li>
<li><p>checked o <input type="checkbox" name="active" th:checked="true" /><br/></p>
</li>
<li><p>checked x <input type="checkbox" name="active" th:checked="false" /><br/></p>
<pre><code>
</code></pre></li>
</ul>
</li>
</ul>
<h3 id="반복---theach--___stat">반복 - th:each , ___Stat</h3>
<ul>
<li><p>___Stat</p>
<ul>
<li><p>index : 0부터 시작하는 값</p>
</li>
<li><p>count : 1부터 시작하는 값</p>
</li>
<li><p>size : 전체 사이즈</p>
</li>
<li><p>even , odd : 홀수, 짝수 여부( boolean )</p>
</li>
<li><p>first , last :처음, 마지막 여부( boolean )</p>
</li>
<li><p>current : 현재 객체</p>
<pre><code class="language-html">&lt;tr th:each=&quot;user, userStat : ${users}&quot;&gt;
&lt;td th:text=&quot;${userStat.count}&quot;&gt;username&lt;/td&gt;
&lt;td th:text=&quot;${user.username}&quot;&gt;username&lt;/td&gt;
&lt;td th:text=&quot;${user.age}&quot;&gt;0&lt;/td&gt;
&lt;td&gt;
index = &lt;span th:text=&quot;${userStat.index}&quot;&gt;&lt;/span&gt;
count = &lt;span th:text=&quot;${userStat.count}&quot;&gt;&lt;/span&gt;
size = &lt;span th:text=&quot;${userStat.size}&quot;&gt;&lt;/span&gt;
even? = &lt;span th:text=&quot;${userStat.even}&quot;&gt;&lt;/span&gt;
odd? = &lt;span th:text=&quot;${userStat.odd}&quot;&gt;&lt;/span&gt;
first? = &lt;span th:text=&quot;${userStat.first}&quot;&gt;&lt;/span&gt;
last? = &lt;span th:text=&quot;${userStat.last}&quot;&gt;&lt;/span&gt;
current = &lt;span th:text=&quot;${userStat.current}&quot;&gt;&lt;/span&gt;
&lt;/td&gt;
&lt;/tr&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="조건부---if-unless-switchcase">조건부 - if, unless, switch/case</h3>
<ul>
<li><p>조건을 만족하지 못할 시 해당 태그 전체가 삭제된다.</p>
<pre><code class="language-html">  &lt;span th:text=&quot;&#39;미성년자&#39;&quot; th:if=&quot;${user.age lt 20}&quot;&gt;&lt;/span&gt;
  &lt;span th:text=&quot;&#39;미성년자&#39;&quot; th:unless=&quot;${user.age ge 20}&quot;&gt;&lt;/span&gt;

  &lt;td th:switch=&quot;${user.age}&quot;&gt;
       &lt;span th:case=&quot;10&quot;&gt;10살&lt;/span&gt;
       &lt;span th:case=&quot;20&quot;&gt;20살&lt;/span&gt;
       &lt;span th:case=&quot;*&quot;&gt;기타&lt;/span&gt;
   &lt;/td&gt;</code></pre>
</li>
</ul>
<h3 id="주석">주석</h3>
<ul>
<li><p>HTML 주석</p>
<pre><code class="language-html">
  &lt;!--
  &lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
  --&gt;</code></pre>
</li>
</ul>
<ul>
<li><p>타임리프 파서 주석</p>
<pre><code class="language-html">  &lt;!--/* [[${data}]] */--&gt;
  &lt;!--/*--&gt;
  &lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
  &lt;!--*/--&gt;</code></pre>
</li>
</ul>
<ul>
<li><p>타임리프 프로토타입 주석 - 파일 자체를 열때는 주석처리,  타임리프를 사용해서 렌더링되는 경우에는 주석 처리 X</p>
<pre><code class="language-html">  &lt;!--/*/
  &lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
  /*/--&gt;</code></pre>
</li>
</ul>
<h3 id="블록---타임리프-자체-태그-thblock">블록 - 타임리프 자체 태그 <a href="th:block">th:block</a></h3>
<h3 id="자바스크립트-인라인">자바스크립트 인라인</h3>
<ul>
<li>자바스크립트 인라인 사용전<ul>
<li><code>&lt;script&gt;~~~&lt;/script&gt;</code></li>
</ul>
</li>
<li>사용<ul>
<li><code>&lt;script th:inline=”javascript”&gt;~~~&lt;/script&gt;</code></li>
</ul>
</li>
</ul>
<h3 id="템플릿-조각-컴포넌트">템플릿 조각 (컴포넌트)</h3>
<ul>
<li><p>컴포넌트 선언 코드</p>
<pre><code class="language-html">  &lt;footer th:fragment=&quot;copyParam (param1, param2)&quot;&gt;
   &lt;p&gt;파라미터 자리 입니다.&lt;/p&gt;
   &lt;p th:text=&quot;${param1}&quot;&gt;&lt;/p&gt;
   &lt;p th:text=&quot;${param2}&quot;&gt;&lt;/p&gt;
  &lt;/footer&gt;</code></pre>
</li>
<li><p>컴포넌트 사용 코드</p>
<ul>
<li><p>replace 대신 insert 사용 시 div 태그 내부로 삽입</p>
<pre><code class="language-html">&lt;h1&gt;파라미터 사용&lt;/h1&gt;
&lt;div th:replace=&quot;~{template/fragment/footer :: copyParam (&#39;데이터1&#39;, &#39;데이터2&#39;)}&quot;&gt;&lt;/div&gt;</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="템플릿-레이아웃">템플릿 레이아웃</h3>
<ul>
<li><p>레이아웃 선언 코드</p>
<pre><code class="language-html">  &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
  &lt;head th:fragment=&quot;common_header(title,links)&quot;&gt;
   &lt;title th:replace=&quot;${title}&quot;&gt;레이아웃 타이틀&lt;/title&gt;
   &lt;!-- 공통 --&gt;
   &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;all&quot; th:href=&quot;@{/css/
  awesomeapp.css}&quot;&gt;
   &lt;link rel=&quot;shortcut icon&quot; th:href=&quot;@{/images/favicon.ico}&quot;&gt;
   &lt;script type=&quot;text/javascript&quot; th:src=&quot;@{/sh/scripts/codebase.js}&quot;&gt;&lt;/
  script&gt;
   &lt;!-- 추가 --&gt;
   &lt;th:block th:replace=&quot;${links}&quot; /&gt;
  &lt;/head&gt;</code></pre>
</li>
<li><p>레이아웃 사용 코드</p>
<pre><code class="language-html">  &lt;!DOCTYPE html&gt;
  &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
  &lt;head th:replace=&quot;template/layout/base :: common_header(~{::title},~{::link})&quot;&gt;
   &lt;title&gt;메인 타이틀&lt;/title&gt;
   &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/css/bootstrap.min.css}&quot;&gt;
   &lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/themes/smoothness/jquery-ui.css}&quot;&gt;
  &lt;/head&gt;
  &lt;body&gt;
  메인 컨텐츠
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
</li>
<li><p>common_header(<del>{::title},</del>{::link}) 이 부분이 핵심이다.</p>
<ul>
<li>::title 은 현재 페이지의 title 태그들을 전달한다.</li>
<li>::link 는 현재 페이지의 link 태그들을 전달한다.</li>
</ul>
</li>
</ul>
<h1 id="✔️-타임리프---스프링-통합과-폼">✔️ 타임리프 - 스프링 통합과 폼</h1>
<h2 id="입력-폼-처리">입력 폼 처리</h2>
<h3 id="thobject"><code>th:object</code></h3>
<ul>
<li>커맨드 객체를 지정한다</li>
<li><code>*{...}</code> : 선택 변수식이라고 하며 <code>th:object</code> 에서 선택한 객체에 접근한다.<ul>
<li><code>*{itemName}</code> == <code>${item.itemName}</code></li>
</ul>
</li>
</ul>
<h3 id="thfield"><code>th:field</code></h3>
<ul>
<li>HTML 태그의 Id, name, value 속성을 자동으로 처리해준다.</li>
</ul>
<h2 id="체크박스">체크박스</h2>
<h3 id="단일1---히든-필드-추가">단일1 - 히든 필드 추가</h3>
<ul>
<li>타임리프를 사용하지 않고 HTML 태그만을 사용해서 체크박스를 구현하면 체크를 했을 때는 True값이 정상적으로 넘어오지만 체크를 하지 않았을 경우에는 아무것도 넘어오지 않아 Null값으로 처리된다.</li>
<li><strong>해결방법</strong><ul>
<li>히든 필드 추가<ul>
<li><code>&lt;input type=&quot;hidden&quot; name=&quot;_open&quot; value=&quot;on&quot;&gt;</code></li>
<li>위와 같이 히든 필드를 추가하면 체크를 하지 않았을 경우 False 값이 넘어오게 된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="단일2---thfield">단일2 - th:field</h3>
<ul>
<li>th:field를 사용하면 <strong>체크박스의 히든필드 부분을 자동으로 생성해서 처리해준다.</strong></li>
<li>체크박스에서 체크를 해서 저장한 후 값을 불러와서 사용할 때는 값이 True이면 Checked 속성을 삽입해주는 것을 개발자가 직접 처리해야한다.<ul>
<li>하지만 타임리프의 th:field를 사용하면 값이 True인 경우 체크를 자동으로 처리해준다.</li>
</ul>
</li>
</ul>
<h3 id="멀티">멀티</h3>
<pre><code class="language-html">&lt;!-- multi checkbox --&gt;
        &lt;div&gt;
            &lt;div&gt;등록 지역&lt;/div&gt;
            &lt;div th:each=&quot;region : ${regions}&quot; class=&quot;form-check form-check-inline&quot;&gt;
                &lt;input type=&quot;checkbox&quot; th:field=&quot;*{regions}&quot; th:value=&quot;${region.key}&quot;
                       class=&quot;form-check-input&quot;&gt;
                &lt;label th:for=&quot;${#ids.prev(&#39;regions&#39;)}&quot;
                       th:text=&quot;${region.value}&quot; class=&quot;form-check-label&quot;&gt;서울&lt;/label&gt;
            &lt;/div&gt;
        &lt;/div&gt;</code></pre>
<ul>
<li><p><code>th:for=&quot;${#ids.prev(&#39;regions&#39;)}&quot;</code></p>
<ul>
<li><p>each 루프 안에서 id값 추적하는 방법</p>
</li>
<li><p>each 루프 안에서 타임리프는 field값으로 넣은 값 뒤에 숫자를 붙여 임의로 id를 선언한다.</p>
<h3 id="modelattribute의-특별한-사용법">@ModelAttribute의 특별한 사용법</h3>
</li>
<li><p>등록폼, 상세화면, 수정폼에서 모두 체크박스를 보여주기 위해서는 각각의 컨트롤러에서 <code>model.addAttribute(...)</code> 를 사용해서 데이터를 반복해서 넣어주어야한다.</p>
<pre><code class="language-html">          @ModelAttribute(&quot;regions&quot;)
      public Map&lt;String, String&gt; regions(){
          Map&lt;String, String&gt; regions = new LinkedHashMap&lt;&gt;();
          regions.put(&quot;SEOUL&quot;, &quot;서울&quot;);
          regions.put(&quot;BUSAN&quot;, &quot;부산&quot;);
          regions.put(&quot;JEJU&quot;, &quot;제주&quot;);
          return regions;
      }</code></pre>
<ul>
<li>이렇게 하면 컨트롤러를 요청할 때 regions에서 반환한 값이 자동으로 model에 담기게 된다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="라디오-버튼">라디오 버튼</h2>
<pre><code class="language-html">&lt;!-- radio button --&gt;
        &lt;div&gt;
            &lt;div&gt;상품 종류&lt;/div&gt;
            &lt;div th:each=&quot;type : ${itemTypes}&quot; class=&quot;form-check form-check-inline&quot;&gt;
                &lt;input type=&quot;radio&quot; th:field=&quot;*{itemType}&quot; th:value=&quot;${type.name()}&quot;
                       class=&quot;form-check-input&quot;&gt;
                &lt;label th:for=&quot;${#ids.prev(&#39;itemType&#39;)}&quot; th:text=&quot;${type.description}&quot;
                       class=&quot;form-check-label&quot;&gt;
                    BOOK
                &lt;/label&gt;
            &lt;/div&gt;
        &lt;/div&gt;</code></pre>
<h3 id="타임리프에서-enum-직접-접근">타임리프에서 ENUM 직접 접근</h3>
<ul>
<li>현재 상태: @ModelAttribute를 통해서 모델에 ENUM을 담아 전달</li>
<li>직접 접근<ul>
<li><code>&lt;div th:each=&quot;type: ${T(hello.itemservice.domain.item.ItemType).values()}&gt;</code></li>
</ul>
</li>
</ul>
<h2 id="셀렉트-박스">셀렉트 박스</h2>
<pre><code class="language-html">&lt;!-- SELECT --&gt;
    &lt;div&gt;
        &lt;div&gt;배송 방식&lt;/div&gt;
        &lt;select th:field=&quot;${item.deliveryCode}&quot; class=&quot;form-select&quot;&gt;
            &lt;option value=&quot;&quot;&gt;==배송 방식 선택==&lt;/option&gt;
            &lt;option th:each=&quot;deliveryCode : ${deliveryCodes}&quot; th:value=&quot;${deliveryCode.code}&quot; disabled
                    th:text=&quot;${deliveryCode.displayName}&quot;&gt;FAST&lt;/option&gt;
        &lt;/select&gt;
    &lt;/div&gt;</code></pre>
<h1 id="✔️-메시지-국제화">✔️ 메시지, 국제화</h1>
<h2 id="메시지">메시지</h2>
<ul>
<li>모든 파일에서 ‘상품명’이라는 단어를 모두 ‘상품이름’으로 고쳐야 할 겨우 모든화면을 찾아가면서 변경하기에는 너무 오랜 시간이 걸린다.</li>
<li><strong>이런 다양한 메시지를 한 곳(별도의 파일)에서 관리하도록 하는 기능</strong>을 메시지 기능이라고 한다.</li>
</ul>
<h2 id="국제화">국제화</h2>
<ul>
<li>영어권 국가에서 들어오는 클라이언트에게는 영어, 한국에서 들어오면 한국어를 제공</li>
<li>메시지 파일을 언어별로 생성해서 국제화하다.</li>
<li>인식 방법<ul>
<li>HTTP accpt-language 헤더 값을 사용</li>
<li>사용자가 직접 언어를 선택 → 쿠키 처리</li>
</ul>
</li>
</ul>
<h2 id="스프링이-제공하는-메시지-국제화">스프링이 제공하는 메시지, 국제화</h2>
<h3 id="테스트-사용">테스트 사용</h3>
<ul>
<li><p>resources 안에 <a href="http://message.properties">message.properties</a> → 디폴트 파일</p>
<p>  message_en.properties → Locale.ENGLISH</p>
</li>
<li><p>코드</p>
<pre><code class="language-java">  @SpringBootTest
  public class MessageSourceTest {

      @Autowired
      MessageSource ms;

      @Test
      void helloMessage(){
          String result = ms.getMessage(&quot;hello&quot;, null, null);
  assertThat(result).isEqualTo(&quot;안녕&quot;);
      }

      @Test
      void notFoundMessageCode(){
  assertThatThrownBy(() -&gt; ms.getMessage(&quot;no_code&quot;, null, null))
                  .isInstanceOf(NoSuchMessageException.class);
          System.out.println(Locale.getDefault());
      }

      @Test
      void notFoundMessageCodeDefaultMessage(){
          String result = ms.getMessage(&quot;no_code&quot;, null, &quot;기본 메시지&quot;, null);
  assertThat(result).isEqualTo(&quot;기본 메시지&quot;);
      }

      @Test
      void argumentMessage(){
          String result = ms.getMessage(&quot;hello.name&quot;, new Object[]{&quot;Spring&quot;}, null);
  assertThat(result).isEqualTo(&quot;안녕 Spring&quot;);
      }

      @Test
      void defaultLang(){
  assertThat(ms.getMessage(&quot;hello&quot;, null, null)).isEqualTo(&quot;안녕&quot;);
  assertThat(ms.getMessage(&quot;hello&quot;, null, Locale.KOREA)).isEqualTo(&quot;안녕&quot;);
      }

      @Test
      void enLang(){
  assertThat(ms.getMessage(&quot;hello&quot;, null, Locale.ENGLISH)).isEqualTo(&quot;hello&quot;);
      }

  }</code></pre>
</li>
</ul>
<h3 id="애플리케이션-적용">애플리케이션 적용</h3>
<ul>
<li>메시지<ul>
<li><code>&lt;div th:text=&quot;#{label.item}&quot;&gt;&lt;/h2&gt;</code></li>
<li>인자: <code>&lt;p th:text=&quot;#{hello.name(${item.itemName})}&quot;&gt;&lt;/p&gt;</code></li>
</ul>
</li>
<li>국제화<ul>
<li>각 locale에 맞는 properties 파일이 있다면 스프링이 Accept-language 헤더값을 사용해서 자동으로 적용시켜준다.</li>
<li><strong>LocaleResolver</strong><ul>
<li>만약 Locale 선택 방식을 변경하려면 LocaleResolver의 구현체를 변경해서 쿠키나 세션 기반의 Locale 선택 기능을 사용할 수 있따.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="✔️-validation">✔️ Validation</h1>
<h2 id="v1---error-map">V1 - Error Map</h2>
<ul>
<li><p>컨트롤러 코드</p>
<ul>
<li><p>ModelAttribute Annotation으로 인해 item이 자동으로 model에 등록되기 때문에 return으로 리렌더링 되어도 기존에 입력했던 값이 그대로 유지된다.</p>
<pre><code class="language-java">@PostMapping(&quot;/add&quot;)
  public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {

      Map&lt;String, String&gt; errors = new HashMap&lt;&gt;();

      if (!StringUtils.hasText(item.getItemName())){
          errors.put(&quot;itemName&quot;, &quot;상품 이름은 필수입니다.&quot;);
      }
      if (item.getPrice() == null || item.getPrice() &lt; 1000 || item.getPrice() &gt; 1000000){
          errors.put(&quot;price&quot;, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다&quot;);
      }
      if(item.getQuantity() == null || item.getQuantity() &gt; 9999){
          errors.put(&quot;quantity&quot;, &quot;수량은 9,999개까지 허용됩니다.&quot;);
      }
      if(item.getPrice() != null &amp;&amp; item.getQuantity() != null){
          int resultPrice = item.getPrice() * item.getQuantity();
          if (resultPrice &lt; 10000) {
              errors.put(&quot;globalError&quot;, &quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice);
          }
      }

      if(!errors.isEmpty()){
          model.addAttribute(&quot;errors&quot;, errors);
          return &quot;validation/v1/addForm&quot;;
      }

      Item savedItem = itemRepository.save(item);
      redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
      redirectAttributes.addAttribute(&quot;status&quot;, true);
      return &quot;redirect:/validation/v1/items/{itemId}&quot;;
  }</code></pre>
</li>
</ul>
</li>
<li><p>HTML 코드</p>
<ul>
<li><p>errors 다음에 있는 ? 연산자</p>
<ul>
<li>?를 붙이지 않고 실행할 때 errors가 null값이라면 null값의 요소를 찾는 것이기 때문에 NullpointException 오류가 난다.</li>
<li>하지만 ?를 사용하면 errors가 null값이라면 바로 null을 반환한다.</li>
</ul>
<pre><code class="language-html">&lt;form action=&quot;item.html&quot; th:action th:object=&quot;${item}&quot; method=&quot;post&quot;&gt;

      &lt;div th:if=&quot;${errors?.containsKey(&#39;globalError&#39;)}&quot;&gt;
          &lt;p th:class=&quot;field-error&quot; th:text=&quot;${errors[&#39;globalError&#39;]}&quot;&gt;전체 오류 메시지&lt;/p&gt;
      &lt;/div&gt;

      &lt;div&gt;
          &lt;label for=&quot;itemName&quot; th:text=&quot;#{label.item.itemName}&quot;&gt;상품명&lt;/label&gt;
          &lt;input type=&quot;text&quot; id=&quot;itemName&quot; th:field=&quot;*{itemName}&quot; class=&quot;form-control&quot; th:classappend=&quot;${errors?.containsKey(&#39;itemName&#39;)} ? &#39;field-error&#39; : _&quot; placeholder=&quot;이름을 입력하세요&quot;&gt;
          &lt;div th:class=&quot;field-error&quot; th:if=&quot;${errors?.containsKey(&#39;itemName&#39;)}&quot; th:text=&quot;${errors[&#39;itemName&#39;]}&quot;&gt;상품명 오류&lt;/div&gt;
      &lt;/div&gt;</code></pre>
</li>
</ul>
</li>
<li><p>남은 문제점</p>
<ul>
<li>뷰에 중복되는 코드가 많다</li>
<li>타입 오류 처리가 안된다.</li>
</ul>
</li>
</ul>
<h2 id="v2---bindingresult">V2 - BindingResult</h2>
<pre><code class="language-java">// FieldError
bindingResult.addError(new FieldError(&quot;item&quot;, &quot;price&quot;, item.getPrice(), false, null, null, &quot;가격은 1,000 ~ 1,000,000 까지 허용합니다&quot;));
// ObjectError
bindingResult.addError(new ObjectError(&quot;item&quot;, null, null,&quot;가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = &quot; + resultPrice));</code></pre>
<h3 id="bindingresult">BindingResult</h3>
<ul>
<li>인자에 <code>BindingResult bindingResult</code> 추가<ul>
<li>@ModelAttribute 다음에 위치해야한다.</li>
<li>Model에 자동으로 포함된다.</li>
</ul>
</li>
</ul>
<h3 id="fielderror"><strong>FieldError</strong></h3>
<pre><code class="language-java">bindingResult.addError(new FieldError(objectName(ModelAttribute 인스턴스 이름)
, field, rejectedValue, bindingFailure(타입 오류같은 바인딩 실패인지, 검증 실패인지 구분 값),
 codes(메시지 코드), arguments(메시지에 사용하는 인자) defaultMessage)</code></pre>
<ul>
<li><strong>FieldError를 사용할 때 잘못된 값을 입력하면 입력값이 유지되지 않는 이유</strong><ul>
<li>사용자의 데이터가 컨트롤러의 @ModelAttribute에 바인딩되는 시점에 에러가 발생하면 모델 객체에 사용자 입력 값을 유지하기 어렵다.</li>
<li>예를 들어서 가격에 숫자가 아닌 문자가 입력된다면 가격은 Integer 타입이므로 문자를 보관할 수 있는 방법이 없다.</li>
<li>그래서 오류가 발생한 경우 사용자 입력 값을 보관하는 별도의 방법이 필요한다.</li>
<li><strong>FieldError가 rejectedValue 속성을 사용해 해당 기능을 제공한다.</strong><ul>
<li>이 때 타임리프의 <strong>th:field</strong>는 정상 상황에서는 <strong>모델 객체의 값을 사용하고, 오류가 발생하면 FieldError에서 보관한 값을 사용</strong>해서 값을 출력함으로써 이 문제를 해결한다.</li>
</ul>
</li>
</ul>
</li>
<li><strong>GlobalError</strong><ul>
<li><code>bindingResult.addError(new ObjectError(objectName, defaultMessage)</code></li>
</ul>
</li>
</ul>
<h3 id="타임리프">타임리프</h3>
<ul>
<li><strong>#fields</strong><ul>
<li>#fields.hasGlobalError()</li>
<li>BindingResult가 제공하는 검증 오류가 접근할 수 있다.</li>
</ul>
</li>
<li><strong>th:errors</strong><ul>
<li>해당 필드에 오류가 있는 경우 태그 출력</li>
</ul>
</li>
<li><strong>th:errorclass</strong><ul>
<li>th:field에서 지정한 필드에 오류가 있으면 class정보를 추가한다.</li>
</ul>
</li>
</ul>
<p><strong>※</strong> @ModelAttribute에 바인딩 시 타입 오류가 발생하면?</p>
<ul>
<li>오류정보(FieldError)를 BindingResult에 담아서 컨트롤러를 정상 호출한다.</li>
</ul>
<h2 id="v3---errorsproperties">V3 - errors.properties</h2>
<ul>
<li><a href="http://errors.properties">errors.properties</a> 파일을 생성하고 <a href="http://application.properties">application.properties</a> 파일에 <code>spring.messages.basename=messages,errors</code> 를 추가한다.</li>
<li><a href="http://errors.properties">errors.properties</a> 파일에 있는 값에 따라 각각 code, argument에 값을 대입한다.</li>
</ul>
<pre><code class="language-java">// FieldError
bindingResult.addError(new FieldError(&quot;item&quot;, &quot;itemName&quot;, item.getItemName(), false, new String[]{&quot;required.item.itemName&quot;}, null, null));
// ObjectError
bindingResult.addError(new ObjectError(&quot;item&quot;, new String[]{&quot;totalPriceMin&quot;}, new Object[]{10000, resultPrice}, null));</code></pre>
<h2 id="v4---reject-rejectvalue">V4 - reject, rejectValue</h2>
<pre><code class="language-java">// FieldError , rejectValue
bindingResult.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null);
// ObjectError, reject
bindingResult.reject(&quot;totalPriceMin&quot;, new Object[]{10000, resultPrice}, null);</code></pre>
<ul>
<li><p>FieldError, ObjectError는 다루기가 번거롭다.</p>
</li>
<li><p>BindingResult는 이미 본인이 검증해야 할 객체인 target을 알고 있다.</p>
<p>  → 이를 활용해 rejectValue와, reject가 FieldError, ObjectError를 대체한다.</p>
</li>
<li><p>오류 코드를 축약해서 입력해도 잘 찾아서 출력한다.</p>
<ul>
<li><p><strong>MessageCodesResolver를 이용</strong>하기 때문</p>
<pre><code class="language-java">  MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
  String [] messageCodes = codesResolver.resolveMessageCodes(&quot;required&quot;, &quot;item&quot;);</code></pre>
<ul>
<li>기본 메시지 (코드) 생성 규칙</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code>        &gt; **객체 오류**
        객체 오류의 경우 다음 순서로 2가지 생성
        1.: code + &quot;.&quot; + object name
        2.: code
        예) 오류 코드: required, object name: item
        1. : required.item
        2. : required
        &gt; 

        &gt; **필드 오류**
        필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
        1.: code + &quot;.&quot; + object name + &quot;.&quot; + field
        2.: code + &quot;.&quot; + field
        3.: code + &quot;.&quot; + field type
        4.: code
        &gt; 

    - 동작방식
        - rejectValue(), reject() 는 내부에서 MessageCodesResolver를 사용한다.
        - **FieldError - rejectValue(”itemName”, “required”)**

            required.item.itemName
            required.itemName
            required.java.lang.String
            required

        - **ObjectError - reject(”totalPriceMin”)**

            totalPriceMin.item
            totalPriceMin</code></pre><h3 id="validationutils">ValidationUtils</h3>
<pre><code class="language-java">if (!StringUtils.hasText(item.getItemName())){
    bindingResult.rejectValue(&quot;itemName&quot;, &quot;required&quot;);
   }   // ==
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, &quot;itemName&quot;, &quot;required&quot;);</code></pre>
<h2 id="스프링이-직접-만든-오류-메시지-처리">스프링이 직접 만든 오류 메시지 처리</h2>
<h3 id="검증-오류-코드-종류">검증 오류 코드 종류</h3>
<ul>
<li><p>개발자가 직접 설정한 오류 코드 → rejectValue()를 직접 호출</p>
</li>
<li><p>스프링이 직접 검증 오류에 추가한 경우</p>
<ul>
<li><p>→ 로그 확인 → 메시지 코드 확인</p>
<p>  typeMismatch.item.price
  typeMismatch.price
  typeMismatch.java.lang.Integer
  typeMismatch</p>
<p>  다음과 같이 4가지 메시지 코드가 입력되어 있다.</p>
<p>  → <strong>스프링은 타입 오류가 발생하면 <code>typeMismatch</code> 라는 오류 코드를 사용</strong>하는데 이 코드가 MessageCodesResolver를 통하면서 4가지 메시지 코드가 생성된 것이다.</p>
</li>
</ul>
</li>
</ul>
<h2 id="v5---validator-분리">V5 - Validator 분리</h2>
<ul>
<li><p>컨트롤러 코드</p>
<pre><code class="language-java">          @PostMapping(&quot;/add&quot;)
      public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

          itemValidator.validate(item, bindingResult);

          if(bindingResult.hasErrors()){
              log.info(&quot;error={}&quot;, bindingResult);
              return &quot;validation/v2/addForm&quot;;
          }

          Item savedItem = itemRepository.save(item);
          redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
          redirectAttributes.addAttribute(&quot;status&quot;, true);
          return &quot;redirect:/validation/v2/items/{itemId}&quot;;
      }</code></pre>
</li>
</ul>
<p>컨트롤러 로직에서 처리하는 것이 너무 많다</p>
<ul>
<li><p>validator 클래스 생성</p>
<pre><code class="language-java">  @Component
  public class ItemValidator implements Validator {
      @Override
      public boolean supports(Class&lt;?&gt; clazz) {
          return Item.class.isAssignableFrom(clazz);
      }

      @Override
      public void validate(Object target, Errors errors) {
          Item item = (Item) target;

          ValidationUtils.rejectIfEmptyOrWhitespace(errors, &quot;itemName&quot;, &quot;required&quot;);

          if (item.getPrice() == null || item.getPrice() &lt; 1000 || item.getPrice() &gt; 1000000){
              errors.rejectValue(&quot;price&quot;, &quot;range&quot;, new Object[]{1000, 1000000}, null);
          }
          if(item.getQuantity() == null || item.getQuantity() &gt; 9999){
              errors.rejectValue(&quot;quantity&quot;, &quot;max&quot;, new Object[]{9999}, null);
          }
          if(item.getPrice() != null &amp;&amp; item.getQuantity() != null){
              int resultPrice = item.getPrice() * item.getQuantity();
              if (resultPrice &lt; 10000) {
                  errors.reject(&quot;totalPriceMin&quot;, new Object[]{10000, resultPrice}, null);
              }
          }
      }
  }</code></pre>
</li>
<li><p>validator를 컴포넌트 스캔했기 때문에 컨트롤러에서는 validator를 주입하고 사용하면 된다.</p>
</li>
</ul>
<h2 id="v6---validated-webdatabinder">V6 - @Validated, WebDataBinder</h2>
<ul>
<li>Validator 클래스에 Validator 인터페이스를 implement한 이유는 체계적으로 검증 기능을 도입하기 위해서이다.</li>
</ul>
<pre><code class="language-java">@InitBinder
    public void init(WebDataBinder dataBinder){
        dataBinder.addValidators(itemValidator);
    } </code></pre>
<ul>
<li>위와 같이 선언한 후에 검증을 하고자 하는 <strong>컨트롤러의 Argument로 @Validated Annotation을 추가하면 앞서 WebDataBinder에 등록한 검증기를 찾아서 실행한다.</strong></li>
<li>이 때 여러 검증기를 등록한다면 그 중 어떤 검증기가 실행되어야 할 지 구분하기 위해 <strong>supports 메서드</strong>가 사용된다.</li>
</ul>
<p>※ 글로벌 설정</p>
<pre><code class="language-java">@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
 public static void main(String[] args) {
     SpringApplication.run(ItemServiceApplication.class, args);
     }
     @Override
     public Validator getValidator() {
     return new ItemValidator();
     }
}</code></pre>
<h1 id="✔️-bean-validation">✔️ Bean Validation</h1>
<ul>
<li>build.gradle에 의존관계 추가<ul>
<li><code>implementation &#39;org.springframework.boot:spring-boot-starter-validation</code></li>
</ul>
</li>
</ul>
<blockquote>
<p>@NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
@NotNull : null 을 허용하지 않는다.
@Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
@Max(9999) : 최대 9999까지만 허용한다.</p>
</blockquote>
<h2 id="검증-순서">검증 순서</h2>
<ol>
<li><p>@ModelAttribute 각각의 필드에 타입 변환 시도</p>
<ol>
<li><p>성공하면 다음으로</p>
</li>
<li><p><strong>실패하면 typeMismatch 로 FieldError 추가</strong>  </p>
<p> <strong>→</strong>  errors.properties에 선언한 에러메시지 출력</p>
</li>
</ol>
</li>
<li><p>Validator 적용</p>
</li>
</ol>
<p><strong>바인딩에 성공한 필드만 Bean Validation 적용</strong>
BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.
타입 변환에 성공해서 바인딩에 성공한 필드여야 BeanValidation 적용이 의미 있다</p>
<h2 id="에러-메시지">에러 메시지</h2>
<pre><code class="language-java">//errors.properties
#Bean Validation 추가

NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}</code></pre>
<ul>
<li>{0} 은 필드명이고, {1} , {2} ...은 각 애노테이션 마다 다르다.</li>
<li><strong>BeanValidation 메시지 찾는 순서</strong><ol>
<li>생성된 메시지 코드 순서대로 <strong>messageSource(errors.properties)</strong> 에서 메시지 찾기</li>
<li><strong>애노테이션의 message 속성</strong> 사용 → @NotBlank(message = &quot;공백! {0}&quot;)</li>
<li>라이브러리가 제공하는 <strong>기본 값</strong> 사용</li>
</ol>
</li>
</ul>
<h2 id="오브젝트-오류">오브젝트 오류</h2>
<ul>
<li><p>FieldError가 아닌 ObjectError를 처리하는 방법</p>
</li>
<li><p>실무에서는 두번째 방법 권장</p>
<ul>
<li><p>@ScriptAssert</p>
<pre><code class="language-java">  @Data
  @ScriptAssert(lang = &quot;javascript&quot;, script = &quot;_this.price * _this.quantity &gt;= 
  10000&quot;)
  public class Item {
   //...
  }</code></pre>
</li>
<li><p>글로벌 오류 직접 추가</p>
<pre><code class="language-java">  @PostMapping(&quot;/add&quot;)
  public String addItem(@Validated @ModelAttribute Item item, BindingResult 
  bindingResult, RedirectAttributes redirectAttributes) {
   //특정 필드 예외가 아닌 전체 예외
   if (item.getPrice() != null &amp;&amp; item.getQuantity() != null) {
   int resultPrice = item.getPrice() * item.getQuantity();
   if (resultPrice &lt; 10000) {
   bindingResult.reject(&quot;totalPriceMin&quot;, new Object[]{10000,
  resultPrice}, null);
   }
   }
   if (bindingResult.hasErrors()) {
   log.info(&quot;errors={}&quot;, bindingResult);
   return &quot;validation/v3/addForm&quot;;
   }
   //성공 로직
   Item savedItem = itemRepository.save(item);
   redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
   redirectAttributes.addAttribute(&quot;status&quot;, true);
   return &quot;redirect:/validation/v3/items/{itemId}&quot;;
  }</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="한계와-해결">한계와 해결</h2>
<ul>
<li>상황<ul>
<li>수정할 때는 id를 필수값으로 하고 수량에 제한을 두지 않지만 등록할 때는 id가 없어도 되고 수량에 제한을 둔다  → 수정과 등록의 Validation을 다르게 설정하는 경우</li>
</ul>
</li>
</ul>
<h3 id="2가지-방법">2가지 방법</h3>
<ol>
<li>BeanValidation의 <strong>Group 기능</strong>을 사용<ol>
<li>각 그룹별 인터페이스 파일 생성</li>
<li>데이터 클래스의 각 Validation Annotation에 groups Argument 추가</li>
<li>컨트롤러의 Validated Annotation에 Argument로 인터페이스 클래스 선언</li>
</ol>
</li>
<li>Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 <strong>별도의 모델 객체를 만들어 사용 - DTO</strong><ul>
<li>주의사항<ul>
<li>@ModelAttribute의 Argument 고려 - 생략할 시 모델 객체 이름으로 등록</li>
<li>레포지토리 메서드의 인자값으로는 item 인스턴스가 들어가야하기 때문에 item 생성 후 삽입</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="bean-validation---http-메시지-컨버터body">Bean Validation - HTTP 메시지 컨버터(Body)</h2>
<blockquote>
<p><code>@ModelAttribute</code>는 HTTP요청 파라미터(URL 쿼리스트링, Post Form)를 다룰 때 사용한다.</p>
<p><code>@RequestBody</code>는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. (API JSON 요청)</p>
</blockquote>
<h3 id="api---3가지-경우">API - 3가지 경우</h3>
<ul>
<li>성공 요청: 성공</li>
<li>실패 요청: JSON 객체로 생성하는 것 자체가 실패</li>
<li>검증 오류 요청: JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패</li>
</ul>
<h3 id="modelattribute-vs-requestbody">@ModelAttribute VS @RequestBody</h3>
<ul>
<li><p>@ModelAttribute는 각각의 필드 단위로 적용된다. 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있다.</p>
<p>  <strong>→ 특정 필드가 바인딩되지 않아도 나머지 필드는 정상 바인딩, Validator 검증 가능</strong></p>
</li>
<li><p>HttpMessageConverter는 전체 객체 단위로 적용된다.</p>
<ul>
<li><p>따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Validated가 적용된다.</p>
<p>  → <strong>RequestBody는 메시지 컨버터 단계에서 JSON 데이터를 객체로 변경하지 못하면 Validator 검증 불가능</strong></p>
</li>
</ul>
</li>
</ul>
<pre><code>→ HttpMessageConverter 단계에서 실패한 예외 처리방법은 뒤의 예외 처리 부분에서 다룸.</code></pre><h1 id="✔️-로그인-처리---쿠키-세션">✔️ 로그인 처리 - 쿠키, 세션</h1>
<h3 id="로그인-회원가입-폼-기능-구현-base">로그인, 회원가입 폼, 기능 구현 (Base)</h3>
<ol>
<li><p>홈 화면 HTML</p>
</li>
<li><p>회원가입</p>
<ol>
<li><p>Member Data Class</p>
</li>
<li><p>MemberRepository</p>
<pre><code class="language-python"> // 자바 문법 참고
 public Optional&lt;Member&gt; findByLoginId(String loginId) {
  return findAll().stream()
  .filter(m -&gt; m.getLoginId().equals(loginId))
  .findFirst();
  }</code></pre>
</li>
<li><p>MemberController</p>
</li>
<li><p>HTML</p>
</li>
</ol>
</li>
<li><p>로그인</p>
<ol>
<li><p>LoginService <strong>(도메인 패키지에 생성)</strong></p>
</li>
<li><p>login DTO (LoginForm)</p>
</li>
<li><p>Login Controller</p>
<ul>
<li><p>글로벌 오류 처리</p>
<pre><code class="language-python">  @PostMapping(&quot;/login&quot;)
  public String login(@Valid @ModelAttribute LoginForm form, BindingResult 
  bindingResult) {
       if (bindingResult.hasErrors()) {
       return &quot;login/loginForm&quot;;
       }
       Member loginMember = loginService.login(form.getLoginId(),form.getPassword());
       &lt;-- 글로벌 오류 처리 --&gt;
       if (loginMember == null) {
           bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다.&quot;);
           return &quot;login/loginForm&quot;;
       }
       &lt;--     --&gt;
       //로그인 성공 처리 TODO
       return &quot;redirect:/&quot;;
   }</code></pre>
</li>
</ul>
</li>
</ol>
</li>
</ol>
<h2 id="쿠키">쿠키</h2>
<ul>
<li><p>쿠키 생성 로직</p>
<pre><code class="language-java">  Cookie cookie = new Cookie(&quot;memberId&quot;, String.valueOf(loginMember.getId()));
  response.addCookie(cookie);</code></pre>
</li>
<li><p>로그아웃 로직</p>
<ul>
<li><p>새로운 쿠키를 만들고 setMaxAge로 유효시간을 0초로 만든다.</p>
<pre><code class="language-java">      @PostMapping(&quot;/logout&quot;)
  public String logout(HttpServletResponse response){
      expiredCookie(response, &quot;memberId&quot;);
      return &quot;redirect:/&quot;;
  }

  private static void expiredCookie(HttpServletResponse response, String cookieName) {
      Cookie cookie = new Cookie(cookieName, null);
      cookie.setMaxAge(0);
      response.addCookie(cookie);
  }</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="쿠키-보안-문제">쿠키 보안 문제</h2>
<h3 id="문제점">문제점</h3>
<ul>
<li>쿠키 값은 임의로 변경할 수 있다.</li>
<li>쿠키에 보관된 정보는 훔쳐갈 수 있다.</li>
<li>해커가 쿠키를 한번 훔쳐가면 평생 사용할 수 있다.</li>
</ul>
<h3 id="대안">대안</h3>
<ul>
<li>쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰을 노출, 서버에서 토큰과 사용과 id를 매핑해서 인식, 서버에서 토큰 관리</li>
<li>토큰이 해킹당해도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 유지한다.</li>
</ul>
<h2 id="세션"><strong>세션</strong></h2>
<h3 id="동작-방식">동작 방식</h3>
<ul>
<li><p>쿠키의 보안 이슈를 해결하려면 중요한 정보는 모두 서버에 저장해야 한다.</p>
<p>  → 이렇게 서버에 중요한 정보를 보관하고 연결을 유지하는 방법을 세션이라고 한다.</p>
<p><img src="https://velog.velcdn.com/images/yeopju_5/post/a16458e8-0007-44f5-ab96-9db24c224b05/image.png" alt=""></p>
</li>
</ul>
<h3 id="세션-직접-만들기">세션 직접 만들기</h3>
<h3 id="세션-매니저-클래스">세션 매니저 클래스</h3>
<pre><code class="language-java">@Component
public class SessionManager {

    private Map&lt;String, Object&gt; sessionStore = new ConcurrentHashMap&lt;&gt;();
    public static final String SESSION_COOKIE_NAME = &quot;mySessionId&quot;;
    /**
     * 세션 생성
     */
    public void createSession(Object value, HttpServletResponse response){
        String sessionId = UUID.randomUUID().toString();
        sessionStore.put(sessionId, value);

        Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
        response.addCookie(mySessionCookie);
    }
    /**
     * 세션 조회
     */
    public Object getSession(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie == null){
            return null;
        }
        return sessionStore.get(sessionCookie.getValue());
    }

    /**
     * 세션 만료
     */
    public void expire(HttpServletRequest request){
        Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
        if (sessionCookie != null){
            sessionStore.remove(sessionCookie.getValue());
        }
    }

    private static Cookie findCookie(HttpServletRequest request, String cookieName) {
        if (request.getCookies() == null){
            return null;
        }
        return Arrays.stream(request.getCookies())
                .filter(c -&gt; c.getName().equals(cookieName))
                .findAny().orElse(null);
    }
}</code></pre>
<ul>
<li><p><strong>세션 테스트</strong></p>
<pre><code class="language-java">  @Test
      void sessionTest(){
          //세션 생성
          MockHttpServletResponse response = new MockHttpServletResponse();
          Member member = new Member();
          sessionManager.createSession(member, response);

          //쿠키 조작
          MockHttpServletRequest request = new MockHttpServletRequest();
          request.setCookies(response.getCookies());

          //세션 조회
          Object session = sessionManager.getSession(request);
          assertThat(session).isEqualTo(member);

          //세션 만료
          sessionManager.expire(request);
          Object expired = sessionManager.getSession(request);
          assertThat(expired).isNull();
      }</code></pre>
</li>
</ul>
<h2 id="서블릿-http-세션">서블릿 HTTP 세션</h2>
<ul>
<li>HttpSession의 쿠키 이름은 <code>JSESSIONID</code> 이고 값은 추정 불가능한 랜덤 ㄱ밧</li>
</ul>
<h3 id="세션-생성과-조회">세션 생성과 조회</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  // 로그인 성공 처리 TODO
  //sessionManager.createSession(loginMember, response);
  HttpSession session = request.getSession();
  session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

  //sessionManager.expire(request);
  HttpSession session = request.getSession(false);
  if (session != null){
      session.invalidate();
  }</code></pre>
</li>
<li><p>세션 생성 및 조회</p>
<ul>
<li><code>request.getSession(True)</code><ul>
<li>True일 경우 - 생성 - Default값<ul>
<li>세션이 있으면 기존 세션 반환</li>
<li>없으면 새로운 세션 생성 후 반환</li>
</ul>
</li>
<li>False일 경우 - 조회<ul>
<li>세션이 있으면 기존 세션 반환</li>
<li>없으면 null 반환</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>세션 삭제</p>
<ul>
<li><code>session.invalidate()</code></li>
</ul>
</li>
</ul>
<h3 id="sessionattribute">@SessionAttribute</h3>
<ul>
<li><p>세션을 편리하게 사용할 수 있도록 스프링에서 지원하는 Annotation</p>
</li>
<li><p>기존코드</p>
<pre><code class="language-java">          //@GetMapping(&quot;/&quot;)
      public String loginHomeV3(HttpServletRequest request, Model model){
          HttpSession session = request.getSession(false);
          if (session == null){
              return &quot;home&quot;;
          }
          Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);
          if (member == null){
              return &quot;home&quot;;
          }
          model.addAttribute(&quot;member&quot;, member);
          return &quot;loginHome&quot;;
      }</code></pre>
</li>
<li><p>변경 후 코드</p>
<pre><code class="language-java">      @GetMapping(&quot;/&quot;)
      public String loginHomeV3Spring(@SessionAttribute(value = SessionConst.LOGIN_MEMBER, required = false) Member member, Model model){
          if (member == null){
              return &quot;home&quot;;
          }
          model.addAttribute(&quot;member&quot;, member);
          return &quot;loginHome&quot;;
      }
  }</code></pre>
</li>
</ul>
<p><strong>※ Traking Mode</strong></p>
<ul>
<li>서버입장에서 웹브라우저가 쿠키를 지원하는지 모르기 때문에 최초 접근 시 쿠기 값도 전달하고 URL에도 <code>JSESSIONID</code> 를 전달한다.<ul>
<li>URL 전달 방식을 끄고 쿠키를 통해서만 세션을 유지하고 싶을 때<ul>
<li><a href="http://application.properties">application.properties</a> - server.servlet.session.tracking-modes=cookie</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="세션-정보와-타임아웃-설정">세션 정보와 타임아웃 설정</h3>
<ul>
<li><p>세션 정보</p>
<pre><code class="language-java">  @GetMapping(&quot;/session-info&quot;)
      public String sessionInfo(HttpServletRequest request) {
          HttpSession session = request.getSession(false);
          if (session == null) {
              return &quot;세션이 없습니다.&quot;;
          }
          //세션 데이터 출력
          session.getAttributeNames().asIterator()
                  .forEachRemaining(name -&gt; log.info(&quot;session name={}, value={}&quot;,
                          name, session.getAttribute(name)));
          log.info(&quot;sessionId={}&quot;, session.getId());
          log.info(&quot;maxInactiveInterval={}&quot;, session.getMaxInactiveInterval());
          log.info(&quot;creationTime={}&quot;, new Date(session.getCreationTime()));
          log.info(&quot;lastAccessedTime={}&quot;, new Date(session.getLastAccessedTime()));
          log.info(&quot;isNew={}&quot;, session.isNew());
          return &quot;세션 출력&quot;;
      }</code></pre>
<blockquote>
<p>sessionId : 세션Id, JSESSIONID 의 값이다. 예) 34B14F008AA3527C9F8ED620EFD7A4E1</p>
</blockquote>
<blockquote>
<p>maxInactiveInterval : 세션의 유효 시간, 예) 1800초, (30분)</p>
</blockquote>
<blockquote>
<p>creationTime : 세션 생성일시</p>
</blockquote>
<blockquote>
<p>lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로</p>
</blockquote>
<blockquote>
<p>sessionId ( JSESSIONID )를 요청한 경우에 갱신된다.</p>
</blockquote>
<blockquote>
<p>isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어졌고, 클라이언트에서 서버로
  sessionId ( JSESSIONID )를 요청해서 조회된 세션인지 여부</p>
</blockquote>
</li>
<li><p><strong>세션 타임아웃 설정</strong></p>
<ul>
<li>문제점<ul>
<li>사용자가 로그아웃을 하지않고 웹 브라우저를 종료한 경우 서버는 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없어서 데이터를 언제 삭제해야 하는지 판단하기가 어렵다.</li>
<li>이 경우 남아있는 세션을 무한정 보관하면 문제가 발생한다.</li>
</ul>
</li>
<li><strong>HttpSession의 해결방법</strong><ul>
<li>위와 같은 문제를 해결하기 위해 세션의 타임아웃 시간을 마지막 접근 시간으로부터 30분을 Default값으로 잡는다.</li>
</ul>
</li>
<li>타임 아웃 시간 설정방법<ul>
<li>글로벌 설정: <a href="http://application.properties">application.properties</a> - <code>server.servlet.session.timeout=60</code> (글로벌 설정은 분 단위로 설정해야한다.)</li>
<li>특정 세션 설정: <code>session.setMaxInactiveInterval(1800);</code></li>
</ul>
</li>
</ul>
</li>
<li><p><strong>실무 주의점</strong></p>
<ul>
<li>세션에는 최소한의 데이터만 보관해야한다. 현재 프로젝트에는 Member 객체 전체를 담았지만 실무에서 사용할 때는 아이디, 닉네임 등 꼭 필요한 정보들만 핏하게 담는게 좋다.</li>
</ul>
</li>
</ul>
<h1 id="✔️-필터-인터셉터">✔️ 필터, 인터셉터</h1>
<h3 id="공통-관심-사항">공통 관심 사항</h3>
<ul>
<li>로그인한 사용자만 상품 관리 페이지에 들어갈 수 있어야한다.<ul>
<li>현재는 URL을 입력하면 로그인을 하지않아도 해당 페이지에 들어갈 수 있다.</li>
</ul>
</li>
<li>이러한 공통관심사는 AOP로도 해결할 수 있찌만. 웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL 정보가 필요하기 때문에 HttpServletRequest를 제공하는 서블릿 필터나 스프링 인터셉터를 사용하는게 좋다.</li>
</ul>
<h2 id="서블릿-필터">서블릿 필터</h2>
<ul>
<li>필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.</li>
</ul>
<p><strong>필터 제한 플로우</strong></p>
<blockquote>
<p>HTTP 요청 -&gt; WAS -&gt; 필터 -&gt; 서블릿 -&gt; 컨트롤러  //로그인 사용자
HTTP 요청 -&gt; WAS -&gt; 필터(적절하지 않은 요청이라 판단, 서블릿 호출X)
//비 로그인 사용자</p>
</blockquote>
<h3 id="필터---요청-로그">필터 - 요청 로그</h3>
<ul>
<li><p>필터 코드 (LogFilter)</p>
<ul>
<li><p>ServletRequest request는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이기 때문에 HTTP를 사용하면 HttpServletRequest로 다운 케스팅하면 된다.</p>
</li>
<li><p><code>chain.doFilter(request, response);</code></p>
<ul>
<li>이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.</li>
</ul>
<pre><code class="language-java">@Slf4j
public class LogFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
      log.info(&quot;log filter init&quot;);
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      log.info(&quot;log filter doFilter&quot;);

      HttpServletRequest httpRequest = (HttpServletRequest) request;
      String requestURI = httpRequest.getRequestURI();

      String uuid = UUID.randomUUID().toString();

      try {
          log.info(&quot;REQUEST [{}][{}]&quot;, uuid, requestURI);
          chain.doFilter(request, response);
      } catch (Exception e){
          throw e;
      } finally {
          log.info(&quot;RESPONSE [{}][{}]&quot;, uuid, requestURI);
      }

  }
  @Override
  public void destroy() {
      log.info(&quot;log filter destroy&quot;);
  }
}</code></pre>
</li>
</ul>
</li>
<li><p>Configuration 코드 (WebConfig) - FilterRegistrationBean</p>
<pre><code class="language-java">  @Configuration
  public class WebConfig {

      @Bean
      public FilterRegistrationBean logFilter(){
          FilterRegistrationBean&lt;Filter&gt; filterRegistrationBean = new FilterRegistrationBean&lt;&gt;();
          filterRegistrationBean.setFilter(new LogFilter());
          filterRegistrationBean.setOrder(1);
          filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);

          return filterRegistrationBean;
      }
  }</code></pre>
</li>
</ul>
<blockquote>
<p><code>@ServletComponentScan</code> <code>@WebFilter(filterName = &quot;logFilter&quot;, urlPatterns = &quot;/*&quot;)</code> 로
필터 등록이 가능하지만 필터 순서 조절이 안된다. 따라서 FilterRegistrationBean 을 사용하자</p>
</blockquote>
<h3 id="필터---인증-체크">필터 - 인증 체크</h3>
<ul>
<li><p>필터 코드 (LoginCheckFilter)</p>
<ul>
<li><p>sendRedirect에 redirectURL을 Param으로 줌으로써 로그인페이지로 리다렉트 한 후 로그인 시 요청했던 페이지로 다시 돌아갈 수 있도록 한다.</p>
<ul>
<li>LoginController login 로직 return 값에 RequestParam으로 받은 redirectURL 추가</li>
</ul>
<pre><code class="language-java">@Slf4j
public class LoginCheckFilter implements Filter {

  private static final String[] whitelist = {&quot;/&quot;, &quot;/members/add&quot;, &quot;/login&quot;, &quot;/logout&quot;, &quot;/css/*&quot;};

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

      HttpServletRequest httpRequest = (HttpServletRequest) request;
      String requestURI = httpRequest.getRequestURI();

      HttpServletResponse httpResponse = (HttpServletResponse) response;

      try{
          log.info(&quot;인증 체크 필터 시작{}&quot;, requestURI);
          if (isLoginCheckPath(requestURI)){
              log.info(&quot;인증 체크 로직 실행 {}&quot;, requestURI);
              HttpSession session = httpRequest.getSession();
              if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
                  log.info(&quot;미인증 사용자 요청 {}&quot;, requestURI);
                  httpResponse.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);
                  return;
              }
          }
          chain.doFilter(request, response);
      } catch (Exception e){
          throw e;
      } finally {
          log.info(&quot;인증 체크 필터 종료 {}&quot;, requestURI);
      }
  }

  private boolean isLoginCheckPath(String requestURI){
      return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
  }
}</code></pre>
</li>
</ul>
</li>
<li><p>Configuration 코드</p>
<pre><code class="language-java">  @Bean
      public FilterRegistrationBean loginCheckFilter(){
          FilterRegistrationBean&lt;Filter&gt; filterRegistrationBean = new FilterRegistrationBean&lt;&gt;();
          filterRegistrationBean.setFilter(new LoginCheckFilter());
          filterRegistrationBean.setOrder(2);
          filterRegistrationBean.addUrlPatterns(&quot;/*&quot;);

          return filterRegistrationBean;
      }</code></pre>
</li>
</ul>
<h2 id="스프링-인터셉터">스프링 인터셉터</h2>
<p><strong>스프링 인터셉터 제한 플로우</strong></p>
<blockquote>
<p>HTTP 요청 -&gt; WAS -&gt; 필터 -&gt; 서블릿 -&gt; 스프링 인터셉터 -&gt; 컨트롤러 //로그인 사용자
HTTP 요청 -&gt; WAS -&gt; 필터 -&gt; 서블릿 -&gt; 스프링 인터셉터
(적절하지 않은 요청이라 판단, 컨트롤러 호출X) // 비 로그인 사용자</p>
</blockquote>
<h3 id="스프링-인터셉터-호출-흐름">스프링 인터셉터 호출 흐름</h3>
<p><img src="https://velog.velcdn.com/images/yeopju_5/post/469b64f9-d60a-4411-aebf-5e4be8dba62f/image.png" alt=""></p>
<h3 id="인터셉터-예외-상황">인터셉터 예외 상황</h3>
<p><img src="https://velog.velcdn.com/images/yeopju_5/post/2b8b7c65-9d11-4f38-8fee-8fba20a67936/image.png" alt=""></p>
<h3 id="스프링-인터셉터---요청-로그">스프링 인터셉터 - 요청 로그</h3>
<ul>
<li><p>인터셉터 코드</p>
<ul>
<li><p>setAttribute를 통해 uuid 값을 전달해서 같은 값을 사용할 수 있다.</p>
<pre><code class="language-java">@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

  public static final String LOG_ID = &quot;logId&quot;;

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      String requestURI = request.getRequestURI();
      String uuid = UUID.randomUUID().toString();
      request.setAttribute(LOG_ID, uuid);

      if (handler instanceof HandlerMethod){
          HandlerMethod hm = (HandlerMethod) handler;
      }

      log.info(&quot;REQUEST [{}][{}][{}]&quot;, uuid, requestURI, handler);
      return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
      log.info(&quot;postHandle [{}]&quot;, modelAndView);
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
      String requestURI = request.getRequestURI();
      String uuid = (String) request.getAttribute(LOG_ID);
      log.info(&quot;RESPONSE [{}][{}]&quot;, uuid, requestURI);
      if(ex != null){
          log.error(&quot;afterCompletion error!!&quot;, ex);
      }
  }
}</code></pre>
</li>
</ul>
</li>
<li><p>Configuration 등록 코드</p>
<pre><code class="language-java">  @Configuration
  public class WebConfig implements WebMvcConfigurer {

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(new LoginInterceptor())
                  .order(1)
                  .addPathPatterns(&quot;/**&quot;)
                  .excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
      }
  }</code></pre>
</li>
</ul>
<h3 id="스프링-인터셉터---인증-요청">스프링 인터셉터 - 인증 요청</h3>
<ul>
<li><p>인터셉터 코드</p>
<ul>
<li><p>URL 체크를 Configuration 클래스에서 하기때문에 인터셉터 로직이 단순해진다.</p>
<pre><code class="language-java">@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      String requestURI = request.getRequestURI();
      log.info(&quot;인증 체크 인터셉터 실행 {}&quot;, requestURI);

      HttpSession session = request.getSession(false);
      if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
          log.info(&quot;미인증 사용자 요청&quot;);
          response.sendRedirect(&quot;login?redirectURL=&quot; + requestURI);
          return false;
      }
      return true;
  }
}</code></pre>
</li>
</ul>
</li>
<li><p>Configuration 등록 코드</p>
<pre><code class="language-java">  @Configuration
  public class WebConfig implements WebMvcConfigurer {

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(new LoginInterceptor())
                  .order(1)
                  .addPathPatterns(&quot;/**&quot;)
                  .excludePathPatterns(&quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);

          registry.addInterceptor(new LoginCheckInterceptor())
                  .order(2)
                  .addPathPatterns(&quot;/**&quot;)
                  .excludePathPatterns(&quot;/&quot;, &quot;/members/add&quot;, &quot;/login&quot;, &quot;/logout&quot;, &quot;/css/**&quot;, &quot;/*.ico&quot;, &quot;/error&quot;);
      }</code></pre>
</li>
</ul>
<h2 id="argument-resolver">Argument Resolver</h2>
<ul>
<li><code>@Login</code> Annotation이 있으면 직접 만든 <strong>ArgumentResolver</strong>가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션에 없다면 null 값을 반환하도록 개발</li>
</ul>
<h3 id="로그인-annotation-생성">로그인 Annotation 생성</h3>
<pre><code class="language-java">@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}</code></pre>
<ul>
<li>Target: 파라미터에만 사용</li>
<li>Retatetion: 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음.</li>
</ul>
<h3 id="loginmenberargumentresolver-생성">LoginMenberArgumentResolver 생성</h3>
<pre><code class="language-java">@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info(&quot;supportParameter 실행&quot;);
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation &amp;&amp; hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        log.info(&quot;resolveArgument 실행&quot;);

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if(session == null){
            return null;
        }

        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}</code></pre>
<ul>
<li><code>supportsParameter</code> : <code>@Login</code> 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver가 사용된다.</li>
<li><code>resolveArgument</code> : 컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성</li>
</ul>
<h3 id="configuration에-설정-추가">Configuration에 설정 추가</h3>
<pre><code class="language-java">@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List&lt;HandlerMethodArgumentResolver&gt; resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
}</code></pre>
<h1 id="✔️-예외-처리와-오류-페이지">✔️ 예외 처리와 오류 페이지</h1>
<p><strong>웹 애플리케이션</strong></p>
<blockquote>
<p>웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리하면 아무런 문제가 없다. 그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달되면 어떻게 동작할까?</p>
</blockquote>
<h3 id="exception">Exception</h3>
<ul>
<li>예외처리를 하지못한 예외가 발생하게 되면 서블릿밖인 WAS(Tomcat)까지 전달이 되고 톰캣이 기본으로 제공하는 오류 화면을 볼 수 있다<ul>
<li>500 - Internal Server Error</li>
</ul>
</li>
<li>라우팅을 해놓지 않은 아무 URL을 호출 시에는 404 - Not Found 오류 화면 반환</li>
</ul>
<aside>
⚙️ **WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)**

</aside>

<h3 id="senderror">SendError</h3>
<ul>
<li><code>response.sendError(HTTP 상태 코드, 오류 메시지)</code><ul>
<li>상태 코드와 오류 메시지도 추가할 수 있다.</li>
</ul>
</li>
<li>해당 메서드가 실행되면 당장 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달 할 수 있다.</li>
</ul>
<aside>
⚙️ **WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러**

</aside>

<h2 id="서블릿-예외-처리">서블릿 예외 처리</h2>
<h3 id="서블릿-오류-페이지-등록">서블릿 오류 페이지 등록</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  package hello.exception;

  @Component
  public class WebServerCustomizer implements WebServerFactoryCustomizer&lt;ConfigurableWebServerFactory&gt; {
      @Override
      public void customize(ConfigurableWebServerFactory factory) {
          ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, &quot;/error-page/404&quot;);
          ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, &quot;/error-page/500&quot;);
          ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, &quot;/error-page/500&quot;);
          factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
      }
  }</code></pre>
</li>
</ul>
<h3 id="오류-처리-컨트롤러-등록">오류 처리 컨트롤러 등록</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @Controller
  public class ServletExController {

      @GetMapping(&quot;/error-ex&quot;)
      public void errorEx(){
          throw new RuntimeException(&quot;예외 발생!&quot;);
      }
      @GetMapping(&quot;/error-404&quot;)
      public void error404(HttpServletResponse response) throws IOException {
          response.sendError(404, &quot;404오류!&quot;);
      }
      @GetMapping(&quot;/error-500&quot;)
      public void error500(HttpServletResponse response) throws IOException {
          response.sendError(500);
      }
  }</code></pre>
</li>
</ul>
<h3 id="서블릿-오류-페이지-작동원리">서블릿 오류 페이지 작동원리</h3>
<aside>
⚙️ **WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)

<p>WAS <code>/error-page/500</code> 다시 요청 -&gt; 필터 -&gt; 서블릿 -&gt; 인터셉터 -&gt; 컨트롤러(/errorpage/500) -&gt; View**</p>
</aside>

<ol>
<li><p>예외가 발생해서 WAS까지 전파된다.</p>
</li>
<li><p>WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다. 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.</p>
<ul>
<li><p>오류 정보 추가</p>
<ul>
<li><p>WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 request 의 attribute에 추가해서 넘겨준다.</p>
<blockquote>
<p>javax.servlet.error.exception : 예외
  javax.servlet.error.exception_type : 예외 타입
  javax.servlet.error.message : 오류 메시지
  javax.servlet.error.request_uri : 클라이언트 요청 URI
  javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
  javax.servlet.error.status_code : HTTP 상태 코드</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="서블릿-예외-처리---필터">서블릿 예외 처리 - 필터</h3>
<ul>
<li><p>오류가 발생하면 WAS에서 다시 한번 호출이 발생하는데 이때 다시 필터와 인터셉터가 호출된다. 그런데 필터와 인터셉터는 오류가 발생한 요청에서 이미 한번 완료된 사항이기 때문에 굳이 호출할 필요가 없다.</p>
</li>
<li><p>서블릿은 이 문제를 해결하기 위해 <code>DispatcherType</code> 이라는 추가정보를 제공한다.</p>
<ul>
<li><p><strong>DispatcherType</strong></p>
<blockquote>
<p>REQUEST : 클라이언트 요청
  ERROR : 오류 요청
  FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
  RequestDispatcher.forward(request, response);
  INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
  RequestDispatcher.include(request, response);
  ASYNC : 서블릿 비동기 호출</p>
</blockquote>
</li>
</ul>
</li>
<li><p>해결방법</p>
<ul>
<li>Configuration에서 Filter를 추가할 때 해당 코드를 추가하면 된다.<ul>
<li><code>filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);</code></li>
<li>setDispatcherTypes의 Default Value는 Request</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="서블릿-예외-처리---인터셉터">서블릿 예외 처리 - 인터셉터</h3>
<ul>
<li>인터셉터에는 DispatcherType과 같은 것이 없다.</li>
<li>excludePathPatterns에 error-page 경로를 추가해주면 된다.</li>
</ul>
<h2 id="스프링부트-예외-처리">스프링부트 예외 처리</h2>
<ul>
<li><p>스프링 부트는 에러페이지를 자동으로 등록한다.</p>
<ul>
<li><code>/error</code> 라는 경로로 기본 오류 페이지를 설정한다.</li>
</ul>
</li>
<li><p>WebServerCustomizer로 상태코드와 예외 처리를 설정하지 않으면 기본 오류 페이지로 사용된다.</p>
</li>
<li><p>BasicErrorController라는 스프링 컨트롤러 또한 자동으로 등록한다.</p>
<ul>
<li>ErrorPage에서 등록한 /error를 매핑해서 처리하는 컨트롤러</li>
</ul>
</li>
<li><p>개발자는 오류페이지(뷰)만 등록하면 된다.</p>
<ul>
<li>우선순위<ol>
<li>뷰템플릿<ol>
<li>resources/templates/error/500.html</li>
<li>resources/templates/error/5xx.html</li>
</ol>
</li>
<li>정적 리소스</li>
<li>적용 대상이 없을 때 뷰 이름(error)</li>
</ol>
</li>
</ul>
</li>
<li><p>오류 정보를 뷰 페이지에 쉽게 노출시킬 수 있다.</p>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  &lt;!DOCTYPE HTML&gt;
  &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
  &lt;head&gt;
   &lt;meta charset=&quot;utf-8&quot;&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&gt;
   &lt;div class=&quot;py-5 text-center&quot;&gt;
   &lt;h2&gt;500 오류 화면 스프링 부트 제공&lt;/h2&gt;
   &lt;/div&gt;
   &lt;div&gt;
   &lt;p&gt;오류 화면 입니다.&lt;/p&gt;
   &lt;/div&gt;
   &lt;ul&gt;
   &lt;li&gt;오류 정보&lt;/li&gt;
   &lt;ul&gt;
   &lt;li th:text=&quot;|timestamp: ${timestamp}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|path: ${path}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|status: ${status}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|message: ${message}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|error: ${error}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|exception: ${exception}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|errors: ${errors}|&quot;&gt;&lt;/li&gt;
   &lt;li th:text=&quot;|trace: ${trace}|&quot;&gt;&lt;/li&gt;
   &lt;/ul&gt;
   &lt;/li&gt;
   &lt;/ul&gt;
   &lt;hr class=&quot;my-4&quot;&gt;
  &lt;/div&gt; &lt;!-- /container --&gt;
  &lt;/body&gt;
  &lt;/html&gt;</code></pre>
</li>
<li><p>옵션</p>
<ul>
<li><p>application.properties</p>
<pre><code class="language-java">  server.error.include-exception=true
  server.error.include-message=on_param
  server.error.include-stacktrace=on_param
  server.error.include-binding-errors=on_param</code></pre>
</li>
<li><p>기본 값이 never 인 부분은 다음 3가지 옵션을 사용할 수 있다.</p>
<ul>
<li>never : 사용하지 않음</li>
<li>always :항상 사용</li>
<li>on_param : 파라미터가 있을 때 사용</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="✔️-api-예외처리">✔️ API 예외처리</h1>
<h3 id="직접-컨트롤러-처리">직접 컨트롤러 처리</h3>
<pre><code class="language-java">        @RequestMapping(value = &quot;/error-page/500&quot;, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity &lt;Map&lt;String, Object&gt;&gt; errorPage500Api(HttpServletRequest request, HttpServletResponse response){
        Map&lt;String, Object&gt; result = new HashMap&lt;&gt;();
        Exception ex = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
        result.put(&quot;status&quot;, request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
        result.put(&quot;message&quot;, ex.getMessage());

        Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        return new ResponseEntity(result, HttpStatus.valueOf(statusCode));
    }</code></pre>
<ul>
<li><p><code>produces=MediaType.APPLICATION_JSON_VALUE</code>를 선언하면 클라이언트가 요청하는 HTTP Header의 Accpet의 값이 application/json일 때 해당 메서드가 우선호출된다.</p>
<p>  <strong>※</strong> Jackson 라이브러리는 Map을 Json 구조로 변환할 수 있다. </p>
</li>
</ul>
<h3 id="스프링-부트-기본-오류-처리">스프링 부트 기본 오류 처리</h3>
<ul>
<li>Request Accept 타입이 HTML일 때와 같이 Application/json 일 때에도 BasicErrorController에서 자동으로 JSON 형식으로 오류처리를 해준다.<ul>
<li>BasicErrorController를 확장하면 JSON 오류 메시지도 변경할 수 있다.</li>
</ul>
</li>
<li>하지만 HTML 형식이 아닌 JSON형식의 경우에는 API마다 발생하는 예외의 경우가 다양하고 복잡하다. 따라서 <strong>BasicErrorController를 이용한 방식은 4xx, 5xx 오류와 같이 일관되게 처리할 수 있는 HTML 화면을 처리할 때 사용</strong>하고 <strong>API 오류처리는 <code>@ExceptionHandler</code> 를 사용하자</strong></li>
</ul>
<h2 id="handlerexceptionresolver">HandlerExceptionResolver</h2>
<ul>
<li>기존에는 컨트롤러에서 예외가 발생해서 서블릿을 넘어 WAS까지 전달되면 500에러를 던져줬다. 이 에러를 400에러, 404에러 등 자신이 원하는 에러로 바꿔서 던져주거나 메시지, 형식을 바꾸기 위해서는 ExceptionResolver를 조작해야한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeopju_5/post/c1f2f4e5-c5e4-4a07-9e9e-48c09557713f/image.png" alt=""></p>
<pre><code>- 코드1

    ```java
    @Slf4j
    public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
     @Override
     public ModelAndView resolveException(HttpServletRequest request,
    HttpServletResponse response, Object handler, Exception ex) {
         try {
                 if (ex instanceof IllegalArgumentException) {
                 log.info(&quot;IllegalArgumentException resolver to 400&quot;);
                 response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                 return new ModelAndView();
         }
         } catch (IOException e) {
                 log.error(&quot;resolver ex&quot;, e);
         }
         return null;
     }
    }
    ```</code></pre><h3 id="반환-값에-따른-동작-방식">반환 값에 따른 동작 방식</h3>
<ul>
<li>HandlerExceptionResolver 의 반환 값에 따른 DispatcherServlet 의 동작 방식은 다음과 같다.<ul>
<li><strong>빈 ModelAndView</strong><ul>
<li>new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.</li>
</ul>
</li>
<li><strong>ModelAndView 지정</strong><ul>
<li>ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링한다.</li>
</ul>
</li>
<li><strong>null</strong><ul>
<li>null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다. 만약 처리할 수 있는
ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="exceptionresolver-활용">ExceptionResolver 활용</h3>
<ul>
<li><p><strong>예외 상태 코드 변환</strong></p>
<ul>
<li>예외를 response.sendError(xxx) 호출로 변경해서 서블릿에서 상태 코드에 따른 오류를
처리하도록 위임</li>
<li>이후 WAS는 서블릿 오류 페이지를 찾아서 내부 호출, 예를 들어서 스프링 부트가 기본으로 설정한 /error 가 호출됨</li>
</ul>
</li>
<li><p><strong>뷰 템플릿 처리</strong></p>
<ul>
<li>ModelAndView 에 값을 채워서 예외에 따른 새로운 오류 화면 뷰 렌더링 해서 고객에게 제공</li>
</ul>
</li>
<li><p><strong>API 응답 처리</strong></p>
<ul>
<li>response.getWriter().println(&quot;hello&quot;); 처럼 HTTP 응답 바디에 직접 데이터를 넣어주는
것도 가능하다. 여기에 JSON 으로 응답하면 API 응답 처리를 할 수 있다.</li>
</ul>
</li>
<li><p><strong><em>코드</em></strong></p>
<pre><code class="language-java">  @Slf4j
  public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
   private final ObjectMapper objectMapper = new ObjectMapper();
   @Override
   public ModelAndView resolveException(HttpServletRequest request,
  HttpServletResponse response, Object handler, Exception ex) {
   try {
       if (ex instanceof UserException) {
           log.info(&quot;UserException resolver to 400&quot;);
           String acceptHeader = request.getHeader(&quot;accept&quot;);
           response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
               if (&quot;application/json&quot;.equals(acceptHeader)) {
                   Map&lt;String, Object&gt; errorResult = new HashMap&lt;&gt;();
                   errorResult.put(&quot;ex&quot;, ex.getClass());
                   errorResult.put(&quot;message&quot;, ex.getMessage());

                   String result = objectMapper.writeValueAsString(errorResult);

                   response.setContentType(&quot;application/json&quot;);
                   response.setCharacterEncoding(&quot;utf-8&quot;);
                   response.getWriter().write(result);
                   return new ModelAndView();
               } else {
               //TEXT/HTML
                   return new ModelAndView(&quot;error/500&quot;);
               }
       }
   } catch (IOException e) {
   log.error(&quot;resolver ex&quot;, e);
   }
   return null;
   }
  }</code></pre>
</li>
</ul>
<h2 id="스프링이-제공하는-exceptionresolver">스프링이 제공하는 ExceptionResolver</h2>
<aside>
⚙️ 1.**ExceptionHandlerExceptionResolver** - `@ExceptionHandler` 2.ResponseStatusExceptionResolver - HTTP 상태 코드 지정3.DefaultHandlerExceptionResolver - 스프링 내부 기본 예외 처리

</aside>

<h3 id="2-responsestatusexceptionresolver">2. ResponseStatusExceptionResolver</h3>
<ol>
<li><p><code>**@ResponseStatus</code> 가 달려있는 예외 처리**</p>
<pre><code class="language-java"> @ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = &quot;잘못된 요청 오류&quot;)
 public class BadRequestException extends RuntimeException {
 }</code></pre>
<ul>
<li><p>sendError를 통해 지정한 상태코드, 메시지 처리</p>
<p>  → WAS에서 다시 오류페이지를 내부 요청</p>
</li>
</ul>
</li>
<li><p><strong>ResponseStatusException 예외 처리</strong></p>
<pre><code class="language-java"> @GetMapping(&quot;/api/response-status-ex2&quot;)
 public String responseStatusEx2() {
  throw new ResponseStatusException(HttpStatus.NOT_FOUND, &quot;error.bad&quot;, new
 IllegalArgumentException());
 }</code></pre>
<ul>
<li>@ResponseStatus는 개발자가 직접 변경할 수 없는 예외에는 적용할 수 없다.<ul>
<li>동적 변경을 위해서는 ReponseStatusException 사용</li>
</ul>
</li>
</ul>
<p>※ 메시지 기능 사용 가능 - messages.properties에 메시지 지정 후 reason 값에 대입</p>
</li>
</ol>
<h3 id="1-exceptionhandlerexceptionresolver---exceptionhandler"><strong>1. ExceptionHandlerExceptionResolver - <code>@ExceptionHandler</code></strong></h3>
<ul>
<li><p><code>@ExceptionHandler</code> 를 선언하고, 해당 컨트롤러에서 처리하고 싶은 예외를 지정해주면 된다.</p>
</li>
<li><p>다음과 같이 다양한 예외를 한번에 처리 할 수 있다.</p>
<ul>
<li><code>@ExceptionHandler({AException.class, BException.class})</code></li>
</ul>
</li>
<li><p><strong>❗실행흐름</strong></p>
<blockquote>
<ul>
<li>컨트롤러를 호출한 결과 IllegalArgumentException 예외가 컨트롤러 밖으로 던져진다.</li>
</ul>
</blockquote>
<ul>
<li>예외가 발생했으로 ExceptionResolver 가 작동한다. 가장 우선순위가 높은
ExceptionHandlerExceptionResolver 가 실행된다.</li>
<li>ExceptionHandlerExceptionResolver 는 해당 컨트롤러에 IllegalArgumentException 을 처리할 수 있는 @ExceptionHandler 가 있는지 확인한다.</li>
<li>illegalExHandle() 를 실행한다. </li>
<li>@RestController 이므로 illegalExHandle() 에도 @ResponseBody 가 적용된다. 따라서 HTTP 컨버터가 사용되고, 응답이 다음과 같은 JSON으로 반환된다.</li>
<li>@ResponseStatus(HttpStatus.BAD_REQUEST) 를 지정했으므로 HTTP 상태 코드 400으로 응답한다.<blockquote>
</blockquote>
</li>
</ul>
</li>
</ul>
<h3 id="controlleradvice"><code>@ControllerAdvice</code></h3>
<ul>
<li><p>대상으로 지정한 여러 컨트롤러에 <code>@ExceptionHandler</code>, <code>@InitBinder</code> 기능을 부여해준다.</p>
<p>  <strong>지정방법</strong></p>
<pre><code class="language-java">  // Target all Controllers annotated with @RestController
  @ControllerAdvice(annotations = RestController.class)
  public class ExampleAdvice1 {}

  // Target all Controllers within specific packages
  @ControllerAdvice(&quot;org.example.controllers&quot;)
  public class ExampleAdvice2 {}

  // Target all Controllers assignable to specific classes
  @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
  public class ExampleAdvice3 {}</code></pre>
</li>
</ul>
<h1 id="✔️-스프링-타입-컨버터">✔️ 스프링 타입 컨버터</h1>
<h3 id="스프링-타입-변환-적용-예">스프링 타입 변환 적용 예</h3>
<ul>
<li>스프링 MVC 요청 파라미터<ul>
<li><code>@RequestParam</code>, <code>@ModelAttribute</code>, <code>@PathVariable</code></li>
</ul>
</li>
<li>뷰를 렌더링 할 때</li>
</ul>
<h2 id="타입-컨버터---converter">타입 컨버터 - Converter</h2>
<ul>
<li>Integer를 String으로 변환하는 Converter와 같이 범용적인 컨버터는 스프링이 제공한다. 하지만 <strong>직접 만든 객체형 타입으로의 변환을 위한 컨버터</strong>는 직접 선언해서 <strong>컨버전 서비스를 이용하는 addFormatters로</strong> 추가해야한다.</li>
</ul>
<ol>
<li>컨버터 인터페이스를 통해 컨버터 클래스를 구현한다.</li>
<li>WebConfig의 addFormatters 메서드를 사용해 컨버터를 추가한다.<ol>
<li>스프링 내부에서 ConversionService를 제공한다.</li>
</ol>
</li>
</ol>
<h2 id="conversionservice">ConversionService</h2>
<ul>
<li>타입 컨버터를 선언해두고 하나씩 직접 찾아서 사용하는 것은 매우 불편하기 때문에 스프링은 개별 컨버터를 모아두고 사용할 수 있는 ConversionService 기능을 제공한다.</li>
</ul>
<h3 id="인터페이스-분리-원칙---isp">인터페이스 분리 원칙 - ISP</h3>
<ul>
<li><p>ISP는 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.</p>
</li>
<li><p><code>DefaultConversionService</code> 다음 두 인터페이스를 구현했다.</p>
<ul>
<li><p><code>ConversionService</code> : 컨버터 사용에 초점</p>
</li>
<li><p><code>ConversionRegistry</code> : 컨버터 등록에 초점</p>
<p>이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 <strong>관심사를 명확하게 분리</strong>할 수 있다. 특히 <strong>컨버터를 사용하는 클라이언트는 ConversionService만 의존하면 되므로</strong>, 컨버터를 어떻게 등록하고 관리하는지 전혀 몰라도 된다.</p>
</li>
</ul>
</li>
</ul>
<h3 id="뷰-템플릿에-컨버터-적용하기">뷰 템플릿에 컨버터 적용하기</h3>
<ul>
<li>타임리프는 <code>${{...}}</code> 를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력한다.<ul>
<li>변수표현식: <code>${...}</code></li>
<li>컨버전 서비스 적용: <code>${{...}}</code></li>
</ul>
</li>
<li><code>th:field</code> 를 사용하는 경우 컨버전 서비스가 자동으로 적용되기 때문에 일반적인 변수표현식을 사용하면 된다.</li>
</ul>
<h2 id="포맷터---formatter">포맷터 - Formatter</h2>
<ul>
<li><p>컨버터는 범용 (객체 → 객체)</p>
</li>
<li><p>포맷터는 문자에 특화 (객체 → 문자, 문자 → 객체) + 현지화(Locale)</p>
</li>
<li><p>코드</p>
<pre><code class="language-java">  public class MyNumberFormatter implements Formatter&lt;Number&gt; {
      @Override
      public Number parse(String text, Locale locale) throws ParseException {
          NumberFormat format = NumberFormat.getInstance(locale);
          return format.parse(text);
      }
      @Override
      public String print(Number object, Locale locale) {
          return NumberFormat.getInstance(locale).format(object);
      }
  }</code></pre>
</li>
</ul>
<h3 id="포맷터를-지원하는-컨버전-서비스---formattingconversionservice">포맷터를 지원하는 컨버전 서비스 - <code>FormattingConversionService</code></h3>
<ul>
<li><code>DefaultFormattingConversionService</code> 는 <code>FormattingConversionService</code> 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.</li>
<li><code>FormattingConversionService</code> 는 <code>ConversionService</code>관련 기능을 상속받기 떄문에 결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.</li>
</ul>
<p>→ 스프링 부트는 <code>DefaultFormattingConversionService</code>를 상속 받은 <code>WebConversionService</code>를 내부에서 사용한다.</p>
<h3 id="스프링이-제공하는-기본-포맷터">스프링이 제공하는 기본 포맷터</h3>
<ul>
<li><p>Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있다. 그런데 포맷터는 기본형식이 지정되어 있기 떄문에 객체의 각 필드마다 다른 형식으로 포맷을 지원하기는 어렵다.</p>
<p>  → 이를 해결하기 위해 스프링에서는 두가지 애노테이션을 제공한다.</p>
<ul>
<li><p><code>@NumberFormat</code></p>
<ul>
<li>숫자 관련 형식 지정 포맷터 사용</li>
</ul>
</li>
<li><p><code>@DateTimeFormat</code></p>
<ul>
<li>날짜 관련 형식 지정 포맷터 사용</li>
</ul>
<pre><code class="language-java">  @Data
static class Form {
   @NumberFormat(pattern = &quot;###,###&quot;)
   private Integer number;
   @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)
   private LocalDateTime localDateTime;
}</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="주의">주의</h3>
<ul>
<li>메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는다.</li>
<li>HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP메시지바디에 입력하는 것이다.</li>
<li>예를 들어 JSON를 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다. JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.</li>
<li>결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.</li>
<li>컨버전 서비스는 <code>@RequestParam</code>, <code>@ModelAttribute</code>, <code>@PathVariable</code>, 뷰 템플릿 등에서 사용할 수 있다.</li>
</ul>
<h1 id="✔️-파일-전송">✔️ 파일 전송</h1>
<h3 id="html-폼-전송-방식">HTML 폼 전송 방식</h3>
<ul>
<li>application/x-www-form-urlencoded</li>
<li>multipart/form-data</li>
</ul>
<blockquote>
<p>문자와 바이너리 파일을 동시에 전송하는 것처럼 여러 형식의 폼을 전송시키기 위해서는 multipart/form-data라는 전송 방식을 사용해야한다.</p>
</blockquote>
<p>→ <code>enctype=&quot;multipart/form-data&quot;</code> 를 Form 태그에 선언</p>
<h3 id="멀티-파트-사용-옵션">멀티 파트 사용 옵션</h3>
<p><strong>업로드 사이즈 제한</strong></p>
<pre><code class="language-java">spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB</code></pre>
<h2 id="서블릿과-파일-업로드">서블릿과 파일 업로드</h2>
<ul>
<li><p>코드</p>
<pre><code class="language-java">          @PostMapping(&quot;/upload&quot;)
      public String saveFileV1(HttpServletRequest request) throws ServletException, IOException {
          log.info(&quot;request={}&quot;, request);
          String itemName = request.getParameter(&quot;itemName&quot;);
          log.info(&quot;itemName={}&quot;, itemName);
          Collection&lt;Part&gt; parts = request.getParts();
          log.info(&quot;parts={}&quot;,parts);

          for (Part part : parts) {
              log.info(&quot;==== PART ====&quot;);
              log.info(&quot;name={}&quot;, part.getName());
              Collection&lt;String&gt; headerNames = part.getHeaderNames();
              for (String headerName : headerNames) {
                  log.info(&quot;header {}: {}&quot;, headerName, part.getHeader(headerName));
              }

              //편의 메서드
              log.info(&quot;submittedFileName={}&quot;, part.getSubmittedFileName());
              log.info(&quot;size={}&quot;, part.getSize());

              //데이터 읽기
              InputStream inputStream = part.getInputStream();
              String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
              log.info(&quot;body={}&quot;, body);

              //파일에 저장하기
              if (StringUtils.hasText(part.getSubmittedFileName())){
                  String fullPath = fileDir + part.getSubmittedFileName();
                  log.info(&quot;파일 저장 fullPath={}&quot;, fullPath);
                  part.write(fullPath);
              }
          }</code></pre>
</li>
<li><p>멀티파트 형식은 전송 데이터를 하나하나 각각 PART로 나누어 전송한다.</p>
</li>
<li><p>서블릿이 제공하는 PART는 멀티파트 형식을 편리하게 읽을 수 있는 메서드를 제공한다.</p>
</li>
</ul>
<h2 id="스프링과-파일-업로드">스프링과 파일 업로드</h2>
<ul>
<li><p>코드</p>
<pre><code class="language-java">          @PostMapping(&quot;/upload&quot;)
      public String saveFile(@RequestParam String itemName,
                             @RequestParam MultipartFile file, HttpServletRequest request) throws IOException {
          log.info(&quot;request={}&quot;, request);
          log.info(&quot;itemName={}&quot;, itemName);
          log.info(&quot;multiPartFile={}&quot;, file);

          if(!file.isEmpty()){
              String fullPath = fileDir + file.getOriginalFilename();
              log.info(&quot;fullPath={}&quot;, fullPath);
              file.transferTo(new File(fullPath));
          }
          return &quot;upload-form&quot;;
      }</code></pre>
</li>
<li><p>@ModelAttribute에서도 MultipartFile을 동일하게 사용할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌱 스프링 MVC 1]]></title>
            <link>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1</link>
            <guid>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-1</guid>
            <pubDate>Mon, 29 May 2023 17:15:06 GMT</pubDate>
            <description><![CDATA[<h1 id="✔️-웹-애플리케이션의-이해">✔️ 웹 애플리케이션의 이해</h1>
<h2 id="웹-서버-웹-애플리케이션-서버">웹 서버, 웹 애플리케이션 서버</h2>
<h3 id="웹-서버">웹 서버</h3>
<ul>
<li>정적 리소스 제공</li>
<li>예) Nginx, Apache</li>
</ul>
<h3 id="웹-애플리케이션-서버">웹 애플리케이션 서버</h3>
<ul>
<li>웹 서버 기능 포함 + 동적 리소스 제공</li>
<li>프로그램 코드를 실행해서 애플리케이션 로직 수행</li>
<li>예) 톰캣, Jetty, Undertow</li>
</ul>
<h2 id="서블릿">서블릿</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/f412a5a8-ea7d-4d9c-bff2-ab4bce9f9f9b/image.png" width="300" height="200">

<ul>
<li>서블릿에서 초록색 박스 안의 것들을 제외하고 모두 대신 처리해준다.</li>
<li>WAS, 서블릿 컨테이너 구조</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/81f8a350-6780-4daa-a33f-6d1a88a4bb5e/image.png" width="700">

<pre><code class="language-java">@WebServlet(name=&quot;helloServlet&quot;, urlPatterns=&quot;/hello&quot;)
public classs HelloServlet extends HttpServlet {

        @Override
        protected void service(HttpsServletRequest request, HttpServletResponse response){
                //  애플리케이션 로직
        }
}</code></pre>
<ul>
<li>톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다</li>
<li>서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 <strong>생명주기를 관리</strong>한다.</li>
<li>서블릿 객체는 <strong>싱글톤으로 관리</strong>된다.</li>
<li>서블릿 컨테이너 종료 시 함께 종료된다.</li>
<li>JSP도 서블릿으로 변화 되어서 사용</li>
<li>동시 요청을 위한 <strong>멀티 쓰레드 처리 지원</strong></li>
</ul>
<h2 id="동시-요청---멀티-쓰레드">동시 요청 - 멀티 쓰레드</h2>
<h3 id="쓰레드">쓰레드</h3>
<ul>
<li>애플리케이션 코드를 하나하나 순차적으로 실행하는 것은 쓰레드</li>
<li>쓰레드가 없다면 자바 애플리케이션 실행이 불가능</li>
</ul>
<h3 id="쓰레드-풀">쓰레드 풀</h3>
<ul>
<li>동시 요청의 경우 요청이 올 때마다 쓰레드를 생성하면 쓰레드가 너무 많이 생성되면 리소스가 임계점을 넘어 서버가 죽을 수도 있고 생성 비용 또한 비효율적이다.</li>
<li>그래서 필요한 <strong>쓰레드를 최대치를 설정해놓고 쓰레드 풀에 보관하고 관리</strong>한다.</li>
<li><strong>쓰레드 풀에 있는 쓰레드가 모두 사용 중이라면</strong> 기다리는 요청은 거절하거나 특정 숫자만큼 대기하도록 설정할 수 있다.</li>
<li>장점<ul>
<li>쓰레드가 미리 생성되어 있어, 쓰레드를 생성하고 종료하는 비용(CPU)이 절약되고 응답시간이 빠르다.</li>
<li>생성 가능한 쓰레드 최대치가 있어 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.</li>
</ul>
</li>
<li>최대 쓰레드를 너무 낮게 설정하면?<ul>
<li>동시 요청이 많을 시, 클라이언트는 금방 응답 지연, 서버 리소스가 여유롭다 → 비효율적</li>
</ul>
</li>
<li>최대 쓰레드를 너무 높게 설정하면?<ul>
<li>동시 요청이 많을 시 리소스 임계점 초과로 서버 다운</li>
</ul>
</li>
<li>적정 숫자 - 성능 테스트<ul>
<li>툴: nGrinder, 아파치 ab, 제이미터</li>
</ul>
</li>
</ul>
<h2 id="자바-웹-기술-역사">자바 웹 기술 역사</h2>
<h3 id="자바-웹-기술-역사-1">자바 웹 기술 역사</h3>
<ul>
<li>서블릿<ul>
<li>HTML 생성이 어려움</li>
</ul>
</li>
<li>JSP<ul>
<li>HTML 생성은 편리하지만, 비즈니스 조직까지 너무 많은 역할 담당</li>
</ul>
</li>
<li>서블릿, JSP 조합 MVC 패턴 사용<ul>
<li>모델, 뷰, 컨트롤러로 역할 나누어 개발</li>
</ul>
</li>
<li>MVC 프레임워크 춘추 전국 시대 - 2000년대 초 ~ 2010년 초</li>
<li>애노테이션 기반의 스프링 MVC 등장<ul>
<li>@Controller</li>
</ul>
</li>
<li>스프링 부트의 등장<ul>
<li>스프링 부트는 서버를 내장</li>
<li>과거에는 서버에 WAS를 직접 설치하고, 소스는 War파일을 만들어서 설치한 WAS에 배포</li>
<li>스프링 부트는 빌드 결과(Jar)에 WAS 서버 포함 → 빌드 배포 단순화</li>
</ul>
</li>
<li>스프링 웹 기술의 분화<ul>
<li>Web Servlet - Spring MVC<ul>
<li>서블릿 기술 사용 O</li>
</ul>
</li>
<li>Web Reactive - Spring WebFlux<ul>
<li>특징<ul>
<li>비동기 넌 블러킹 처리</li>
<li>최소 쓰레드로 최대 성능 - 쓰레드 컨텍스트 스위칭 비용 효율화</li>
<li>함수형 스타일로 개발 - 동시처리 코드 효율화</li>
<li>서블릿 기술 사용 X</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="자바-뷰-템플릿-역사">자바 뷰 템플릿 역사</h3>
<ul>
<li>JSP<ul>
<li>속도 느림, 기능 부족</li>
</ul>
</li>
<li>프리마커, Velocity (벨로시티)<ul>
<li>속도 문제 해결, 다양한 기능</li>
</ul>
</li>
<li>타임리프(Thymeleaf)<ul>
<li>내추럴 템플릿: HTML의 모양을 유지하면서 뷰 템플릿 적용 가능</li>
<li>스프링 MVC와 강력한 기능 통합, 성능은 프리마커, 벨로시티가 더 빠름</li>
</ul>
</li>
</ul>
<h1 id="✔️-서블릿">✔️ 서블릿</h1>
<h2 id="서블릿-기본-설정">서블릿 기본 설정</h2>
<ul>
<li><code>ServletComponentScan</code><ul>
<li>스프링 부트는 해당 Annotation을 통해서블릿을 직접 등록할 수 있도록 한다</li>
</ul>
</li>
<li><code>@WebServlet</code> 서블릿 애노테이션<ul>
<li>name: 서블릿 이름</li>
<li>urlPatterns: URL 매핑</li>
</ul>
</li>
<li>HTTP요청을 통해 매핑된 URL이 호출되면 서블릿 컨테이너는 다음 메서드를 호출한다.<ul>
<li><code>protected void service(HttpsServletRequest request, HttpServletResponse response)</code></li>
<li>단축키 : ctrl + o</li>
</ul>
</li>
</ul>
<h3 id="http요청-메시지-로그로-확인하기">HTTP요청 메시지 로그로 확인하기</h3>
<ul>
<li>Application.properties<ul>
<li><code>logging.level.org.apache.coyote.http11=debug</code></li>
</ul>
</li>
</ul>
<h2 id="httpservletrequest">HttpServletRequest</h2>
<h3 id="기능">기능</h3>
<ul>
<li>임시 저장소 기능<ul>
<li>해당 HTTP 요청이 시작부터 끝날 때까지 유지되는 임시 저장소 기능<ul>
<li>저장: <code>request.setAttribute(name, value)</code></li>
<li>조회: <code>request.getAttribute(name)</code></li>
</ul>
</li>
</ul>
</li>
<li>세션 관리 기능<ul>
<li><code>request.getSession(create: true)</code></li>
</ul>
</li>
</ul>
<h3 id="기본-사용법-header-조회">기본 사용법, Header 조회</h3>
<ul>
<li><p>조회 코드</p>
<pre><code class="language-java">  //http://localhost:8080/request-header?username=hello
  @WebServlet(name = &quot;requestHeaderServlet&quot;, urlPatterns = &quot;/request-header&quot;)
  public class RequestHeaderServlet extends HttpServlet {
      @Override
      protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          printStartLine(request);
          printHeaders(request);
          printHeaderUtils(request);
          printEtc(request);
          response.getWriter().write(&quot;ok&quot;);
      }

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

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

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

      //기타 정보
      private void printEtc(HttpServletRequest request) {
          System.out.println(&quot;--- 기타 조회 start ---&quot;);
          System.out.println(&quot;[Remote 정보]&quot;);
          System.out.println(&quot;request.getRemoteHost() = &quot; +
                  request.getRemoteHost()); //
          System.out.println(&quot;request.getRemoteAddr() = &quot; +
                  request.getRemoteAddr()); //
          System.out.println(&quot;request.getRemotePort() = &quot; +
                  request.getRemotePort()); //
          System.out.println();
          System.out.println(&quot;[Local 정보]&quot;);
          System.out.println(&quot;request.getLocalName() = &quot; +
                  request.getLocalName()); //
          System.out.println(&quot;request.getLocalAddr() = &quot; +
                  request.getLocalAddr()); //
          System.out.println(&quot;request.getLocalPort() = &quot; +
                  request.getLocalPort()); //
          System.out.println(&quot;--- 기타 조회 end ---&quot;);
          System.out.println();
      }
      }</code></pre>
</li>
</ul>
<h3 id="http-요청-메시지-바디-조회">HTTP 요청 메시지 바디 조회</h3>
<ul>
<li><p>Get - 쿼리 파라미터</p>
<ul>
<li><p><code>getParameter</code> 코드</p>
<pre><code class="language-java">  @WebServlet(name = &quot;requestParamServlet&quot;, urlPatterns = &quot;/request-param&quot;)
  public class RequestParamServlet extends HttpServlet {
      @Override
      protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
          System.out.println(&quot;[전체 파라미터 조회] - start&quot;);
          request.getParameterNames().asIterator()
                  .forEachRemaining(paramName -&gt; System.out.println(paramName + &quot;=&quot; + request.getParameter(paramName)));
          System.out.println();

          System.out.println(&quot;[단일 파라미터 조회]&quot;);
          String username = request.getParameter(&quot;username&quot;);
          String age = request.getParameter(&quot;age&quot;);
          System.out.println(&quot;age = &quot; + age);
          System.out.println(&quot;username = &quot; + username);
          System.out.println();

          System.out.println(&quot;[이름이 같은 복수 파라미터 조회]&quot;);
          String[] usernames = request.getParameterValues(&quot;username&quot;);
          for (String name : usernames) {
              System.out.println(&quot;username = &quot; + name);
          }

          response.getWriter().write(&quot;ok&quot;);

      }
  }</code></pre>
</li>
<li><p>GET URL 쿼리 파라미터 형식은 http 메시지 바디를 사용하지 않기 때문에 content-type이 없다.</p>
</li>
</ul>
</li>
<li><p>Post - HTML Form</p>
<ul>
<li>content-type: application/x-www-form-urlencoded</li>
<li>message body: username=hello&amp;age=20</li>
<li><code>request.getParameter()</code> 는 GET 쿼리 파라미터 형식도 지원하고 POST HTML Form 형식도 지원한다.</li>
</ul>
</li>
<li><p>HTTP API - MessageBody → Postman 테스트</p>
<ul>
<li><p>JSON 형식 전송</p>
<ul>
<li>contenxt-type: application/json</li>
<li>message body: {”username”:”hello”, “age”:”20}</li>
</ul>
</li>
<li><p>코드</p>
<pre><code class="language-java">  @WebServlet(name= &quot;requestBodyJsonServlet&quot;, urlPatterns = &quot;/request-body-json&quot;)
  public class RequestBodyJsonServlet extends HttpServlet {

      private ObjectMapper objectMapper = new ObjectMapper();

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

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

          HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);

          System.out.println(&quot;helloData.getUsername() = &quot; + helloData.getUsername());
          System.out.println(&quot;helloData.getAge() = &quot; + helloData.getAge());
      }
  }</code></pre>
</li>
<li><p><code>req.getInputStream()</code>을 사용해서 인코딩된 바디 메시지를 받아서 <code>StreamUtils</code> 를 통해 디코딩한 바디 메시지를 저장할 수 있다.</p>
</li>
<li><p>JSON 결과를 파싱해서 사용할 수 있는 자바 객체로 변환하려면 먼저 <strong>자바 객체 형식(HelloData.class)</strong>을 먼저 선언해준다</p>
</li>
<li><p>그 후 스프링 부트로 Spring MVC를 선택하면 기본으로 제공하는 <strong>Jackson (<code>ObjectMapper</code>)</strong>이라는 JSON 변환 라이브러리를 사용한다.</p>
</li>
</ul>
</li>
</ul>
<h2 id="httpservletresponse">HttpServletResponse</h2>
<h3 id="기본-사용법-header-조회-1">기본 사용법, Header 조회</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @WebServlet(name = &quot;responseHeaderServlet&quot;, urlPatterns = &quot;/response-header&quot;)
  public class ResponseHeaderServlet extends HttpServlet {

      @Override
      protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          //[Status-line]
          resp.setStatus(HttpServletResponse.SC_OK);

          //[response-headers]
          resp.setHeader(&quot;Content-Type&quot;, &quot;text/plain&quot;);
          resp.setHeader(&quot;Cache-Control&quot;, &quot;no-cache, no-store, must-revalidate&quot;);
          resp.setHeader(&quot;Pragma&quot;, &quot;no-cache&quot;);
          resp.setHeader(&quot;my-header&quot;, &quot;hello&quot;);

          //[Header 편의 메서드]
          content(resp);
          cookie(resp);
          redirect(resp);

          resp.getWriter().write(&quot;ok&quot;);
      }

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

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

      private void redirect(HttpServletResponse response) throws IOException {
          //Status Code 302
          //Location: /basic/hello-form.html
          //response.setStatus(HttpServletResponse.SC_FOUND); //302
          //response.setHeader(&quot;Location&quot;, &quot;/basic/hello-form.html&quot;);
          response.sendRedirect(&quot;/basic/hello-form.html&quot;);
      }
  }</code></pre>
</li>
</ul>
<h3 id="http-응답-메시지-바디-조회">HTTP 응답 메시지 바디 조회</h3>
<ul>
<li><p>HTML 응답</p>
<pre><code class="language-java">  @WebServlet(name = &quot;responseBodyHtmlServlet&quot;, urlPatterns = &quot;/response-html&quot;)
  public class ResponseBodyHtmlServlet extends HttpServlet {
      @Override
      protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          //Content-Type: text/html;charset=utf-8
          resp.setContentType(&quot;text/html&quot;);
          resp.setCharacterEncoding(&quot;utf-8&quot;);

          PrintWriter writer = resp.getWriter();
          writer.println(&quot;&lt;html&gt;&quot;);
          writer.println(&quot;&lt;body&gt;&quot;);
          writer.println(&quot; &lt;div&gt;안녕?&lt;/div&gt;&quot;);
          writer.println(&quot;&lt;/body&gt;&quot;);
          writer.println(&quot;&lt;/html&gt;&quot;);
      }</code></pre>
</li>
<li><p>HTTP API JSON 응답</p>
<pre><code class="language-java">  @WebServlet(name = &quot;responseJsonServlet&quot;, urlPatterns = &quot;/response-json&quot;)
  public class ResponseJsonServlet extends HttpServlet {

      ObjectMapper objectMapper = new ObjectMapper();

      @Override
      protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          //Content-Type: application/json
          resp.setContentType(&quot;application/json&quot;);
          resp.setCharacterEncoding(&quot;utf-8&quot;);

          HelloData helloData = new HelloData();
          helloData.setUsername(&quot;kim&quot;);
          helloData.setAge(20);

          //{&quot;username&quot;:&quot;kim&quot;, &quot;age&quot;:&quot;20}
          String s = objectMapper.writeValueAsString(helloData);
          resp.getWriter().write(s);
      }
  }</code></pre>
</li>
</ul>
<h1 id="✔️-서블릿-jsp-mvc">✔️ 서블릿, JSP, MVC</h1>
<ul>
<li>각각 서블릿, JSP, MVC(서블릿 + JSP)를 사용해서 회원가입 서비스를 만든다.</li>
</ul>
<h2 id="mvc-패턴">MVC 패턴</h2>
<ul>
<li><p>회원 등록 폼 - 컨트롤러</p>
<pre><code class="language-java">  @WebServlet(name = &quot;mvcMemberFormServlet&quot;, urlPatterns = &quot;/servlet-mvc/members/new-form&quot;)
  public class MvcMemberFormServlet extends HttpServlet {
      @Override
      protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
          String viewPath = &quot;/WEB-INF/views/new-form.jsp&quot;;
          RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
          dispatcher.forward(req, resp);
      }
  }</code></pre>
<ul>
<li>WEB/INF<ul>
<li>이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다.</li>
<li>서버 내부에서 호출</li>
</ul>
</li>
<li>Redirect vs Forward<ul>
<li>리다이렉트는 클라이언트에 응답이 나갔다가 클라이언트가 Redirect 경로로 다시 요청한다. 클라이언트가 인지 가능</li>
<li>포워드는 서버 내부에서 일어나는 호출이기 때문에 클라이언트 인지 불가능</li>
</ul>
</li>
</ul>
</li>
<li><p>회원 저장 - 컨트롤러</p>
<pre><code class="language-java">  @WebServlet(name = &quot;mvcMemberSaveServlet&quot;, urlPatterns = &quot;/servlet-mvc/members/ save&quot;)
          public class MvcMemberSaveServlet extends HttpServlet {
          private MemberRepository memberRepository = MemberRepository.getInstance();

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

              String username = request.getParameter(&quot;username&quot;);
              int age = Integer.parseInt(request.getParameter(&quot;age&quot;));
              Member member = new Member(username, age);
              System.out.println(&quot;member = &quot; + member);
              memberRepository.save(member);

              //Model 에 데이터를 보관한다.
              request.setAttribute(&quot;member&quot;, member);
              String viewPath = &quot;/WEB-INF/views/save-result.jsp&quot;;
              RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
              dispatcher.forward(request, response);
              }
           }</code></pre>
<ul>
<li>request.setAttribute()<ul>
<li>request 객체에 데이터를 보관해서 뷰에 전달할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="mvc-컨트롤러의-단점">MVC 컨트롤러의 단점</h3>
<ul>
<li>ViewPath의 중복</li>
<li>Forward 명령어의 중복</li>
<li>공통 처리가 어렵다. (공통으로 처리할 문제들을 메서드로 묶어서 처리하기가 어렵다.)</li>
</ul>
<h1 id="✔️-mvc-프레임워크-만들기">✔️ MVC 프레임워크 만들기</h1>
<h2 id="front-controller-특징">Front Controller 특징</h2>
<ul>
<li>프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음</li>
<li>공통 처리 가능</li>
<li><strong>프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.</strong></li>
</ul>
<h2 id="front-controller-도입---v1">Front Controller 도입 - V1</h2>
<h3 id="기존-상태">기존 상태</h3>
<ul>
<li>모든 컨트롤러에서 WebServlet Annotation을 사용해서 URL 매핑</li>
</ul>
<h3 id="v1">V1</h3>
<ul>
<li>각 로직이 들어갈 컨트롤러들의 인터페이스를 만들어 각 컨트롤러가 해당 인터페이스를 상속받을 수 있게 해서 일관성을 높였다.</li>
<li>프론트 컨트롤러를 두어 URL 매핑을 해주었다.<ul>
<li>해쉬맵에 Key(url), value(controller instance) 형태로 저장해두고</li>
<li>url을 getRequestURI를 통해 받아와 해쉬맵에서 조회한다.</li>
<li>만약 일치하는 Key가 없다면 404 Error 있다면 해당 컨트롤러 호출</li>
</ul>
</li>
</ul>
 <img src="https://velog.velcdn.com/images/yeopju_5/post/cd13e340-8f70-453a-bf12-9f8fe53a6d49/image.png" width="700">

<h2 id="v2">V2</h2>
<h3 id="v2-1">V2</h3>
<ul>
<li>MyView라는 부모 클래스를 만들어서 각 컨트롤러가 이 클래스를 반환하게 만들었다.</li>
<li>FrontController에서 이 클래스의 인스턴스를 받아 render 메서드를 통해 forward한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/a9a522c9-a51e-4a31-80e0-ca11f49e9dba/image.png" width="600">


<h2 id="v3">V3</h2>
<h3 id="서블릿-종속성-제거">서블릿 종속성 제거</h3>
<ul>
<li>FrontController를 제외한 각 컨트롤러는 서블릿을 사용할 필요가 없다.</li>
<li>request 객체 대신 Map 형태로 정보를 받고 Request 반환 값 대신 Model을 만들어 반환한다.</li>
<li>구현 코드가 단순해지고 테스트하기가 쉽다.</li>
</ul>
<h3 id="뷰-이름-중복-제거">뷰 이름 중복 제거</h3>
<ul>
<li>ViewResolver 메서드를 사용해 뷰 이름의 중복된 부분을 제거한다.</li>
<li>ModelView를 통해 뷰의 논리 이름을 전달하고 FrontController에서 변환하여 MyView 객체에 전달한다.</li>
</ul>
<img src="https://velog.velcdn.com/images/yeopju_5/post/73c77e9d-2826-42aa-b09e-459c9e5a0125/image.png" width="600">

<h2 id="v4">V4</h2>
<ul>
<li>기존 구조에서 모델을 파라미터로 넘기고, 컨트롤러에서 뷰의 논리 이름을 반환한다.</li>
<li>ModelView 클래스를 사용하지 않는다.</li>
</ul>
<h2 id="v5">V5</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/61b55392-589d-496f-ae25-dde37af23c57/image.png" width="600">

<h1 id="✔️-스프링-mvc">✔️ 스프링 MVC</h1>
<h2 id="스프링-mvc-전체-구조">스프링 MVC 전체 구조</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/027b518d-0582-46c1-af6d-4e5f1053d7d7/image.png" width="600">

<h3 id="dispacherservlet-서블릿-등록">DispacherServlet 서블릿 등록</h3>
<ul>
<li><code>DispacherServlet</code> 은 <code>HttpServlet</code> 을 상속받아서 사용하고 서블릿으로 동작<ul>
<li>서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.</li>
</ul>
</li>
<li>스프링 부트는 <code>DispacherServlet</code> 을 자동으로 등록하면서 모든 경로(urlPatterns=”/”)에 대해서 매핑 한다.</li>
</ul>
<h3 id="spring-mvc-동작-순서">Spring MVC 동작 순서</h3>
<ol>
<li>Handler 조회</li>
<li>Handler Adapter 조회</li>
<li>Handler Adapter 실행</li>
<li>Handler 실행</li>
<li>ModelAndView 반환</li>
<li>ViewResolver 호출</li>
<li>View 반환</li>
<li>View 랜더링</li>
</ol>
<h3 id="주요-인터페이스">주요 인터페이스</h3>
<ol>
<li>Handler Mapping</li>
<li>Handler Adapter</li>
<li>View Resolver</li>
<li>View</li>
</ol>
<h2 id="핸들러-매핑과-핸들러-어댑터">핸들러 매핑과 핸들러 어댑터</h2>
<h3 id="스프링-부트가-자동-등록하는-핸들러-매핑과-핸들러-어댑터">스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터</h3>
<ul>
<li>HandlerMapping<ul>
<li>0 = RequestMappingHandlerMapping: 애노테이션 기반의 컨트롤러인 <code>@RequestMapping</code> 에서 사용</li>
<li>1 = BeanNameUrlHandlerMapping: 스프링 빈의 이름으로 핸들러를 찾는다.</li>
</ul>
</li>
<li>Handler Adapter<ul>
<li>0 = RequestMappingHandlerAdapter: <code>@RequestMapping</code> 어댑터</li>
<li>1 = HttpRequestHandlerAdapter : HttpRequestHandler 어댑터</li>
<li>2 = SimpleControllerHandlerAdapter: Contoller 인터페이스 어댑터(Annotation X)</li>
</ul>
</li>
</ul>
<h3 id="handlerequest---old-controller-호출-과정">HandleRequest - Old Controller 호출 과정</h3>
<pre><code class="language-java">@Component(&quot;/springmvc/old-controller&quot;)
public class OldController implements Controller {
     @Override
     public ModelAndView handleRequest(HttpServletRequest request,
     HttpServletResponse response) throws Exception {
     System.out.println(&quot;OldController.handleRequest&quot;);
     return null;
     }
}</code></pre>
<ol>
<li>핸들러 매핑으로 핸들러 조회<ol>
<li><code>BeanNameUrlHandlerMapping</code> 을 통해 매핑</li>
<li><code>OldController</code> 반환</li>
</ol>
</li>
<li>핸들러 어댑터 조회</li>
<li>핸들러 어댑터 실행<ol>
<li><code>SimpleControllerHandlerAdapter</code> 를 실행하면서 핸들러 정보도 함께 넘겨준다.</li>
<li><code>SimpleControllerHandlerAdapter</code> 는 핸들러인 <code>OldController</code> 를 내부에서 실행하고 그 결과로 ModelAndView를 반환한다.</li>
</ol>
</li>
</ol>
<h2 id="뷰-리졸버">뷰 리졸버</h2>
<h3 id="스프링-부트가-자주-등록하는-뷰-리졸버">스프링 부트가 자주 등록하는 뷰 리졸버</h3>
<ul>
<li>1 = BeanNameViewResolver: 빈 이름으로 뷰를 찾아서 반환</li>
<li>2 = InternalResourceViewResolver: JSP를 처리할 수 있는 뷰를 반환<ul>
<li>스프링 부트는 <code>InternalResourceViewResolver</code> 라는 뷰 리졸버를 자동으로 등록한다</li>
<li>이 때 <code>[application.properties](http://application.properties)</code> 에 등록한 <code>spring.mvc.view.prefix</code>, <code>spring.mvc.view.suffix</code> 설정 정보를 사용해서 등록한다</li>
</ul>
</li>
</ul>
<h2 id="mvc-적용">MVC 적용</h2>
<h3 id="controller"><code>@Controller</code></h3>
<ul>
<li>스프링이 자동으로 스프링 빈으로 등록한다.</li>
<li>스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.</li>
</ul>
<h3 id="requestmapping"><code>@RequestMapping</code></h3>
<ul>
<li>요청 정보 매핑</li>
<li>애노테이션 기반 작동, 해당 URL 호출 시 이 메서드가 호출된다.</li>
</ul>
<h3 id="getmapping-postmapping"><code>@GetMapping</code>, <code>@PostMapping</code></h3>
<ul>
<li>@GetMapping = @RequestMapping(method=RequestMethod.Get</li>
</ul>
<h1 id="✔️-스프링-mvc-기본기능">✔️ 스프링 MVC 기본기능</h1>
<h2 id="로깅-restcontroller">로깅, RestController</h2>
<ul>
<li>SLF4J라는 인터페이스를 쓰고 구현체로 Logback을 사용한다.</li>
</ul>
<h3 id="선언-방법">선언 방법</h3>
<ul>
<li><code>private Logger log = LoggerFactory.getLogger(getClass());</code></li>
<li><code>@Slf4j</code> Annotation 사용</li>
</ul>
<h3 id="로그-정보">로그 정보</h3>
<ul>
<li><strong>포멧</strong><ul>
<li>시간, 로그 레벨, 프로세스ID, 쓰레드 명, 클래스명, 로그 메시지</li>
</ul>
</li>
<li><strong>로그 레벨</strong><ul>
<li>TRACE &gt; DEBUG &gt; INFO &gt; WARN &gt; ERROR</li>
<li>개발 서버는 DEBUG</li>
<li>운영 서버는 INFO</li>
</ul>
</li>
<li><strong>로그 레벨 수정 방법</strong><ul>
<li><code>[application.properties](http://application.properties)</code> - <code>logging.level.hello,springmvc=debug</code></li>
</ul>
</li>
<li>올바른 로그 사용법<ul>
<li><code>log.debug(&quot;data=&quot; + data)</code> X , <code>log.debug(&quot;data={}, data)</code> O</li>
<li>첫번째 방법으로 진행 시 로그 출력 레벨을 INFO로 설정해도 문자 더하기 연산이 발생한다.</li>
</ul>
</li>
<li><strong>장점</strong><ul>
<li>쓰레드 정보, 클래스 이름과 같은 부가정보를 함께 볼 수 있고 출력 모양 조정 가능</li>
<li>로그 레벨 조절을 통해 로그 출력을 상황에 맞게 조절할 수 있다.</li>
<li>시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다.</li>
<li>성능이 System.out보다 좋다.</li>
</ul>
</li>
</ul>
<h3 id="restcontroller">@RestController</h3>
<ul>
<li>@Controller는 반환 값이 String이면 뷰 이름으로 인식되어 뷰를 찾고 뷰가 랜더링된다.</li>
<li>@RestController는 반환 값으로 뷰를 찾는 것이 아니라 HTTP 메시지 바디에 바로 입력한다.</li>
</ul>
<h2 id="요청-매핑">요청 매핑</h2>
<pre><code class="language-java">        @RequestMapping(&quot;/hello-basic&quot;)
    public String helloBasic(){
        log.info(&quot;helloBasic&quot;);
        return &quot;ok&quot;;
    }

    /**
     * method 특정 HTTP 메서드 요청만 허용
     * GET, HEAD, POST, PUT, PATCH, DELETE
     */
    @RequestMapping(value = &quot;/mapping-get-v1&quot;, method = RequestMethod.GET)
    public String mappingGetV1() {
        log.info(&quot;mappingGetV1&quot;);
        return &quot;ok&quot;;
    }

    /**
     * 편리한 축약 애노테이션 (코드보기)
     * @GetMapping
     * @PostMapping
     * @PutMapping
     * @DeleteMapping
     * @PatchMapping
     */
    @GetMapping(value = &quot;/mapping-get-v2&quot;)
    public String mappingGetV2() {
        log.info(&quot;mapping-get-v2&quot;);
        return &quot;ok&quot;;
    }

    /**
     * PathVariable 사용
     * 변수명이 같으면 생략 가능
     * @PathVariable(&quot;userId&quot;) String userId -&gt; @PathVariable userId
     */
    @GetMapping(&quot;/mapping/{userId}&quot;)
    public String mappingPath(@PathVariable(&quot;userId&quot;) String data) {
        log.info(&quot;mappingPath userId={}&quot;, data);
        return &quot;ok&quot;;
    }

    /**
     * PathVariable 사용 다중
     */
    @GetMapping(&quot;/mapping/users/{userId}/orders/{orderId}&quot;)
    public String mappingPath(@PathVariable String userId, @PathVariable Long
            orderId) {
        log.info(&quot;mappingPath userId={}, orderId={}&quot;, userId, orderId);
        return &quot;ok&quot;;
    }

    /**
     * 파라미터로 추가 매핑
     * params=&quot;mode&quot;,
     * params=&quot;!mode&quot;
     * params=&quot;mode=debug&quot;
     * params=&quot;mode!=debug&quot; (! = )
     * params = {&quot;mode=debug&quot;,&quot;data=good&quot;}
     */
    @GetMapping(value = &quot;/mapping-param&quot;, params = &quot;mode=debug&quot;)
    public String mappingParam() {
        log.info(&quot;mappingParam&quot;);
        return &quot;ok&quot;;
    }

    /**
     * 특정 헤더로 추가 매핑
     * headers=&quot;mode&quot;,
     * headers=&quot;!mode&quot;
     * headers=&quot;mode=debug&quot;
     * headers=&quot;mode!=debug&quot; (! = )
     */
    @GetMapping(value = &quot;/mapping-header&quot;, headers = &quot;mode=debug&quot;)
    public String mappingHeader() {
        log.info(&quot;mappingHeader&quot;);
        return &quot;ok&quot;;
    }

    /**
     * Content-Type 헤더 기반 추가 매핑 Media Type
     * consumes=&quot;application/json&quot;
     * consumes=&quot;!application/json&quot;
     * consumes=&quot;application/*&quot;
     * consumes=&quot;*\/*&quot;
     * MediaType.APPLICATION_JSON_VALUE
     */
    @PostMapping(value = &quot;/mapping-consume&quot;, consumes = &quot;application/json&quot;)
    public String mappingConsumes() {
        log.info(&quot;mappingConsumes&quot;);
        return &quot;ok&quot;;
    }

    /**
     * Accept 헤더 기반 Media Type
     * produces = &quot;text/html&quot;
     * produces = &quot;!text/html&quot;
     * produces = &quot;text/*&quot;
     * produces = &quot;*\/*&quot;
     */
    @PostMapping(value = &quot;/mapping-produce&quot;, produces = &quot;text/html&quot;)
    public String mappingProduces() {
        log.info(&quot;mappingProduces&quot;);
        return &quot;ok&quot;;
    }</code></pre>
<h2 id="http-요청---기본-헤더-조회">HTTP 요청 - 기본, 헤더 조회</h2>
<pre><code class="language-java">@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping(&quot;/headers&quot;)
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap&lt;String, String&gt; headerMap,
                          @RequestHeader(&quot;host&quot;) String host,
                          @CookieValue(value = &quot;myCookie&quot;, required = false) String cookie){
        log.info(&quot;request={}&quot;, request);
        log.info(&quot;response={}&quot;, response);
        log.info(&quot;httpMethod={}&quot;, httpMethod);
        log.info(&quot;locale={}&quot;, locale);
        log.info(&quot;headerMap={}&quot;, headerMap);
        log.info(&quot;header host={}&quot;, host);
        log.info(&quot;myCookie={}&quot;, cookie);
        return &quot;ok&quot;;
    }
}</code></pre>
<ul>
<li>HttpServletResponse</li>
<li>HttpMethod : HTTP 메서드를 조회한다.</li>
<li>Locale : Locale 정보를 조회한다.</li>
<li>@RequestHeader MultiValueMap&lt;String, String&gt; headerMap<ul>
<li>모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다.</li>
</ul>
</li>
<li>@RequestHeader(&quot;host&quot;) String host<ul>
<li>특정 HTTP 헤더를 조회한다.</li>
</ul>
</li>
<li>@CookieValue(value = &quot;myCookie&quot;, required = false) String cookie<ul>
<li>특정 쿠키를 조회한다.</li>
<li>속성<ul>
<li>필수 값 여부: required</li>
<li>기본 값: defaultValue</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="http-요청-파라미터---쿼리-파라미터-modelattribute">HTTP 요청 파라미터 - 쿼리 파라미터, ModelAttribute</h2>
<h3 id="클라이언트에서-서버로-요청-데이터를-전달-할-때-사용되는-방법">클라이언트에서 서버로 요청 데이터를 전달 할 때 사용되는 방법</h3>
<ol>
<li><strong>GET - 쿼리 파라미터</strong><ol>
<li>/url?username=hello&amp;age=20</li>
</ol>
</li>
<li><strong>POST - HTML Form</strong><ol>
<li>content-type: application/x-www-form-urlencoded</li>
<li>메시지 바디에 쿼리 파라미터 형식으로 전달 username=hello&amp;age=20</li>
</ol>
</li>
<li><strong>HTTP message body에 데이터를 직접 담아서 요청</strong><ol>
<li>주로 JSON 사용</li>
</ol>
</li>
</ol>
<p>※ 1번과 2번은 Parameter 조회, 3번은 Body 조회</p>
<h3 id="prameter-조회-방법">Prameter 조회 방법</h3>
<ol>
<li><code>request.getParameter()</code></li>
<li><code>@RequestParam</code><ul>
<li>변수명과 Parameter Key를 같은 이름으로 설정 시 Key 값 생략가능</li>
<li>String, int, Integer 등의 단순 타입이면 <code>@RequestParam</code> 도 생략 가능</li>
<li>기본값이 <code>@RequestParam(required=true)</code></li>
<li>DefaultValue 설정 가능</li>
<li>Map으로 조회<ul>
<li><code>@RequestParam Map&lt;String, Object&gt; paramMap</code></li>
</ul>
</li>
</ul>
</li>
</ol>
<h3 id="modelattribute">@ModelAttribute</h3>
<ul>
<li><p>실제 개발을 하면 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 한다.</p>
<ul>
<li><p>ModelAttribute를 사용하지 않을 경우</p>
<pre><code class="language-java">  @RequestParam String username;
  @RequestParam int age;
  HelloData data = new HelloData();
  data.setUsername(username);
  data.setAge(age);</code></pre>
</li>
<li><p>사용할 경우</p>
<pre><code class="language-java">  @ResponseBody
  @RequestMapping(&quot;/model-attribute-v1&quot;)
  public String modelAttributeV1(@ModelAttribute HelloData helloData) {
   log.info(&quot;username={}, age={}&quot;, helloData.getUsername(),
  helloData.getAge());
   return &quot;ok&quot;;
  }</code></pre>
</li>
</ul>
</li>
<li><p><strong>동작원리</strong></p>
<ul>
<li>HelloData 객체를 생성한다.</li>
<li>HelloData 객체의 포로퍼티를 찾아 setter를 호출해서 파라미터의 값을 바인딩한다.<ul>
<li>ex) 파라미터 이름이 username이면 setUsername() 메서드를 찾아서 호출</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="http-요청-메시지-바디">HTTP 요청 메시지 바디</h2>
<h3 id="text-형식">Text 형식</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @Controller
  public class RequestBodyStringController {

      @PostMapping(&quot;/request-body-string-v1&quot;)
      public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException{
          ServletInputStream inputStream = request.getInputStream();
          String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

          log.info(&quot;messageBody={}&quot;, messageBody);

          response.getWriter().write(&quot;ok&quot;);
      }

      @PostMapping(&quot;/request-body-string-v2&quot;)
      public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException{
          String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
          log.info(&quot;messageBody={}&quot;, messageBody);
          responseWriter.write(&quot;ok&quot;);
      }

      @PostMapping(&quot;/request-body-string-v3&quot;)
      public HttpEntity&lt;String&gt; requestBodyStringV3(HttpEntity&lt;String&gt; httpEntity) throws IOException{
          String messageBody = httpEntity.getBody();
          log.info(&quot;messageBody={}&quot;, messageBody);
          return new HttpEntity&lt;&gt;(&quot;ok&quot;);
      }

      @ResponseBody
      @PostMapping(&quot;/request-body-string-v4&quot;)
      public String requestBodyStringV4(@RequestBody String messageBody) throws IOException{
          log.info(&quot;messageBody={}&quot;, messageBody);
          return &quot;ok&quot;;
      }
  }</code></pre>
</li>
<li><p>HttpEntity : Http header, body 정보를 편리하게 조회</p>
<ul>
<li>메시지 바디 정보를 직접 조회</li>
<li>응답에도 사용 가능</li>
</ul>
</li>
<li><p>RequestEntity</p>
</li>
<li><p>ResponseEntity</p>
<ul>
<li>HTTP 상태 코드 설정 가능</li>
</ul>
</li>
</ul>
<blockquote>
<p>스프링MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, 이때 HTTP 메시지 컨버터라는 기능을 사용한다.</p>
</blockquote>
<h3 id="requestbody">@RequestBody</h3>
<ul>
<li>메시지 바디를 가장 편하게 조회할 수 있는 Annotation<ul>
<li><code>@RequestBody String messageBody</code></li>
</ul>
</li>
</ul>
<h3 id="json-형식">Json 형식</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @Controller
  public class RequestBodyJsonController {
      private ObjectMapper objectMapper = new ObjectMapper();

      @PostMapping(&quot;/request-body-json-v1&quot;)
      public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException{
          ServletInputStream inputStream = request.getInputStream();
          String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

          HelloData data = objectMapper.readValue(messageBody, HelloData.class);
          log.info(&quot;username={}, age ={}&quot;, data.getUsername(), data.getAge());

          response.getWriter().write(&quot;ok&quot;);
      }

      @ResponseBody
      @PostMapping(&quot;/request-body-json-v2&quot;)
      public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException{
          HelloData data = objectMapper.readValue(messageBody, HelloData.class);
          log.info(&quot;username={}, age ={}&quot;, data.getUsername(), data.getAge());
          return &quot;ok&quot;;
      }

      @ResponseBody
      @PostMapping(&quot;/request-body-json-v3&quot;)
      public String requestBodyJsonV3(@RequestBody HelloData data){
          log.info(&quot;username={}, age ={}&quot;, data.getUsername(), data.getAge());
          return &quot;ok&quot;;
      }

      @ResponseBody
      @PostMapping(&quot;/request-body-json-v4&quot;)
      public String requestBodyJsonV4(HttpEntity&lt;HelloData&gt; httpEntity){
          HelloData data = httpEntity.getBody();
          log.info(&quot;username={}, age ={}&quot;, data.getUsername(), data.getAge());
          return &quot;ok&quot;;
      }

      @ResponseBody
      @PostMapping(&quot;/request-body-json-v5&quot;)
      public HelloData requestBodyJsonV5(@RequestBody HelloData data){
          log.info(&quot;username={}, age ={}&quot;, data.getUsername(), data.getAge());
          return data;
      }
  }</code></pre>
</li>
<li><p><code>ObjectMapper</code> : Json 데이터를 자바 객체로 변환할 때 사용</p>
</li>
</ul>
<h3 id="requestbody-1">@RequestBody</h3>
<ul>
<li><code>@RequestBody HelloData data</code>  다음과 같은 형식으로 선언 시 자동으로 data에 메시지 바디가 바인딩된다.<ul>
<li>JSON 응답 → HTTP 메시지 컨버터 → 객체</li>
</ul>
</li>
<li>생략 불가능, 생략 시 ModelAttribute로 인식</li>
</ul>
<h3 id="reponsebody">@ReponseBody</h3>
<ul>
<li>리턴값으로 바디 메시지 사용 가능</li>
<li>객체 리턴 가능<ul>
<li>객체 → HTTP 메시지 컨버터 → JSON 응답</li>
</ul>
</li>
</ul>
<h2 id="http-응답">HTTP 응답</h2>
<h3 id="정적-리소스">정적 리소스</h3>
<ul>
<li>스프링 부트는 클래스 패스의 다음 디렉토리에 있는 정적 리소스를 제공한다</li>
<li>/static, /public, /resources. /META-INF/resources</li>
</ul>
<h3 id="뷰-템플릿">뷰 템플릿</h3>
<ul>
<li><p>경로 : src/main/resources/templetes</p>
</li>
<li><p>뷰 템플릿 호출 컨트롤러 코드</p>
<pre><code class="language-java">  @Controller
  public class ResponseViewController {

      @RequestMapping(&quot;/response-view-v1&quot;)
      public ModelAndView responseView1(){
          ModelAndView mv = new ModelAndView(&quot;response/hello&quot;)
                  .addObject(&quot;data&quot;, &quot;hello!&quot;);
          return mv;
      }
      @RequestMapping(&quot;/response-view-v2&quot;)
      public String responseView2(Model model){
          model.addAttribute(&quot;data&quot;, &quot;hello!&quot;);
          return &quot;response/hello&quot;;
      }

      @RequestMapping(&quot;/response/hello&quot;)
      public void responseView3(Model model){
          model.addAttribute(&quot;data&quot;, &quot;hello!&quot;);
      }
  }</code></pre>
</li>
<li><p>Thymeleaf &lt;appication.properties&gt; 기본값</p>
<pre><code class="language-java">  spring.thymeleaf.prefix=classpath:/templates/
  spring.thymeleaf.suffix=.html</code></pre>
</li>
</ul>
<h3 id="http-메시지">HTTP 메시지</h3>
<ul>
<li><p>코드</p>
<pre><code class="language-java">  @Slf4j
  @Controller
  public class ResponseBodyController {

      @GetMapping(&quot;/response-body-string-v1&quot;)
      public void responseBodyV1(HttpServletResponse response) throws IOException {
          response.getWriter().write(&quot;ok&quot;);
      }

      @GetMapping(&quot;/response-body-string-v2&quot;)
      public ResponseEntity&lt;String&gt; responseBodyV2(){
          return new ResponseEntity&lt;&gt;(&quot;ok&quot;, HttpStatus.OK);
      }

      @ResponseBody
      @GetMapping(&quot;/response-body-string-v3&quot;)
      public String responseBodyV3(){
          return &quot;ok&quot;;
      }

      @GetMapping(&quot;/response-body-json-v1&quot;)
      public ResponseEntity&lt;HelloData&gt; responseBodyJsonV1(){
          HelloData helloData = new HelloData();
          helloData.setUsername(&quot;oh&quot;);
          helloData.setAge(20);
          return new ResponseEntity&lt;&gt;(helloData, HttpStatus.CREATED);
      }

      @ResponseStatus(HttpStatus.OK)
      @ResponseBody
      @GetMapping(&quot;/response-body-json-v2&quot;)
      public HelloData responseBodyJsonV2(){
          HelloData helloData = new HelloData();
          helloData.setUsername(&quot;oh&quot;);
          helloData.setAge(20);
          return helloData;
      }
  }</code></pre>
</li>
<li><p><strong>responseBodyV1</strong></p>
<ul>
<li>서블릿을 직접 다룰 때 처럼 HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달한다.</li>
<li>response.getWriter().write(&quot;ok&quot;)</li>
</ul>
</li>
<li><p><strong>responseBodyV2</strong></p>
<ul>
<li>ResponseEntity 엔티티는 HttpEntity 를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다. ResponseEntity 는 여기에 더해서 HTTP 응답 코드를 설정할 수 있다.</li>
<li>HttpStatus.CREATED 로 변경하면 201 응답이 나가는 것을 확인할 수 있다.</li>
</ul>
</li>
<li><p><strong>responseBodyV3</strong></p>
<ul>
<li>@ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력할 수 있다. ResponseEntity 도 동일한 방식으로 동작한다.</li>
</ul>
</li>
<li><p><strong>responseBodyJsonV1</strong></p>
<ul>
<li>ResponseEntity 를 반환한다. HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환된다.</li>
</ul>
</li>
<li><p><strong>responseBodyJsonV2</strong></p>
<ul>
<li>ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 이런 것을 설정하기 까다롭다.</li>
<li>@ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정할 수 있다.
물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다. <strong>프로그램 조건에 따라서 동적으로 변경하려면 ResponseEntity 를 사용하면 된다.</strong></li>
</ul>
</li>
</ul>
<h3 id="restcontroller-1">RestController</h3>
<ul>
<li>@Controller + @ResponseBody</li>
<li>클래스 레벨에 두면 전체 메서드에 적용</li>
</ul>
<h2 id="http-메시지-컨버터">HTTP 메시지 컨버터</h2>
<h3 id="0--bytearrayhttpmessageconverter">0 = ByteArrayHttpMessageConverter</h3>
<ul>
<li>바이트 데이터를 처리한다 <code>data[]</code></li>
<li>클래스 타입: <code>data[]</code>, 미디어 타입: <code>*/*</code></li>
</ul>
<h3 id="1--stringhttpmessageconverter">1 = StringHttpMessageConverter</h3>
<ul>
<li>String 문자로 데이터를 처리한다.</li>
<li>클래스 타입: <code>String</code>, 미디어 타입: <code>*/*</code></li>
</ul>
<h3 id="2--mappingjackson2httpmessageconverter">2 = MappingJackson2HttpMessageConverter</h3>
<ul>
<li>클래스 타입: 객체 또는 <code>HashMap</code></li>
<li>미디어 타입: <code>application/json</code> 관련</li>
</ul>
<pre><code class="language-java">content-type: application/json                                                 EX
@RequestMapping
void hello(@RequestBody String data) {}              -&gt; StringHttpMessageConverter</code></pre>
<h3 id="http-요청-데이터-읽기">HTTP 요청 데이터 읽기</h3>
<ul>
<li>메시지 컨버터가 canRead() 를 호출하고 대상 클래스 타입을 지원하는지 확인한 후 HTTP 요청의 Content-Type 미디어 타입을 지원하는지 확인한다.</li>
<li>조건을 만족하면 read() 를 호출해서 객체를 생성하고 반환한다.</li>
</ul>
<h3 id="http-응답-데이터-읽기">HTTP 응답 데이터 읽기</h3>
<ul>
<li>메시지 컨버터가 canWrite() 를 호출하고 대상 클래스 타입을 지원하는지 확인한 후 HTTP 응답의 Accept 미디어 타입을 지원하는지 확인한다.</li>
<li>조건을 만족하면 write() 를 호출해서 HTTP 메시지 바디에 데이터를 생성한다.</li>
</ul>
<h2 id="요청-매핑-핸들러-구조">요청 매핑 핸들러 구조</h2>
<img src="https://velog.velcdn.com/images/yeopju_5/post/92b27308-7430-4061-a0cf-288ff6e7fc79/image.png" width="600">


<h3 id="argumentresolver-handlermethodargumentresolver">ArgumentResolver (HandlerMethodArgumentResolver)</h3>
<ul>
<li>애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있는데 이를 유연하게 처리해주는게 ArgumentResolver이다<ul>
<li><code>HttpServletRequest</code>, <code>Model</code>, <code>@RequestParam</code>, <code>@ModelAttribute</code>, <code>HttpEntity</code></li>
</ul>
</li>
</ul>
<h3 id="returnvaluehandler-handlermethodreturnvaluehandler">ReturnValueHandler (HandlerMethodReturnValueHandler)</h3>
<blockquote>
<p>요청의 경우 <code>@RequestBody</code>와 <code>@HttpEntity</code>를 처리하는 <code>ArgumentResolver</code> 가 있는데 이는 바디의 메시지를 받아와 처리해야하기 때문에 이 <code>ArgumentResolver</code> 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다.</p>
</blockquote>
<blockquote>
<p>응답의 경우 <code>@ReponseBody</code>와 <code>@HttpEntity</code>를 처리하는 <code>ReturnValueHandler</code> 가 있는데 여기서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.</p>
</blockquote>
<h3 id="기능-확장">기능 확장</h3>
<ul>
<li><p>스프링이 필요한 대부분의 기능을 제공하지만</p>
</li>
<li><p>기능확장을 위해서는 <code>WebMvcConfigurer</code> 를 상속 받아서 스프링 빈으로 등록하면 된다.</p>
<pre><code class="language-java">  @Bean
  public WebMvcConfigurer webMvcConfigurer() {
   return new WebMvcConfigurer() {
   @Override
   public void addArgumentResolvers(List&lt;HandlerMethodArgumentResolver&gt;
  resolvers) {
   //...
   }
   @Override
   public void extendMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt;
  converters) {
   //...
   }
   };
  }</code></pre>
</li>
</ul>
<h1 id="✔️-그-외">✔️ 그 외</h1>
<h3 id="modelattribute-1">@ModelAttribute</h3>
<ul>
<li><code>@ModelAttribute(&quot;name&quot;)</code> 다음과 같이 선언 시<ul>
<li><code>model.addAttribute(”name”, 객체)</code> 자동 실행</li>
</ul>
</li>
<li>“name” 생략 시 첫 글자를 소문자로 바꾼 클래스명이 name으로 자동 설정</li>
<li>data 타입이 객체나 Map일 경우 @ModelAttribute 생략가능</li>
<li></li>
</ul>
<h3 id="prgpost-redirect-get">PRG(Post-Redirect-Get)</h3>
<img src="https://velog.velcdn.com/images/yeopju_5/post/b5f22e70-d658-4dd2-940b-126834855059/image.png" width="600">


<p>그림과 같이 Redirect를 사용하지 않고 View를 반환값으로 띄워준 상태에서 새로고침을 하면 PostRequest가 계속해서 실행된다.</p>
<p>→ 이를 방지하기 위해 Redirect를 통해 Get Method를 호출해준다.</p>
<h3 id="redirectattributes">RedirectAttributes</h3>
<pre><code class="language-java">@PostMapping(&quot;/add&quot;)
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes){
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute(&quot;itemId&quot;, savedItem.getId());
        redirectAttributes.addAttribute(&quot;status&quot;, true);
        // model.addAttribute(&quot;item&quot;, item);
        return &quot;redirect:/basic/items/{itemId}&quot;;
    }</code></pre>
<ul>
<li>addAttribute한 값이 URL에서 사용되지 않는 나머지 값들은 Query값으로 들어간다.</li>
<li>redirectAttribute를 사용하면 인코딩을 자동으로 해준다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[🌱 스프링 원리 (기본)]]></title>
            <link>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8</link>
            <guid>https://velog.io/@yeopju_5/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8</guid>
            <pubDate>Mon, 29 May 2023 16:58:19 GMT</pubDate>
            <description><![CDATA[<h2 id="✔️-스프링-핵심원리-이해2---객체-지향-원리-적용">✔️ 스프링 핵심원리 이해2 - 객체 지향 원리 적용</h2>
<p>다형성을 고려해서 인터페이스와 객체를 구분하여 코드를 구현했지만 객체를 갈아끼우는 과정에서 DIP와 OCP가 지켜지지않는 문제가 생긴다. 이 문제를 해결하기 위한 과정을 코드를 통해 이해하는 챕터</p>
<p>→ 개방 폐쇄 원칙인 OCP(확장에는 열려있어야 하지만 변경에는 닫혀 있어야 한다.)와 의존 역전 원칙인 DIP(객체는 고차원의 모듈에만 의존할 뿐 저차원의 모듈에 대해서 의존해서는 안된다)를 지키기 위해서 AppConfig를 사용해 의존 관계 주입을 해준다.</p>
<ul>
<li><p><strong>AppConfig</strong></p>
<p>  애플리케이션의 <strong>전체 동작 방식을 구성하기 위해 구현 객체를 생성하고, 연결하는</strong> 책임을 가지는 별도의 설정 클래스</p>
<p>  AppConfig에서 각 구현 객체를 생성하며 이를 <strong>생성자를 통해 주입시켜준다(생성자 주입)</strong></p>
<p>  이에 따라 구현 클래스들은 구현체에 의존하지 않으며 어떤 구현 객체를 주입할지는 오직 외부에서 결정된다.</p>
</li>
</ul>
<h3 id="좋은-객체-지향-설계의-5가지-원칙-적용-solid">좋은 객체 지향 설계의 5가지 원칙 적용 SOLID</h3>
<ul>
<li><strong>SRP</strong> 단일 책임 원칙 - 한 클래스는 하나의 책임만 가져야 한다.<ul>
<li>구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당</li>
<li>클라이언트 객체는 실행하는 책임만 담당</li>
</ul>
</li>
<li><strong>DIP 의존관계 역전 원칙 - 추상화에 의존해야지, 구체화에 의존하면 안된다.</strong><ul>
<li>AppConfig가 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입 ( 생성자 주입 )</li>
</ul>
</li>
<li>OCP 개방 폐쇄 원칙 - 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.<ul>
<li>다형성을 사용하고 클라이언트가 DIP를 지킴</li>
<li>애플리케이션을 사용영역과 구성영역으로 나눔</li>
<li>소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.</li>
</ul>
</li>
</ul>
<h3 id="ioc-di-컨테이너">IoC, DI, 컨테이너</h3>
<ul>
<li>IoC (Inversion of Control) 제어의 역전<ul>
<li>기존의 프로그램에서는 클라이언트 코드에서 객체의 생성과 주입을 관리하는게 일반적이었다. 하지만 DIP와 OCP가 지켜지지 않는 문제가 있어 제어의 흐름을 AppConfig에게 전임해 이 DI컨테이너가 객체의 생성과 연결, 즉 애플리케이션의 전체 구성을 관리한다.</li>
<li>이렇듯 제어의 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.</li>
</ul>
</li>
<li>DI (Dependency Injection)<ul>
<li>정적인 클래스 의존 관계와 동적인 객체 인스턴스 의존관계<ul>
<li>정적인 클래스 의존관계는 해당 클래스의 Import 내역들을 보면 파악할 수 있다.</li>
<li>동적인 객체 인스턴스 의존관계는 애플리케이션 실행 시점(런타임)에 객체가 생성, 참조가 연결된 의존관계이다</li>
</ul>
</li>
<li>애플리케이션 실행 시점(런타임)에 외부에서 객체 인스턴스를 생성하고 클라이언트에 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 한다.</li>
<li>의존관계 주입을 사용하면 정적인 클래스 의존관계를 수정하지 않고 쉽게 동적인 객체 인스턴스 의존관계를 변경할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="✔️-스프링-컨테이너와-스프링-빈">✔️ 스프링 컨테이너와 스프링 빈</h2>
<h3 id="스프링-적용">스프링 적용</h3>
<ol>
<li>AppConfig 클래스에 <code>@Configuration</code> Annotation을 붙이고 각 메서드에 <code>@Bean</code> Annotation을 붙인다.</li>
<li>이전에는 필요한 객체를 부를 때 AppConfig를 선언해 직접 불러왔지만 이제는 스프링 컨테이너를 이용해서 필요한 스프링 빈(객체)를 찾아야 한다.</li>
</ol>
<pre><code class="language-java">//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean(&quot;memberService&quot;, MemberService.class);</code></pre>
<ul>
<li>ApplicationContext를 스프링 컨테이너라고 한다.</li>
<li>AnnotationConfigApplicationContext는 ApplicationContext 인터페이스의 구현체이다.</li>
</ul>
<h3 id="스프링-빈-조회">스프링 빈 조회</h3>
<ul>
<li><p>예제코드</p>
<pre><code class="language-java">  package hello.core.beanfind;

  import hello.core.discount.DiscountPolicy;
  import hello.core.discount.FixDiscountPolicy;
  import hello.core.discount.RateDiscountPolicy;
  import org.junit.jupiter.api.Assertions;
  import org.junit.jupiter.api.DisplayName;
  import org.junit.jupiter.api.Test;
  import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
  import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  import org.springframework.context.annotation.Bean;
  import org.springframework.context.annotation.Configuration;

  import java.util.Map;

  public class ApplicationContextExtendsFindTest {

      AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

      @Test
      @DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.&quot;)
      void findBeanByParentTypeDuplicate(){
          Assertions.assertThrows(NoUniqueBeanDefinitionException.class,
                  () -&gt; ac.getBean(DiscountPolicy.class));
      }

      @Test
      @DisplayName(&quot;부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다.&quot;)
      void findByParentTypeBeanName() {
          DiscountPolicy bean = ac.getBean(&quot;RateDiscountPolicy&quot;,DiscountPolicy.class);
          org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
      }

      @Test
      @DisplayName(&quot;특정 하위 타입으로 조회&quot;)
      void findBeanBySubType(){
          RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
          org.assertj.core.api.Assertions.assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
      }

      @Test
      @DisplayName(&quot;부모 타입으로 모두 조회하기&quot;)
      void findBeanByParentType() {
          Map&lt;String, DiscountPolicy&gt; beansOfType = ac.getBeansOfType(DiscountPolicy.class);
          org.assertj.core.api.Assertions.assertThat(beansOfType.size()).isEqualTo(2);
          for (String key : beansOfType.keySet()) {
              System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
          }
      }

      @Test
      @DisplayName(&quot;부모 타입으로 모두 조회하기 - Object&quot;)
      void findAllBeanByObjectType() {
          Map&lt;String, Object&gt; beansOfType = ac.getBeansOfType(Object.class);
          for (String key : beansOfType.keySet()) {
              System.out.println(&quot;key = &quot; + key + &quot; value = &quot; + beansOfType.get(key));
          }
      }

      @Configuration
      static class TestConfig{

          @Bean
          public DiscountPolicy RateDiscountPolicy(){
              return new RateDiscountPolicy();
          }

          @Bean
          public DiscountPolicy FixDiscountPolicy(){
              return new FixDiscountPolicy();
          }
      }
  }</code></pre>
</li>
</ul>
<p>※ static 을 붙이지 않은 inner class는 outer class에 종속된다. 이런 경우 outer class의 객체를 선언한 후 inner class를 사용할 수 있다. 하지만 static을 붙일 경우 inner class임에도 불구하고 독립적인 객체로 선언이 가능하다.  → Static inner class는 해당 outer class 내에서 선언하여 이용하려는 경우 사용</p>
<h3 id="beanfactory와-applicationcontext">BeanFactory와 ApplicationContext</h3>
<ul>
<li>BeanFactory<ul>
<li>스프링 컨테이너의 최상위 인터페이스</li>
<li>스프링 빈을 관리하고 조회하는 역할을 담당</li>
</ul>
</li>
<li>ApplicationContext<ul>
<li>BeanFactory의 기능을 모두 상속받는다</li>
<li>애플리케이션 개발에 필요한 부가기능들이 Extends 되어있다.<ul>
<li>메시지 소스를 활용한 국제화 기능</li>
<li>환경변수</li>
<li>애플리케이션 이벤트 - 이벤트를 발행하고 구독하는 모델을 편리하게 조회</li>
<li>편리한 리소스 조회  - 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="beandefinition">BeanDefinition</h3>
<ul>
<li>스프링은 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용한다.</li>
</ul>
<h2 id="✔️-싱글톤-패턴">✔️ 싱글톤 패턴</h2>
<ul>
<li>클래스의 인스턴스가 1개만 생성되도록 보장하는 디자인 패턴</li>
<li>싱글톤 패턴을 사용하지 않고 유저의 요청이 있을 때마다 인스턴스를 생성한다면 메모리 효율성이 떨어지게 된다.</li>
</ul>
<h3 id="스프링을-사용하지-않고-싱글톤-패턴-만들기">스프링을 사용하지 않고 싱글톤 패턴 만들기</h3>
<pre><code class="language-java">package hello.core.singleton;
public class SingletonService {
 //1. static 영역에 객체를 딱 1개만 생성해둔다.
 private static final SingletonService instance = new SingletonService();
 //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록
허용한다.
 public static SingletonService getInstance() {
 return instance;
 }
 //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
 private SingletonService() {
 }
 public void logic() {
 System.out.println(&quot;싱글톤 객체 로직 호출&quot;);
 }</code></pre>
<ul>
<li>싱글톤 패턴을 구현하는 방법은 여러가지이다. 여기서 사용한 방법은 객체를 미리 생성해두는 가장 안전하고 단순한 방법이다.</li>
<li>스프링을 사용하지 않고 싱글톤 패턴을 사용할 경우 다음과 같은 문제점들이 있다.<ul>
<li>싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.</li>
<li>의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.</li>
<li>클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.</li>
<li>테스트하기 어렵다.</li>
<li>내부 속성을 변경하거나 초기화 하기 어렵다.</li>
<li>private 생성자로 자식 클래스를 만들기 어렵다.</li>
<li>결론적으로 유연성이 떨어진다.</li>
<li>안티패턴으로 불리기도 한다</li>
</ul>
</li>
</ul>
<h3 id="스프링-컨테이너-→-싱글톤-컨테이너">스프링 컨테이너 (→ 싱글톤 컨테이너)</h3>
<ul>
<li>스프링 컨테이너 (ApplicationContext or AnnotationConfigApplicationContext)를 사용하면 위와 같은 코드가 필요없이 자동으로 싱글톤으로 관리된다.</li>
<li>싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.</li>
<li>DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.</li>
</ul>
<h3 id="싱글톤-방식의-주의점">싱글톤 방식의 주의점</h3>
<ul>
<li>싱글톤 방식은 여러 클라이언트가 하나의 객체를 공유하기 때문에 싱글톤 객체는 상태를 유지(Stateful)하게 설계하면 안된다.</li>
<li>무상태로 설계해야한다!!<ul>
<li>특정 클라이언트에 의존적인 필드가 있으면 안된다.</li>
<li>특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!</li>
<li>가급적 읽기만 가능해야 한다.</li>
<li>필드 대신에 자바에서 공유되지 않는, <strong>지역변수, 파라미터, ThreadLocal</strong> 등을 사용해야 한다.</li>
</ul>
</li>
</ul>
<p>공유필드를 조심하고 항상 무상태로 설계하자!</p>
<h3 id="configuration과-싱글톤">@Configuration과 싱글톤</h3>
<ul>
<li><code>@Configuration</code> 를 붙이면 바이트 코드를 조작하는 라이브러리인 CGLIB을 사용하여 내가 만든 AppConfig를 상속받은 임의의 자식 클래스를 만들고, 그 클래스를 스프링 빈으로 등록한다.</li>
<li>이 임의의 자식 클래스가 싱글톤을 보장해준다.</li>
</ul>
<h2 id="✔️-컴포넌트-스캔">✔️ 컴포넌트 스캔</h2>
<h3 id="컴포넌트-스캔-의존관계-자동-주입">컴포넌트 스캔, 의존관계 자동 주입</h3>
<ul>
<li><p>지금까지는 자바코드에 Bean Annotation 또는 XML파일의 <bean/>을 사용해 스프링 빈을 등록해주었다. 하지만 스프링 빈의 개수가 많아짐에 따라 일일이 등록하기에는 효율성이 떨어진다.</p>
<p>  → 그래서 스프링은 설정정보 없이도 자동으로 스프링 빈을 등록해주는 컴포넌트 스캔이라는 기능을 제공한다. 또한 의존관계 또한 자동으로 주입해주는 @Autowired 라는 기능을 제공한다.</p>
</li>
</ul>
<ol>
<li>설정 정보에 <code>@ComponentScan</code>을 붙여준다.</li>
<li>스프링 빈으로 등록하고 싶은 클래스에 <code>@Component</code> 를 붙여준다</li>
<li>의존관계 주입을 위해 해당 클래스의 생성자에 <code>@Autowired</code> 를 붙여준다.</li>
</ol>
<p>※ <code>@Configuration</code> 소스코드를 열어보면 <code>@Component</code> Annotation이 붙어있기 때문에 <code>@Configuration</code> 또한 컴포넌트 스캔의 대상이 된다. 이를 막기 위해서는 다음과 같이 설정해주면 된다.</p>
<pre><code class="language-java">@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}</code></pre>
<ul>
<li><p>@ComponentScan</p>
<ul>
<li><p>@Component가 붙은 클래스를 스프링 빈으로 등록하는데 이때 스프링 빈의 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.</p>
</li>
<li><p>빈 이름 직접 지정: <code>@Component(&quot;memberService2&quot;)</code></p>
</li>
<li><p>모든 자바 클래스들을 컴포넌트 스캔하면 시간이 오래 걸리기 때문에 필요한 위치부터 탐색하도록 시작 위치를 지정 할 수 있다</p>
<pre><code class="language-java">  @ComponentScan(
          basePackages = &quot;hello.core&quot;, // {&quot;hello.core&quot;, &quot;hello.service&quot;} 여러 위치 지정
  )</code></pre>
</li>
<li><p><code>basePackageClasses</code> : 지정한 클래스의 패키지를 탐색 시작 위치로 지정</p>
</li>
<li><p>Default : @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치</p>
</li>
</ul>
</li>
<li><p>@Autowired</p>
<ul>
<li>@Autowired 지정시 스프링 컨테이너가 자동으로 타입이 같은 빈을 찾아서 주입해준다.</li>
</ul>
</li>
<li><p>Component Annotation이 있는 Annotation들의 기능</p>
<ul>
<li>@Controller : 스프링 MVC 컨트롤러로 인식</li>
<li>@Repository : 스프링 데이터 접근 계층으로 인식, 데이터 계층의 예외를 스프링 예외로 변환 → DB의 변경을 자유롭게 하기 위해</li>
<li>@Configuration : 스프링 설정 정보로 인식, 스프링 빈이 싱글톤을 유지하도록 처리</li>
<li>@Service : 개발자들이 비즈니스 계층을 인식하는데 도움이 된다.</li>
</ul>
</li>
</ul>
<h3 id="스프링-빈-중복-등록">스프링 빈 중복 등록</h3>
<ol>
<li>자동 빈 등록 vs 자동 빈 등록<ul>
<li>스프링에서 <code>ConflictingBeanDefinitionException</code> 예외 발생</li>
</ul>
</li>
<li>수동 빈 등록 vs 자동 빈 등록<ul>
<li>수동 빈이 자동 빈을 오버라이딩하기 때문에 수동 빈이 우선 순위를 가진다.</li>
<li>하지만 스프링 부트에서는 오류가 나도록 기본 값을 설정해두었다.</li>
<li><a href="http://application.properties">application.properties</a> 파일에 오류발생이 나오는 로그인 <code>spring.main.allow-bean-definition-overriding=true</code> 를 추가하면 오류가 나지않고 오버라이딩이 되도록 설정할 수 있다.</li>
</ul>
</li>
</ol>
<h3 id="필터">필터</h3>
<ul>
<li>컴포넌트 스캔의 대상을 추가, 제외 시킨다.</li>
</ul>
<pre><code class="language-java">@Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{
    }</code></pre>
<ul>
<li><p>IncludeFilter로 사용할 Annotation과 ExcludeFilter로 사용할 Annotation를 각각 선언한 후 제외 또는 추가하고 싶은 클래스에 해당 어노테이션을 붙인다.</p>
<pre><code class="language-java">  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface MyIncludeComponent {
  }</code></pre>
</li>
</ul>
<h2 id="✔️-의존관계-자동-주입">✔️ 의존관계 자동 주입</h2>
<h3 id="다양한-의존관계-주입-방법">다양한 의존관계 주입 방법</h3>
<ul>
<li><p>생성자 주입</p>
<ul>
<li>생성자 호출 시점에 딱 1번만 호출되는 것이 보장</li>
<li><strong>불변, 필수 의존관계</strong>에 사용</li>
<li>※ 생성자가 1개일 경우 @Autowired를 생략해도 자동 주입</li>
</ul>
</li>
<li><p>수정자 주입 (Setter 주입)</p>
<ul>
<li><p>수정자(Setter) 메서드를 통해 의존관계를 주입하는 방법</p>
</li>
<li><p><strong>선택, 변경 가능성</strong>이 있는 의존관계에 사용</p>
</li>
<li><p>@Autowired의 기본동작은 주입할 대상이 없으면 오류를 발생시킨다. 대상이 없어도 동작하게 하기 위해서는 <code>@Autowired(required=false)</code></p>
<pre><code class="language-java">private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository){
          this.memberRepository = memberRepository;
}</code></pre>
</li>
</ul>
</li>
<li><p>필드 주입</p>
<ul>
<li><p>코드가 간결하지만 외부에서 변경이 불가능하여 테스트하기가 어렵다.</p>
</li>
<li><p>DI 프레임워크가 없으면 아무것도 할 수 없다.</p>
<p>  → 스프링 컨테이너를 사용하지 않은 순수 자바코드로는 테스트가 불가능하다. 가능하게 하기 위해서는 Setter 메서드를 따로 선언해주어야 한다.</p>
</li>
</ul>
</li>
<li><p>일반 메서드 주입</p>
<ul>
<li>잘 사용하지 않는다.</li>
</ul>
</li>
</ul>
<h3 id="autowired-옵션-처리">Autowired 옵션 처리</h3>
<ul>
<li><code>@Autowired(required=false)</code> : 자동 주입할 대상이 없으면 수정자 메서드가 호출되지 않는다.</li>
<li><code>@Nullable</code> : 자동 주입할 대상이 없으면 null이 입력된다.</li>
<li><code>Optional&lt;&gt;</code> : 자동 주입할 대상이 없으면 Optional.empty가 입력된다.</li>
</ul>
<h3 id="생성자-주입을-선택해라">생성자 주입을 선택해라!</h3>
<ul>
<li>불변<ul>
<li>대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.</li>
<li>수정자 주입을 위해서는 set 메서드를 열어두어야 하는데, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.</li>
</ul>
</li>
<li>수정자 주입을 한 상태에서 순수한 자바 코드를 단위 테스트 하는 경우 컴파일 오류가 발생한다.</li>
<li>final 키워드를 사용해 컴파일 오류를 일으킬 수 있다.<ul>
<li>컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!</li>
</ul>
</li>
</ul>
<h3 id="롬복과-최신-트렌드">롬복과 최신 트렌드</h3>
<ul>
<li><p>롬복 라이브러리 적용 방법</p>
<p>  build.gradle 에 라이브러리 및 환경 추가</p>
<pre><code class="language-java">
  plugins {
  id &#39;org.springframework.boot&#39; version &#39;2.3.2.RELEASE&#39;
  id &#39;io.spring.dependency-management&#39; version &#39;1.0.9.RELEASE&#39;
  id &#39;java&#39;
  }
  group = &#39;hello&#39;
  version = &#39;0.0.1-SNAPSHOT&#39;
  sourceCompatibility = &#39;11&#39;
  //lombok 설정 추가 시작
  configurations {
  compileOnly {
  extendsFrom annotationProcessor
  }
  }
  //lombok 설정 추가 끝
  repositories {
  mavenCentral()
  }
  dependencies {
  implementation &#39;org.springframework.boot:spring-boot-starter&#39;

  //lombok 라이브러리 추가 시작
  compileOnly &#39;org.projectlombok:lombok&#39;
  annotationProcessor &#39;org.projectlombok:lombok&#39;
  testCompileOnly &#39;org.projectlombok:lombok&#39;
  testAnnotationProcessor &#39;org.projectlombok:lombok&#39;
  //lombok 라이브러리 추가 끝

  testImplementation(&#39;org.springframework.boot:spring-boot-starter-test&#39;) {
  exclude group: &#39;org.junit.vintage&#39;, module: &#39;junit-vintage-engine&#39;
  }
  }
  test {
  useJUnitPlatform()
  }</code></pre>
<ol>
<li>Preferences(윈도우 File Settings) plugin lombok 검색 설치 실행 (재시작)</li>
<li>Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)</li>
<li>임의의 테스트 클래스를 만들고 @Getter, @Setter 확인</li>
</ol>
</li>
<li><p>롬복 라이브러리가 제공하는 @RequiredArgsConstructor Annotation을 사용하면 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.</p>
</li>
</ul>
<h3 id="의존관계-주입-시-조회-빈이-2개-이상일-경우----autowired-필드명--qualifier-primary">의존관계 주입 시 조회 빈이 2개 이상일 경우 -  @Autowired 필드명 , @Qualifier, @Primary</h3>
<ul>
<li><p>문제 상황</p>
<ul>
<li>@Autowired는 타입으로 조회 → <code>ac.getBean(DiscountPolicy.class)</code> 와 유사하게 동작</li>
<li>DiscountPolicy를 통해 구체화된 빈이 두개 이상 있을 경우 의존관계 자동 주입 시 <code>NoUniqueBeanDefinitionException</code> 오류 발생</li>
<li>하위 타입(구체화 타입)을 지정할 수도 있지만 DIP를 위배하고 유연성이 떨어진다.</li>
</ul>
</li>
<li><p>해결 방법</p>
<ul>
<li><p><strong>Autowired 필드 명 매칭</strong></p>
<ul>
<li><p>Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있을 경우 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.</p>
<p>  → 필드 이름이나 파라미터 이름을 주입하고 싶은 클래스 이름과 똑같이 설정한다면 해당 클래스로 매칭된다.</p>
</li>
</ul>
</li>
<li><p><strong>@Qualifier</strong></p>
<ol>
<li><p>빈 등록시 @Qualifier를 붙여준다.</p>
<pre><code class="language-java"> @Component
 @Qualifier(&quot;mainDiscountPolicy&quot;)
 public class RateDiscountPolicy implements DiscountPolicy {}</code></pre>
</li>
<li><p>주입 시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.</p>
<pre><code class="language-java"> @Autowired
 public OrderServiceImpl(MemberRepository memberRepository, 
                                     @Qualifier(&quot;mainDiscountPolicy&quot;) DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
 }</code></pre>
</li>
</ol>
</li>
<li><p><strong>@Primary</strong></p>
<ul>
<li>@Primary Annotation이 붙은 클래스가 우선권을 가진다.</li>
</ul>
</li>
<li><p>Qualifier와 Primary 중 Qualifier가 우선순위가 높다.</p>
</li>
</ul>
</li>
</ul>
<h3 id="annotation-만들기">Annotation 만들기</h3>
<pre><code class="language-java">@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier(&quot;mainDiscountPolicy&quot;)
public @interface MainDiscountPolicy {
}</code></pre>
<h3 id="조회한-빈이-모두-필요할-때-list-map">조회한 빈이 모두 필요할 때 List, Map</h3>
<p>예) 할인 서비스를 제공하는데, 클라이언트가 할인의 종류를 선택할 수 있도록 하는 경우</p>
<ul>
<li><p>코드 보고 이해하기!!</p>
<pre><code class="language-java">  public class AllBeanTest {

      @Test
      void findAllBean() {
          AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
          DiscountService discountService = ac.getBean(DiscountService.class);
          Member member = new Member(1L, &quot;name&quot;, Grade.VIP);
          int discountPrice = discountService.discount(member, 10000, &quot;fixDiscountPolicy&quot;);

          Assertions.assertThat(discountPrice).isEqualTo(1000);

          int rateDiscountPrice = discountService.discount(member, 20000, &quot;rateDiscountPolicy&quot;);

          Assertions.assertThat(rateDiscountPrice).isEqualTo(2000);
      }

      static class DiscountService {
          private final Map&lt;String, DiscountPolicy&gt; policyMap;
          private final List&lt;DiscountPolicy&gt; policies;

          @Autowired
          public DiscountService(Map&lt;String, DiscountPolicy&gt; policyMap, List&lt;DiscountPolicy&gt; policies) {
              this.policyMap = policyMap;
              this.policies = policies;
              System.out.println(&quot;policyMap = &quot; + policyMap);
              System.out.println(&quot;policies = &quot; + policies);
          }

          public int discount(Member member, int price, String discountCode) {
              DiscountPolicy discountPolicy = policyMap.get(discountCode);
              return discountPolicy.discount(member, price);
          }
      }
  }</code></pre>
</li>
</ul>
<h3 id="실무에서의-자동-수동-등록">실무에서의 자동, 수동 등록</h3>
<ul>
<li>편리한 자동 기능을 기본으로 사용</li>
<li>웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 리포지토리 등의 <strong>업무로직 빈은 자동 등록</strong>, 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용되는 <strong>기술 지원 빈은 수동 등록</strong></li>
<li>비즈니스 로직 중에 다형성을 적극 활용할 때 (예 →조회한 빈이 모두 필요할 때)는 수동등록을 고민해보자 - 자동 등록 시 구현 객체가 한눈에 드러나지 않기 때문에, 그래도 자동등록을 사용하고 싶을 경우 같은 추상화 객체를 사용한 구현체는 같은 패키지에 묶어두자</li>
</ul>
<h2 id="✔️-빈-생명주기-콜백">✔️ 빈 생명주기 콜백</h2>
<ul>
<li><p>스프링 빈의 이벤트 라이프 사이클</p>
<p>  스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료</p>
</li>
<li><p>스프링 빈은 객체를 생성하고 의존관계 주입을 끝내고 나서야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 끝나고 나서 실행되어야한다.</p>
</li>
<li><p>그러기 위해서는 <strong>개발자가 초기화 콜백 시점을 알아야한다.</strong></p>
</li>
</ul>
<blockquote>
<p><strong>객체의 생성과 초기화를 분리하자</strong></p>
<p>생성자에 초기화 코드를 넣으면 되지 않을까라는 생각을 할 수 있다. 하지만 생성자는 필수정보(파라미터)를 받고 객체를 생성하는 책임을 가지며, 초기화는 이렇게 생성된 값들을 활용하여 외부 커넥션을 연결하는 등의 무거운 작업을 수행한다.</p>
<p>따라서 생성자 안에서 무거운 초기화 작업을 함께하는 것보다 나누는 것이 유지보수 관점에서 더 나을 수 있다.</p>
</blockquote>
<h3 id="콜백-시점을-아는-방법-3가지">콜백 시점을 아는 방법 3가지</h3>
<h3 id="1-인터페이스-initializingbean-disposablebean">1. 인터페이스 InitializingBean, DisposableBean</h3>
<ul>
<li>클래스에 InitializingBean, DisposableBean을 Implements해서 메서드를 오버라이딩한 후 실행하고 싶은 코드를 각 메서드 안에 넣는다.</li>
<li>단점<ul>
<li>스프링 전용 인터페이스에 의존한다.</li>
<li>초기화, 소멸 메서드의 이름을 변경할 수 없다</li>
<li>내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.</li>
</ul>
</li>
</ul>
<h3 id="2-빈-등록-초기화-소멸-메서드-지정">2. 빈 등록 초기화, 소멸 메서드 지정</h3>
<ul>
<li>설정 정보에 <code>Bean(initMethod= &quot;init&quot;, destroyMethod= &quot;close&quot;)</code> 처럼 초기화, 소멸 메서드를 지정할 수 있다.</li>
<li>특징<ul>
<li>메서드 이름을 자유롭게 줄 수 있다.</li>
<li>스프링 빈이 스프링 코드에 의존하지 않는다.</li>
<li>외부 라이브러리에도 초기화, 소멸 메서드를 적용할 수 있다.</li>
</ul>
</li>
<li>종료 메서드 추론<ul>
<li>외부 라이브러리는 대부분 <code>close</code> , <code>shutdown</code> 이라는 이름의 종료 메서드를 사용한다.</li>
<li>@Bean의 destoryMethod 는 기본값이 <code>(inferred)</code> 로 등록되어 있는데 이는 <code>close</code>, <code>shutdown</code> 이라는 이름의 메서드를 자동으로 호출 해준다.</li>
</ul>
</li>
</ul>
<h3 id="3-annotation-postconstruct-predestroy">3. Annotation @PostConstruct, @PreDestroy</h3>
<ul>
<li>초기화, 소멸 메서드에 해당 Annotation을 붙이면 된다.</li>
<li>스프링에 종속적인 기술이 아니라 자바 표준 → 스프링이 아닌 다른 컨테이너에서도 동작</li>
<li>코드를 고칠 수 없는 외부라이브러리에는 적용할 수 없다. → 2번 방법 사용</li>
</ul>
<h2 id="✔️-빈-스코프">✔️ 빈 스코프</h2>
<ul>
<li>빈 스코프는 말 그래로 빈이 존재할 수 있는 범위(기간)를 의미한다</li>
</ul>
<h3 id="스코프-지정-방법">스코프 지정 방법</h3>
<p><code>@Scope(&quot;singleton&quot;)</code> , <code>@Scope(&quot;prototype&quot;)</code> 과 같이 Annotation을 붙여주면 된다. 컴포넌트 스캔 사용시 해당 클래스에, Bean Annotation을 사용한 수동주입 시 Bean Annotation에.</p>
<h3 id="스프링이-지원하는-스코프">스프링이 지원하는 스코프</h3>
<ul>
<li>싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프</li>
<li>프로토타입: 프로토타입 빈의 생성과 의존관계 주입, 초기화까지만 관리하고 더는 관리하지 않는 스코프(의존관계 주입이 끝나는 동시에 컨테이너밖으로 던져버린다.)<ul>
<li>요청이 올 때마다 항상 새로운 프로토타입 빈을 생성해서 반환한다.</li>
<li>먼저 Destroy되기 때문에 종료 메서드가 호출되지 않는다.</li>
</ul>
</li>
<li>웹 관련 스코프<ul>
<li>request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프</li>
<li>session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프</li>
<li>application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프</li>
</ul>
</li>
</ul>
<h3 id="프로토타입-스코프---싱글톤-빈과-함께-사용-시-문제점">프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 문제점</h3>
<ul>
<li>스프링 컨테이너에 프로토타입 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께(싱글톤 빈에 주입해서) 사용할 때는 싱글톤 빈이 생성될 때만 의존관계 주입이 일어나기 때문에 해당 싱글톤 빈은 계속해서 같은 프로토타입 빈을 가지고 있게 된다.</li>
</ul>
<h3 id="해결방법">해결방법</h3>
<ol>
<li><p>스프링 애플리케이션 컨텍스트 전체 주입</p>
<p> 단순하게 ApplicationContext(컨테이너) 자체를 싱글톤 빈에 주입해서 로직을 실행할 때마다 getBean을 통해 빈을 불러오고 새로운 프로토타입 빈을 생성해준다.</p>
<ul>
<li><strong>이렇게 의존관계를 주입받는게 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 탐색이라고 한다. - 지연 주입?할 때 사용 가능</strong></li>
<li>단점: 스프링 컨테이너에 종속적인 코드가 되고 , 단위 테스트가 어려워 진다.</li>
</ul>
</li>
<li><p>ObjectFactory, <strong>ObjectProvider → DL이 필요한 경우 사용</strong></p>
<ul>
<li>지정한 빈을 컨테이너에서 찾아주는 DL서비스를 제공하는 것이 ObjectProvider</li>
<li>ObjectFactory에 기능이 몇 가지 추가된 갓이 ObjectProvider</li>
<li>스프링에 의존하긴 하지만 기능이 단순하기 때문에 단위테스트를 만들거나 mock코드를 만들기 쉽다.</li>
</ul>
</li>
<li><p>Provider</p>
<ul>
<li>자바 표준, DL 서비스 제공</li>
<li>다른 컨테이너에서 사용 가능</li>
</ul>
</li>
</ol>
<h3 id="웹-스코프">웹 스코프</h3>
<ul>
<li><p>웹 환경에서만 동작한다.</p>
</li>
<li><p>스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.</p>
</li>
<li><p><strong>request</strong>: http 요청 하나가 들어오고 나깔 떄까지 유지되는 스코프, 각각의 http 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.</p>
</li>
<li><p>session</p>
</li>
<li><p>application</p>
</li>
<li><p>websocket</p>
</li>
</ul>
<h3 id="request-스코프-만들기">Request 스코프 만들기</h3>
<ul>
<li><p>Request 스코프 클래스 코드 (주석 포함)</p>
<pre><code class="language-java">  @Component
  @Scope(&quot;request&quot;)
  public class MyLogger {
      private String uuid;
      private String requestURL;

      //RequestURL 은 빈이 생성되는 시점에는 알 수 없기 때문에, 외부에서 setter 로 입력받는다.
      public void setRequestURL(String requestURL) {
          this.requestURL = requestURL;
      }

      public void log(String message){
          System.out.println(&quot;[&quot; + uuid + &quot;]&quot; + &quot;[&quot; + requestURL + &quot;]&quot; + message);
      }

      @PostConstruct
      public void init(){
          uuid = UUID.randomUUID().toString(); // 이 빈은 Http 요청 당 하나 씩 생성되므로 UUID를 저장해두면 다른 Http 요청과 구분할 수 있다.
          System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean create:&quot; + this);

      }

      @PreDestroy
      public void close(){
          System.out.println(&quot;[&quot; + uuid + &quot;] request scope bean close:&quot; + this);
      }
  }</code></pre>
</li>
<li><p>Request 스코프를 만들기 위해서는 Https 요청이 들어와야하고 요청이 들어오기 위해서는 컨트롤러와 서비스 클래스를 만들어야 한다.</p>
</li>
</ul>
<h3 id="request-스코프와-providerprovider-사용-시기">Request 스코프와 Provider(Provider 사용 시기)</h3>
<ul>
<li>MyLogger는 Request 스코프이기 때문에 Http 요청이 들어와서 스프링 빈에 올라간다. 그렇기 때문에 처음 스프링을 띄울 때 의존관계 주입이 이루어지지 않는다.</li>
<li>이를 해결하기 위해 MyLogger 빈 대신에 MyLogger빈을 찾을 수 있는(DL) ObjectProvider 빈을 주입한다.</li>
<li>그리고 Http 요청이 들어오고 난 후 ObjectProvider를 통해 MyLogger를 찾는다.</li>
</ul>
<h3 id="request-스코프와-프록시">Request 스코프와 프록시</h3>
<ul>
<li>Provider를 사용하지 않고 싱글톤을 사용할 때처럼 코드를 간결하게 할 수 있는 방법</li>
<li><code>@Scope(value=&quot;request&quot; proxyMode= ScopedProxyMode.TARGET_CLASS)</code></li>
</ul>
<p>→ 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 Http 요청과 상관없이 가짜 프록시 클래스를 미리 주입해 둘 수 있다. 이 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.</p>
<blockquote>
<p>🔥 Provider를 사용하든, 프록시를 사용하든 중요한 것은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다. 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[🖥️ Github-actions, Code-Deploy, Docker, Nginx, EC2, RDS, S3, ALB 서버 배포]]></title>
            <link>https://velog.io/@yeopju_5/CICD-%EA%B3%BC%EC%A0%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yeopju_5/CICD-%EA%B3%BC%EC%A0%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 24 May 2023 08:45:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yeopju_5/post/390ad348-ebf2-4d88-af8d-d54486653777/image.png" alt=""></p>
<h1 id="🌀-deploy-flow">🌀 Deploy Flow</h1>
<ol>
<li>Github <code>main</code> branch Push</li>
<li>Github Actions workflow 실행<ul>
<li>Github Secrets를 통한 <code>application.properties</code> 파일 생성</li>
<li>애플리케이션 빌드</li>
<li>S3에 파일 업로드(빌드 파일, 도커 파일, appspec, sh 파일 등)</li>
<li>CodeDeploy에 배포 요청</li>
</ul>
</li>
<li>EC2 내부의 CodeDeploy Agent에게 배포 요청이 오면 <code>appspec.yml</code> 파일 실행<ul>
<li><code>deploy.sh</code> 파일 실행<ul>
<li>블루, 그린 여부에 따라서 <code>docker-compose.blue.yml</code> 또는 <code>docker-compose.green.yml</code> 실행</li>
<li>docker-compose 파일에 의해서 Dockerfile을 바탕으로 build 후 컨테이너 실행</li>
<li>컨테이너 실행시 ENTRYPOINT로 선언해놓은 명령어가 실행되면서 애플리케이션 실행</li>
</ul>
</li>
</ul>
</li>
</ol>
<h1 id="🌀-request-flow">🌀 Request Flow</h1>
<ol>
<li><p>클라이언트의 요청이 로드밸런서에 전달</p>
<ul>
<li><p>클라이언트가 Http로 요청할 때</p>
<ul>
<li><p>기존 : 로드밸런서가 Https로 리다이렉트 한 다음 웹 서버(Nginx)에게 Https를 해제해서 Http로 넘긴다</p>
</li>
<li><p>수정 후 : 로드밸런서에서는 상태 코드 301로만 리다이렉트되어 Get메서드로 재요청이 들어오며 이는 415 Method Not Allowed 에러를 발생시킨다. </p>
</li>
<li><blockquote>
<p>로드밸런서는 Http 그대로 통과한 후 Nginx에서 308 Https로 리다이렉트한다.</p>
</blockquote>
</li>
</ul>
</li>
<li><p>클라이언트가 Https로 요청할 때</p>
<ul>
<li>웹 서버에게 Https를 해제해서 Http로 넘긴다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>웹 서버에 Http로 넘기는 이유</p>
</blockquote>
<ul>
<li>로드 밸런서와 클라이언트와의 통신에서 보안을 유지하면서도 내부 웹 서버와의 통신에서는 암호화 비용을 절감할 수 있어 효율적이다.</li>
</ul>
<ol start="2">
<li>Nginx로 Http 요청 전달</li>
<li>도커 컨테이너 8081 또는 8082 포트로 전달<ul>
<li>불필요한 3, 4-way-handshake가 계속해서 발생하는 것을 방지하기 위해 <code>keepalive</code>, <code>keepalive_timeout</code> 고려하기</li>
</ul>
</li>
<li>도커에서 8080으로 연결</li>
</ol>
<h1 id="❓-에러-정리">❓ 에러 정리</h1>
<h2 id="✔️-깃허브-워크-플로우-에러">✔️ 깃허브 워크 플로우 에러</h2>
<p><code>syntax error near unexpected token &#39;(&#39;</code></p>
<ul>
<li>application.properties 파일 내에서 주석 처리한 부분에 &#39;(&#39;가 있어도 에러가 발생한다.</li>
</ul>
<hr>
<pre><code>contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:98
        Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1804
            Caused by: javax.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
                Caused by: org.hibernate.exception.JDBCConnectionException at SQLStateConversionDelegate.java:112
                    Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException at SQLError.java:174
                        Caused by: com.mysql.cj.exceptions.CJCommunicationsException at NativeConstructorAccessorImpl.java:-2
                            Caused by: java.net.ConnectException at PlainSocketImpl.java:-2</code></pre><ul>
<li>build 진행 중 test에서 발생하는 에러</li>
<li>build.gradle 파일에 Junit을 사용한다고 선언 후 test 코드가 없는 경우 발생</li>
<li>해결 방법<ul>
<li>build.gradle에 test task 선언한 파트 제거 </li>
<li>Github action workflow에 <code>./gradlew build -x test</code> 와 같이 테스트 무시 커멘드 추가</li>
</ul>
</li>
</ul>
<hr>
<pre><code>An error occurred (DeploymentLimitExceededException) when calling the CreateDeployment operation: The Deployment Group &#39;~~~&#39; already has an active Deployment &#39;d-6USSQTOMN&#39;</code></pre><ul>
<li>CodeDeploy에서 발생하는 에러</li>
<li>파일을 생성하고자 하는 디렉터리에 이미 같은 파일이 존재할 경우 해당 에러 발생</li>
<li>해결 방법<ul>
<li><code>appspec.yml</code> 파일에 덮어쓰기 설정 추가</li>
<li><code>file_exists_behavior: OVERWRITE</code></li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>대부분의 에러는 오타!! 파일명, 포트 불일치!!</li>
</ul>
<h2 id="✔️-nginx-에러">✔️ NGINX 에러</h2>
<p>502 BadGateway</p>
<ul>
<li>다양한 이유가 있지만 나의 경우에는 하나의 server 블록 안에서 location 블록이 선언되어있는 <code>cond.f/default.conf</code> 을 include 하고 location 블록을 하나 더 선언하면서 <code>proxy_pass</code> 가 비정상적으로 작동해 발생했다.</li>
<li>해결 방법<ul>
<li><code>cond.f/default.conf</code>과 <code>nginx.conf</code> 파일 통합</li>
</ul>
</li>
</ul>
<hr>
<p>415 Method Not Allowed</p>
<ul>
<li>CORS 문제</li>
<li>해결 방법<ul>
<li>Server 블록 내 location 블록에 다음 코드 추가<pre><code>location / {
    if ($request_method = &#39;OPTIONS&#39;) {
        add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39;;
        add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, DELETE, PATCH, OPTIONS&#39;;
        add_header &#39;Access-Control-Allow-Headers&#39; &#39;Content-Type, Authorization&#39;;
        add_header &#39;Access-Control-Max-Age&#39; 86400;
    }
    add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always;
}</code></pre></li>
</ul>
</li>
</ul>
<hr>
<p>Post Request가 Get Request로 바뀌어서 나가는 문제</p>
<ul>
<li>https가 아닌 요청이 들어왔을 경우 다음과 같이 301 redirect 되도록 선언해놨는데 301번 Moved Permanetly는 redirect시킬 때 method를 GET으로 바꾸어서 전송한다.</li>
<li>해결 방법<ul>
<li>308 Permanent Redirect는 전송 받은 Http Method를 유지한다.</li>
</ul>
</li>
</ul>
<h1 id="❗-남은-문제">❗ 남은 문제</h1>
<ul>
<li>EC2 보안 그룹 설정 에러</li>
<li>uri: /.env와 같은 불특정 URL로의 요청(해킹 시도)을 막을 수 있는 방법</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[❗Spring Security 예외처리]]></title>
            <link>https://velog.io/@yeopju_5/Spring-Security-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@yeopju_5/Spring-Security-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Mon, 15 May 2023 14:14:01 GMT</pubDate>
            <description><![CDATA[<h2 id="spring-security-예외-처리를-위해-일반적으로-사용되는-클래스">Spring Security 예외 처리를 위해 일반적으로 사용되는 클래스</h2>
<h3 id="1-authenticationentrypoint">1. AuthenticationEntryPoint</h3>
<ul>
<li>Authentication</li>
<li><strong>인증되지 않은 사용자가 보호된 리소스에 액세스</strong>하려고 할 때 발생하는 예외를 처리하는 클래스입니다. 이 클래스는 401 Unauthorized 오류를 반환하도록 구성되어 있으며, 이 오류는 사용자가 로그인을 해야한다는 메시지를 포함합니다.</li>
</ul>
<h3 id="2-accessdeniedhandler">2. AccessDeniedHandler</h3>
<ul>
<li>Authorization</li>
<li><strong>인증된 사용자가 보호된 리소스에 액세스하지 못할 때</strong> 발생하는 예외를 처리하는 클래스입니다. 이 클래스는 403 Forbidden 오류를 반환하도록 구성되어 있으며, 이 오류는 사용자가 해당 리소스에 대한 권한이 없다는 메시지를 포함합니다.</li>
</ul>
<h3 id="3-authenticationexceptionhandler">3. AuthenticationExceptionHandler</h3>
<ul>
<li>Validate Check</li>
<li><strong>JWT 토큰 유효성 검사에서 발생하는 예외를 처리하는 클래스</strong>입니다. 이 클래스는 401 Unauthorized 오류를 반환하도록 구성되어 있으며, 이 오류는 JWT 토큰이 유효하지 않다는 메시지를 포함합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yeopju_5/post/ea521b0c-520d-45f7-9370-fbfdbf838a1f/image.png" alt=""></p>
<h3 id="validate-exception-2가지-방법">Validate Exception 2가지 방법</h3>
<ol>
<li>ValidateToken에서 예외의 터트린다.</li>
<li>ValidateToken에서 reponse에 {exception: ExceptionName} 형태로 담아서 전달한다.</li>
</ol>
<ul>
<li><p>1번 방법이 더 좋은 방법</p>
<ul>
<li>해당 예외를 빠르게 처리하고 확인하기 위해서</li>
<li>2번 방법은 예외가 발생해도 예외가 발생했음을 알리지 않고, 예외 처리를 하지 않으면 예외가 무시되는 위험성이 있다.</li>
</ul>
</li>
<li><p>❓가정</p>
<ul>
<li><del>인증과정에서 내부적으로 발생하는 에러는 JwtAuthenticationExceptionHandler에서 처리하지만 내가 선언한 ValidateToken에서 발생하는 예측할 수 있는 예외는 AuthenticationException을 Extends한 JwtAuthenticationException으로 발생시킴으로써 JwtAuthenticationEntryPoint에서 처리하도록 한다?</del></li>
</ul>
</li>
<li><p>로그 확인 결과</p>
<ul>
<li><strong>토큰 인증 과정에서 발생하는 에러는 무조건 JwtAuthenticationExceptionHandler에서 처리</strong></li>
</ul>
</li>
</ul>
<h3 id="autherrorresponse를-선언한-이유"><del>AuthErrorResponse를 선언한 이유</del></h3>
<ul>
<li><p><del>validateToken에서 발생하는 예외를 AuthenticationException을 Extends한 JwtAuthenticationException로 발생시켰을 때JwtAuthenticationEntryPoint가 예외를 처리하게 된다.</del></p>
</li>
<li><p>이때 원인이 되는Error를 Response에 담아서 전달하기 위해 Cause가 포함된 AuthErrorResponse를 생성했다.</p>
<p>  <strong>→ JwtAuthenticationExceptionHandler의 authException에서 message를 불러와 Message 자체에 담아주기 때문에 원래 목적의 casue가 필요없어졌다. 그래서 Class 정보를 담는 것으로 대체</strong></p>
</li>
</ul>
<p><del>생각해보니까 이게 원인이 되는 에러메시지를 담기 위해서 validateToken에서 예외를 나눈게 아니다….!!</del></p>
<p><del>시큐리티 인증 단에서 터지는 에러는 모두 401, 403 에러로 처리하는 것이 맞고 validateToken에서 예외를 나눈 이유는 validateToken을 컨트롤러 계층에서 사용할 때 예외처리를 하기 위해 나눈 것이다…</del><strong>혼자 상상의 나래 펼쳤네..</strong> </p>
<p><strong>아니야 지금 생각해보니까 맞아 JwtAccessDenied, JwtAuthenticationEntryPoint에서는 401, 403으로 처리하고 JwtAuthentcationExceptionHandler에서는 에러 메시지를 받아와서 예외 유형별로 처리해줘. 처음 필터 거칠 때 validateToken에서 터진 에러를 JwtAuthenticationExceptionHandler에서 메시지별로 잡아주기 위해서 나눈 것도 맞고, 컨트롤러 계층에서 사용할 때 예외 처리를 하기 위해 나눈 것이라는 말도 맞는말이고!!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[⛓️HTTPS-SSL]]></title>
            <link>https://velog.io/@yeopju_5/HTTPS-SSL</link>
            <guid>https://velog.io/@yeopju_5/HTTPS-SSL</guid>
            <pubDate>Mon, 15 May 2023 06:29:42 GMT</pubDate>
            <description><![CDATA[<h2 id="https란">HTTPS란?</h2>
<p>HTTPS는 HyperText Transport Protocl Secure의 약자로, SSL/TLS를 사용하는 HTTP 프로토콜의 보안 버전입니다. HTTP 프로토콜을 사용해 서버와 통신할 때 내부 정보들이 텍스트 그대로 노출되는 것을 방지합니다.</p>
<h2 id="ssltls란">SSL/TLS란?</h2>
<p>SSL은 클라이언트와 서버의 통신을 보증해주는 인증서입니다. 해당 인증서를 발급하는 기관은 CA(Certificate Authority)라고 합니다. SSL은 1996년 이후, 소유권 이전으로 인해 TLS라는 명칭으로 변경하여 업데이트를 진행하였습니다.</p>
<p>SSL 인증은 암호화된 SSL을 가지고 있는 서버와 클라이언트 간의 헨드셰이크를 통해 이루어지며 통신 연결을 위한 TCP 3-way-handshake 이후에 진행됩니다.</p>
<h3 id="ssl-핸드셰이크-과정">SSL 핸드셰이크 과정</h3>
<ol>
<li>클라이언트 -&gt; 서버<ul>
<li>ACK에 Cipher suit 목록, sessionId, SSL version을 포함해서 서버에 전송합니다.</li>
<li>Cipher suit에는 암호화 방식, 프로토콜 종류(SSL/TLS) 등이 포함됩니다.</li>
</ul>
</li>
<li>서버 -&gt; 클라이언트<ul>
<li>선택한 Cipher suit, <strong>암호화된 SSL 인증서</strong>를 담아 전송합니다.</li>
<li>SSL 인증서에는 서버의 공개키와 암호화 알고리즘 방식이 포함되어 있습니다.</li>
</ul>
</li>
<li>서버 -&gt; 클라이언트<ul>
<li>서버가 행동을 마쳤다는 의미로 Done 패킷을 하나 더 보냅니다.</li>
</ul>
</li>
<li>클라이언트 인증서 확인<ul>
<li>클라이언트 측에서 정상적인 인증서인지 확인하기 위해서 클라이언트가 가지고 있는 CA 리스트에서 CA 공개키를 얻습니다.</li>
</ul>
</li>
<li>클라이언트 비밀키 공유<ul>
<li>공개키를 통해 인증서를 복호화하여 서버의 공개키를 얻습니다.</li>
<li>클라이언트의 비밀키를 서버의 공개키로 암호화해 서버에 전송합니다.</li>
<li>서버는 본인의 비밀키를 사용해 복호화하여 클라이언트의 비밀키를 얻습니다.</li>
</ul>
</li>
<li>통신 준비 완료<ul>
<li>서버는 클라이언트의 비밀키를 통해서 통신할 준비가 끝났다는 의미로 클라이언트에 Change ciper spec을 전송합니다.</li>
</ul>
</li>
</ol>
<h3 id="그럼-서버는-ssl을-어떻게-갖게돼">그럼 서버는 SSL을 어떻게 갖게돼?</h3>
<ol>
<li>서버가 공개키와 비밀키를 생성합니다.</li>
<li>서버가 서버의 정보와 공개키를 CA에 전달합니다.</li>
<li>CA가 SSL을 발급합니다(서버의 정보와 공개키 포함)</li>
<li>CA에서 공개키와 비밀키를 생성합니다. CA의 비밀키를 이용해 SSL을 암호화합니다.</li>
<li>암호화된 SSL을 서버에 전달합니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[3학년 2학기 회고]]></title>
            <link>https://velog.io/@yeopju_5/3%ED%95%99%EB%85%84-2%ED%95%99%EA%B8%B0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@yeopju_5/3%ED%95%99%EB%85%84-2%ED%95%99%EA%B8%B0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 01 Jan 2023 00:59:54 GMT</pubDate>
            <description><![CDATA[<h2 id="학점😐">학점😐</h2>
<p>이번 학기 학점은 4.29이다. 사실 4.29은 높은 학점에 속하지만 그 어떤 기쁨도 성취감도 느끼지 못했다. 그 이유는 이 중 하나일 것이다. 학점이 나의 미래, 취업에 큰 도움이 되지 않는다고 생각해서, 그렇기에 높은 학점을 받기 위해 그다지 많은 노력 들이지 않아서, 혹은 이미 전 학기에 이보다 높은 학점을 받았었기 때문에. 이 이유들이 모두 복합적으로 작용했겠지만 첫번째와 두번째 요인들의 영향이 가장 컸다고 생각한다. 학점을 받기위해 많은 노력을 들이지 않았음에도 성적을 잘 받을 수 있었던 것은 나의 사고력과 개발실력이 이미 대학교 내에서는 상위권에 위치해 있기 때문일 것이다. 그럼에도 나는 항상 나의 결핍과 타인에 비해 부족한 부분만을 찾아내 보완하려고 노력한다. 분명 이러한 노력이 나의 실력적인 부분에 대해서는 큰 도움이 될 것이다. 하지만 이 노력은 나의 자존감과 자신감을 갉아먹고 결과적으로 마음 속의 여유가 사라져간다. 그렇다면 이 노력은 계속해 나가는 것이 맞는 것일까? 사실 정답은 알고 있다. 타인과의 비교를 그만두고 과거의 나와의 비교를 통해 앞으로 나아가야 한다는 것. 머리로는 알고 있는 이 정답을 행동으로 옮기기 위한 방법은 어떤게 있을까? 학점 얘기하다가 너무 멀리왔다...2022년 회고에서 마저하자</p>
<h3 id="✔️-데이터베이스">✔️ 데이터베이스</h3>
<p>ERD, 정규화, SQL 쿼리문, 데이터베이스의 역사, 현재, 미래에 대해 알 수 있었던 유익한 강의였다. 나에게는 이러한 지식이 많다. 어렴풋이 알고있지만 책을 읽은 것이 아닌 경험으로 안 것들이기 때문에 확신할 수 없고 타인에게 설명하기에는 부족한 지식들. 그렇기에 <strong>이에 대한 책을 읽는 것이 필요</strong>하고 그 중 하나가 데이터베이스에 대한 지식이었다.</p>
<h3 id="✔️-컴퓨터-알고리즘">✔️ 컴퓨터 알고리즘</h3>
<p>이 또한 데이터베이스와 마찬가지의 경우이다. 코딩테스트 공부랍시고 무작정 문제만 풀어 알고리즘에 대한 지식을 어렴풋이만 알고 있었던 것이다. 이 강의를 통해 정렬, 그래프, 트리, 최단거리 알고리즘 등의 이론에 대해 알 수 있었고 코딩테스트는 문제풀이 전에 알고리즘에 대한 이론이 바탕이 되어야만 한다는 확신이 생겼다. 이번 겨울방학동안은 이론을 중점적으로 공부하되 문제풀이를 병행하는 식으로 진행할 것이다.</p>
<h3 id="✔️-uiux-창업캡스톤-디자인">✔️ UI/UX, 창업캡스톤 디자인</h3>
<p>이번 학기 가장 힘들었던 두 강의이다. 두 강의 모두 팀프로젝트로 진행되었고 친한 동기들과 팀을 이루어 진행하였다. 나의 동기들은 대충대충, 어느정도만 해서 적당한 성적을 받길 원했다. 이 것은 나 또한 마찬가지였다. 하지만 전체적인 퀄리티에 있어서 어느정도라는 것이고 다른 공부에 더 치중하자는 의미였으며 정해진 시간동안에는 최선을 다하기를 원했다. 하지만 팀원들은 자신에게 주어진 일, 주어진 시간 동안에도 최선을 다하지 않는 모습을 보였다. 어쩌면 이러한 모습이 자신의 무능력함을 숨기기위한 행동일 수도 있고 내가 보기에 대충했다고 생각되는 일도 그 사람 입장에서는 열심히 한 일 일수도 있을 것이다. 그렇다면 내가 그 사람의 결과물에 대해서 왜 대충했냐라고 하는 것은 굉장히 실례되는 말 일 수도 있다는 것을 깨달았다. 말그릇이라는 책에서는 나의 감정을 올바른 방법으로 전달하기 위해서 내가 느끼는 감정의 근원이되는 감정을 지각하고 보괸한 후 표현해야 한다고 한다. 나는 최근 이 책을 읽고 근원이되는 감정을 찾아 솔직하게 표현하기 위해 노력하고 있다. 그러자 친구들은 느끼고 있는 감정 그대로 솔직하게 말해줘서 좋다, 고맙다. 매력으로 다가온다. 라고 했다. 하지만 솔직한 것은 매력이 되어서는 안된다고 생각한다. 매력이라는 것은 내가 그만큼 자주 솔직하다라는 것이며 솔직한 것은 타인에게 쉽게 상처를 준다라는 말로 해석될 수 있기 때문이다. 그렇다면 때로는 타인의 과오를 보고도 지적하지 않아야하는 것일까... 하지만 모든 팀원들이 그 사람과의 관계를 위해서 결과물에 대해 지적하지 않는다면 그 사람이 상처받을 가능성을 줄 일 수는 있더라도 그 사람도, 결과물도 더 이상의 발전은 없을 것이다. 그렇다면 모두의 발전을 위해 누군가는 꼭 지적해야만 하는데 나는 항상 이 역할을 자처한다. 지적에서 오는 부정적인 영향만을 생각하면서 뒤로 한걸음 물러서 상황을 지켜보기만 하는 행동은 옳지 않다고 생각하기 때문이다. 물론 지적 할 때는 상대방이 상처 받지 않는 화법을 쓰고자 노력 해야한다. 하지만 혹시나 상처 받지는 않을까 항상 조심스러운 것이 사실이다. 모든 팀원들이 타인의 지적을 품을 수 있는 높은 자존감을 가지고 있고 피드백에 적극적인 분위기라면 좋겠지만 그렇지 않더라도 나는 앞으로도 계속 이 역할을 자처할 생각이다. 이 역할을 잘 수행하기 위해서 화법을 익히고 다독하자!</p>
<h3 id="✔️-사랑의-역사-사랑에-대한-철학적-성찰">✔️ 사랑의 역사, 사랑에 대한 철학적 성찰</h3>
<h3 id="✔️-교양-테니스">✔️ 교양 테니스</h3>
<h2 id="umc-운영진-📲">UMC 운영진 📲</h2>
<p>첫 동아리 운영진 활동!! 새로운 사람들을 만날 수 있었던 기회이자 오픈소스 컨트리뷰션 컨퍼런스 등 여러 세미나에 참여할 수 있었던 가치있는 활동이었다.</p>
<h2 id="doro-개발-🖥️">DORO 개발 🖥️</h2>
<h3 id="9월">9월</h3>
<ul>
<li>Animation 추가</li>
<li>SEO 개선</li>
<li>Swiper 스크롤 기능 추가</li>
<li>git commit Templete 추가<h3 id="10월">10월</h3>
</li>
<li>매칭 페이지 기획</li>
<li>카카오톡 Notification 구현</li>
<li>현재 가용 강사 인원수 API (Discard)</li>
<li>프론트엔드 상세정보 배열처리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 10]]></title>
            <link>https://velog.io/@yeopju_5/Chapter10</link>
            <guid>https://velog.io/@yeopju_5/Chapter10</guid>
            <pubDate>Tue, 26 Jul 2022 20:16:59 GMT</pubDate>
            <description><![CDATA[<h1 id="📑-payment">📑 Payment</h1>
<h2 id="🔷-platform">🔷 PlatForm</h2>
<h3 id="🔹-paddle">🔹 Paddle</h3>
<p>(소프트웨어 회사를 위한 결제 인프라)
오직 소프트웨어와 디지털 내용물만 거래 가능한 온라인 결제 처리
<a href="https://paddle.com">https://paddle.com</a></p>
<h3 id="🔹-stripe">🔹 Stripe</h3>
<p>인터넷 비즈니스를 위한 온라인 결제 처리 (한국 지원X, 실제 회사 필요)
<a href="https://stripe.com">https://stripe.com</a></p>
<h3 id="🔹-braintree">🔹 Braintree</h3>
<p>PayPal, Venmo(미국), 신용카드 및 직불카드, Apple Pay 및 Google Pay와 같은 인기 디지털 pay를 원활한 통합환경에서 제공하는 유일한 결제 플랫폼
<a href="https://www.braintreepayments.com">https://www.braintreepayments.com</a></p>
<h2 id="-many-to-one--one-to-many-relations">※ Many-to-one / One-to-many relations</h2>
<p>@OneToMany는 @ManyToOne 없이 존재할 수 없다.
그러나  @ManyToOne은 관련 엔터티에 @OneToMany 없이 관계를 정의할 수 있다.</p>
<h2 id="🔷-task-scheduling">🔷 Task Scheduling</h2>
<p>Task Scheduling을 사용하면 고정된 날짜/시간, 반복 간격 또는 지정된 간격마다 특정 메서드나 함수를 실행되도록 예약할 수 있다.</p>
<p>Node.js의 경우 cron과 유사한 기능을 애뮬레이트하는 여러 패키지가 있는데, Nest는 Node.js node-cron 패키지와 통합되는 @nestjs/schedule 패키지를 제공한다.</p>
<p><a href="https://docs.nestjs.com/techniques/task-scheduling">https://docs.nestjs.com/techniques/task-scheduling</a></p>
<p><code>npm install --save @nestjs/schedule</code>
<code>npm install --save-dev @types/cron</code></p>
<h3 id="🔹-cron-패턴순서">🔹 Cron 패턴(순서)</h3>
<p>초, 분, 시, 일, 월, 요일</p>
<h3 id="🔹-intervals">🔹 Intervals</h3>
<p>메서드가 지정된 간격으로 실행되어야 한다고 선언하려면 @Interval() 데코레이터를 사용한다.
간격 값을 밀리초 단위의 숫자로 데코레이터에 전달한다.</p>
<h3 id="🔹-timeouts">🔹 Timeouts</h3>
<p>메서드가 지정된 시간에 한 번 실행되어야 한다고 선언하려면 @Timeout() 데코레이터를 사용한다.
오프셋(밀리 초) 전달</p>
<h3 id="🔹-cron-jobs">🔹 Cron jobs</h3>
<p>SchedulerRegistry API를 사용하여 코드의 어느 곳에서나 name으로 CronJob 인스턴스를 가져올 수 있다. 이를 위해서는 우선 표준 constructor injection을 사용해 SchedulerRegistry를 추가한다.</p>
<p><a href="https://docs.nestjs.com/techniques/task-scheduling#dynamic-cron-jobs">https://docs.nestjs.com/techniques/task-scheduling#dynamic-cron-jobs</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 9]]></title>
            <link>https://velog.io/@yeopju_5/Chapter9</link>
            <guid>https://velog.io/@yeopju_5/Chapter9</guid>
            <pubDate>Mon, 25 Jul 2022 21:33:13 GMT</pubDate>
            <description><![CDATA[<h1 id="📑-order-subscriptions">📑 Order Subscriptions</h1>
<h2 id="🔷-graphql-subscriptions">🔷 Graphql-subscriptions</h2>
<p>GraphQL subscriptions은 GraphQL에서 subscriptions을 구현하기 위해 pubsub 시스템과 GraphQL을 연결할 수 있는 Npm 패키지이다.
모든 GraphQL 클라이언트 및 서버와 함께 사용할 수 있다.</p>
<p><code>npm i graphql-subscriptions</code>
<a href="https://www.npmjs.com/package/graphql-subscriptions">https://www.npmjs.com/package/graphql-subscriptions</a></p>
<h3 id="🔸web-socketws-error">🔸Web Socket(ws) Error</h3>
<p>다음과 같이 PubSub 인스턴스를 생성하고 Subscription decorator를 사용해 <code>pubsub.asyncIterator</code>를 return 할 경우 <code>&quot;error&quot;: &quot;Could not connect to websocket endpoint ws://localhost:3000/graphql. Please check if the endpoint url is correct.&quot;</code> 와 같은 에러를 return한다.</p>
<p>우리 서버는 http 프로토콜을 사용하고 있는데 ws 또한 프로토콜이면 realtime을 처리하는 WebSocket을 말한다. mutation과 query는 http를 필요로 하고 subscription은 webSocket을 필요로 하기때문에 두 곳에서 모두 서버가 돌아갈 수 있어야한다. </p>
<pre><code class="language-typescript">const pubsub = new PubSub();

@Subscription((returns) =&gt; String)
  hotPotatos() {
    return pubsub.asyncIterator(&#39;hotPotatos&#39;);
  }</code></pre>
<h4 id="---해결방법-subscriptions-활성화">-  해결방법 (Subscriptions 활성화)</h4>
<p>subscriptions을 활성화하려면 app module에서 다음과 같이 설정해야한다. 이렇게 하면 서버에서 두가지 프로토콜을 모두 사용할 수 있게된다.</p>
<pre><code class="language-typescript">GraphQLModule.forRoot&lt;ApolloDriverConfig&gt;({
  driver: ApolloDriver,
  subscriptions: {
    &#39;graphql-ws&#39;: true
  },
})</code></pre>
<h2 id="🔷-using-both-http-and-ws">🔷 Using both &#39;http&#39; and &#39;ws&#39;</h2>
<ul>
<li>http프로토콜에는 request가 있고 ws프로토콜에는 이와 같은 역할을 하는 connection이 있다.</li>
</ul>
<h3 id="🔹-flow">🔹 Flow</h3>
<p>먼저 Guard와 AuthUser 데코레이터에서 user를 받아오는 과정에 대해 알아보자</p>
<p>로그인해서 받은 토큰을 http header에 넣어서 request를 보내면 jwtmiddleware에서 토큰을 받아 decoding한 후 user를 req에 넣어서 리턴한다. 
그럼 graphql module에서 context에 req를 넣어 guard에서 받은 후 context를 통해 role을 알아낼 수 있다. </p>
<h3 id="🔹-problem--fix">🔹 Problem &amp; Fix</h3>
<p>하지만 middleware를 사용해서는 request만 받아올 수 있어 http프로토콜만 사용이 가능하며 connection을 받아와야하는 ws프로토콜은 사용이 불가능하다. 그래서 app module에서 middleware를 제거해준 후 graphql module에 아래의 코드를 추가해준다. </p>
<p>subscriptions 내부의 코드는 connection에 토큰을 추가해주는 코드이고 아래의 context 내부의 코드는 req에 토큰을 추가해주는 코드이다.</p>
<pre><code class="language-typescript">GraphQLModule.forRoot({
      driver: ApolloDriver,
      subscriptions: {
        &#39;subscriptions-transport-ws&#39;: {
          onConnect: (connectionParams) =&gt; {
            console.log(&#39;connectionParams&#39;, connectionParams);
            const authToken = connectionParams[&#39;x-jwt&#39;];
            if (!authToken) {
              throw new Error(&#39;Token is not valid&#39;);
            }
            const token = authToken;
            return { token };
          },
        },
      },
      context: ({ req }) =&gt; ({ token: req.headers[&#39;x-jwt&#39;] }),
    }),</code></pre>
<p>이후 Guard에서 토큰을 받아 middleware에서 하던 decoding을 Guard에서 아래의 코드와 같이 진행한다.</p>
<pre><code class="language-typescript">@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
    private readonly userService: UserService,
  ) {}
  async canActivate(context: ExecutionContext) {
    const roles = this.reflector.get&lt;AllowedRoles&gt;(
      &#39;roles&#39;,
      context.getHandler(),
    );
    if (!roles) {
      return true;
    }
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const token = gqlContext.token;
    if (token) {
      const decoded = this.jwtService.verify(token.toString());
      if (typeof decoded === &#39;object&#39; &amp;&amp; decoded.hasOwnProperty(&#39;id&#39;)) {
        const { user } = await this.userService.findById(decoded[&#39;id&#39;]);
        if (user) {
          gqlContext[&#39;user&#39;] = user;
          if (roles.includes(&#39;Any&#39;)) {
            return true;
          }
          return roles.includes(user.role);
        }
      }
    }
    return false;
  }
}</code></pre>
<h2 id="🔷-pubsub">🔷 PubSub</h2>
<ul>
<li>PubSub 모델은 특정한 주제(Topic)에 대하여 구독(Subscribe)한 모두에게 메시지를 발생(Publish)하는 통신 방법이다. </li>
</ul>
<p>PubSub은 App 전체에서 하나의 인스턴스만 생성되어야 한다. 이를 위해 인스턴스를 common Module에 다음과 같이 선언한 후 common Module을 <code>@Global()</code> 처리 해준다.</p>
<pre><code class="language-typescript">const pubsub = new PubSub();

@Global()
@Module({
  providers: [
    {
      provide: PUB_SUB,
      useValue: pubsub,
    },
  ],
  exports: [PUB_SUB],
})
export class CommonModule {}</code></pre>
<p>Resolver에서는 PubSub을 사용하고자 할 때는 다음과 같이 <code>@Inject</code>를 통해 사용할 수 있다</p>
<pre><code class="language-typescript">@Resolver(of =&gt; Order)
export class OrderResolver {
  constructor(
    @Inject(PUB_SUB) private readonly pubSub: PubSub,
  ) {}</code></pre>
<h2 id="🔷-subscriptions-option">🔷 Subscriptions Option</h2>
<h3 id="🔹-filtering-subscriptions">🔹 Filtering Subscriptions</h3>
<p>Subscriptions을 사용할 때 특정 이벤트를 필터링하려면 필터 속성을 필터 함수로 설정할 수 있다.</p>
<pre><code class="language-typescript">//EX
@Subscription(returns =&gt; Comment, {
filter: (payload, variables, context) =&gt;
payload.commentAdded.title === variables.title,
})
commentAdded(@Args(&#39;title&#39;) title: string) {
return pubSub.asyncIterator(&#39;commentAdded&#39;);
}</code></pre>
<ul>
<li>payload: pubsub.publish()를 통해 전달한 객체</li>
<li>variables: subscription에 전달한 객체(인자)</li>
<li>context: gqlContext객체</li>
</ul>
<pre><code class="language-typescript">  @Mutation((returns) =&gt; Boolean)
  async potatoReady(@Args(&#39;potatoId&#39;) potatoId: number) {
    await this.pubSub.publish(&#39;hotPotatos&#39;, {
      readyPotato: potatoId,
    });
    return true;
  }

  @Subscription((returns) =&gt; String, {
    filter: ({ readyPotato }, { potatoId }) =&gt; {
      return readyPotato === potatoId;
    },
  })
  @Role([&#39;Any&#39;])
  readyPotato(@AuthUser() user: User, @Args(&#39;potatoId&#39;) potatoId: number) {
    return this.pubSub.asyncIterator(&#39;hotPotatos&#39;);
  }</code></pre>
<p>potatoId가 같을 경우에만 결과를 Load한다.</p>
<p><a href="https://docs.nestjs.com/graphql/subscriptions#filtering-subscriptions">https://docs.nestjs.com/graphql/subscriptions#filtering-subscriptions</a></p>
<h3 id="🔹-resolve">🔹 Resolve</h3>
<p>resolver함수가 리턴하는 값은 pubsub.asyncIterator()를 통해 받는 값이 된다. publish한 event payload를 변형하려면 resolve 속성을 함수로 설정한다. 함수는 이벤트 payload를 수신하고 적절한 값을 반환힌다.</p>
<pre><code class="language-typescript">@Subscription((returns) =&gt; String, {
    filter: ({ readyPotato }, { potatoId }) =&gt; {
      return readyPotato === potatoId;
    },
    resolve: ({ readyPotato }) =&gt;
      `Your potato with the id ${readyPotato} is ready!`,
  })</code></pre>
<h2 id="🔷-pending-orders">🔷 Pending Orders</h2>
<pre><code class="language-typescript">//orders.service.ts / createOrder part
await this.pubSub.publish(NEW_PENDING_ORDER, {
        pendingOrders: { order, ownerId: restaurant.ownerId },
      });</code></pre>
<pre><code class="language-typescript">//orders.resolver.ts
@Subscription((returns) =&gt; Order, {
    filter: ({ pendingOrders: { ownerId } }, _, { user }) =&gt; {
      return ownerId === user.id;
    },
    resolve: ({ pendingOrders: { order } }) =&gt; order,
  })
  @Role([&#39;Owner&#39;])
  pendingOrders() {
    return this.pubSub.asyncIterator(NEW_PENDING_ORDER);
  }</code></pre>
<p>service의 payload에 Order이 아닌 <code>{pendingOrders: { order, ownerId: restaurant.ownerId }}</code> 이와 같은 Object를 넣었기 때문에 Resolve Option을 통해 Order를 반환해줘야한다.</p>
<h2 id="🔷-eager-relations">🔷 Eager relations</h2>
<p>Eager relation은 데이터베이스에서 엔티티를 로드할 때마다 자동으로 relation 필드들을 로드한다. (eager: true를 추가)</p>
<pre><code class="language-typescript">@ManyToMany(type =&gt; Category, category =&gt; category.questions, {
eager: true
})
@JoinTable()
categories: Category[];</code></pre>
<p><a href="https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#eager-relations">https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#eager-relations</a></p>
<h2 id="🔷-lazy-relations">🔷 Lazy relations</h2>
<p>Lazy relation은 해당 필드에 접근하면 로드된다. Lazy relation은 타입으로 Promise를 가져야 한다. Promise에 값을 저장하고 로드할 때도 Promise를 반환힌다.</p>
<pre><code class="language-typescript">@ManyToMany(type =&gt; Question, question =&gt; question.categories)
questions: Promise&lt; Question[]&gt;;

@ManyToMany(type =&gt; Category, category =&gt; category.questions)
@JoinTable()
categories: Promise&lt; Category[]&gt;;

const question = await connection.getRepository(Question).findOne(1);
const categories = await question.categories;</code></pre>
<p><a href="https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#lazy-relations">https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#lazy-relations</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 8]]></title>
            <link>https://velog.io/@yeopju_5/Chapter7-l5mpis4h</link>
            <guid>https://velog.io/@yeopju_5/Chapter7-l5mpis4h</guid>
            <pubDate>Sun, 24 Jul 2022 21:50:27 GMT</pubDate>
            <description><![CDATA[<h1 id="📑-dish-and-order-crud">📑 Dish and Order CRUD</h1>
<h2 id="🔷-many-to-many-relations">🔷 Many-to-many relations</h2>
<ul>
<li>Many-to-many relations는 A가 B의 여러 인스턴스를 포함하고 B가 A의 여러 인스턴스를 포함하는 관계이다. @ManyToMany 관계에는 @JoinTable()이 필요하다. @JoinTable은 관계의 한쪽(소유) 쪽에 넣어야 한다.<pre><code class="language-typescript">@Field((type) =&gt; [Dish])
@ManyToMany((type) =&gt; Dish)
@JoinTable()
dishes: Dish[];</code></pre>
<a href="https://typeorm.io/#/many-to-many-relations">https://typeorm.io/#/many-to-many-relations</a>
<a href="https://orkhan.gitbook.io/typeorm/docs/many-to-many-relations">https://orkhan.gitbook.io/typeorm/docs/many-to-many-relations</a></li>
</ul>
<h2 id="🔷-arrayprototypeflat">🔷 Array.prototype.flat()</h2>
<p>Ex) <code>const newArr = arr.flat([depth])</code>
flat() 메서드는 모든 하위 배열 요소를 지정한 깊이까지 재귀적으로 이어붙인 새로운 배열을 생성한다. depth는 중첩 배열 구조를 평탄화할 때 사용할 깊이 값이며 기본값은 1이다.</p>
<ul>
<li>중첩 배열 평탄화<pre><code class="language-typescript">[ [1], [2], [], [], [5], [6] ].flat()
// [1, 2, 5, 6]
</code></pre>
</li>
</ul>
<p>const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]</p>
<p>const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]</p>
<pre><code>- 배열 구멍 제거
```typescript
const arr5 = [1, 2, , 4, 5];
arr5.flat();
// [1, 2, 4, 5]</code></pre><p><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flat">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flat</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 7]]></title>
            <link>https://velog.io/@yeopju_5/Chapter7</link>
            <guid>https://velog.io/@yeopju_5/Chapter7</guid>
            <pubDate>Thu, 21 Jul 2022 22:14:44 GMT</pubDate>
            <description><![CDATA[<h1 id="📑-restaurant-crud">📑 Restaurant CRUD</h1>
<h2 id="🔷-many-to-one--one-to-many">🔷 Many-to-one / One-to-many</h2>
<p>Many-to-one / One-to-many 관계는 A가 B의 여러 인스턴스를 포함하지만 B는 A의 인스턴스를 하나만 포함하는 관계이다.</p>
<ul>
<li>@OneToMany(): 일대다 관계에서 &#39;다&#39;에 속할 때 사용
(DB에 해당 컬럼은 저장되지 않음)</li>
<li>@ManyToOne(): 일대다 관계에서 &#39;일&#39;에 속할 때 사용
(DB에 user면 userId로 id값만 저장됨) </li>
</ul>
<pre><code class="language-typescript">  // Category Entity
  @Field((type) =&gt; [Restaurant])
  @OneToMany((type) =&gt; Restaurant, (restaurant) =&gt; restaurant.category)
  restaurants: Restaurant[];</code></pre>
<pre><code class="language-typescript">  // Restaurant Entity
  @Field((type) =&gt; Category)
  @ManyToOne((type) =&gt; Category, (category) =&gt; category.restaurants)
  category: Category;</code></pre>
<h2 id="🔷-unique-schema-name">🔷 Unique Schema Name</h2>
<p>스키마의 이름은 고유 해야한다. 하지만 각 Entity에서 <code>@ObjectType</code> 과 <code>@InputType</code> 가 같은 이름으로 스키마가 된다. 아래의 에러 해결을 위해서는 <code>@InputType</code> 에 따로 이름을 부여해야한다. 
<code>Error: Schema must contain uniquely named types but contains multiple types named &quot;Category&quot;.</code></p>
<pre><code class="language-typescript">@InputType(&#39;CategoryInputType&#39;, { isAbstract: true })
@ObjectType()
@Entity()
export class Category extends CoreEntity {
     //.....
}</code></pre>
<h2 id="🔷-database-relations-option">🔷 Database Relations Option</h2>
<p>2개의 데이터베이스 테이블을 연결했을 때, 한 쪽의 테이블 요소를 삭제했을 경우 연결된 쪽의 테이블의 상태를 결정할 수 있다. </p>
<ul>
<li>onDelete: &quot;RESTRICT&quot;|&quot;CASCADE&quot;|&quot;SET NULL&quot;
Specifies how foreign key should behave when referenced object is deleted</li>
</ul>
<p><a href="https://orkhan.gitbook.io/typeorm/docs/relations#relation-options">https://orkhan.gitbook.io/typeorm/docs/relations#relation-options</a></p>
<pre><code class="language-typescript">@Field((type) =&gt; Category, { nullable: true })
@ManyToOne((type) =&gt; Category, (category) =&gt; category.restaurants, {
    nullable: true,
    onDelete: &#39;SET NULL&#39;,
})
category: Category;</code></pre>
<h2 id="🔷-role-authorization--authentication">🔷 Role Authorization / Authentication</h2>
<h3 id="🔹metadata--role-decorator">🔹MetaData / Role Decorator</h3>
<p>Resolver는 각각 다른 권한 체계를 가질 수 있다. 유연하고 재사용 가능한 방식으로 권한 체계를 구현하기 위해서 metadata가 사용된다. Nest는 <code>@SetMetadata()</code> 데코레이터를 통해 Resolver에 커스텀 메타데이터를 붙이는 기능을 제공한다.</p>
<ul>
<li>ex) @SetMetadata(&#39;role&#39;, Role.Owner)
key - 메타데이터가 저장되는 키를 정의하는 값
value - 키와 연결될 메타데이터</li>
</ul>
<p><a href="https://docs.nestjs.com/guards#setting-roles-per-handler">https://docs.nestjs.com/guards#setting-roles-per-handler</a></p>
<p>route에서 직접 @SetMetadata()를 사용하는 것은 좋은 습관이 아니다.
대신 아래와 같이 직접 커스텀 데코레이터를 만들 수 있다.</p>
<pre><code class="language-typescript">//role decorator
import { SetMetadata } from &#39;@nestjs/common&#39;;
import { UserRole } from &#39;src/users/entities/user.entity&#39;;

type AllowedRoles = keyof typeof UserRole | &#39;Any&#39;;
export const Role = (roles: AllowedRoles) =&gt; SetMetadata(&#39;roles&#39;, roles);</code></pre>
<h3 id="🔹-app_guard">🔹 APP_GUARD</h3>
<p>다음과 같이 APP_GUARD를 사용할 시 모든 resolver에 대해 Guard가 적용됩니다.</p>
<pre><code class="language-typescript">//app.module.ts
import { Module } from &#39;@nestjs/common&#39;;
import { APP_GUARD } from &#39;@nestjs/core&#39;;
import { AuthGuard } from &#39;./auth.guard&#39;;

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useValue: AuthGuard,
    },
  ],
})
export class AuthModule {}
</code></pre>
<p><a href="https://docs.nestjs.com/guards#binding-guards">https://docs.nestjs.com/guards#binding-guards</a></p>
<h3 id="❗-authguard와-role-decoratormetadata-연결">❗ AuthGuard와 Role Decorator(MetaData) 연결</h3>
<ul>
<li>Reflector를 사용해 MetaData를 받는다.</li>
</ul>
<ul>
<li>Metadata가 설정되어있지 않으면 public(누구나 접근 가능)</li>
<li>Metadata가 설정되어있으면 private(MetaData를 이용해 특정 role만 접근 가능하도록 제한)</li>
</ul>
<pre><code class="language-typescript">@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(context: ExecutionContext) {
    const roles = this.reflector.get&lt;AllowedRoles&gt;(
      &#39;roles&#39;,
      context.getHandler(),
    );
    if (!roles) {
      return true;
    }
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const user: User = gqlContext[&#39;user&#39;];
    if (!user) {
      return false;
    }
    if (roles.includes(&#39;Any&#39;)) {
      return true;
    }
    return roles.includes(user.role);
  }
}</code></pre>
<h2 id="🔷-edit-restaurant-crud">🔷 Edit Restaurant CRUD</h2>
<h3 id="🔹--relationid">🔹 @ RelationId</h3>
<p>Field에 특정 relation의 id를 로드한다. 예를 들어 Restuarant 엔터티에 Many-to-one이 있는 경우 새 속성을 @RelationId로 표시하여 새 Relation ID를 가질 수 있다. 이 기능은 many-to-many를 포함한 모든 종류의 관계에서 작동한다. Relation ID는 추가/제거/변경되지 않는다.</p>
<pre><code class="language-typescript">  //Restaurant Entity

  @Field((type) =&gt; User)
  @ManyToOne((type) =&gt; User, (user) =&gt; user.restaurants, {
    onDelete: &#39;CASCADE&#39;,
  })
  owner: User;

  @RelationId((restaurant: Restaurant) =&gt; restaurant.owner)
  ownerId: number;</code></pre>
<h3 id="🔹-custom-repositories">🔹 Custom repositories</h3>
<p>데이터베이스 작업을 위한 메소드를 포함해야 하는 사용자 정의 레포지토리를 생성할 수 있다.
일반적으로 사용자 지정 리포지토리는 단일 엔티티에 대해 생성되고 특정 쿼리를 포함합니다. </p>
<p>예를 들어, 아래와 같이 <code>getOrCreate(name: string)</code>이라는 메서드가 있다고 가정하면 <code>this.categories.getOrCreate(...)</code>와 같이 호출할 수 있다.</p>
<pre><code class="language-typescript">// @EntityRepository(Category)
@CustomRepository(Category)
export class CategoryRepository extends Repository&lt;Category&gt; {
  async getOrCreate(name: string): Promise&lt;Category&gt; {
    const categoryName = name.trim().toLowerCase();
    const categorySlug = categoryName.replace(/ /g, &#39;-&#39;);
    let category = await this.findOne({ where: { slug: categorySlug } });
    if (!category) {
      category = await this.save(
        this.create({ slug: categorySlug, name: categoryName }),
      );
    }
    return category;
  }
}
</code></pre>
<blockquote>
<p>TypeOrm 0.2.45까지는 @EntityRepository를 사용했지만 deprecated 되면서 직접 decorator를 생성해야한다.
 <a href="https://stackoverflow.com/questions/71557301/how-to-workraound-this-typeorm-error-entityrepository-is-deprecated-use-repo">https://stackoverflow.com/questions/71557301/how-to-workraound-this-typeorm-error-entityrepository-is-deprecated-use-repo</a>
다운그레이드해도 에러 stackoverflow대로 해도 에러 8시간 잡아먹음...<br><del>세상이 날 억까하네ㅋㅋㅋ일단 진도 나가고 다음에 고치자..</del></p>
</blockquote>
<h2 id="🔷-categories-part">🔷 Categories Part</h2>
<h3 id="🔹-resolvefield">🔹@ ResolveField()</h3>
<p>매 request마다 계산된 field값을 가져온다.
(DB에는 존재하지 않고 GraphQL 스키마에만 존재)</p>
<pre><code class="language-typescript">@Resolver((of) =&gt; Category)
export class CategoryResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

  @ResolveField((type) =&gt; Int)
  restaurantCount(): number {
    return 80;
  }

  @Query((returns) =&gt; AllCategoriesOutput)
  allCategories(): Promise&lt;AllCategoriesOutput&gt; {
    return this.restaurantService.allCategories();
  }
}</code></pre>
<h2 id="🔷-repository-method-option">🔷 Repository method Option</h2>
<h3 id="🔹-like">🔹 Like</h3>
<pre><code class="language-typescript">const [restaurants, totalResults] = await this.restaurants.findAndCount({
        where: {
          name: Like(`%${query}%`),
        },
      });</code></pre>
<p> 위의 코드는 아래 쿼리를 실행한다.</p>
<pre><code class="language-sql">SELECT * FROM &quot;post&quot; WHERE &quot;title&quot; LIKE &#39;%{query}%&#39;</code></pre>
<p><a href="https://orkhan.gitbook.io/typeorm/docs/find-options#advanced-options">https://orkhan.gitbook.io/typeorm/docs/find-options#advanced-options</a>
<a href="https://github.com/typeorm/typeorm/blob/master/docs/find-options.md#advanced-options">https://github.com/typeorm/typeorm/blob/master/docs/find-options.md#advanced-options</a></p>
<h3 id="🔹-ilike">🔹 ILike</h3>
<p>Like는 대소문자를 구분하지만 ILike는 대소문자를 구분하지 않는다.</p>
<h3 id="🔹-raw">🔹 Raw</h3>
<pre><code class="language-typescript">const [restaurants, totalResults] = await this.restaurants.findAndCount({
        where: {
          name: Raw(name =&gt; `${name} ILIKE &#39;%${query}%&#39;`),
        },</code></pre>
<h3 id="🔹sql---like-clause">🔹SQL - LIKE Clause</h3>
<p>SQL LIKE 절은 wildcard 연산자를 사용하여 값을 유사한 값과 비교하는 데 사용된다. 퍼센트 기호(%)는 0, 하나 또는 여러 문자를 나타낸다. 밑줄(_)은 단일 숫자 또는 문자를 나타낸다. 이러한 기호는 조합하여 사용할 수 있다.</p>
<p>EX )</p>
<blockquote>
<p>WHERE SALARY LIKE &#39;200%&#39; : 200으로 시작하는 모든 값을 찾습니다.
WHERE SALARY LIKE &#39;%200%&#39;: 어느 위치든 200이 있는 값을 찾습니다.
WHERE SALARY LIKE &#39;_00%&#39;: 두 번째 및 세 번째 위치에 00이 있는 값을 찾습니다.
WHERE SALARY LIKE &#39;%2&#39;: 2로 끝나는 값을 찾습니다.
<a href="https://www.tutorialspoint.com/sql/sql-like-clause.htm">https://www.tutorialspoint.com/sql/sql-like-clause.htm</a></p>
</blockquote>
<h2 id="❓-questions">❓ Questions</h2>
<ul>
<li>express로 백엔드 구축했을 때는 Many-to-one / One-to-many 와 같은 관계를 어떻게 구현했었는지?</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 6 (Jest)]]></title>
            <link>https://velog.io/@yeopju_5/Uber-Eats-Chapter-9</link>
            <guid>https://velog.io/@yeopju_5/Uber-Eats-Chapter-9</guid>
            <pubDate>Wed, 20 Jul 2022 21:58:49 GMT</pubDate>
            <description><![CDATA[<h1 id="end-to-end-testing">End To End Testing</h1>
<h2 id="🔷-setup">🔷 SetUp</h2>
<p><code>Jest did not exit one second after the test run has completed.</code>와 같은 에러가 뜬다면 app.close()를 통해 테스트가 끝나고 난 후, app을 종료시킬 수 있다.</p>
<pre><code class="language-typescript">afterAll(async () =&gt; {
await app.close();
});</code></pre>
<h3 id="🔹-connection">🔹 Connection</h3>
<p>Connection은 특정 데이터베이스에 대한 단일 데이터베이스 ORM 연결. 
원래는 <code>getConnection()</code>이라는 함수를 통해 Connection을 진행할 수 있었지만 Deprecated된 이후에는 아래의 afterAll 부분의 코드와 같이 작성해야한다.</p>
<h3 id="🔹dropdatabase">🔹dropDatabase()</h3>
<p>데이터베이스와 모든 데이터를 삭제합니다. 데이터베이스 연결이 완료된 후에만 사용할 수 있습니다. test가 끝난 후에 test를 위해 생성했던 데이터를 삭제해주기 위해 사용한다.</p>
<pre><code class="language-typescript">/* eslint-disable prettier/prettier */
import { Test, TestingModule } from &#39;@nestjs/testing&#39;;
import { INestApplication } from &#39;@nestjs/common&#39;;
import { AppModule } from &#39;../src/app.module&#39;;
import { DataSource } from &#39;typeorm&#39;;

const GRAPHQL_ENDPOINT = &#39;/graphql&#39;;

const testUser = {
  email: &#39;yeop@naver.com&#39;,
  password: &#39;12345&#39;,
};

jest.mock(&#39;got&#39;, () =&gt; {
  return {
    post: jest.fn(),
  };
});

describe(&#39;AppController (e2e)&#39;, () =&gt; {
  let app: INestApplication;

  beforeAll(async () =&gt; {
    const module: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = module.createNestApplication();
    await app.init();
  });

  afterAll(async () =&gt; {
    const dataSource = new DataSource({
      type: &#39;postgres&#39;,
      host: &#39;localhost&#39;,
      port: 5432,
      username: &#39;juyeop&#39;,
      password: &#39;12345&#39;,
      database: &#39;nuber-eats-test&#39;,
    });
    const connection = await dataSource.initialize();
    await connection.dropDatabase();
    await connection.destroy();
    app.close();
  });

  it.todo(&#39;can createAccount&#39;);
  it.todo(&#39;userProfile&#39;);
  it.todo(&#39;login&#39;);
  it.todo(&#39;me&#39;);
  it.todo(&#39;verifyEmail&#39;);
  it.todo(&#39;userEdit&#39;);
});
</code></pre>
<h2 id="🔷-resolvers-testing">🔷 Resolvers Testing</h2>
<h3 id="🔹--detectopenhandles">🔹--detectOpenHandles</h3>
<p>&quot;Jest did not exit one second after the test run has completed.</p>
<p>This usually means that there are asynchronous operations that weren&#39;t stopped in your tests. Consider running Jest with <code>--detectOpenHandles</code> to troubleshoot this issue. &quot;</p>
<p>이와 같은 에러가 발생할 때 package.json에 <code>--detectOpenHandles</code>를 추가하면 어느 부분이 asynchronous operations that weren&#39;t stopped 인지 알려준다.</p>
<p>이번 경우에는 <code>got</code> moudule에서 에러가 났으며 이를 고치기 위해 got.post를 mock함수로 만들었다.</p>
<h3 id="🔹-login-mutation">🔹 Login Mutation</h3>
<pre><code class="language-typescript">describe(&#39;login&#39;, () =&gt; {
    it(&#39;Should login with correct credentials&#39;, () =&gt; {
      return request(app.getHttpServer())
        .post(GRAPHQL_ENDPOINT)
        .send({
          query: `
          mutation {
            login(input:{
              email:&quot;${testUser.email}&quot;,
              password:&quot;${testUser.password}&quot;,
            }) {
              ok
              error
              token
            }
          }
        `,
        })
        .expect(200)
        .expect((res) =&gt; {
          const {
            body: {
              data: { login },
            },
          } = res;
          expect(login.ok).toEqual(true);
          expect(login.error).toEqual(null);
          expect(login.token).toEqual(expect.any(String));
          jwtToken = login.token;
        });
    });
  });</code></pre>
<h3 id="🔹-userprofile">🔹 UserProfile</h3>
<ul>
<li>userId를 데이터베이스에서 가져오기 위해서는 user 테이블의 정보를 가져와야하는데 이는 모듈을 통해 다음과 같이 가져올 수 있다.<pre><code class="language-typescript">usersRepository = module.get&lt;Repository&lt;User&gt;&gt;(getRepositoryToken(User));</code></pre>
또한 superTest로 request를 보낼 때, <code>.post()</code> 다음 <code>.set()</code>을 이용해서 http header를 보낼 수 있다.</li>
</ul>
<pre><code class="language-typescript">describe(&#39;userProfile&#39;, () =&gt; {
    let userId: number;
    beforeAll(async () =&gt; {
      const [user] = await usersRepository.find();
      userId = user.id;
    });
    it(&quot;Should see a User&#39;s Profile&quot;, () =&gt; {
      return request(app.getHttpServer())
        .post(GRAPHQL_ENDPOINT)
        .set(&#39;X-JWT&#39;, jwtToken)
        .send({
          query: `
          {
          userProfile(userId:${userId}){
            ok
            error
            user {
              id
            }
          }
        }
          `,
        })
        .expect(200)
        .expect((res) =&gt; {
          const {
            body: {
              data: {
                userProfile: {
                  ok,
                  error,
                  user: { id },
                },
              },
            },
          } = res;
          expect(ok).toEqual(true);
          expect(error).toEqual(null);
          expect(id).toEqual(userId);
        });
    });</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[NestJs Chapter 5 (Jest)]]></title>
            <link>https://velog.io/@yeopju_5/Chapter5</link>
            <guid>https://velog.io/@yeopju_5/Chapter5</guid>
            <pubDate>Mon, 18 Jul 2022 20:31:27 GMT</pubDate>
            <description><![CDATA[<h1 id="unit-testing-jwt-and-mail">Unit Testing Jwt And Mail</h1>
<h2 id="🔷-nodejs-package-mocking">🔷 Node.js Package Mocking</h2>
<pre><code class="language-typescript">import * as jwt from &#39;jsonwebtoken&#39;;

jest.mock(&#39;jsonwebtoken&#39;, () =&gt; {
  sign: jest.fn(() =&gt; &#39;TOKEN&#39;);
});</code></pre>
<h2 id="-beforeeach-describe">※ BeforeEach, Describe</h2>
<p><code>describe</code>는 여러 관련된 테스트들의 그룹화하는 블록이며 BeforeEach는 각 테스트 전에 실행하는 함수인데 여기서 테스트의 기준은 <code>describe</code>가 아닌 <code>it</code>이다.</p>
<h2 id="🔷-testcov-check">🔷 Test:Cov Check</h2>
<p><code>npm run test:cov</code>를 실행하면 coverage라는 디렉토리가 생성된다.  이 디렉토리는 유닛테스팅의 현황을 완성된 html로 제공한다. 진행도와 유닛테스팅이 되지 않은 라인까지 알 수 있다.
<img src="https://velog.velcdn.com/images/yeopju_5/post/a5a9d15c-12a1-40a8-86b2-8ebcfe8b2626/image.png" alt=""></p>
<h2 id="❓-questions">❓ Questions</h2>
<ul>
<li><code>jest.spyOn</code> 과 <code>jest.fn()</code> 의 차이점이 &#39;호출을 추적하는지&#39; 라고 하는데 여기서 말하는 호출은 Argument이며 그렇다면 <code>jest.fn()</code> 또한 호출을 추적하는 것이 아닌가요?</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>