<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yh_lee.log</title>
        <link>https://velog.io/</link>
        <description>공부기록</description>
        <lastBuildDate>Thu, 09 Feb 2023 06:52:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yh_lee.log</title>
            <url>https://velog.velcdn.com/images/yh_lee/profile/77590f63-d7c5-4228-8c90-753f5603cc52/image.PNG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yh_lee.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yh_lee" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[JAVA 8, 9, 10, 11]]></title>
            <link>https://velog.io/@yh_lee/JAVA-8-9-10-11</link>
            <guid>https://velog.io/@yh_lee/JAVA-8-9-10-11</guid>
            <pubDate>Thu, 09 Feb 2023 06:52:05 GMT</pubDate>
            <description><![CDATA[<h1 id="chap-1">Chap 1</h1>
<h2 id="java-8">Java 8</h2>
<p>자바 8을 사용하면 자연어에 더 가깝게 간단한 방식으로 코드 구현 가능.</p>
<pre><code class="language-java">inventory.sort(comparing(Apple::getWeight)
//java 8 - 사과의 무게를 비교해서 목록에서 정렬

Collections.sort(inventory, new Comparator&lt;Apple&gt;){
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
}
//이전 자바</code></pre>
<p>자바는 병렬 실행 환경을 쉽게 관리하고 에러가 덜 발생하는 방향으로 진화하려 노력함. 자바 5에서는 스레드 풀, 병렬 실행 컬렉션 등 강력한 도구를 도입.
<strong>자바 9에서는 리액티브 프로그래밍이라는 병렬 실행 기법을 지원함.</strong></p>
<p>자바 8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용이라는 두 가지 요구사항을 기반으로 함.</p>
<ul>
<li>스트림 API</li>
<li>메서드에 코드를 전달하는 기법</li>
<li>인터페이스의 디폴트 메서드</li>
</ul>
<h3 id="스트림-api">스트림 API</h3>
<p>자바 8은 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 병렬 연산을 지원하는 스트림이라는 새로운 API 지원.
즉, 스트림을 이용하면 <strong>에러를 자주 일으키며 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 sysnchronized를 사용하지 않아도 됨.</strong>
다른 관점에선 메서드에 1)코드를 전달하는 간결 기법(메서드 참조와 람다)과 인터페이스의 2)디폴트 메서드가 존재할 수 있음을 알 수 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자 수에 따른 규모 확장성]]></title>
            <link>https://velog.io/@yh_lee/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</link>
            <guid>https://velog.io/@yh_lee/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</guid>
            <pubDate>Sun, 13 Nov 2022 10:57:07 GMT</pubDate>
            <description><![CDATA[<h1 id="단일-서버">단일 서버</h1>
<p>모든 컴포넌트가 단 한 대의 서버에서 실행되는 시스템. 웹앱, 데이터베이스, 캐시 등이 전부 서버 한 대에서 실행되는 상태.<img src="https://velog.velcdn.com/images/yh_lee/post/2af271d2-70b8-4eaf-8871-6c103a41ef77/image.png" alt=""></p>
<p>1) 사용자는 도메인 이름(api.mysite.com)을 이용해서 웹 사이트에 접속
2) 접속을 위해서는 도메인 이름 서비스(DNS)에서 질의하여 IP 주소 변환
3) 15.125.23.214 웹 서버의 주소를 반환 받은 후 해당 IP 주소로 HTTP 요청이 전달
4) 요청 받은 웹 서버는 HTML 페이지나 JSON 형태의 응답을 반환</p>
<h1 id="데이터베이스">데이터베이스</h1>
<p>웹/모바일 트래픽 처리 서버(웹 계층)와 데이터베이스 서버(데이터 계층)를 분리하면 각각 독립적으로 확장해 나갈  수 있다.
<img src="https://velog.velcdn.com/images/yh_lee/post/412354ca-55ac-4869-873f-a15cc26efc5d/image.png" alt=""></p>
<h2 id="데이터베이스-종류">데이터베이스 종류</h2>
<ul>
<li><p>관계형 데이터베이스(RDBMS): 관계형 데이터베이스 관리 시스템, 자료를 열과 컬럼으로 표현한다. SQL을 사용하면 여러 테이블에 있는 데이터를 관계에 따라 조인(join)하여 합칠 수 있다. </p>
</li>
<li><p>비 관계형 데이터베이스(NoSQL): 일반적으로 조인 연산은 지원하지 않는다.</p>
<ul>
<li>키-값 저장소(key-value store)</li>
<li>그래프 조장소(graph store)</li>
<li>칼럼 저장소(column store)</li>
<li>문서 저장소(document store)</li>
</ul>
</li>
</ul>
<h3 id="비관계형-데이터베이스가-바람직한-선택일-수-있는-경우">비관계형 데이터베이스가 바람직한 선택일 수 있는 경우</h3>
<ul>
<li>아주 낮은 응답 지연시간(latency)</li>
<li>비정형 데이터를 다룰 때</li>
<li>데이터(JSON, YAML, XML)를 직렬화하거나(serialize) 역직렬화(deserialize)할 수 있기만 하면 됨</li>
<li>아주 많은 양의 데이터를 저장할 필요가 있을 때</li>
</ul>
<h2 id="수직적-규모-확장-vs-수평적-규모-확장">수직적 규모 확장 vs 수평적 규모 확장</h2>
<ul>
<li>수직적 규모 확장(scale up): 서버에 고사양 자원(CPU,RAM)을 추가하는 행위</li>
<li>수평적 규모 확장(scale out): 더 많은 서버를 추가해서 성능을 개선하는 행위</li>
</ul>
<p>서버로 유입되는 트래픽 양이 적을 때는 <strong>수직적 확장이</strong> 좋은 선택이다. 하지만 한 대의 서버에 CPU나 메모리를 무한대로 증설할 수 있는 방법이 없고, 장애에 대한 자동복구나 다중화 방안을 제시하지 않는다. 서버에 장애가 발생하면 웹사이트/앱은 완전히 중단된다. 
수직적 확장의 이런 단점 때문에 대규모 애플리케이션을 지원할 때는 <strong>수평적 규모 확장법</strong>이 적절하다.</p>
<h2 id="로드밸런서">로드밸런서</h2>
<p>로드밸런서는 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다. <img src="https://velog.velcdn.com/images/yh_lee/post/f651b8b7-b898-49d5-adc8-77c44606971a/image.png" alt=""></p>
<h2 id="데이터베이스-다중화">데이터베이스 다중화</h2>
<p>서버 사이에 주(master)-부(salve) 관계를 설정하고 데이터 원본은 주 서버에, 사본은 부 서버에 저장하는 방식이다. 
쓰기 연산은 마스터에서만 지원한다. 부 데이터베이스는 주 데이터베이스로부터 사본을 전달받으며 읽기 연산만을 지원한다. 데이터베이스를 변경하는 명령어들(insert, delete, update)은 주 데이터베이스로만 전달되어야 한다. 대부분의 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높다. 그래서 부 데이터베이스가 주 데이터베이스의 수보다 많다. <img src="https://velog.velcdn.com/images/yh_lee/post/428cde6f-dc5d-4791-8cd9-c89c7347e496/image.png" alt=""></p>
<p>데이터베이스 다중화로 인한 이득은 다음과 같다.</p>
<ul>
<li>더 나은 성능: 병렬로 처리될 수 있는 질의(query)의 수가 늘어나므로 성능이 좋아진다.</li>
<li>안정성(reliability): 서버 일부가 파괴되어도 데이터는 보존될 것이다. 데이터를 지역적으로 떨어진 여러 장소에 다중화시켜 두었기 때문이다.</li>
<li>가용성(availability): 데이터를 여러 지역에 복제해 둠으로써 하나의 데이터베이스가 장애가 생겨도 다른 서버에 있는 데이터를 가져와서 계속 서비스할 수 있게 된다.</li>
</ul>
<h1 id="캐시">캐시</h1>
<p>캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고 뒤이은 요청이 빨리 처리될 수 있도록 하는 저장소이다. 애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우되는데, 캐시는 그런 문제를 완화할 수 있다. </p>
<h2 id="캐시-계층">캐시 계층</h2>
<p>캐시 계층(cache tier)은 데이터가 잠시 보관되는 곳으로 데이터베이스보다 훨씬 빠르다. 성능 개선과 데이터베이스의 부하를 줄일 수 있고, 캐시 계층의 규모를 독립적으로 확장시키는 것도 가능해진다. <img src="https://velog.velcdn.com/images/yh_lee/post/ac22a5da-577d-407f-9a74-ee5001feea59/image.png" alt=""></p>
<p>캐시 서버를 이용하는 방법은 대부분의 캐시 서버들이 API를 제공하기 때문에 이를 사용하면 된다.</p>
<pre><code class="language-python">SECONDS=1
cache.set(&#39;myKey&#39;, &#39;hi there&#39;,3600*SECONDS)
cache.get(&#39;myKey&#39;)</code></pre>
<h2 id="캐시-사용-유의점">캐시 사용 유의점</h2>
<ul>
<li>데이터 갱신은 자주 일어나지 않지만 참조는 빈번하게 일어난다면 사용을 고려</li>
<li>캐시 서버가 재시작되면 데이터는 사라지기 때문에 중요한 데이터는 주 데이터베이스에 두는 것이 좋다.</li>
<li>캐시 서버를 한 대만 두면 단일 장애 지점(Single Point of Failure, SPOF)이 되어버릴 가능성이 있다. 어떤 특정 지점에서의 장애가 전체 시스템의 동작을 중단시켜버릴 수 있는 경우를 의미하는데, 이를 방지하기 위해 여러 지역에 캐시 서버를 분산시켜야 한다.<img src="https://velog.velcdn.com/images/yh_lee/post/d1caa87f-126d-44e0-9aa4-c284981f977a/image.png" alt=""></li>
</ul>
<h1 id="콘텐츠-전송-네트워크cdn">콘텐츠 전송 네트워크(CDN)</h1>
<p>CDN은 정적 콘텐츠를 전송하는데 쓰이는 분산된 서버의 네트워크이다. 이미지, 비디오, CSS, JavaScript 파일 등을 캐시할 수 있다.<img src="https://velog.velcdn.com/images/yh_lee/post/de8c93c4-32e4-47c2-b4d0-3771ce3a9594/image.png" alt=""></p>
<h2 id="cdn-사용-유의점">CDN 사용 유의점</h2>
<ul>
<li><p>CDN은 보통 제3 사업자(third-party providers)에 의해 운영되며, CDN으로 들어가고 나가는 데이터 전송 양에 따라 요금을 내게 된다.</p>
</li>
<li><p>콘텐츠 무효화(invalidation) 방법: 아직 만료되지 않은 콘텐츠라 하더라도 아래 방법을 사용하면 CDN에서 제거할 수 있다.</p>
<ul>
<li>CDN 서비스 사업자가 제공하는 API를 사용해서 콘텐츠 무효화</li>
<li>콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(object versioning) 이용</li>
</ul>
</li>
</ul>
<h2 id="cdn과-캐시가-추가된-설계">CDN과 캐시가 추가된 설계</h2>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/8084b860-352c-440e-b1af-4b2657f9f554/image.png" alt=""></p>
<ol>
<li>정적 콘텐츠(JS, CSS, 이미지 등)는 웹 서버를 통해 서비스하지 않고 CDN을 통해 제공한다.</li>
<li>캐시가 데이터베이스 부하를 줄여준다.</li>
</ol>
<h1 id="무상태stateless-웹-계층">무상태(stateless) 웹 계층</h1>
<p>웹 계층을 수평적으로 확장하기 위해 상태 정보(사용자 세션 데이터 등)를 웹 계층에서 제거해야 한다. 상태 정보를 관계형 데이터베이스나 NoSQL 같은 지속성 저장소에 보관하고 필요할 때 가져오도록 한다. 이렇게 구성된 웹 계층을 무상태 웹 계층이라 부른다.</p>
<h2 id="상태-정보-의존적인-아키텍쳐">상태 정보 의존적인 아키텍쳐</h2>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/3276b0b6-115b-4f73-91ae-fcf6975d439a/image.png" alt=""></p>
<h2 id="무상태-아키텍쳐">무상태 아키텍쳐</h2>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/9fedb019-9a5d-4ebf-b404-78fddeee26a6/image.png" alt=""></p>
<br>
출처: 가상 면접 사례로 배우는 대규모 시스템 설계 기초(알렉스 쉬 지음)]]></description>
        </item>
        <item>
            <title><![CDATA[@SpringBootTest vs @RunWith(SpringRunner.class)]]></title>
            <link>https://velog.io/@yh_lee/SpringBootTest-vs-RunWithSpringRunner.class</link>
            <guid>https://velog.io/@yh_lee/SpringBootTest-vs-RunWithSpringRunner.class</guid>
            <pubDate>Mon, 17 Oct 2022 08:36:59 GMT</pubDate>
            <description><![CDATA[<h3 id="junit4에서-사용하는-runwithspringrunnerclass와-springboottest의-차이는-무엇일까">Junit4에서 사용하는 @RunWith(SpringRunner.class)와 @SpringBootTest의 차이는 무엇일까?</h3>
<p>@SpringBootTest 사용시엔 applicatino context를 전부 로딩해서 잘못하면 무거운 프로젝트의 역할을 한다.</p>
<p>하지만 Junit4의 @RunWith(SpringRunner.class)를 사용하면 @Autowired, @MockBean에 해당되는 것만 application context를 로딩하게 돼서 Junit4에서 필요한 조건에 맞춰서 @RunWith(SpringRunner.class)를 사용한다.</p>
<h3 id="그렇다면-runwithspringrunnerclass의-역할은">그렇다면 @RunWith(SpringRunner.class)의 역할은?</h3>
<p>JUnit 테스트 라이브러리를 Spring TestContext Framework와 결합한다. 이를 @RunWith(SpringRunner.class)라고 한다. 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSRF란?]]></title>
            <link>https://velog.io/@yh_lee/CSRF%EB%9E%80</link>
            <guid>https://velog.io/@yh_lee/CSRF%EB%9E%80</guid>
            <pubDate>Wed, 05 Oct 2022 08:06:40 GMT</pubDate>
            <description><![CDATA[<h1 id="csrfcross-site-request-forgery">CSRF(Cross Site Request Forgery)</h1>
<p>정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다.
A 도메인에서 인정된 사용자 user1이 위조된 request를 포함한 link, email을 사용했을 경우 A 도메인에서는 이 사용자가 일반 유저인지 악용된 공격인지 구분할 수 없다.</p>
<p>CSRF protection은 spring security에서 default로 설정된다. protection을 통해서 GET 요청을 제외한 상태를 변화시킬 수 있는 POST, PUT, DELETE 요청으로부터 보호한다.</p>
<pre><code class="language-java">@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthTokenProvider authTokenProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
        ...
        return http.build();
    }</code></pre>
<p>CSRF protection을 적용하면 html에서 아래와 같은 csrf 토큰이 포함되어야 요청을 받아들이게 되어 위조 요청을 방지한다.</p>
<pre><code>&lt;input type=&quot;hidden&quot; name=&quot;${_csrf.parameterName}&quot; value=&quot;${_csrf.token}&quot;/&gt;</code></pre><h2 id="rest-api에서-csrf">REST API에서 CSRF</h2>
<h3 id="csrf을-disable한-이유">CSRF을 disable한 이유</h3>
<p>공식문서에서 non-browser clients만을 위한 서비스라면 csrf를 disable 해도 좋다고 명시되어 있다. 이유는 rest api를 이용하는 서버라면 세션 기반 인증과는 다르게 <strong>stateless하기 때문에 서버에 인증정보를 보관하지 않는다.</strong> rest api에서 client는 <strong>권한이 필요한 요청</strong>을 하기 위해서 요청에 필요한 인증정보(OAuth2, jwt 토큰)을 포함시켜야 한다. </p>
<p>그래서 서버에 인증정보를 저장하지 않기 때문에 불필요한 csrf 코드를 작성하지 않아도 된다.</p>
<p>참고
<a href="https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80">https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로드밸런싱]]></title>
            <link>https://velog.io/@yh_lee/%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1</link>
            <guid>https://velog.io/@yh_lee/%EB%A1%9C%EB%93%9C%EB%B0%B8%EB%9F%B0%EC%8B%B1</guid>
            <pubDate>Mon, 29 Aug 2022 13:40:54 GMT</pubDate>
            <description><![CDATA[<h1 id="로드-밸런싱load-balancing">로드 밸런싱(Load balancing)</h1>
<blockquote>
<p>부하분산 또는 로드 밸런싱은 컴퓨터 네트워크 기술의 일종으로 둘 혹은 셋 이상의 중앙처리장치 혹은 저장장치와 같은 컴퓨터 자원들에게 작업을 나누는 것을 의미한다.</p>
</blockquote>
<h2 id="종류">종류</h2>
<p>로드 밸런싱의 종류는 OSI 7계층에 따라 나뉜다. </p>
<ul>
<li>L4: Transport 계층을 사용, IP 주소와 포트 번호 부하 분산이 가능하다.</li>
<li>L7: Application 계층을 사용, URL 또는 HTTP 헤더에서 부하 분산이 가능하다. <h4 id="osi-7계층">OSI 7계층</h4>
<img src="https://velog.velcdn.com/images/yh_lee/post/8972120b-0610-4ce9-a85b-d485e63e0db6/image.png" alt=""></li>
</ul>
<h2 id="nlb-vs-alb">NLB vs ALB</h2>
<p>로드 밸런서는 몇 계층에서 분산작업을 수행하느냐에 따라 NLB와 ALB로 나눌 수 있다.</p>
<h3 id="nlbnetwork-loadbalancer">NLB(Network LoadBalancer)</h3>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/dd2cbbe3-764f-4f22-a17d-d87a25a37f09/image.png" alt=""></p>
<ul>
<li>&quot;Client IP와 서버&quot;사이에 서버로 들어오는 트래픽은 Load Balancer를 통하고, 나가는 트래픽은 Client IP와 직접 통신한다.</li>
<li>NLB는 Security Group이 적용되지 않아서 서버에 적용된 Security Group에서 보안이 가능하다.</li>
<li>Client -&gt; Server에서 Access 제한이 가능하다.</li>
<li>NLB는 할당한 Elastic IP를 Static IP로 사용이 가능하여 DNS Name과 IP 주소 모두 사용 가능하다.</li>
<li>Name Server 또는 Route 53에서 A Record 사용이 가능하다.</li>
</ul>
<h3 id="albapplication-loadbalancer">ALB(Application LoadBalancer)</h3>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/db09dba7-1baf-4042-a1a7-55e78f9360d7/image.png" alt=""></p>
<ul>
<li>Reverse Proxy 대로 Client IP와 서버사이에 들어오고 나가는 트래픽이 모두 Load Balancer와 통신한다.</li>
<li>CLB/ALB는 Security Group을 통한 보안이 가능하다.</li>
<li>Client -&gt; Load Balancer의 Access 제한이 가능하다.</li>
<li>ALB/CLB는 IP 주소가 변동되기 때문에 Client에서 Access할 ELB의 DNS Name을 이용해야 한다. </li>
<li>Name Server 또는 Route 53에서 CNAME을 사용해야 Domain Name 연동이 가능하다.</li>
</ul>
<h2 id="방식">방식</h2>
<h3 id="라운드-로빈-방식">라운드 로빈 방식</h3>
<p>클라이언트로부터 받은 요청을 로드밸런싱 대상 서버에 순서대로 할당받는 방식이다. 균등한 분산이 이루어진다.</p>
<h3 id="가중-라운드-로빈-방식">가중 라운드 로빈 방식</h3>
<p>실제 서버에 서로 다른 처리 용량을 지정할 수 있다. 각 서버에 가중치를 부여할 수 있으며 여기서 지정한 정수값을 통해 처리 용량을 정한다.</p>
<h3 id="최소-연결-방식">최소 연결 방식</h3>
<p>연결 수가 가장 적은 서버에 네트워크 연결방향을 정한다. 동적인 분산 알고리즘으로 각 서버에 대한 연결 수를 동적으로 카운트하고, 동적으로 변하는 요청에 대한 부하를 분산시킨다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2장 객체 생성과 파괴]]></title>
            <link>https://velog.io/@yh_lee/2%EC%9E%A5-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%EA%B3%BC-%ED%8C%8C%EA%B4%B4</link>
            <guid>https://velog.io/@yh_lee/2%EC%9E%A5-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%EA%B3%BC-%ED%8C%8C%EA%B4%B4</guid>
            <pubDate>Sun, 28 Aug 2022 07:45:12 GMT</pubDate>
            <description><![CDATA[<h1 id="1-생성자-대신-정적-팩터리-메서드를-고려하라">1. 생성자 대신 정적 팩터리 메서드를 고려하라</h1>
<p>클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 또 다른 기법으로 클래스가 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공하는 방법이 있다. 클래스의 인스턴스를 반환하는 단순한 정적 메서드이다. </p>
<p><strong>장점</strong></p>
<ol>
<li>이름을 가질 수 있다.</li>
<li>호출될 때마다 인스턴스를 새로 생성하지 않아도 된다. </li>
<li>반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.</li>
<li>입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. </li>
<li>정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. </li>
</ol>
<p><strong>단점</strong></p>
<ol>
<li>상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.</li>
<li>정적 팩터리 메서드는 프로그래머가 찾기 어렵다. </li>
</ol>
<blockquote>
<p>정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 해도 정적 팩터리를 사용하는 게 유리한 경우가 많으니 무작정 public 생성자를 제공하던 습관이 있으면 고치자.</p>
</blockquote>
<h1 id="2-생성자에-매개변수가-많다면-빌더를-고려하라">2. 생성자에 매개변수가 많다면 빌더를 고려하라</h1>
<p>정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이 있다. </p>
<p>점층적 생성자 패턴(telescoping constructor pattern)을 사용할 수도 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. </p>
<p>또 다른 방식으로 생성자를 객체로 만든 후 세터(setter) 메서드를 호출해 매개변수를 설정하는 자바빈즈 패턴(JavaBeans pattern)이 있다. 하지만 이 패턴은 객체 하나를 만드려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지 일관성(consistency)가 무너진 상태에 놓인다. 따라서 클래스를 불변으로 만들 수 없다.</p>
<h2 id="빌더-패턴builder-pattern">빌더 패턴(Builder Pattern)</h2>
<p>점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattern)이 있다. </p>
<p><strong>동작과정</strong></p>
<p>1) 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
2) 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 
3) 매개변수가 없는 build 메서드를 호출해 드디어 우리에게 필요한 (보통은 불변인) 객체를 얻는다. </p>
<pre><code class="language-java">public class NutrutuibFacts{
    private final int servingSize;//필수
    private final int servings;//필수
    private final int calories;
    private final int fat;

    public static class Builder{
        //필수 매개변수
        private final int servingSize;
        private final int servings;

        //선택 - 기본값으로 초기화
        private int calories=0;
        private int fat=0;

        public Builder(int servingSize, int servings){
            this.servingSize=servingSize;
            this.servings=servings;
        }

        public Builder calories(int val){
            calories=val;
            return this;
        }
        public Builder fat(int val){
            fat=val;
            return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder){
        servingSize=builder.servingSize;
        servings=builder.servings;
        calories=builder.calories;
        fat=builder.fat;
    }
}</code></pre>
<p>NutritionFacts 클래스는 불변이며 모든 매개변수의 기본값을 한곳에 모아 두었다. 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이런 방식을 플루언트 API(fluent API) 또는 메서드 연쇄(method chaining)라 한다. </p>
<p>다음은 클라이언트 코드의 모습이다. </p>
<pre><code class="language-java">NutritionFacts cocaCola = new NutritionFacts.Builder(240,8)
                                .calories(100)
                                   .fat(2)
                                   .build();</code></pre>
<p>빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수(named optional parameters)를 흉내 낸 것이다.</p>
<p>빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. </p>
<blockquote>
<p>생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.</p>
</blockquote>
<h1 id="3-private-생성자나-열거-타입으로-싱글톤임을-보증하라">3. private 생성자나 열거 타입으로 싱글톤임을 보증하라</h1>
<p>싱글톤(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 클래스를 싱글톤으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.</p>
<p>타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글톤이 아니라면 싱글톤 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다. </p>
<p><strong>싱글톤 생성 방식1 - public static final 필드 방식</strong></p>
<pre><code class="language-java">public class Elvis{
    public static final Elvis INSTANCE = new Elvis;
    private Elvis(){
        ...
    }
}</code></pre>
<p>private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출된다. public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임을 보장한다. </p>
<p><strong>싱글톤 생성 방식2 - 정적 팩터리 방식</strong></p>
<pre><code class="language-java">public class Elvis{
    private static final Elvis INSTANCE = new Elvis;
    private Elvis(){
        ...
    }
    public static Elvis getInstance(){
        return INSTANCE;
    }
}    </code></pre>
<p>Elvis.getInstance는 항상 같은 객체의 참조를 반환하므로 두번째 인스턴스는 결코 만들어지지 않는다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다. </p>
<p>정적 팩터리 방식의 첫번째 장점은 API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다는 것이다. 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다. 두번째 장점은 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다는 점이다. Elvis::getInstance를 Supplier&lt;Elvis&gt;로 사용하는 식이다. </p>
<p><strong>싱글톤 생성 방식3 - 열거 타입 방식</strong></p>
<pre><code class="language-java">public enum Elvis{
    INSTANCE;
    public void leaveTheBuilding(){...}
}</code></pre>
<p>public 필드 방식과 비슷하지만 더 간결하고 추가 노력없이 직렬화할 수 있다. 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글톤을 만드는 가장 좋은 방법이다. 단 만들려는 싱글톤이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다. 열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다. </p>
<h1 id="4-인스턴스화를-막으려거든-private-생성자를-사용하라">4. 인스턴스화를 막으려거든 private 생성자를 사용하라</h1>
<p>정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다. java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다. 또한 java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수도 있다. 마지막으로 final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가능하기 때문이다. </p>
<p>추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다. </p>
<pre><code class="language-java">public class UtilityClass{
    private UtilityClass(){
        throw new AssertionError();
    }
    ...
}    </code></pre>
<p>명시적 생성자가 private이니 클래스 바깥에서는 접근할 수 없다. 이 코드는 어떤 환경에서도 클래스가 인스턴스화되는 것은 막아준다. 이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private으로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀버린다. </p>
<h1 id="5-자원을-직접-명시하지-말고-의존-객체-주입을-사용하라">5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로필과 프로퍼티 파일]]></title>
            <link>https://velog.io/@yh_lee/%ED%94%84%EB%A1%9C%ED%95%84%EA%B3%BC-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%ED%8C%8C%EC%9D%BC</link>
            <guid>https://velog.io/@yh_lee/%ED%94%84%EB%A1%9C%ED%95%84%EA%B3%BC-%ED%94%84%EB%A1%9C%ED%8D%BC%ED%8B%B0-%ED%8C%8C%EC%9D%BC</guid>
            <pubDate>Mon, 15 Aug 2022 08:06:01 GMT</pubDate>
            <description><![CDATA[<h1 id="chap17-프로필과-프로퍼티-파일">Chap17 프로필과 프로퍼티 파일</h1>
<h2 id="프로필">프로필</h2>
<p>개발을 진행하는 동안에는 실제 서비스 목적으로 운영중인 DB를 이용할 수는 없다. 실제 서비스 장비에 배포하기 전에 설정 정보를 변경하고 배포하는 방법을 사용할 수도 있지만 너무 원시적이고 실수하기 쉽다. 
에러를 방지하기 위해 처음부터 개발 목적 설정과 실 서비스의 목적 설정을 구분해서 작성하는 것이다. 이를 위한 스프링 기능을 프로필(profile)이라 한다.</p>
<p>프로필은 논리적인 이름으로서 설정 집합에 프로필을 지정할 수 있다. 스프링 컨테이너는 설정 집합 중에서 지정한 이름을 사용하는 프로필을 선택하고 해당 프로필에 속한 설정을 이용해서 컨테이너를 초기화할 수 있다. </p>
<p>예를 들어 로컬 개발 환경을 위한 DataSource 설정을 &quot;dev&quot; 프로필로 지정하고 실 서비스를 위한 설정을 &quot;real&quot; 프로필로 지정한 뒤, &quot;dev&quot; 프로필을 이용해서 스프링 컨테이너를 초기화할 수 있다. 그럼 &quot;dev&quot; 프로필에 정의된 빈을 사용하게 된다. <img src="https://velog.velcdn.com/images/yh_lee/post/86160b7b-6cc1-467b-be85-26f24fa43c55/image.png" alt=""></p>
<h3 id="configuration-설정에서-프로필-사용하기">@Configuration 설정에서 프로필 사용하기</h3>
<p>@Configuration 어노테이션을 이용한 설정에서 프로필을 지정하려면 @Profile 어노테이션을 이용한다. </p>
<pre><code class="language-java">@Configuration
@Profile(&quot;dev&quot;)
public class DsDevConfig{
    @Bean(destroyMethod=&quot;close&quot;)
    public DataSource dataSource(){
        ...
    }
}

@Configuration
@Profile(&quot;real&quot;)
public class DsRealConfig{
    @Bean(destroyMethod=&quot;close&quot;)
    public DataSource dataSource(){
        ...
    }
}</code></pre>
<p>스프링 컨테이너를 초기화할 때 &quot;dev&quot; 프로필을 활성화하면 DsDevConfig 클래스를 설정으로 사용한다. 
두 dataSource 빈 중에 어떤 빈을 사용할지는 활성화한 프로필에 따라 달라진다. 특정 프로필을 선택하려면 컨테이너를 초기화하기 전에 setActiveProfiles() 메서드를 사용해서 프로필을 설정해야 한다.</p>
<pre><code>context.getEnviroment().setActiveProfiles(&quot;dev&quot;);
context.register(MemberConfig.class, DsDevConfig.class, DsRealConfig.class);
context.refresh();</code></pre><p>프로필을 사용할 때 주의할 점은 설정 정보를 전달하기 전에 어떤 프로필을 사용할지 지정해야 한다는 점이다. dev로 설정 후 register() 메서드로 설정 파일 목록을 지정한다. 이후 refresh() 메서드를 실행해서 컨테이너를 초기화했다. </p>
<p>시스템 프로퍼티를 이용하여 설정할 수도 있다. </p>
<pre><code>java -Dspring.profiles.active=dev main.Main</code></pre><p>자바의 시스템 프로퍼티뿐만 아니라 OS의 &quot;spring.profiles.active&quot; 환경 변수에 값을 설정해도 된다. 프로필의 우선순위는 다음과 같다. </p>
<ul>
<li>setActiveProfiles()</li>
<li>자바 시스템 프로퍼티 </li>
<li>OS 환경 변수</li>
</ul>
<h3 id="configuration을-이용한-프로필-설정">@Configuration을 이용한 프로필 설정</h3>
<p>중첩 클래스를 이용해서 프로필 설정을 한 곳으로 모을 수 있다.</p>
<pre><code class="language-java">@Configuration
public class MemberConfigWithProfile{
    @Autowired
    private DataSource dataSource;

    @Bean
    public MemberDao memberDao(){
        return new MemberDao(dataSource);
    }

    @Profile(&quot;dev&quot;)
    public class DsDevConfig{
        @Bean(destroyMethod=&quot;close&quot;)
            public DataSource dataSource(){
        ...
        }
    }

    @Profile(&quot;real&quot;)
    public class DsRealConfig{
        @Bean(destroyMethod=&quot;close&quot;)
        public DataSource dataSource(){
            ...
        }
    }    
}</code></pre>
<h3 id="다수-프로필-설정">다수 프로필 설정</h3>
<p>스프링 설정은 두 개 이상의 프로필 이름을 가질 수 있다. </p>
<pre><code class="language-java">@Configuration
@Profile(&quot;real,test&quot;)
public class DataSourceJndiConfig{
    ...
}</code></pre>
<p>프로필을 설정할 때 느낌표를 사용하면 해당 프로필이 활성화되지 않았을 때 사용한다는 것을 의미한다. 보통 &quot;!프로필&quot; 형식은 특정 프로필이 사용되지 않을 때 기본으로 사용할 설정을 지정하는 용도로 사용된다.</p>
<pre><code class="language-java">@Configuration
@Profile(&quot;!real&quot;)
public class DataSourceJndiConfig{
    ...
}</code></pre>
<h2 id="프로퍼티-파일을-이용한-프로퍼티-설정">프로퍼티 파일을 이용한 프로퍼티 설정</h2>
<p>스프링은 외부의 프로퍼티 파일을 이용해서 스프링 빈을 설정하는 방법을 제공하고 있다. 다음과 같은 db.properties 파일이 있다고 하자. 이 파일의 프로퍼티 값을 자바 설정에서 사용할 수 있으며 이를 통해 설정 일부를 외부 프로퍼티 파일을 사용해서 변경할 수 있다. </p>
<pre><code>db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/spring5fs?characterEncoding=utf8
db.user=spring5
db.password=spring5</code></pre><h2 id="configuration-어노테이션-이용-자바-설정에서의-프로퍼티-사용">@Configuration 어노테이션 이용 자바 설정에서의 프로퍼티 사용</h2>
<p>자바 설정에서 프로퍼티 파일을 이용하려면 다음 두 가지를 설정한다.</p>
<ul>
<li>PropertySourcesPlaceholderConfigurer 빈 설정</li>
<li>@Value 어노테이션으로 프로퍼티 값 사용</li>
</ul>
<pre><code class="language-java">@Configuration
public class PropertyConfig{
    @Bean
    public static PropertySourcesPlaceholderConfigurer properties(){
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setLocations(
            new ClassPathResource(&quot;db.properties&quot;),
            new ClassPathResource(&quot;info.properties&quot;));

        return configurer;
    }
}</code></pre>
<p>setLocations()는 프로퍼티 파일 목록을 인자로 전달받는다. 이때 스프링은 Resource 파입을 이용해서 파일 경로를 전달한다. </p>
<p>위 코드에서 주의할 점은 PropertySourcesPlaceholderConfigurer 타입 빈을 설정하는 메서드가 정적(static)이라는 것이다. 이는 PropertySourcesPlaceholderConfigurer 클래스가 특수한 목적의 빈이기 때문이며 정적 메서드로 지정하지 않으면 원하는 방식으로 동작하지 않는다. </p>
<p>PropertySourcesPlaceholderConfigurer 타입 빈은 setLocation() 메서드로 전달받은 프로퍼티 파일 목록 정보를 읽어와 필요할 때 사용한다. 이를 위한 것이 @Value 어노테이션이다. </p>
<pre><code class="language-java">@Configuration
public class DsConfigWithProp{
    @Value(&quot;${db.driver}&quot;)
    private String driver;
    ...
}</code></pre>
<p>@Value 어노테이션이 ${구분자} 형식의 플레이스홀더 값으로 갖고 있다. ${db.driver} 플레이스홀더 값을 db.properties에 &quot;db.driver&quot; 프로퍼티 값으로 치환한다. 따라서 실제 빈을 생성하는 메서드는 @Value 어노테이션이 붙은 필드를 통해서 해당 프로퍼티의 값을 사용할 수 있다. </p>
<h3 id="빈-클래스에서-사용하기">빈 클래스에서 사용하기</h3>
<p>빈으로 사용할 클래스에도 @Value 어노테이션을 붙일 수 있다. @Value 어노테이션을 필드에 붙이면 플레이스홀더에 해당하는 프로퍼티를 필드에 할당한다. </p>
<pre><code class="language-java">public class Info{
    @Value(&quot;${info.version}&quot;)
    private String version
    ...
}</code></pre>
<p>다음과 같이 @Value 어노테이션을 set 메서드에 적용할 수도 있다. </p>
<pre><code class="language-java">public class Info{
    private String version;
       ...
    @Value(&quot;${info.version}&quot;)
    public void setVersion(String version){
        this.version=version;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[JSON 응답과 요청 처리]]></title>
            <link>https://velog.io/@yh_lee/JSON-%EC%9D%91%EB%8B%B5%EA%B3%BC-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@yh_lee/JSON-%EC%9D%91%EB%8B%B5%EA%B3%BC-%EC%9A%94%EC%B2%AD-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Mon, 15 Aug 2022 06:48:33 GMT</pubDate>
            <description><![CDATA[<h1 id="chap16-json-응답과-요청-처리">Chap16 JSON 응답과 요청 처리</h1>
<p>웹 페이지에서 Ajax를 이용해서 서버 API를 호출하는 사이트가 많다. 이들 API는 웹 요청에 대한 응답으로 HTML 대신 JSON이나 XML을 사용한다. 웹 요청에도 쿼리 문자열 대신 JOSN, XML을 데이터로 보내기도 한다. GET, POST만 사용하지 않고 PUT, DELETE와 같은 다른 방식도 사용한다. 스프링 MVC를 사용하면 이를 위한 웹 컨트롤러를 쉽게 만들 수 있다. </p>
<h2 id="jackson-의존-설정">Jackson 의존 설정</h2>
<p>Jackson은 자바 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리이다. 스프링 MVC에서 Jackson 라이브러리를 이용해서 자바 객체를 JSON으로 변환하려면 클래스 패스에 Jackson 라이브러리를 추가하면 된다. </p>
<p>Jackson은 아래 그림과 같이 자바 객체와 JSON 사이의 변환을 처리한다.<img src="https://velog.velcdn.com/images/yh_lee/post/02e31332-01f4-4bf5-8e1b-cdaae6aeb5f8/image.png" alt=""></p>
<h2 id="restcontroller로-json-형식-응답">@RestController로 JSON 형식 응답</h2>
<p>스프링 MVC에서 JSON 형식으로 데이터를 응답하기 위해선 @Controller 대신 @RestController 어노테이션을 사용하면 된다.</p>
<pre><code class="language-java">@RestController
public class RestMemberController{
    private MemberDao memberDao;
    private MemberRegisterService registerService;

    @GetMapping(&quot;/api/members&quot;)
    public List&lt;Member&gt; members(){
        return memberDao.selectAll();
    }

    @GetMapping(&quot;/api/members/{id}&quot;)
    public Member member(@PathVariable Long id, HttpServletResponse response) throws IOException{
        ...
    }
    ...
}</code></pre>
<p>@RestController 어노테이션을 붙이면 스프링 MVC는 요청 매핑 어노테이션을 붙인 메서드가 리턴한 객체를 알맞은 형식으로 변환해서 응답데이터로 전송한다. 이때 Jackson을 사용하면 JSON 형식의 문자열로 변환해서 응답한다. </p>
<p>리턴 타입이 List&lt;Member&gt;인 경우에 List 객체를 JSON 형식의 배열로 변환한 결과이다. <img src="https://velog.velcdn.com/images/yh_lee/post/610332b0-fa1a-4930-ab35-43a46426b14b/image.png" alt=""></p>
<blockquote>
<p>@RestController 어노테이션이 추가되기 전에는 다음과 같이 @Controller 어노테이션과 @ResponseBody 어노테이션을 사용했다. </p>
</blockquote>
<pre><code class="language-java">@Controller
public class RestMemberController{
    private MemberDao memberDao;
    private MemberRegisterService registerService;

    @RequestMapping(path=&quot;/api/members&quot;, method=RequestMethod.GET)
    @ResponseBody
    public List&lt;Member&gt; members(){
        return memberDao.selectAll();
    }
}</code></pre>
<h3 id="jsonignore를-이용한-제외-처리">@JsonIgnore를 이용한 제외 처리</h3>
<p>응답 결과에 password가 포함되어 있으면 안되기 때문에 응답 결과에서 제외시켜야 한다. Jackson이 제공하는 @JsonIgnore 어노테이션을 사용하면 이를 간단히 처리할 수 있다. 다음과 같이 JSON 응답에 포함시키지 않을 대상에 어노테이션을 붙인다.</p>
<pre><code class="language-java">public class Member{
    private Long id;
    private String email;
    @JsonIgnore
    private String password;
    private String name;
    private LocalDateTime registerDateTime;
}</code></pre>
<h3 id="jsonformat-사용">@JsonFormat 사용</h3>
<p>Jackson에서 날짜나 시간 값을 특정한 형식으로 표현하는 가장 쉬운 방법은 @JsonFormat을 사용하는 것이다. </p>
<pre><code class="language-java">public class Member{
    private Long id;
    private String email;
    private String name;
    @JsonFormat(pattern=&quot;yyyyMMddHHmmss&quot;)
    private LocalDateTime registerDateTime;
}</code></pre>
<p>Json 응답 결과는 다음과 같다. </p>
<pre><code class="language-json">{
  &quot;id&quot;:1,
  &quot;email&quot;:&quot;abc@gmail.com&quot;,
  &quot;name:&quot;spring&quot;,
  &quot;registerDateTime&quot;:&quot;202208150130&quot;
}</code></pre>
<h3 id="날짜-형식-변환-처리">날짜 형식 변환 처리</h3>
<p>스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter라는 것을 사용한다. JSON으로 변환할 때 사용하는 컨버터를 새롭게 등록해서 날짜 형식을 원하는 형태로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할 수 있다. </p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfiguration{
    ...
    @Override
    public viod extendMessageConverter{
        List&lt;HttpMessageConverter&lt;?&gt;&gt; converters){
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
            .json()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .build();
            converters.add(0,new MappingJackson2HttpMessageConverter(objectMapper));
        }    
    }
}</code></pre>
<h3 id="응답-데이터의-컨텐츠-형식">응답 데이터의 컨텐츠 형식</h3>
<p>응답 헤더의 Content-type이 application/json인 것을 알 수 있다. </p>
<h2 id="requestbody로-json-요청-처리">@RequestBody로 JSON 요청 처리</h2>
<p>POST, PUT 방식을 사용하면 name=이름&amp;age=20과 같은 쿼리 문자열 형식이 아니라 다음과 같은 JSON 형식의 데이터를 요청 데이터로 전송할 수 있다. </p>
<pre><code class="language-json">{&quot;name&quot;:&quot;이름&quot;, &quot;age&quot;:20}</code></pre>
<p>JSON 형식으로 전송된 요청 데이터를 커맨드 객체로 전달받는 방법은 커맨드 객체에 @RequestBody 어노테이션을 붙이면 된다.</p>
<pre><code class="language-java">@RestController{
    ...
    @PostMapping(&quot;/api/members&quot;)
    public void new Member(@RequestBody @Valid RegisterRequest regReq, HttpServletResponse response){
        ...
    }
}</code></pre>
<h3 id="json-데이터의-날짜-형식-다루기">JSON 데이터의 날짜 형식 다루기</h3>
<p>특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하고 싶다면 @JsonFormat 어노테이션의 pattern 속성을 이용해서 패턴을 지정한다.</p>
<pre><code class="language-java">@JsonFormat(pattern=&quot;yyyyMMddHHmmss&quot;)
private LocalDateTime birthDateTime;

@JsonFormat(pattern=&quot;yyyyMMdd HHmmss&quot;)
private Date birthDate;</code></pre>
<h3 id="요청-객체-검증하기">요청 객체 검증하기</h3>
<p>newMember()의 regReq 파라미터에 @Valid 어노테이션이 붙어있다. JSON 형식으로 전송한 데이터를 변환한 객체도 동일한 방식으로 @Valid 어노테이션이나 별도 Validator를 이용해서 검증할 수 있다. Validator를 사용할 경우 직접 상태 코드를 처리해야 한다.</p>
<h2 id="responseentity로-객체-리턴하고-응답-코드-지정하기">ResponseEntity로 객체 리턴하고 응답 코드 지정하기</h2>
<p>지금까지는 상태 코드를 지정하기 위해 HttpServletResponse의 setStatus(), sendError()를 사용했다. 문제는 HttpServletResponse를 이용해서 404 응답을 하면 JSON 형식이 아닌 서버가 기본으로 제공하는 HTML을 응답 결과로 제공한다는 점이다. </p>
<p>API를 호출하는 입장에서 JSON과 HTML을 모두 처리하는 것은 부담스럽다. 처리에 실패한 경우 HTML 응답 데이터 대신에 JSON 형식의 응답 데이터를 전송해야 API 호출 프로그램이 일관된 방법으로 응답을 처리할 수 있다. </p>
<h3 id="responseentity를-이용한-응답-데이터-처리">ResponseEntity를 이용한 응답 데이터 처리</h3>
<p>정상인 경우와 비정상인 경우 모두 JSON 응답을 전송하는 방법은 ResponseEntity를 사용하는 것이다. </p>
<p>에러 상황일 때 응답으로 사용할 ErrorResponse 클래스이다. </p>
<pre><code class="language-java">public class ErrorResponse{
    private String message;

    public ErrorResponse(String message){
        this.message = message;
    }

    public String getMessage(){
        return message;
    }
}</code></pre>
<p>ResponseEntity를 사용하면 member() 메서드를 아래와 같이 구현할 수 있다.</p>
<pre><code class="language-java">@RestController
public class RestMemberController{
    ...
    @GetMapping(&quot;/api/members/{id}&quot;)
    public ResponseEntity&lt;Object&gt; member(@PathVariable Long id){
        Member member = memberDao.selectById(id);
        if(member==null){
            //ErrorResponse를 body로 지정해서,
            //ErrorResponse를 JSON으로 변환한다.
            return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                   .body(new ErrorResponse(&quot;no member&quot;));
        }
        //member를 body로 지정해서, member 객체를 JSON으로 변환한다.
        return ResponseEntity.status(HttpStatus.OK).body(member);
    }
}</code></pre>
<p>스프링 MVC는 리턴 타입이 ResponseEntity이면 body로 지정한 객체를 사용해서 변환을 처리한다. ResponseEntity의 status로 지정한 값을 응답 상태 코드로 사용한다.</p>
<p>존재하지 않는 ID를 이용해서 실행한 결과 404 상태 코드와 함께 JSON 형식으로 응답 데이터를 전송한 것을 확인할 수 있다.<img src="https://velog.velcdn.com/images/yh_lee/post/46cfa06d-3639-4184-81c8-8f8bbab3a22a/image.png" alt=""></p>
<p>ResponseEntity를 생성하는 기본 방법은 status와 body를 이용해서 상태 코드와 JSON으로 변환할 객체를 지정하는 것이다.</p>
<pre><code>ResponseEntity.status(상태코드).body(객체)</code></pre><p>200(OK) 응답 코드와 몸체 데이터를 생성할 경우 다음과 같이 ok() 메서드를 이용할 수 있다.</p>
<pre><code>ResponseEntity.ok(member)</code></pre><p>만약 몸체 내용이 없다면 다음과 같이 body를 지정하지 않고 build()로 바로 생성한다.</p>
<pre><code>ResponseEntity.status(HttpStatus.NOT_FOUND).build()</code></pre><p>몸체 내용이 없는 경우 status() 메서드 대신에 다음과 같이 관련 메서드를 사용해도 된다. </p>
<pre><code>ResponseEntity.notFound().build()</code></pre><ul>
<li>noContent(): 204</li>
<li>badRequest(): 400</li>
<li>notFound(): 404</li>
</ul>
<p>newMember()에서 201(Created) 상태 코드와 Location 헤더를 함께 전송하는 방법</p>
<pre><code class="language-java">//1
response.setHeader(&quot;Location&quot;, &quot;/api/members/&quot;+newMemberId);
response.setStatus(HttpServletResponse.SC_CREATED);

//2
URI uri = URI.create(&quot;/api/members/&quot;+newMemberId);
return ResponseEntity.created(uri).build();</code></pre>
<h3 id="exceptionhandler-적용-메서드에-responseentity로-응답하기">@ExceptionHandler 적용 메서드에 ResponseEntity로 응답하기</h3>
<p>앞선 코드는 member가 존재하지 않을 때 기본 HTML 에러 응답 대신에 JSON 응답을 제공하기 위해 ResponseEntity를 사용했다. 그런데 회원이 존재하지 않을 때 404 상태 코드를 응답해야 하는 기능이 많으면 에러 응답을 위해 ResponseEntity를 생성하는 코드 중복이 발생한다. 이때 @ExceptionHandler 어노테이션을 적용한 메서드에서 에러 응답을 처리하도록 구현하면 중복을 없앨 수 있다.</p>
<pre><code class="language-java">@GetMapping(&quot;/api/members/{id}&quot;)
public ResponseEntity&lt;Object&gt; member(@PathVariable Long id){
      Member member = memberDao.selectById(id);
    if(member==null){
        throw new MemberNotFoundException();    
    }
    return member;
}

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity&lt;ErrorResponse&gt; handleNoData(){
    return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse(&quot;no member&quot;);
}</code></pre>
<p>위 코드의 member() 메서드는 Member 자체를 리턴한다. 회원 데이터가 존재하면 Member 객체를 리턴하므로 JSON으로 변환한 결과를 응답한다. 회원 데이터가 존재하지 않으면 MemberNotFoundException을 발생한다. 이 익셉션이 발생하면 @ExceptionHandler 어노테이션을 사용한 handleNoData() 메서드가 에러를 처리한다. 404 상태 코드와 ErrorResponse 객체를 몸체로 갖는 ResponseEntity를 반환한다. 즉, MemberNotFoundException가 발생하면 상태코드가 404이고 몸체가 JSON 형식인 응답을 전송한다. </p>
<p>@RestControllerAdvice 어노테이션을 이용해서 에러 처리 코드를 별도 클래스로 분리할 수도 있다. @RestControllerAdvice는 @ControllerAdvice와 동일하다. 차이는 @RestController와 동일하게 응답을 JSON, XML 형식으로 변환한다는 것이다.</p>
<pre><code class="language-java">@RestControllerAdvice(&quot;controller&quot;)
public class ApiExceptionAdvice{
    @ExceptionHandler(MemberNotFoundException.class)
    public ResponseEntity&lt;ErrorResponse&gt; handleNoData(){
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse(&quot;no member&quot;);
    }
}    </code></pre>
<p>@RestControllerAdvice를 사용하면 에러 처리 코드가 한 곳에 모여 효과적으로 에러 응답을 관리할 수 있다. </p>
<h3 id="valid-에러-결과를-json으로-응답하기">@Valid 에러 결과를 JSON으로 응답하기</h3>
<p>@Valid 어노테이션을 붙인 커맨드 객체가 값 검증에 실패하면 400 상태 코드를 응답한다. </p>
<pre><code class="language-java">@PostMapping(&quot;/api/members&quot;)
public ResponseEntity&lt;Object&gt; newMember(@RequestBody @Valid RegisterRequest regReq){
    ...
}</code></pre>
<p>문제는 HttpServletResponse를 이용해서 상태 코드를 응답했을 때와 마찬가지로 HTML 응답을 전송한다. @Valid 어노테이션을 이용한 검증을 실패했을 때 HTML 응답 대신 JSON 형식의 응답을 제공하고 싶으면 Errors 타입 파라미터를 추가해서 직접 에러 응답을 생성하면 된다. </p>
<pre><code class="language-java">@PostMapping(&quot;/api/members&quot;)
public ResponseEntity&lt;Object&gt; newMember(@RequestBody @Valid RegisterRequest regReq, Errors errors){
    if(errors.hasErrors()){
        String errorCodes=errors.getAllErrors()//List&lt;ObjectError&gt;
                .stream()
                .map(error-&gt;error.getCodes()[0])//error는 ObjectError
                .collect(Collectors.joining(&quot;,&quot;));

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(new ErrorResponse(&quot;errorCodes=&quot;+errorCodes); 
    }
    ...
}</code></pre>
<p>hasErrors() 메서드를 이용해서 검증 에러가 있는지 확인한다. 검증에러가 존재하면 모든 에러 정보를 구하고 각 에러 코드 값을 연결한 문자열을 생성해서 errorCodes 변수에 할당한다. 이처럼 코드를 수정한 후 검증에 실패하는 데이터를 전송하면 JSON 응답이 오는 것을 확인할 수 있다. </p>
<p>@RequestBody를 붙인 경우 @Valid를 붙인 객체의 검증에 실패했을 때 Errors 타입 파라미터가 존재하지 않으면 MethodArgumentNotValidException이 발생한다. 따라서 다음과 같이 @ExceptionHandler 어노테이션을 이용해서 검증 실패시 에러 응답을 생성해도 된다. </p>
<pre><code class="language-java">@RestControllerAdvice(&quot;controller&quot;)
public class ApiExceptionAdvice{
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&lt;ErrorResponse&gt; handleBindException(MethodArgumentNotValidException ex){
        String errorCodes=ex.getBindingResult().getAllErrors()
                        .stream()
                        .map(error-&gt;error.getCodes()[0])
                        .collect(Collectors.joining(&quot;,&quot;));

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(new ErrorResponse(&quot;errorCodes=&quot;+errorCodes);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 어플리케이션의 구조]]></title>
            <link>https://velog.io/@yh_lee/%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%98-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@yh_lee/%EC%9B%B9-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98%EC%9D%98-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Mon, 15 Aug 2022 02:36:36 GMT</pubDate>
            <description><![CDATA[<h1 id="chap15-간단한-웹-어플리케이션-구조">Chap15 간단한 웹 어플리케이션 구조</h1>
<h2 id="웹-어플리케이션의-구성-요성">웹 어플리케이션의 구성 요성</h2>
<p>웹 어플리케이션을 개발할 때 사용하는 전형적인 구조는 다음 요소를 포함한다.</p>
<ul>
<li>프론트 서블릿</li>
<li>컨트롤러 + 뷰</li>
<li>서비스</li>
<li>DAO</li>
</ul>
<p>프론트 서블릿은 웹 브라우저의 모든 요청을 받는 창구 역할을 한다. 프론트 서블릿은 요청을 분석해서 알맞은 컨트롤러에 전달한다. 스프링 MVC에서는 DispatcherServlet이 프론트 서블릿의 역할을 수행한다.<img src="https://velog.velcdn.com/images/yh_lee/post/1b79cbf2-11fa-44b0-a4fa-d322e0ff7be3/image.png" alt="">컨트롤러는 실제 웹 브라우저의 요청을 처리한다. 지금까지 구현해본 스프링 컨트롤러가 이에 해당한다. 컨트롤러는 클라이언트(브라우저)의 요청을 처리하기 위해 알맞은 기능을 실행하고 그 결과를 뷰에 전달한다. </p>
<h4 id="컨트롤러의-주요-역할">컨트롤러의 주요 역할</h4>
<ul>
<li>클라이언트가 요구한 기능을 실행</li>
<li>응답 결과를 생성하는데 필요한 모델 생성</li>
<li>응답 결과를 생성할 뷰 선택</li>
</ul>
<p>컨트롤러는 로직 실행을 서비스에 위임한다. 컨트롤러는 어플리케이션이 제공하는 기능과 사용자 요청을 연결하는 매개체이고, 기능 제공을 위한 로직을 직접 수행하지는 않는다. 앞에서 작성했던 ChangePasswordController의 경우 아래 코드처럼 ChangePasswordService에 비밀번호 변경 처리를 위임했다.</p>
<pre><code class="language-java">@PostMapping
public String submit(
        @ModelAttribute(&quot;command&quot;) ChangePwdCommand pwdCmd, 
        Errors errors, 
        HttpSession session){

    new ChangePwdCommandValidator().validate(pwdCmd, errors);
    if(errors.hasErrors()){
        return &quot;edit/changePwdForm&quot;;
    }    
    AuthInfo authInfo = (AuthInfo) session.getAttribute(&quot;authInfo&quot;);
    try{
        //컨트롤러는 로직 실행을 서비스에 위임
        changePasswordService.changePassword(
                authInfo.getEmail(),
                pwdCmd.getCurrendPassword(),
                pwdCmd.getNewPassword());

        return &quot;edit/changePwd&quot;;
    }catch(IdPasswordNotMatchingException e){
        errors.rejectValue(&quot;currentPassword&quot;, &quot;notMatching&quot;);
        return &quot;edit/changePwdForm&quot;;
    }
}</code></pre>
<p>서비스는 기능의 로직을 구현한다. 서비스는 DB 연동이 필요하면 DAO(Data Access Object)를 사용한다. DB와 웹 어플리케이션 간에 데이터를 이동시켜 주는 역할을 한다. </p>
<h2 id="서비스의-구현">서비스의 구현</h2>
<p>서비스는 핵심이 되는 기능의 로직을 제공한다. 예를 들어 비밀번호 변경 기능은 아래와 같은 로직을 서비스에서 수행한다.</p>
<ul>
<li>DB에서 비밀번호를 변경할 회원의 데이터를 구한다.</li>
<li>존재하지 않으면 익셉션을 발생시킨다.</li>
<li>회원 데이터의 비밀번호를 변경한다.</li>
<li>변경 내역을 DB에 반영한다.</li>
</ul>
<p>웹 어플리케이션을 사용하든 명령행에서 실행하든 비밀번호 변경 기능을 위해서 서비스는 동일한 로직을 수행한다. 중간에 실패하면 이전까지 했던 것을 취소해야 하고, 모든 과정을 성공적으로 진행했을 때 완료해야 한다. 이런 이유로 서비스 메서드를 트랜잭션 범위에서 실행한다. </p>
<pre><code class="language-java">@Transactional
public void changePassword(String email, String oldPwd, String newPwd){
    Member member = memberDao.selectByEmail(email);
    if(member==null)
        throw new MemberNotFoundException();

    member.changePassword(oldPwd, newPwd);

    memberDao.update(member);
}</code></pre>
<p>같은 데이터를 사용하는 기능들을 한 개의 서비스 클래스에 모아서 구현할 수 있다. 예를 들어 회원 가입 기능과 비밀번호 변경 기능은 모두 회원에 대한 기능이므로 다음과 같이 MemberService 클래스에 기능을 구현할 수 있다.</p>
<pre><code class="language-java">public class MemberService{
    @Transactional
    public void regist(RegisterRequest req){...}

    @Transactional 
    public void changePassword(String email, String lodPwd, String newPwd){...}
}</code></pre>
<p>회원가입 기능은 RegisterRequest 클래스를 파라미터로 사용했다. 필요한 데이터를 전달받기 위해 별도 타입을 만들면 스프링 MVC의 커맨드 객체로 해당 타입을 사용할 수 있어 편하다. </p>
<p>회원 가입 요청을 처리하는 컨트롤러 클래스의 코드는 다음과 같이 서비스 메서드의 입력 파라미터로 사용되는 타입을 커맨드 객체로 사용했다. </p>
<pre><code class="language-java">@PostMapping(&quot;/register/step3&quot;)
public String handleSteo3(RegisterRequest regReq, Errors errors){
    ...
    memberRegisterService.regist(regReq);
    ...
}</code></pre>
<p>비밀번호 변경의 changePassword() 메서드처럼 웹 요청 파라미터를 커맨드 객체로 받고 커맨드 객체의 프로퍼티를 서비스 메서드에 인자로 전달할 수도 있다.</p>
<pre><code class="language-java">@RequestMapping(method=RequestMethod.POST)
public String submit(@ModelAttribute(&quot;command&quot;) ChangePwdCommand pwdCmd, Errors errors, HttpSession session){
    ...
    changePasswordService.changePassword(
        authInfo.getEmail(),
        pwdCmd.getCurrentPassword(),
        pwdCmd.getNewPassword()
    );
    ...
}</code></pre>
<p><strong>커맨드 클래스를 작성한 이유는 스프링 MVC가 제공하는 폼 값 바인딩과 검증, 스프링 폼 태그와의 연동 기능을 사용하기 위함이다.</strong></p>
<h2 id="컨트롤러에서-dao-접근">컨트롤러에서 DAO 접근</h2>
<p>서비스 메서드에서는 어떤 로직도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝나는 경우도 있다. 예를 들어 회원 데이터 조회를 위한 서비스 메서드를 다음과 같이 구현하곤 한다.</p>
<pre><code class="language-java">public class MemberSerivce{
    ...
    public Member getMember(Long id){
        return memberDao.selectById(id);
    }
}</code></pre>
<p>여기서 MemberService 클래스의 getMember() 메서드는 MemberDao의 selectByID()만 실행할 뿐 추가 로직은 없다. 컨트롤러 클래스는 이 서비스 메서드를 통해 회원 정보를 구한다.</p>
<pre><code class="language-java">@RequestMapping(&quot;/member/detail/{id}&quot;)
public String detail(@PathVariable(&quot;id&quot;) Long id, Model model){
    //사실상 DAO를 직접 호출하는 것과 동일
    Member member = memberService.getMember(id);
  //Member member = memberDao.selectByEmail(id);
    if(member==null)
        return &quot;member/noFound&quot;;
    model.addAttribute(&quot;member&quot;,member);
    return &quot;member/memberDatail&quot;;
}</code></pre>
<h2 id="패키지-구성">패키지 구성</h2>
<p>패키지의 구성을 조금 더 정확하게 분리하면 아래 그림처럼 &#39;웹 요청을 처리하기 위한 것&#39;과 &#39;기능을 제공하기 위한 것&#39;으로 구분할 수 있다.
<img src="https://velog.velcdn.com/images/yh_lee/post/815c9e52-bcb6-4ca2-84eb-e23afca29ad3/image.png" alt="">웹 요청을 처리하기 위한 영역에는 컨트롤러 클래스와 관련 클래스들이 위치한다. 커맨드 객체의 값을 검증하기 위한 Validator도 웹 요청 처리 영역에 위치할 수 있는데 관점에 따라 기능 제공 영역에 위치시킬 수 있다. 웹 영역의 패키지는 web.member와 같이 사용한다.</p>
<p>기능 제공 영역에는 기능 제공에 필요한 서비스, DAO, 그리고 Member와 같은 모델 클래스가 위치한다. 실제 어플리케이션에서는 domain.member와 같이 기능을 잘 표현하는 패키지 이름을 사용한다. </p>
<p>기능 영역은 다음과 같이 service, dao, model 같은 세부 패키지로 구분하기도 한다. 
<img src="https://velog.velcdn.com/images/yh_lee/post/db829fb8-491c-4213-aa5c-d27ba197549b/image.png" alt=""></p>
<blockquote>
<p>컨트롤러-서비스-DAO 구조는 간단한 웹 어플리케이션을 개발하기에는 무리가 없다. 문제는 로직이 복잡해지면서 구조의 코드도 복잡해지는 경향이 있다. 이를 완화하는 방법 중 하나는 도메인 주도 설계를 적용하는 것이다. 
도메인 주도 설계는 UI-서비스-도메인-인프라의 네 영역으로 어플리케이션을 구성한다. 여기서 UI는 컨트롤러 영역에 대응하고 인프라는 DAO 영역에 대응한다. 중요한 점은 주요한 도메인 모델과 업무 로직이 서비스 영역이 아닌 도메인 영역에 존재하는 것이다. 또한 도메인 영역은 정해진 패턴에 따라 모델을 구현하다. 이를 통해 업무가 복잡해져도 일정 수준의 복잡도로 코드를 유지할 수 있게 한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC4 - 날짜 값 변환, @Pathvariable, 익셉션 처리]]></title>
            <link>https://velog.io/@yh_lee/MVC4-%EB%82%A0%EC%A7%9C-%EA%B0%92-%EB%B3%80%ED%99%98-Pathvariable-%EC%9D%B5%EC%85%89%EC%85%98-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@yh_lee/MVC4-%EB%82%A0%EC%A7%9C-%EA%B0%92-%EB%B3%80%ED%99%98-Pathvariable-%EC%9D%B5%EC%85%89%EC%85%98-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 10 Aug 2022 06:13:28 GMT</pubDate>
            <description><![CDATA[<h1 id="chap14-mvc4">Chap14 MVC4</h1>
<h2 id="datetimeformat">@DateTimeFormat</h2>
<p>회원이 가입한 일시를 기준으로 회원을 검색하기 위해 시작 시간과 끝 시간 기준을 파라미터로 전달받는다고 하자. 검색 기준 시간을 표현하기 위한 커맨드 클래스이다.</p>
<pre><code class="language-java">public class ListCommand{
    private LocalDateTime from;
    private LocalDateTime to;
    ...
}    </code></pre>
<p>검색을 위한 입력 폼은 다음과 같다.</p>
<pre><code>&lt;input type=&quot;text&quot; name=&quot;from&quot;/&gt;
&lt;input type=&quot;text name=&quot;to&quot;/&gt;</code></pre><p>여기서 문제는 &lt;input&gt;에 입력한 문자열을 LocalDateTime 타입으로 변환해야 한다는 것이다. 이때 @DateTimeFormat을 적용하면 된다. </p>
<pre><code class="language-java">public class ListCommand{
    @DateTimeFormat(pattern=&quot;yyyyMMddHH&quot;)
    private LocalDateTime from;
    @DateTimeFormat(pattern=&quot;yyyyMMddHH&quot;)
    private LocalDateTime to;
    ...
}</code></pre>
<p>예로 &quot;2022081011&quot;라는 문자열을 &quot;2022년 8월 10일 11시&quot; 값을 갖는 LocalDateTiem 객체로 변환해준다. </p>
<br>
컨트롤러에서는 별도 설정 없이 ListCommand 클래스를 커맨드 객체로 사용하면 된다.

<pre><code class="language-java">@Controller
public class MemberListController{
    private MemberDao memberDao;

    public void setMemberDao(MemberDao memberDao){
        this.memberDao=memberDao;
    }        

    @RequestMapping(&quot;/members&quot;)
    public String list(@ModelAttribute(&quot;cmd&quot;) ListCommand listCommand,Model model){
        if(listCommand.getFrom()!=null&amp;&amp;listCommand.getTo()!=null){
            List&lt;Member&gt; members = memberDao.selectByRegdate(listCommand.getFrom(),listCommand.getTo());
            model.addAttribute(&quot;members&quot;,members);
        }
        return &quot;member/memberList&quot;;
    }
}</code></pre>
<h2 id="변환-에러-처리">변환 에러 처리</h2>
<p>형식에 맞지 않는 &quot;20220810&quot;을 입력하면 지정한 형식 &quot;yyyyMMddHH&quot;에 맞지 않기 때문에 400 에러가 발생한다. 이때 400 에러 대신 폼에 알맞는 에러 메시지를 보여주고 싶으면 Errors 타입 파라미터를 RequestMapping 적용 메서드에 추가하면 된다. </p>
<pre><code class="language-java">@Controller
public class MemberListController{
    @RequestMapping(&quot;/members&quot;)
    public String list(@ModelAttribute(&quot;cmd&quot;) ListCommand listCommand,
                        Errors errors, 
                        Model model
                        ){
        if(errors.hasError()){
            return &quot;member/memberList&quot;;
        }    
        ...
    }
}</code></pre>
<p>Errors 타입 파라미터를 가질 경우 @DateTimeFormat에 지정한 형식이 맞지 않으면 Errors 객체에 &quot;typeMismatch&quot; 에러 코드를 추가한다. </p>
<h2 id="변환-처리-이해">변환 처리 이해</h2>
<p>@DataTimeFormat을 사용하면 지정한 형식의 문자열을 LocalDateTime 타입으로 변환해준 것을 확인했다. 이때 WebDataBinder가 값 변환에 관여한다. </p>
<p>스프링 MVC는 메서드와 DispatcherServlet 사이를 연결하기 위해 RequestMappingHandlerAdapter 객체를 사용한다. 이 핸들러 어댑터 객체는 요청 파라미터와 커맨드 객체 사이 변환 처리를 위해 WebDataBinder를 이용한다.</p>
<p>WebDataBinder는 커맨드 객체를 생성한다. 그리고 커맨드 객체의 프로퍼티와 같은 이름을 갖는 요청 파라미터를 이용해서 프로퍼티 값을 생성한다.<img src="https://velog.velcdn.com/images/yh_lee/post/3ce59162-0dcb-4a05-9b0b-a0a991497993/image.png" alt="">
WebDataBinder는 직접 타입을 변환하지 않고 ConversionService에 역할을 위임한다. 스프링 MVC를 위한 설정인 @EnableWebMvc를 사용하면 DefaultFormattingConversionService를 ConversionService로 사용한다.</p>
<h2 id="pathvariable-경로-변수-처리">@PathVariable 경로 변수 처리</h2>
<p>다음은 ID가 10인 회원의 정보를 조회하기 위한 URL이다.</p>
<pre><code>http://localhost:8080/sp5-chap14/members/10</code></pre><p>이 형식의 URL을 사용하면 각 회원마다 경로의 마지막 부분이 달라진다. 이때 @PathVariable을 사용하면 가변 경로를 처리할 수 있다.</p>
<pre><code class="language-java">@Controller
public class MemberDetailController{
    pirvate MemberDao memberDao;
    public void setMemberDao(MemberDao memberDao){
        this.memberDao=memberDao;
    }    

    @GetMapping(&quot;/members/{id}&quot;)
    public String detail(@PathVariable(&quot;id&quot;) Long memId, Model model){
        Member member = memberDao.selectById(memberId);
        if(member=null){
            throw new MemberNotFoundException();
        }
        model.addAttribute(&quot;member&quot;,member);
        return &quot;member/memberDetail&quot;;
    }
}    </code></pre>
<p>매핑 경로에 {id}와 같이 중괄호로 둘러 쌓인 부분을 경로 변수라고 한다. {id}에 들어오는 변수는 @PathVariable 파라미터에 전달된다. </p>
<h2 id="컨트롤러-익셉션-처리">컨트롤러 익셉션 처리</h2>
<p>없는 ID를 경로변수로 사용하면 MemberNotFoundException이 발생한다. 숫자가 들어가야 하는데 &#39;a&#39;같은 문자가 변수로 들어가면 400 에러가 발생한다. 에러 발생 화면이 출력 되는데 알맞게 익셉션을 처리해서 사용자에게 더 적합한 안내를 해주는 것이 좋다. 그런데 타입 변환 실패에 따른 익셉션은 어떻게 에러 화면을 보여줄 수 있을까? 이때 @ExceptionHandler를 사용할 수 있다.</p>
<p>같은 컨트롤러에 ExceptionHandler가 붙은 메서드가 있다면 이 메서드가 익셉션을 처리한다. 따라서 컨트롤러에서 발생한 익셉션을 직접 처리하고 싶으면 ExceptionHandler를 붙인 메서드를 구현하면 된다.</p>
<pre><code class="language-java">@Controller
public class MemberDetailController{
    private MemberDao memberDao;
    ...

    @ExceptionHandler(TypeMismatchException.class)
    public String handleTypeMismatchException(){
        return &quot;member/invalidId&quot;;
    }

    @ExceptionHandler(MemberNotFoundException.class)
    public String handleTypeMismatchException(){
        return &quot;member/noMember&quot;;
    }
}</code></pre>
<h3 id="controlleradvice를-이용한-공통-익셉션-처리">@ControllerAdvice를 이용한 공통 익셉션 처리</h3>
<p>여러 다른 컨트롤러에서 동일 타입의 익셉션 처리 코드를 적용하고 싶을 때 @ControllerAdvice를 사용해서 중복을 없앨 수 있다.</p>
<pre><code class="language-java">@ControllerAdvice(&quot;spring&quot;)
public class CommonExceptionHandler{

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(){
        return &quot;error/commonException&quot;;
    }    
}</code></pre>
<p>@ControllerAdvice 어노테이션이 적용된 클래스는 지정한 범위의 컨트롤러에 공통으로 사용될 설정을 지정할 수 있다. 위의 코드는 spring 패키지와 그 하위 패키지에 속한 컨트롤러에서 RuntimeException이 발생하면 익셉션을 처리한다.</p>
<h3 id="exceptionhandler-적용-메서드의-우선-순위">@ExceptionHandler 적용 메서드의 우선 순위</h3>
<p>@ControllerAdvice 클래스에 있는 @ExceptionHandler 메서드보다 컨트롤러 클래스에 있는 @ExceptionHandler 메서드가 우선순위가 높기 때문에 먼저 작동한다.</p>
<ol>
<li>같은 컨트롤러에 위치한 @ExceptionHandler 메서드 중 익셉션을 처리할 수 있는 메서드 검색</li>
<li>같은 클래스에 위치한 메서드가 익셉션을 처리할 수 없는 경우 @ControllerAdvice 클래스에 위치한 @ExceptionHandler 메서드를 검색</li>
</ol>
<h3 id="exceptionhandler-적용-메서드의-파라미터와-리턴-타입">@ExceptionHandler 적용 메서드의 파라미터와 리턴 타입</h3>
<p>@ExceptionHandler를 붙인 메서드는 다음 파라미터를 가질 수 있다.</p>
<ul>
<li>HttpServletRequest, HttpServletResponse, HttpSession</li>
<li>Model</li>
<li>익셉션
리턴 타입은 다음과 같다.</li>
<li>ModelAndView</li>
<li>String(뷰 이름)</li>
<li>(@ReponseBody를 붙인 경우) 임의 객체</li>
<li>ResponseEntity</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC3 - 세션, 인터셉터, 쿠키]]></title>
            <link>https://velog.io/@yh_lee/MVC3-%EC%84%B8%EC%85%98-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0-%EC%BF%A0%ED%82%A4</link>
            <guid>https://velog.io/@yh_lee/MVC3-%EC%84%B8%EC%85%98-%EC%9D%B8%ED%84%B0%EC%85%89%ED%84%B0-%EC%BF%A0%ED%82%A4</guid>
            <pubDate>Tue, 09 Aug 2022 08:16:59 GMT</pubDate>
            <description><![CDATA[<h1 id="chap13-mvc3">Chap13 MVC3</h1>
<h2 id="로그인-기능-구현">로그인 기능 구현</h2>
<p>로그인 성공 후 인증 상태 정보를 세션에 보관할 때 사용할 AuthInfo 클래스이다. </p>
<pre><code class="language-java">public class AuthInfo{
    private Long id;
    private String email;
    private String name;
       ...
}</code></pre>
<p>암호 일치 여부를 확인하기 위한 matchPassword() 메서드를 Member 클래스에 추가한다.</p>
<pre><code class="language-java">public class Member{
    private Long id;
    private String email;
    private String password;
    private String name;
    private LocalDateTime registerDateTime;
    ...
    public boolean matchPassword(String password){
        return this.password.equals(password);
    }
}</code></pre>
<p>이메일과 비밀번호가 일치하는지 확인해서 AuthInfo 객체를 생성하는 AuthService 클래스이다.</p>
<pre><code class="language-java">public class AuthService{
    private MemberDao memberDao;
       ...
    public AuthInfo authenticate(String email, String password){
          Member member = memberDao.selectbyEmail(email);
        if(member = null){
            throw new WrongIdPasswordException();
        }
        if(!member.matchPassword(password)){
            throw new WrongIdPasswordException();
        }
        return new AuthInfo(member.getId(), member.getEmail(),member.getName());
    }    
}</code></pre>
<p>AuthService를 이용해서 로그인을 처리하는 LoginController와 폼에 입력한 값을 전달받기 위한 LoginCommand클래스와 입력된 값을 검증하는 LoginCommandValidator가 있다.</p>
<pre><code class="language-java">public class LoginCommand{
    private String email;
    private String password;
    private boolean rememberEmail;
    ...
}</code></pre>
<pre><code class="language-java">public class LoginCommandValidator implements Validator{
    @Override
    public boolean supports(Class&lt;?&gt; clazz){
        return LoginCommand.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors){
        Validation Utils.rejectIfEmptyOrWhitespace(errors, &quot;email&quot;, &quot;required&quot;);
        ValidationUtils.rejectIfEmpty(errors, &quot;password&quot;, &quot;required&quot;);
    }
}</code></pre>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LonginController{
    private AuthService authService;

    public void setAuthService(AuthService authService){
        this.authService=authService;
    }

    @GetMapping
    public String form(LoginCommand loginCommand){
        new LoginCommandValidator().validate(loginCommand,errors);
        if(errors.hasErrors()){
            return &quot;login/loginForm&quot;;
        }
        try{
            AuthInfo authInfo = authService.authenticate(
                loginCommand.getEmail();
                loginCommand.getPassword());
               //TODO 세션에 authInfo 저장해야 함
            return &quot;login/loginSuccess&quot;;
        }catch(WrongIdPasswordException e){
            errors.reject(&quot;idPasswordNotMatching&quot;);
            return &quot;login/loginForm&quot;;
        }

    }
}</code></pre>
<h2 id="컨트롤러에서-httpsession-사용하기">컨트롤러에서 HttpSession 사용하기</h2>
<p>로그인 구현에서 바로 로그인 상태를 유지하는 것이 빠졌다. 로그인 상태를 유지하는 방법은 크게 HttpSession을 이용하는 방법과, 쿠키를 이용하는 방법이 있다. </p>
<p>컨트롤러에서 HttpSession을 사용하면 다음의 두 가지 방법 중 하나를 사용하면 된다. </p>
<ol>
<li>요청 매핑 어노테이션 적용 메서드에 HttpSession 파라미터 추가하기.</li>
<li>요청 매핑 어노테이션 적용 메서드에 HttpServletRequest 파라미터를 추가하고 HttpServletRequest를 이용해서 HttpSession을 구하기.</li>
</ol>
<h3 id="첫번째-방법">첫번째 방법</h3>
<pre><code class="language-java">@PostMapping
public String form(LoginCommand loginCommand, Errors errors, HttpSession session){
    //session을 사용하는 코드
}</code></pre>
<p>@PostMapping을 붙인 메서드에 HttpSession 파라미터가 존재할 경우 스프링 MVC는 컨트롤러의 메서드를 호출할 때 HttpSession 객체를 파라미터로 전달한다. HttpSession을 생성하기 전이면 새로운 HttpSession를 생성하고 그렇지 않으면 기존에 존재하는 HttpSession을 전달한다.</p>
<h3 id="두번째-방법">두번째 방법</h3>
<p>HttpServletRequest의 getSession() 메서드를 이용하는 것이다. 첫번째 방법은 항상 HttpSession을 생성하지만 두번째 방법은 필요한 시점에만 HttpSession을 생성할 수 있다. </p>
<pre><code class="language-java">@PostMapping
public String submit(
                LoginCommand loginCommand, 
                Errors errors,
                HttpServletRequest req
                        ){
    HttpSession session = req.getSession();
    //session을 사용하는 코드
}</code></pre>
<br>

<p>LoginController 코드에서 인증 후에 인증 정보를 세션에 담도록 submit() 메서드의 코드를 수정하자. </p>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LoginController{
    ...
    @PostMapping
    public String submit(
                        LoginCommand loginCommand,
                        Errors errors,
                        HttpSession session
                            ){
        new LoginCommandValidator().validate(loginCommand,errors);
        if(errors.hasErrors()){
            return &quot;login/loginForm&quot;;
        }

        try{
            AuthInfo authInfo=authService.authenticate(
                            loginCommand.getEamil(),
                            loginCommand.getPassword());

            session.setAttribute(&quot;authInfo&quot;,authInfo);

            return &quot;login/loginSuccess&quot;;
        }
        catch(IdPasswordNotMatchingException e){
            errors.reject(&quot;idPasswordNotMatching&quot;);
            return &quot;login/loginForm&quot;;
        }
    }    
}    </code></pre>
<p>로그인에 성공하면 &quot;session.setAttribute(&quot;authInfo&quot;,authInfo)&quot; 처럼 HttpSession의 &quot;authInfo&quot; 속성에 인증 정보 객체(autoInfo)를 저장하도록 코드를 추가했다. </p>
<br>

<p>로그아웃을 위한 컨트롤러 클래스는 HttpSession을 제거하면 된다. 로그아웃 처리를 하는 LogoutController 코드를 보자.</p>
<pre><code class="language-java">@Controller
public class LogoutController{
    @RequestMapping(&quot;/logout&quot;)
    public String logout(HttpSession session){
        session.invalidate();
        return &quot;redirect:/main&quot;;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/11e5ea65-c5b9-42cd-91ac-301e2c09ae56/image.png" alt="">LoginController는 로그인에 성공할 경우 HttpSession의 &quot;authInfo&quot; 속성에 인증 정보 객체를 저장한다. 따라서 로그인에 성공하면 17행 조건절이 true가 되어서 19~22행 내용이 출력된다. </p>
<h2 id="비밀번호-변경-기능-구현">비밀번호 변경 기능 구현</h2>
<p>비밀번호 변경 컨트롤러</p>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/edit/changePassword&quot;)
public class ChangePwdController{
    private ChangePasswordService changePasswordService;
    ...
    @GetMapping
    public String form(
                @ModelAttribute(&quot;command&quot;) ChangePasswordService pwdCmd
            ){
        return &quot;edit/changePwdForm&quot;;
    }    

    @PostMapping
    public String submit(
        @ModelAttribute(&quot;command&quot;) ChangePasswordService pweCmd,
        Errors errors,
        HttpSession session
                    ){
        new ChangePwdCommandValidator().validate(pwdCmd,errors)l
        if(errors.hasError()){
            return &quot;edit/changePwdForm&quot;;
        }
        AuthInfo authInfo = (AuthInfo) session.getAttribute(&quot;authInfo&quot;);
        try{
            changePasswordService.changePassword(
                    authInfo.getEmail(),
                    pwdCmd.getCurrentPassword(),
                    pwdCmd.getNewPassword());
            return &quot;edit/changedPwd&quot;;
        }catch(WrongIdPasswordException e){
            errors.rejectValue(&quot;currentPassword&quot;,&quot;notMatching&quot;);
            return &quot;edit/changePwdForm&quot;;
        }

    }
}</code></pre>
<h2 id="인터셉터-사용하기">인터셉터 사용하기</h2>
<p>로그인하지 않은 상태에서 <a href="http://localhost:8080/sp5-chap13/edit/changePassword%EB%A5%BC">http://localhost:8080/sp5-chap13/edit/changePassword를</a> 입력하면 비밀번호 변경 폼이 출력된다. 로그인하지 않았는데 변경 폼이 출력되는 것은 이상하다. 그것보다 로그인하지 않은 상태에서 비밀번호 변경 폼을 요청하면 로그인 화면으로 이동시키는 것이 더 좋은 방법이다. </p>
<p>이를 위해 HttpSession에 &quot;authInfo&quot; 객체가 존재하는지 검사하고 존재하지 않으면 <strong>로그인 경로로 리다이렉트</strong>하도록 ChangePwdController 클래스를 수정한다.</p>
<pre><code class="language-java">@GetMapping
public String form(
                    @ModelAttribute(&quot;command&quot;) ChangePwdCommand pwdCmd,
                    HttpSession session
                    ){
    AuthInfo authInfo = (AuthInfo) session.getAttribute(&quot;authInfo&quot;);
    if(authInfo==null){
        return &quot;redirect:/login&quot;;
    }
    return &quot;edit/changePwdForm&quot;;
}</code></pre>
<p>그런데 실제 웹 어플리케이션에서는 비밀번호 변경 기능 외에 더 많은 기능에 로그인 여부를 확인해야 한다. 각 기능을 구현한 컨트롤러 코드마다 세션 확인 코드를 삽입하는 것은 많은 중복을 일으킨다. </p>
<p>이렇게 다수의 컨트롤러에 대해 동일한 기능을 적용해야 할 때 사용할 수 있는 것이 HandlerInterceptor이다.</p>
<h3 id="handlerinterceptor-인터페이스-구현">HandlerInterceptor 인터페이스 구현</h3>
<p>세 시점에 공통 기능을 넣을 수 있다.</p>
<ol>
<li>컨트롤러(핸들러) 실행 전 <pre><code class="language-java">boolean preHandle(HttpServletRequest request,HttpServletReponse response, Object handler) throws Exceptrion;</code></pre>
preHandle()은 컨트롤러(핸들러) 객체를 실행하기 전에 필요한 기능을 구현할 때 사용한다. handler 파라미터는 웹 요청을 처리할 컨트롤러(핸들러) 객체이다. preHandle()이 false를 리턴하면 컨트롤러(다음 HandlerInterceptor)를 실행하지 않는다. 이 메서드를 사용하면 다음 작업이 가능하다.</li>
</ol>
<ul>
<li>로그인 하지 않은 경우 컨트롤러를 실행하지 않음</li>
<li>컨트롤러를 실행하기 전에 컨트롤러에서 필요로 하는 정보 생성</li>
</ul>
<ol start="2">
<li>컨트롤러(핸들러) 실행 후, 아직 뷰를 실행하기 전 <pre><code class="language-java">void postHandle(HttpServletRequest request,HttpServletReponse response, Object handler, ModelAndView modelAndView) throws Exceptrion;</code></pre>
postHandle()는 컨트롤러(핸들러)가 정상적으로 실행된 이후에 추가 기능을 구현할 때 사용한다. 컨트롤러가 익셉션을 발생하면 postHandle()은 실행하지 않는다. </li>
<li>뷰를 실행한 이후<pre><code class="language-java">void afterCompletion(HttpServletRequest request,HttpServletReponse response, Object handler, Exception ex) throws Exceptrion;</code></pre>
afterCompletion()은 뷰가 클라이언트에 응답을 전송한 뒤에 실행된다. 컨트롤러 실행 과정에서 익셉션이 발생하면 이 메서드의 네번째 파라미터로 전달된다. 따라서 컨트롤러 실행 후 예기치 않게 발생한 익셉션을 로그로 남긴다거나 실행 시간을 기록하는 등의 후처리를 하기에 적합한 메서드다.<img src="https://velog.velcdn.com/images/yh_lee/post/55363a27-7145-4835-8a6c-15de6e709303/image.png" alt=""></li>
</ol>
<p>비밀번호 변경 기능에 접근할 때 HandlerInterceptor를 사용하면 로그인 여부에 따라 로그인 폼으로 보내거나 컨트롤러를 실행하도록 구현할 수 있다. </p>
<pre><code class="language-java">public class AuthCheckInterceptor implements HandlerInterceptor{
    @Override
    public boolean preHandle(HttpServletRequest request,HttpServletReponse response, Object handler) throws Exceptrion){
        HttpSession session = request.getSession(false);
        if(session!=null){
            Object authInfo = session.getAttribute(&quot;authInfo&quot;);
            if(authInfo!=null){
                return true;
            }
        }
        response.sendRedirect(request.getContextPath()+&quot;/lgoin&quot;);
        return false;
    }
}</code></pre>
<p>preHandle()이 true를 반환하면 컨트롤러를 실행하므로 로그인 상태면 컨트롤러를 실행한다. 반대로 false를 리턴하면 로그인 상태가 아니므로 지정한 경로로 리다이렉트 한다. </p>
<h3 id="handlerinterceptor-설정하기">HandlerInterceptor 설정하기</h3>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
    ...
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(authCheckInterceptor())
        .addPathPatterns(&quot;/edit/**&quot;);
    }

    @Bean
    public AuthCheckInterceptor authCheckInterceptor(){
        return new AuthCheckInterceptor();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/ac478e14-7b97-4acc-a3c5-513e5f2936f9/image.png" alt=""></p>
<p>addPathPatterns()에서 지정한 경로 중 패턴 일부를 제외하고 싶으면 excludePathPatterns()를 사용한다.</p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
    ...
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(authCheckInterceptor())
        .addPathPatterns(&quot;/edit/**&quot;);
        .excludePathPatterns(&quot;/edit/help/**&quot;);
    }
}</code></pre>
<h2 id="컨트롤러에서-쿠키-사용하기">컨트롤러에서 쿠키 사용하기</h2>
<p>사용자에 편의를 위해 아이디를 기억해두었다가 다음 로그인할 때 아이디를 자동으로 넣어주는 사이트가 많다. 이 기능을 구현할 때 쿠키를 사용한다. </p>
<p>이메일 기억하기 기능을 위해 수정할 코드는 네 곳이다. </p>
<ul>
<li>loginForm.jsp: 이메일 기억하기 체크박스 추가</li>
<li>LoginController의 form(): 쿠키가 존재할 경우 폼에 전달할 커맨드 객체의 email 프로퍼티를 쿠키의 값으로 설정한다. </li>
<li>LoginController의 submit(): 이메일 기억하기 옵션을 선택한 경우 로그인 성공 후에 이메일을 담고 있는 쿠키를 생성한다. </li>
<li>label.properties: 메시지를 추가한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/195eb3de-a48d-4c77-bb2e-c7a3ebd8d5fc/image.png" alt=""><img src="https://velog.velcdn.com/images/yh_lee/post/85bc8553-a41b-4bd8-abd6-a72dcc787bea/image.png" alt=""></p>
<p>LoginController의 form() 메서드는 이메일 정보를 기억하고 있는 쿠키가 존재하면 해당 쿠키의 값을 이용해서 LoginCommand 객체의 email 프로퍼티 값을 설정하면 된다. </p>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LoginController{
    private AuthService authService;
    public void setAuthService(AuthService authService){
        this.authService = authService;
    }

    @GetMapping
    public String form(
            LoginCommand loginCommand, 
            @CookieValue(value=&quot;REMEMBER&quot;,required=false) Cookie rCookie){
           if(rCookie!=null){
            loginCommand.setEmail(rCookie.getValue());
            loginCommand.setRememberEmail(true);
        }
        return &quot;login/loginForm&quot;;
    }
}</code></pre>
<p>@CookieValue 어노테이션의 value 속성은 쿠키의 이름을 지정한다. 이름이 REMEMBER인 쿠키를 Cookie 타입으로 전달받는다. 지정한 이름을 가진 쿠키가 존재하지 않을 수도 있다면 required 속성 값을 false로 둔다.</p>
<br>
실제로 REMEMBER 쿠키를 생성하는 부분은 로그인을 처리하는 submit() 메서드이다. 쿠키를 생성하려면 HttpServletResponse 객체가 필요하므로 submit() 메서드 파라미터로 전달한다. 

<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LoginController{
    ...
    @PostMapping
    public String submit(
            LoginCommand loginCommand,
            Errors errors,
            HttpSession session,
            HttpServletResponse response
                    ){
        new LoginCommandValidator().validate(loginCommand,errors);
        if(errors.hasError()){
            return &quot;login/loginForm&quot;;
        }
        try{
            AuthInfo authInfo = authService.authticate(
                loginCommand.getEmail(),
                loginCommand.getPassword());

            session.setAttribute(&quot;authInfo&quot;,authInfo);

            Cookie rememberCookie = new Cookie(&quot;REMEMBER&quot;, loginCommand.getEmail());
            rememberCookie.setPath(&quot;/&quot;);
            if(loginComand.isRememberEmail()){
                rememberCookie.setMaxAge(60*60*24*30);
            }else{
                rememberCookie.setMaxAge(0);
            }
            response.addCookie(rememberCookie);

            return &quot;login/loginSuccess&quot;;
        }catch(IdPasswordNotMatchingException e){
            errors.reject(&quot;idPasswordNotMatching&quot;);
            return &quot;login/loginForm&quot;;
        }
    }    
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[가상화(컨테이너와 가상머신)]]></title>
            <link>https://velog.io/@yh_lee/%EA%B0%80%EC%83%81%ED%99%94%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0</link>
            <guid>https://velog.io/@yh_lee/%EA%B0%80%EC%83%81%ED%99%94%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EA%B0%80%EC%83%81%EB%A8%B8%EC%8B%A0</guid>
            <pubDate>Mon, 08 Aug 2022 13:13:51 GMT</pubDate>
            <description><![CDATA[<h1 id="가상화virtualization">가상화(Virtualization)</h1>
<p>가상화는 물리적인 컴포넌트(HW장치)를 논리적인 객체로 추상화하는 것을 의미한다. 하나의 장치를 마치 여러 개처럼 동작시키거나, 반대로 여러 개의 장치를 묶어 하나의 장치인 것처럼 사용자에게 공유자원으로 제공할 수 있어 클라우드 컴퓨팅 구현을 위한 핵심기술이다. </p>
<p>가상화의 대상이 되는 컴퓨팅 자원은 프로세서(CPU), 메모리(Memory), 스토리지(Storage), 네트워크(Network)를 포함한다. </p>
<h2 id="가상머신vm">가상머신(VM)</h2>
<p>가상화를 통해 구현되는 복제된 컴퓨팅 환경</p>
<h2 id="하이퍼바이저">하이퍼바이저</h2>
<p>공유 컴퓨팅 자원을 관리하고 가상머신들을 컨트롤(I/O 명령 처리) 하는 중간관리자</p>
<h2 id="컨테이너">컨테이너</h2>
<p>모듈화되고 격리된 컴퓨팅 공간을 의미한다. 시스템 환경 의존성을 탈피하고 안정적으로 구동한다. </p>
<h2 id="가상머신-vs-컨테이너">가상머신 VS 컨테이너</h2>
<ol>
<li>컨테이너는 가상화에서 하이퍼바이저와 게스트OS가 불필요하다. </li>
<li>컨테이너는 OS 레벨에서 프로세스를 격리하여 모듈화된 프로그램 패키지로써 수행한다. </li>
<li>따라서 컨테이너는 가상 머신보다 가볍고 빠르다.</li>
<li>이는 더 많은 응용프로그램을 더 쉽게 하나의 물리적 서버에서 구동시키는 것을 가능하게 한다. 
<img src="https://velog.velcdn.com/images/yh_lee/post/58cb2fd2-5683-4ef8-9f58-af323ad83b30/image.png" alt=""></li>
</ol>
<p>컨테이너 기술: 쿠버네틱스, 도커</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC2 메시지, 커맨드 객체 검증]]></title>
            <link>https://velog.io/@yh_lee/MVC2-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@yh_lee/MVC2-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Wed, 03 Aug 2022 07:46:33 GMT</pubDate>
            <description><![CDATA[<h1 id="springmessage-태그로-메시지-출력"><a href="spring:message">spring:message</a> 태그로 메시지 출력</h1>
<pre><code>&lt;label&gt;이메일&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;email&quot;&gt;</code></pre><p>&#39;이메일&#39;을 &#39;이메일 주소&#39;로 변경하게 되면 각 폼을 출력하는 모든 JSP를 찾아서 모두 변경해야 한다. 그리고 하드 코딩 되어 있을 경우에 다국어 지원에 유연성이 떨어진다. &#39;이메일&#39;을 &#39;E-mail&#39;로 각 언어에 맞게 따로 뷰 코드를 만들어야 하는 상황이 발생한다.</p>
<p>이런 문제를 해결할 수 있는 방법은 뷰 코드에서 사용할 문자열을 언어별로 파일에 보관하고 뷰 코드는 언어에 따라 파일을 읽어와 출력하는 것이다. 스프링이 자체적으로 이 기능을 제공하고 있다. </p>
<h4 id="labelproperties">label.properties</h4>
<pre><code>member.register = 회원가입

term = 약관
term.agree = 약관동의
next.btn = 다음단계

member.info = 회원정보
email = 이메일
name = 이름
password = 비밀번호
pqssword.confirm = 비밀번호 확인
register.btn = 가입 완료

register.done = {0}님, 회원 가입을 완료했습니다. 

go.main = 메인으로 이동</code></pre><p>설정파일에서 MessageSource 타입의 빈을 추가한다.</p>
<pre><code class="language-java">@Configuration
@EnalbeWebMvc
public class MvcVonfig implements WebMvcConfigurer{
    ...
    @Bean
    public MessageSource messageSource(){
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasenames(&quot;message.label&quot;);
        ms.setDefaultEncoding(&quot;UTF-8&quot;);
        return ms;
    }
}</code></pre>
<p>위 코드의 ms.setBasename(&quot;message.label&quot;)에서 프로퍼티 값으로 &quot;message.label&quot;을 주었다. message 패키지에 속한 label 프로퍼티 파일로부터 메시지를 읽어온다고 설정한 것이다. 따라서 label.properties 파일로부터 메시지를 읽어온다. </p>
<p>JSP 코드는 아래와 같이 수정한다.<img src="https://velog.velcdn.com/images/yh_lee/post/207e5634-4d27-43ae-a352-946a847814fa/image.png" alt=""></p>
<p><a href="spring:message">spring:message</a> 태그는 MessageSource로부터 코드에 해당하는 메시지를 읽어온다. </p>
<p><strong>다국어 지원을 위한 메세지 파일</strong>
각 프로퍼티 파일 이름에 언언에 해당하는 로케일 문자를 추가하면 된다. </p>
<ul>
<li>lable_ko.properties</li>
<li>lable_en.properties</li>
</ul>
<h2 id="메세지-처리를-위한-messagesource와-springmessage-태그">메세지 처리를 위한 MessageSource와 <a href="spring:message">spring:message</a> 태그</h2>
<p>스프링은 로케일(지역) 에 상관없이 일관된 방법으로 문자열(메시지)을 관리할 수 있는 MessageSource 인터페이스를 정의하고 있다. </p>
<h2 id="springmessage-태그의-인자처리"><a href="spring:message">spring:message</a> 태그의 인자처리</h2>
<p>lable.properties를 보면 다음과 같은 프로퍼티를 포함한다. 이 프로퍼티의 {0} 부분은 인덱스 기반 변수 중 0번 인덱스(첫번째 인덱스) 값으로 대치된다.</p>
<pre><code>register.done = {0}님, 회원 가입을 완료했습니다. </code></pre><p>Object 배열을 생성해서 인덱스 기반 변수값을 전달할 수 있다. </p>
<pre><code class="language-java">Object[] args=new Object[1];
args[0]=&quot;Java&quot;;
messageSource.getMessage(&quot;register.done&quot;,args,Locale.KOREA);</code></pre>
<p><a href="spring:message">spring:message</a> 태그를 사용할 때는 arguments 속성을 사용해서 인덱스 기반 변수값을 전달한다. </p>
<pre><code>&lt;spring:message code=&quot;registe.done&quot; arguments=&quot;${registerRequest.name}&quot;/&gt;</code></pre><h1 id="커맨드-객체의-값-검증과-에러-메시지-처리">커맨드 객체의 값 검증과 에러 메시지 처리</h1>
<p>앞서 작성한 회원 가입 처리 코드는 동작은 하지만 비정상 값을 입력해도 동작하는 문제가 있다. 이를 해결하기 위해 입력한 값에 대한 검증 처리를 해야한다. 또 다른 문제는 중복된 이메일 주소를 입력해서 다시 폼을 보여줄 때 왜 가입에 실패했는지 이유를 알려주지 않는다. 그럼 사용자는 혼란을 겪게 된다.</p>
<p>폼 값 검증과 에러 메시지 처리는 어플리케이션을 개발할 때 놓쳐서는 안된다. 스프링은 이 두가지를 해결하기 위해 아래 방법을 제공한다.</p>
<ul>
<li>커맨드 객체를 검증하고 결과를 에러 코드로 저장</li>
<li>JSP에서 에러 코드로부터 메시지를 출력</li>
</ul>
<h2 id="커맨드-객체-검증과-에러-코드-지정">커맨드 객체 검증과 에러 코드 지정</h2>
<p>스프링 MVC에서 커맨드 객체의 값이 올바른지 검사하려면 다음의 두 인터페이스를 사용한다.</p>
<ul>
<li><p>org.springframework.validation.Validator</p>
</li>
<li><p>org.springframework.validation.Errors</p>
<pre><code class="language-java">public interface Validator{
  boolean supports(Class&lt;?&gt; clazz);
  void validate(Object target, Errors errors);
}</code></pre>
<p>supports()는 Validator가 검증할 수 있는 타입인지 검사한다.
validate() 첫번째 파라미터로 전달받은 객체를 검증하고 오류 결과를 Errors에 담는 기능을 한다.</p>
</li>
<li><p>검사 대상 객체가 올바른지 검사</p>
</li>
<li><p>올바르지 않으면 Errors의 rejectValue() 메서드를 이용해서 에러 코드 저장</p>
</li>
</ul>
<p>Errors의 첫번째 파라미터로 프로퍼티 이름을 전달받고, 두번째 파라미터로 에러코드를 전달받는다. &quot;email&quot; 프로퍼티 값이 존재하지 않으면 에러 코드로 &quot;required&quot;를 추가한다.</p>
<pre><code class="language-java">public class RegisterRequestValidator implements Validator{
    @Override
    public void validate(Object target, Errors errors){
        RegisterRequest regReq = (RegisterRequest) target;
        if(regReq.getEmail()==null || regReq.getEmail().trim().isEmpty()){
            errors.rejectValue(&quot;email&quot;,&quot;required&quot;);
        }
        ...

        //errors 객체의 getFieldValue(&quot;password&quot;) 메서드를 실행해서
        //커맨드 객체의 password 프로퍼티 값을 구함
        //커맨드 객체를 직접 전달하지 않아도 값 검증 가능
        VaildationUtils.rejectIfEmpty(erros,&quot;password&quot;,&quot;required&quot;)
    }
}</code></pre>
<p>validate()를 실행하는 과정에서 유효하지 않은 값이 존재하면 Errors의 rejectValue()를 실행한다. 이 메서드가 한번이라도 불리면 Errors.hasErrors()는 true를 반환한다.</p>
<p>커맨드 객체 자체에 문제가 있을 때는 reject()를 사용한다. 아이디, 비밀번호가 잘못 입력한 경우 불일치한다는 메시지를 보여줘야 한다. 이경우 에러를 추가하기 보다 커맨드 객체 자체에 에러를 추가한다. 
<em><strong>reject() 메서드는 개별 프로퍼티가 아닌 객체 자체에 에러 코드를 추가하므로 이 에러를 글로벌 에러라고 한다.</strong></em></p>
<pre><code class="language-java">try{
    ...
}catch(WrongIdPasswordException ex){
    errors.reject(&quot;notMatchingIdPassword&quot;);
    return &quot;login/loginForm&quot;;
}</code></pre>
<p>Errors 타입 파라미터는 반드시 커맨드 객체를 위한 파라미터 다음에 위치해야 한다. 그렇지 않으면 익셉션이 발생한다.</p>
<pre><code class="language-java">@PostMapping              //익셉션 발생
public String handleStep3(Errors errors, RegisterRequest regReq){
    ...
}</code></pre>
<h2 id="커맨드-객체의-에러-메시지-출력">커맨드 객체의 에러 메시지 출력</h2>
<p>에러코드에 해당하는 메시지 코드를 찾을 때는 다음 규칙을 따른다.</p>
<p>1) 에러코드.커맨트객체이름.필드명[인덱스].필드명
2) 에러코드.필드명
3) 에러코드.필드타입
4) 에러코드</p>
<p>errors.rejectValue(&quot;email&quot;,&quot;required&quot;) 코드로 &quot;email&quot; 프로퍼티에 &quot;required&quot; 에러 코드를 추가했고 커맨드 객체 이름이 &quot;registerRequest&quot;이면 다음 순서대로 메세지 코드를 검색한다.</p>
<p>1) required.registerRequest.email
2) required.email
3) required.String
4) required</p>
<h1 id="글로벌-범위-validator와-컨트롤러-범위-validator">글로벌 범위 Validator와 컨트롤러 범위 Validator</h1>
<h2 id="글로벌-범위-validator-설정과-valid-어노테이션">글로벌 범위 Validator 설정과 @Valid 어노테이션</h2>
<p>글로벌 범위 Validator는 모든 컨트롤러에 적용할 수 있는 Validator이다.</p>
<p>설정 클래스에서 WebMvcConfigurer의 getValidator()가 Validator 구현 객체를 리턴하도록 구현</p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcCofigurer{
    @Override
    public Validator getValidator(){
        return new RegisterRequestValidator();
    }
}</code></pre>
<p>스프링 MVC는 WebMvcConfigurer 인터페이스의 getValidator() 메서드가 리턴한 객체를 글로벌 범위 Validator로 사용한다. 글로벌 범위로 지정하면 @Valid 어노테이션을 사용해서 Validator를 적용할 수 있다. </p>
<pre><code class="language-java">@Controller
public class RegisterController{
    @PostMapping
    public String handleStep3(@Valid RegisterRequest regReq, Errors error){
        ...
    }    
}</code></pre>
<h2 id="initbinder-어노테이션을-이용한-컨트롤러-범위-validator">@InitBinder 어노테이션을 이용한 컨트롤러 범위 Validator</h2>
<pre><code class="language-java">@Controller
public class RegisterController{
    @PostMapping
    public String handleStep3(@Valid RegisterRequest regReq, Errors error){
        ...
    }
    @InitBinder
    protected void initBinder(WebDataBinder binder){
        binder.setValidator(new RegisterRequestValidator());
    }
}</code></pre>
<h1 id="bean-validation을-이용한-값-검증-처리">Bean Validation을 이용한 값 검증 처리</h1>
<p>@Valid 어노테이션은 Bean Validation 스펙에 정의되어 있다. 이 스펙은 @NotNull, @Digits, @Size 등의 어노테이션을 정의하고 있다. </p>
<p>Bean Validation이 제공하는 어노테이션을 이용해서 커맨드 객체 값을 검증하는 방법은 다음과 같다.</p>
<ul>
<li>Bean Validatoin과 관련된 의존을 설정에 추가한다.</li>
<li>커맨드 객체에 @NotNull, @Digits 등의 어노테이션을 이용해서 검증 규칙을 설정한다.</li>
</ul>
<pre><code class="language-java">public class RegisterRequest{
    @NotNull
    @Email
    private String email;

    @Size(min=6)
    private String password;

    @NotEmpty
    private String confirmPassword;

    @NotEmpty
    private String name;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC1 요청매핑, 커맨드 객체, 리다이렉트, 모델]]></title>
            <link>https://velog.io/@yh_lee/MVC1-%EC%9A%94%EC%B2%AD%EB%A7%A4%ED%95%91-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8-%EB%AA%A8%EB%8D%B8</link>
            <guid>https://velog.io/@yh_lee/MVC1-%EC%9A%94%EC%B2%AD%EB%A7%A4%ED%95%91-%EC%BB%A4%EB%A7%A8%EB%93%9C-%EA%B0%9D%EC%B2%B4-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8-%EB%AA%A8%EB%8D%B8</guid>
            <pubDate>Tue, 02 Aug 2022 11:27:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>스프링 MVC를 사용해서 웹 어플리케이션을 개발하는 것은 <strong>컨트롤러와 뷰 코드를 구현한다는 것을 뜻한다.</strong> 컨트롤러를 통해 어떤 요청 경로를 처리할지 결정하고, 웹 브라우저가 전송한 요청에서 필요한 값을 구하고, 처리 결과를 화면에 뿌려주면 된다. </p>
</blockquote>
<h1 id="요청-매핑-어노테이션을-이용한-경로-매핑">요청 매핑 어노테이션을 이용한 경로 매핑</h1>
<ul>
<li>특정 URL을 처리할 코드</li>
<li>처리 결과를 HTML과 같은 형식으로 응답하는 코드<pre><code class="language-java"></code></pre>
</li>
</ul>
<p>@Controller
public class HelloController {
    @GetMapping(&quot;/hello&quot;)
    public String hello(Model model, @RequestParam(value = &quot;name&quot;, required = false) String name) {
        model.addAttribute(&quot;greeting&quot;, &quot;안녕하세요 &quot; + name);
        return &quot;hello&quot;;
    }
}</p>
<pre><code>
@RequestMapping 어노테이션을 사용하면 동일한 경로에 대해 전체 메서드에 적용시켜줄 수 있다. 
```java
@Controller
@RequestMapping(&quot;/register&quot;)//각 메서드에 공통되는 경로
public class RegistController {

    @RequestMapping(&quot;/step1&quot;)//하위 세부 경로
    public String handleStep1() {
        return &quot;reister/step1&quot;;
    }

    public String handleStep2() {
        return &quot;reister/step2&quot;;
    }
    ...
} </code></pre><p>남은 작업은 ControllerConfig.java에 RegisterController 클래스를 빈으로 등록하는 것이다. </p>
<h1 id="getmapping-postmapping">@GetMapping, @PostMapping</h1>
<p>스프링 MVC는 별도의 설정이 없으면 GET과 POST 방식에 상관없이 @requestMapping에 지정한 경로와 일치하는 요청을 처리한다. 만약 @GetMapping이나 @PostMapping을 사용하면 요청 방식을 제한할 수 있다. </p>
<pre><code class="language-java">@Controller
public class LoginController
    @GetMapping(&quot;/member/login&quot;)
    public String form(){
        ...
    }

    @PostMapping(&quot;member/login&quot;)
    public String login(){
        ...
    }
} </code></pre>
<h1 id="요청-파라미터-접근">요청 파라미터 접근</h1>
<p>컨트롤러 메서드에서 요청 파라미터를 사용하는 첫번째 방법은 HttpServletRequest를 직접 이용하는 것이다. HttpServletRequest의 getParameter() 메서드를 이용해서 파라미터의 값을 구하면 된다.</p>
<pre><code>&lt;form action=&quot;step2&quot; method=&quot;post&quot;&gt;
&lt;label&gt;
  &lt;input type=&quot;checkbox&quot; name=&quot;agree&quot; value=&quot;true&quot;&gt; 약관 동의
&lt;/label&gt;
&lt;/form&gt;</code></pre><pre><code class="language-java">@Controller
public class RegisterController {
    @RequestMapping(&quot;/register/step1&quot;)
    public String handleStep1() {
        return &quot;register/step1&quot;;
    }

    @PostMapping(&quot;/register/step2&quot;)
    public String handleStep2(HttpServletRequest request) {
        String agreeParam = request.getParameter(&quot;agree&quot;);
        if (agreeParam == null || !agreeParam.equals(&quot;true&quot;)) {
            return &quot;register/step1&quot;;
        }
        return &quot;register/step2&quot;;
    }
}</code></pre>
<p>요청 파라미터에 접근하는 두번째 방법능 @RequestParam 어노테이션을 사용하는 것이다. 아래 코드는 agree 파라미터의 값을 읽어와 agreeVal 파라미터에 할당한다. 요청 파라미터의 값이 없으면 &quot;false&quot; 문자열을 값으로 사용한다. 스프링 MVC는 파라미터 타입에 맞게 String 값을 변환해준다. </p>
<pre><code class="language-java">@Controller
public class RegisterController {

    @PostMapping(&quot;/register/step2&quot;)
    public String handleStep2(@RequestParam(value = &quot;agree&quot;, defaultValue = &quot;false&quot;) Boolean agree) {
        if (!agree) {
            return &quot;register/step1&quot;;
        }
        return &quot;register/step2&quot;;
    }
}</code></pre>
<h1 id="리다이렉트-처리">리다이렉트 처리</h1>
<p>웹 브라우저에 <a href="http://localhost:8080/sp5-chap11/register/step2">http://localhost:8080/sp5-chap11/register/step2</a> 주소를 직접 입력하면 에러 화면이 출력된다. 
RegisterController 클래스의 handleStep2()는 POST 방식만 처리하기 때문에 웹 브라우저에 직접 주소를 입력할 때 사용되는 GET 방식 요청은 처리하지 않는다. 그래서 405 상태 코드를 응답한다. </p>
<p>잘못된 전송 방식으로 요청이 왔을 때 에러 화면보다 알맞은 경로로 리다이렉트할 수 있다. </p>
<p>뷰 값으로 &quot;redirect:/register/step&quot;을 사용했는데 이동 경로가 &quot;/&quot;로 시작하므로 실제 리다이렉트할 경로는 웹 어플리케이션 경로인 &quot;/sp5-chap11&quot;과 &quot;/register/step&quot;을 연결한 &quot;/sp5-chap11/register/step1&quot;이 된다. </p>
<pre><code class="language-java">@Controller
public class RegisterController {

    @GetMapping(&quot;/register/step2&quot;)
    public String handleStep2Get(){
        return &quot;redirect:/register/step1;
    }
}</code></pre>
<h1 id="커맨드-객체를-이용해서-요청-파라미터-사용">커맨드 객체를 이용해서 요청 파라미터 사용</h1>
<p>step2.jsp가 생성하는 폼은 다음 파라미터를 이용해서 정보를 서버에 전송한다. </p>
<ul>
<li><p>email</p>
</li>
<li><p>name</p>
</li>
<li><p>password</p>
</li>
<li><p>confirmPassword
폼 전송 요청을 처리하는 컨트롤러를 아래와 같이 생성했다. 하지만 이 코드는 올바르게 동작하지만 요청 파라미터 개수가 증가할 때마다 코드의 길이도 함께 길어지는 단점이 있다. </p>
<pre><code class="language-java">@PostMapping(&quot;register/step3&quot;)
public String handleStep3(HttpServletRequest request) {
  String email = request.getParameter(&quot;email&quot;);
  String name = request.getParameter(&quot;name&quot;);
  String password = request.getParameter(&quot;password&quot;);
  String confirmPassword = request.getParameter(&quot;confirmPassword&quot;);

  RegisterRequest regReq = new RegisterRequest;
  regReq.setEmail(email);
  regReq.setName(name);
}</code></pre>
<p>스프링은 이런 불편함을 줄이기 위해 요청 파라미터 값을 커맨드(command) 객체에 담아주는 기능을 제공한다. 커맨드 객체는 요청 파라미터의 값을 전달받을 수 있는 세터 메서드를 포함하는 객체이다. </p>
</li>
</ul>
<p>helloStep3()는 MemberRegisterService를 이용해서 회원 가입을 처리한다. </p>
<pre><code class="language-java">@Controller
public class ReigsterController {
    private MemberRegisterService memberRegisterService;

    public void setMemberRegisterService(MemberRegisterService memberRegisterService) {
        this.memberRegisterService = memberRegisterService;
    }
    ...

    @PostMapping(&quot;/register/step3&quot;)
    public String handelStep3(RegisterRequest regReq) {
        try {
            memberRegisterService.regist(regReq);
            return &quot;register/step3&quot;;
        } catch (DuplicateMemberException exception) {
            return &quot;register/step2&quot;;
        }
    }
}</code></pre>
<h1 id="뷰-jsp-코드에서-커맨드-객체-사용">뷰 JSP 코드에서 커맨드 객체 사용</h1>
<p>JSP의 ${registerRequest.name} 코드가 있다. 여기서 registerRequest가 커맨드 객체에 접근할 때 사용한 속성 이름이다. 스프링 MVC는 커맨드 객체의 (첫 글자를 소문자로 바꾼) 클래스 이름과 동일한 속성 이름을 사용해서 커맨드 객체를 뷰에 전달한다. 커맨드 객체의 클래스 이름이 RegisterRequest인 경우 JSP 코드는 registerRequest 이름을 사용해서 커맨드 객체에 접근할 수 있다. </p>
<pre><code class="language-jsp">&lt;p&gt;${registerRequest.name}님 회원가입을 완료했습니다.&lt;/p&gt;</code></pre>
<pre><code class="language-java">@PostMapping(&quot;register/step3&quot;)
public String handleStep3(RegisterRequest regReq){
    ...
}</code></pre>
<h1 id="modelattribute-어노테이션으로-커맨드-객체-속성-이름-변경">@ModelAttribute 어노테이션으로 커맨드 객체 속성 이름 변경</h1>
<p>커맨드 객체에 접근할 때 사용할 속성 이름을 변경하고 싶으면 커맨드 객체로 사용할 파라미터에 @ModelAttribute 어노테이션을 적용하면 된다. </p>
<pre><code class="language-java">@PostMapping(&quot;/register/step3&quot;)
public String handleStep3(@ModelAttribute(&quot;formData&quot;) RegisterRequest regReq){
    ...
}</code></pre>
<p>@ModelAttribute는 모델에서 사용할 속성 이름을 값으로 설정한다. 위 설정을 이용하면 뷰 코드에서 &quot;formData&quot;라는 이름으로 커맨드 객체에 접근할 수 있다.</p>
<h1 id="커맨드-객체와-스프링-폼-연동">커맨드 객체와 스프링 폼 연동</h1>
<p>회원 정보 입력 폼에서 중복된 이메일 주소를 입력하면 텅 빈 폼을 보여준다. 폼이 비어있으면 사용자가 입력값을 다시 입력해야 하는 불편함이 있다. 이때 커맨드 객체에 값을 폼에 채워주면 이런 불편함을 해소할 수 있다.</p>
<pre><code class="language-java">&lt;input type=&quot;text&quot; name=&quot;email&quot; id=&quot;email&quot; value=&quot;${registerRequest.email}&quot;&gt;
...
&lt;input type=&quot;text&quot; name=&quot;name&quot; id=&quot;name&quot; value=&quot;${registerRequest.name}&quot;&gt;</code></pre>
<p>스프링 MVC가 제공하는 커스텀 태그를 사용할 수도 있다. <a href="form:form">form:form</a> 태그와 <a href="form:input">form:input</a> 태그를 지원한다. </p>
<pre><code class="language-jsp">...
&lt;form:form action=&quot;step3&quot; modelAttribute=&quot;registerRequest&quot;&gt;
...
&lt;form:input path=&quot;email&quot;/&gt;
...
&lt;form:password path=&quot;password&quot;/&gt;
...
&lt;/form:form&gt;</code></pre>
<ul>
<li>action: &lt;form&gt;태그의 action 속성과 동일한 값을 사용한다. </li>
<li>modelAttribute: 커맨드 객체의 속성 이름을 지정한다.</li>
</ul>
<p>step1에서 step2로 넘어오는 단계에서 이름이 &quot;registerRequest&quot;인 객체를 모델에 넣어야 <a href="form:form">form:form</a> 캐그가 정상 동작한다. 이를 위해 RegisterController에 코드를 추가했다.</p>
<pre><code class="language-java">@Controller
public class RegisterController {
     ...
    @PostMapping(&quot;/register/step2&quot;)
    public String handleStep2(@RequestParam(value = &quot;agree&quot;, defaultValue = &quot;false&quot;) Boolean agree, Model model) {
        if (!agree) {
            return &quot;register/step1&quot;;
        }
        model.addAttribute(&quot;registerRequest&quot;, new RegisterRequest());
        return &quot;register/step2&quot;;
    }
}</code></pre>
<h1 id="컨트롤러-구현-없는-경로-매핑">컨트롤러 구현 없는 경로 매핑</h1>
<pre><code class="language-java">//변경 전
@Controller
public class MainController{
    @RequestMapping(&quot;/main&quot;)
    public String main(){
        return &quot;main&quot;;
    }
}
//변경 후
@Override
public void addViewControllers(ViewControllerRegistry registry){
    registry.addViewController(&quot;/main&quot;).setViewName(&quot;main&quot;);
}</code></pre>
<p>변경 전처럼 요청 결오와 뷰 이름을 연결해주는 기능만 있는 컨트롤러를 만드는 것은 성가신 일이다. WebMvcConfigurer 인터페이스의 addViewControllers()를 사용하여 컨트롤러 구현없이 요청 경로와 뷰 이름을 간단하게 연결할 수 있다.</p>
<h1 id="주요-에러-발생-상황">주요 에러 발생 상황</h1>
<h2 id="요청-매핑-관련-익셉션">요청 매핑 관련 익셉션</h2>
<ol>
<li>요청 경로를 처리할 컨트롤러가 존재하지 않거나 WebMvcConfigurer를 이용한 설정이 없다면 404 에러가 발생한다. 아래 항목은 에러가 발생하면 확인해 볼 사항들이다.</li>
</ol>
<ul>
<li>요청 경로가 올바른지</li>
<li>컨트롤러에 설정한 경로가 올바른지</li>
<li>컨트롤러 클래스를 빈으로 등록했는지</li>
<li>컨트롤러 클래스에 @Controller 어노테이션을 적용했는지</li>
</ul>
<ol start="2">
<li>뷰 이름에 해당하는 JSP 파일이 존재하지 않아도 404 에러가 발생한다. 차이점은 존재하지 않는 JSP 파일의 경로가 출력된다는 점이다. </li>
<li>지원하지 않는 전송 방식을 사용한 경우 405 에러가 발생한다. POST 방식만 처리하는 요청 경로를 GET 방식으로 연결하면 에러가 발생한다.</li>
</ol>
<h2 id="requestparam이나-커맨드-객체-관련-익셉션">@RequestParam이나 커맨드 객체 관련 익셉션</h2>
<ol>
<li>&lt;input&gt; 태그에 값을 체크하지 않은 상태로 넘어오게 되면 파라미터에 아무 값도 전송하지 않는다. 파라미터가 존재하지 않는다는 익셉션이 발생한다. 스프링 MVC는 이 익셉션이 발생하면 400 에러를 응답으로 전송한다. </li>
<li>요청 파라미터 값을 @RequestParam이 적용된 파라미터 타입으로 변환할 수 없는 경우에도 에러가 발생한다. 아래 코드가 400 에러가 발생하는 이유는 &quot;true1&quot; 값을 Boolean 타입으로 변환할 수 없기 때문이다.<pre><code>&lt;input type=&quot;checkbox&quot; name=&quot;agree&quot; value=&quot;true1&quot;&gt;</code></pre></li>
</ol>
<h1 id="커맨드-객체-중첩-콜렉션-프로퍼티">커맨드 객체: 중첩, 콜렉션 프로퍼티</h1>
<p>스프링 MVC는 커맨드 객체가 리스트 타입의 프로퍼티를 가졌거나 중첩 프로퍼티를 가진 경우에도 요청 파라미터의 값을 알맞게 커맨드 객체에 설정해주는 기능을 제공하고 있다. </p>
<pre><code class="language-java">public class Respondent{
    private int age;
    private String location;
    ...
}
public class AnsweredData{
    private List&lt;String&gt; responses;
    private Respondent res;
    ...
}</code></pre>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/survey&quot;)
public class SuveyController {
    @GetMapping
    public String form() {
        return &quot;survey/surveyForm&quot;;
    }

    @PostMapping
    public String submit(@ModelAttribute(&quot;ansData&quot;) AnsweredData data) {
        return &quot;survey/submitted&quot;;
    }
}</code></pre>
<p>submit() 메서드는 커맨드 객체로 AnsweredData 객체를 사용한다.</p>
<p>surveyForm.jsp를 살펴보자. &lt;input&gt; 태그의 name 속성은 다음과 같이 커맨드 객체의 프로퍼티에 매핑된다. 
<img src="https://velog.velcdn.com/images/yh_lee/post/24cd6ea1-2bae-4942-8998-5576df600923/image.png" alt=""></p>
<h1 id="model을-통해-컨트롤러에서-뷰에-데이터-전달">Model을 통해 컨트롤러에서 뷰에 데이터 전달</h1>
<p>컨트롤러는 뷰가 응답 화면을 구성하는데 필요한 데이터를 생성해서 전달해야 한다. 이때 사용하는 것이 Model이다. </p>
<pre><code class="language-java">@Controller
public class HelloController{
    public String hello(Model model, @RequestParam(value=&quot;name&quot;, required=false) String name){
        model.addAttribute(&quot;greeting&quot;, &quot;안녕하세요 &quot;+name);
        return &quot;hello&quot;;
  }
}</code></pre>
<p>addAttribute()의 첫번째 파라미터는 속성 이름이다. 뷰 코드는 이 이름을 사용해서 데이터에 접근한다. JSP는 다음과 같이 표현식을 사요해서 속성값에 접근한다.</p>
<pre><code>${greeting}</code></pre><p>앞서 작성한 SurveyController는 surveyFrom.jsp에 설문 항목을 하드 코딩했다. 설문 항목을 컨트롤러에서 생성해서 뷰에 전달하는 방식으로 변경해보자. 먼저 개별 설문 항목 데이터를 담기 위한 클래스를 아래와 같이 작성한다.</p>
<pre><code class="language-java">public class Question{
    private String title;
    private List&lt;String&gt; options;
}</code></pre>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/survey&quot;)
public class SurveyController {

    @GetMapping
    public String form(Model model) {
        List&lt;Question&gt; questions = createQuestions();
        model.addAttribute(&quot;questions&quot;, questions);
        return &quot;survey/survryForm&quot;;
    }

    private List&lt;Question&gt; createQuestions() {
        Question q1 = new Question(&quot;당신의 역할은 무엇입니까?&quot;, Array.asList(&quot;서버&quot;, &quot;프론트&quot;, &quot;풀스택&quot;));
    }
}</code></pre>
<p>form()에서 Model 타입의 파라미터를 추가헀고 questions라는 이름으로 모델에 추가했다. <img src="https://velog.velcdn.com/images/yh_lee/post/216cf9aa-2b65-4f43-9bad-43857cad69d3/image.png" alt=""></p>
<p>Question 리스트를 사용해서 폼 화면은 생성하도록 JSP 코드를 수정했다. SurveyController에서 Model을 통해 전달한 Question 리스트를 이용해서 설문 폼을 생성할 수 있다.</p>
<h2 id="modelandview를-통한-뷰-선택과-모델-전달">ModelAndView를 통한 뷰 선택과 모델 전달</h2>
<p>지금까지 구현한 컨트롤러는 두가지 특징이 있다. </p>
<ul>
<li>Model을 이용해서 뷰에 전달할 데이터 설정</li>
<li>결과를 보여줄 뷰 이름을 리턴</li>
</ul>
<p>ModelAndView를 사용ㅇ하면 이 두가지를 한번에 처리할 수 있다. 요청 매핑 어노테이션을 적용한 메서드는 String 타입 대신 ModelAndView를 리턴할 수 있다. 뷰에 전달할 모델 데이터는 addObject()로 추가한다. 뷰 이름은 setViewName() 메서드를 이용해서 지정한다.</p>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/survey&quot;)
public class SurveyController{
    @GetMapping
    public ModelAndview form(){
        List&lt;Question&gt; questions = createQuestions();
        ModelAndView mav = new ModelAndView();
        mav.addObject(&quot;questions&quot;, questions);
        mav.setViewName(&quot;survey/surveyForm&quot;);
        return mav;
    }
}</code></pre>
<h2 id="get-방식과-post-방식에-동일-이름-커맨드-객체-사용">GET 방식과 POST 방식에 동일 이름 커맨드 객체 사용</h2>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/login&quot;)
public class LoginController{
    @GetMapping
    public String form(@MoodelAttribute(&quot;login&quot;) LoginCommand loginCommand){
        return &quot;login/loginForm&quot;;
    }

    @PostMapping
    public String form(@MoodelAttribute(&quot;login&quot;) LoginCommand loginCommand){
        ...
    }

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[컴퍼지트 패턴]]></title>
            <link>https://velog.io/@yh_lee/%EC%BB%B4%ED%8D%BC%EC%A7%80%ED%8A%B8-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@yh_lee/%EC%BB%B4%ED%8D%BC%EC%A7%80%ED%8A%B8-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 01 Aug 2022 11:48:01 GMT</pubDate>
            <description><![CDATA[<h1 id="chap14-컴퍼지트-패턴">Chap14 컴퍼지트 패턴</h1>
<h2 id="컴퓨터에-추가-장치-지원하기">컴퓨터에 추가 장치 지원하기</h2>
<p>컴퓨터를 모델링해보자. 컴퓨터의 구성 장치인 Keybord, Body, Monitor 클래스를 정의한다. Computer 클래스는 이 구성 장치들을 포함하는 것으로 구현할 수 있다. 클래스 다이어그램에서는 Computer 클래스의 구성 장치 간의 관계를 함성(Composition) 관계로 표현할 수 있다. <img src="https://velog.velcdn.com/images/yh_lee/post/d2bab5d1-e72b-4027-9857-5fc49c3e77ac/image.png" alt=""></p>
<p><strong>Keyboard, Body, Monitor 클래스 코드</strong></p>
<pre><code class="language-java">public clas Keyboard{
    private int price;
    private int power;

    public Keyboard(int power, int price){
        this.power = power;
        this.price = price;
    }

    public int getPrice(){
        return price;
    }

    public int getPower(){
        return power;
    }
}
public class Body{
    private int price;
    private int power;

    public Body(int power, int price){
        this.power = power;
        this.price = price;
    }

    public int getPrice(){
        return price;
    }

    public int getPower(){
        return power;
    }
}    
public class Monitor{
    private int price;
    private int power;

    public Monitor(int power, int price){
        this.power = power;
        this.price = price;
    }

    public int getPrice(){
        return price;
    }

    public int getPower(){
        return power;
    }
}    </code></pre>
<p>그리고 Computer 클래스는 이들 클래스의 객체를 부분으로 갖는다. 
<strong>Computer 클래스</strong></p>
<pre><code class="language-java">public class Computer{
    private Body body;
    private Keyboard keyboard;
    private Monitor monitor;

    public void addBody(Body body){
        this.body=body;
    }
    ...
    public int getPrice(){
        int bodyPrice = body.getPrice();
        int keyboardPrice = keyboard.getPrice();
        int monitorPrice = monitor.getMonitor();
        return bodyPrice+keyboardPrice+monitorPrice;
    }
    ...
}</code></pre>
<h2 id="문제점">문제점</h2>
<h3 id="computer-클래스의-부품으로-speaker-또는-mouse-객체를-추가한다면">Computer 클래스의 부품으로 Speaker 또는 Mouse 객체를 추가한다면?</h3>
<p>만약 Speaker 부품을 추가해야 한다고 하면 똑같이 Speaker 클래스를 만들어 사용해야 한다.</p>
<pre><code class="language-java">public class Speaker{
    private int price;
    private int power;

    public Speaker(int power, int price){
        this.power = power;
        this.price = price;
    }

    public int getPrice(){
        return price;
    }

    public int getPower(){
        return power;
    }
}    </code></pre>
<p>Computer 클래스에도 Speaker 객체를 사용할 수 있도록 수정한다.</p>
<pre><code class="language-java">public class Computer{
    private Body body;
    private Keyboard keyboard;
    private Monitor monitor;
    private Speaker speaker;
    ...
}</code></pre>
<p>Speaker 클래스를 지원하려고 Computer 클래스를 수정했지만 이 방식의 설계는 확장성이 좋지 않다. 새로운 부품을 위한 클래스를 Computer 클래스에 추가할 때마다 Computer 클래스의 코드를 수정해야 하기 때문이다. 이는 OCP를 위반한다. </p>
<h2 id="해결책">해결책</h2>
<p>앞선 설계는 Computer 클래스에 속한 부품의 구체적인 객체를 가리키게 되어 OCP를 위반하게 된다. 이를 해결하기 위한 방법은 구체적인 부품들을 일반화한 클래스를 정의하고 이를 Computer 클래스가 가리키게 하는 것이 올바른 설계이다.<img src="https://velog.velcdn.com/images/yh_lee/post/6d0399a1-4393-4164-98ca-ab9a5835626a/image.png" alt="">
Computer가 가질 수 있는 부품들을 일반화해서 ComputerDevice 클래스를 정의했다. 이 클래스는 부품들의 공통 기능만 가지며, ComputerDevice 객체를 실제로 생성할 수는 없다. 그러므로 ComputerDevice는 추상 클래스가 된다. </p>
<p><strong>ComputerDevice와 Keyboard 클래스</strong></p>
<pre><code class="language-java">public abstract class ComputerDevice{
    public abstract int getPrice();
    public abstract int getPower();
}

public class Keyboard extends ComputerDevice{
    private int price;
    private int power;

    public Keyboard(int power, int price){
        this.power=power;
        this.price=price;
    }
    public int getPrice(){
        return price;
    }
    public int getPower(){
        return power;
    }
}</code></pre>
<p>Computer 클래스는 ComputerDevice의 하위 클래스이면서 복수 개의 ComputerDevice를 갖도록 설계했다. </p>
<p><strong>Computer 클래스</strong></p>
<pre><code class="language-java">public class Computer extends ComputerDevice{
    //여러 개의 ComputerDevice 객체를 가리킴
    private List&lt;ComputerDevice&gt; components = new ArrayList&lt;ComputerDevice&gt;();

    //ComputerDevice 객체를 Computer 클래스에 추가함
    public void addComponent(ComputerDevice component){
        components.add(component);
    }

    //ComputerDevice 객체를 Computer 클래스에서 제거함
    public void removeComponent(ComputerDevice component){
        components.remove(component);
    }

    //전체 가격을 포함하는 각 부품의 가격 합산
    public int getPrice(){
        int price=0;
        for(ComputerDevice component:components){
            price+=component.getPrice();
        }
        return price;
    }

    //전체 소비 전력량을 포함하는 각 부품의 소비 전력량을 합산
    public int getPower(){
        int power=0;
        for(ComputerDevice component:components){
            power+=component.getPower();
        }
        return power;
    }    
}</code></pre>
<p><em><strong>addComponent()를 통해 ComputerDevice의 구체적인 부품인 Keyboard, Body, Monitor 객체 등을 Computer 클래스의 부품으로 설정했다.</strong></em></p>
<pre><code class="language-java">public class Client{
    public static void main(String[] args){
        ...
        Computer comp = new Computer();
        comp.addComponent(body);
        comp.addComponent(keyboard);
        comp.addComponent(monitor);
        ...
    }
}    </code></pre>
<p>이제 Computer는 OCP를 준수한다. 즉, Computer 클래스에 새로운 부품 객체를 추가해서 Computer 클래스를 확장하려고 할 때 Computer 클래스의 코드는 변경할 필요가 없다. 일반화된 부품을 의미하는 ComputerDevice 클래스만 이용하기 때문이다. Speaker를 추가하고 싶으면 ComputerDevice의 하위 클래스로 구현하면 된다. </p>
<h2 id="컴퍼지트-패턴composite-pattern">컴퍼지트 패턴(Composite Pattern)</h2>
<p>컴퍼지트 패턴은 부분-전체의 관계를 갖는 객체들을 정의할 때 유용하다. Monitor, Body 등의 객체가 Computer 클래스 전체 객체의 일부분으로 정의되었다. 이런 경우 부분 객체의 추가나 삭제 등이 있어도 전체 객체의 클래스 코드를 변경하지 않으면 컴퍼지트 패턴이 유용하다. 그리고 클라이언트는 전체와 부분을 구분하지 않고 동일한 인터페이스를 사용할 수 있다. <img src="https://velog.velcdn.com/images/yh_lee/post/653660c5-959b-43d2-9387-0e8215d4f497/image.png" alt=""></p>
<h3 id="component">Component</h3>
<p>구체적인 부분이다. 즉, Leaf 클래스와 전체에 해당하는 Composite 클래스에 공통 인터페이스를 정의한다. </p>
<h3 id="leaf">Leaf</h3>
<p>구체적인 부분 클래스로 Composite 객체의 부품으로 설정한다.</p>
<h3 id="composite">Composite</h3>
<p>전체 클래스로 복수 개의 Component를 갖도록 정의한다. 그러므로 복수 개의 Leaf, 또는 복수 개의 Composite 객체를 부분으로 가질 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/08d9866e-bceb-4bc3-ae16-65049c1495a2/image.png" alt=""></p>
<ul>
<li>ComputerDevice 클래스는 Component 역할을 한다.</li>
<li>Keyboard 클래스, Body 클래스, Monitor 클래스는 각각 Leaf 역할을 한다.</li>
<li>Computer 클래스는 Composite 역할을 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[추상 팩토리 패턴]]></title>
            <link>https://velog.io/@yh_lee/%EC%B6%94%EC%83%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@yh_lee/%EC%B6%94%EC%83%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 01 Aug 2022 11:47:45 GMT</pubDate>
            <description><![CDATA[<h1 id="chap13-추상-팩토리-패턴">Chap13 추상 팩토리 패턴</h1>
<h2 id="엘리베이터-부품-업체-변경하기">엘리베이터 부품 업체 변경하기</h2>
<p>엘리베이터를 구성하는 부품 중에 모터와 문을 생각해보자. 예시로 LG는 LG 모터와 LG 문을 제공하고 현대는 현대모터와 현대 문을 제공한다고 하자. 두 종류의 모터와 문은 구체적인 제어 방식은 다르지만 엘리베이터 입장에서는 기능적인 부분에선 동일하다. 그러므로 추상 클래스로 Motor를 정의하고 하위 클래스로 LGMotor, HyundaiMotor를 하위 클래스로 정의할 수 있다. LGDoor, HyundaiDoor를 Door 클래스의 하위 클래스로 정의할 수 있다.<img src="https://velog.velcdn.com/images/yh_lee/post/0b42a6fe-cecf-444d-bcf9-610c20bcad75/image.png" alt=""></p>
<p>Motor의 핵심 기능인 이동은 move() 메서드로 정의한다. </p>
<pre><code class="language-java">public void move(Direction direction){
    //1. 이미 이동 중이면 무시
    //2. 만약 문이 열려 있으면 닫음
    //3. 모터를 구동해서 이동
    //4. 모터 상태를 이동 중으로 설정
}    </code></pre>
<p>이러한 4단계의 move 동작은 LGMotor, HyundaiMotor 클래스 모두 동일하다. 하지만 &quot;3.모터를 구동해서 이동&quot; 부분만 달라진다. 일반적인 흐름을 동일하지만 특정 부분만 다른 동작을 하는 경우에 일반적인 기능을 상위 클래스에 템플릿 메서드로 설계할 수 있다. </p>
<p>Door 클래스에서도 open과 close 메서드 각각에 템플릿 메서드 패턴을 적용할 수 있다. 그리고 문을 닫는 부분만 하위 클래스에서 구현하도록 한다.</p>
<pre><code class="language-java">public abstract class Door{
    private DoorStatus doorStatus;
    publi Door(){
        doorStatus=DoorStatus.CLOSED;
    }

    public void close(){//템플릿 메서드 
        if(doorStatus==DoorStatus.CLOSED)
            return;

        doClose();//하위 클래스에서 오버라이드
        doorStatus=DoorStatus.CLOSED;
    }
    protected abstract void doClose();//hook or primitive method

    public void open(){//템플릿 메서드
        if(doorStatus==DoorStatus.OPENED)
            return;

        doOpen();//하위 클래스에서 오버라이드
        doorStatus=DoorStatus.OPENED;
    }
    protected abstract void doOpen();//hook or primitive method
}

public class LGDoor extends Door{
    protected void doClose(){
        System.out.println(&quot;close LG Door&quot;);
    }
    protected void doOpen(){
        System.out.println(&quot;open LG Door&quot;);
    }    
}
public class HyundaiDoor extends Door{
    ...
}</code></pre>
<p>엘리베이터 입장에서 모터와 문을 제어하는 클래스가 필요하다. 이 경우에도 팩토리 메서드 패턴을 적용할 수 있따. 즉, MotorFactory 클래스를 정의해서 LGMotor와 HyundaiMotor 중에서 특정 제조 업체에 따라 해당 Motor 객체를 생성할 수 있다.<img src="https://velog.velcdn.com/images/yh_lee/post/a1c52718-8ae9-45e8-8ecf-2230fb06f47b/image.png" alt=""></p>
<pre><code class="language-java">public enum VenderId{LG, HYUNDAI}

public class MotorFactory{
    public static Motor createMotr(VenderID venderID){
        Motor motor = null;
        switch(venderID){
            case LG:
              motor=new LGMotor();
              break;
            case HYUNDAI:
              motor=new HyundaiMotor();
              break;
        }    
        return motor;
    }
}</code></pre>
<h2 id="문제점">문제점</h2>
<h3 id="현재-사용하는-lg-대신-현대-부품을-사용해야-한다면">현재 사용하는 LG 대신 현대 부품을 사용해야 한다면?</h3>
<p>MotorFactory와 DoorFactory 클래스를 이용해서 이미 정의된 HyundaiMotor 객체와 HyundaiDoor 객체를 생성하도록 프로그램을 수정한다.</p>
<pre><code class="language-java">public class Client{
    public static void main(String[] args){
        Door hyundaiDoor = DoorFactory.createDoor(VendorID.HYUNDAI);
        Motor hyundaiMotor=MotorFactory.createMotor(VendorID.YUNDAI);
        hyundaiMotor.setDoor(hyundaiDoor);

        hyundaiDoor.open();
        hyundaiMotor.move(Direction.UP);
    }
}</code></pre>
<p>이런 구조라면 다른 부품을 추가할 때마다 각 Factory 클래스를 구현하고 이들의 Factory 객체를 각각 생성해야 한다. 코드의 길이가 길어지고 복잡해질 수 있다. 그리고 코드를 수정하는 것이 쉽지 않다.</p>
<h3 id="새로운-제조-업체인-삼성의-부품을-지원해야-한다면">새로운 제조 업체인 삼성의 부품을 지원해야 한다면?</h3>
<p>만약 삼성 부품을 사용하고 싶으면 MotorFactory와 DoorFactory 클래스가 SamsungMotor와 SamsungDoor 클래스를 지원할 수 있도록 수정해야 한다. </p>
<p>결론적으로 발생하는 문제점은 기존의 팩토리 메서드 패턴을 이용한 객체 생성 때문에 여러 개의 객체를 일관성 있는 방식으로 생성하는 경우에 많은 코드 변경이 발생하게 된다.</p>
<h2 id="해결책">해결책</h2>
<p>MotorFactory, DoorFactory 클래스와 같이 부품별로 Factory 클래스를 만드는 대신 LGElevatorFactory나 HyundaiElevatorFactory 클래스와 같이 제조 업체별로 Factory 클래스를 만들 수 있다.</p>
<pre><code class="language-java">public abstract class ElevatorFactory{
    public abstract Motor createMotor();
    public abstract Door createDoor();
}

public class LGElevatorFactory extends ElevatorFactory{
    public Motor createMotor(){
        return new LGMotor();
    }
    public Door createDoor(){
        return new LGDoor();
    }
}

public class HyundaiElevatorFactory extends ElevatorFactory{
    public Motor createMotor(){
        return new HyundaiMotor();
    }
    public Door createDoor(){
        return new HyundaiDoor();
    }
}</code></pre>
<p>제조 업체별로 Factory 클래스를 정의했으므로 부품 객체를 간단하게 생성할 수 있다.</p>
<h2 id="추상-팩토리-패턴abstract-factory-pattern">추상 팩토리 패턴(Abstract Factory Pattern)</h2>
<p>추상 팩토리 패턴은 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성하는 경우에 유용하다. 부품별로 Factory를 정의하는 대신 관련 객체들을 일관성 있게 생성할 수 있도록 Factory 클래스를 정의하는 것이 효과적이다. MotorFactory, DoorFactory 클래스를 정의하는 것 대신 LG 부품들을 위한 LGFactory, 현대 부품들을 위한 HyundaiFactory를 정의하는 것이 바람직하다.</p>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/435a93c4-a89f-491a-8fac-fc3849776ab7/image.png" alt=""></p>
<h3 id="abstractfactory">AbstractFactory</h3>
<p>실제 팩토리 클래스의 공통 인터페이스이다. 각 제품의 부품을 생성하는 기능을 추상 메서드로 정의한다.</p>
<h3 id="concretefactory">ConcreteFactory</h3>
<p>구체적인 팩토리 클래스로 AbstractFactory 클래스의 추상 메서드를 오버라이드함으로써 구체적인 제품을 생성한다.</p>
<h3 id="abstractproduct">AbstractProduct</h3>
<p>제품의 공통 인터페이스</p>
<h3 id="concreteproduct">ConcreteProduct</h3>
<p>구체적인 팩토리 클래스에서 생성되는 구체적인 제품</p>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/3427d1a9-7bb6-4cdb-b93a-297f77cf9315/image.png" alt=""></p>
<ul>
<li>ElevatorFactory 클래스는 AbstractFactory 역할을 한다.</li>
<li>LGElevatorFactory, HyundaiElevatorFactory 클래스는 ConcreteFactory 역할을 한다.</li>
<li>Motor 클래스는 AbstractProductA 역할을 한다.</li>
<li>LGMotor 클래스와 HyundaiMotor 클래스는 ConcreteProductA 역할을 한다.</li>
<li>Door 클래스는 AbstractProductB 역할을 한다.</li>
<li>LGDoor 클래스와 HyundaiDoor 클래스는 ConcreteProductB 역할을 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[팩토리 메서드 패턴]]></title>
            <link>https://velog.io/@yh_lee/%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@yh_lee/%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 01 Aug 2022 11:47:30 GMT</pubDate>
            <description><![CDATA[<h1 id="chap12-팩토리-메서드-패턴">Chap12 팩토리 메서드 패턴</h1>
<h2 id="여러-가지-방식의-엘리베이터-스케줄링-방법-지원하기">여러 가지 방식의 엘리베이터 스케줄링 방법 지원하기</h2>
<p>여러 대의 엘리베이터가 있는 경우를 생각해보자. 사용자가 엘리베이터 외부에서 버튼(FloorButton)을 누른 경우에는 여러 대의 엘리베이터 중 하나를 선택해 이동시켜야 한다. 이와 같이 주어진 요청(목적지 층과 방향)을 받았을 때 여러 대의 엘리베이터 중 하나를 선택하는 것을 &#39;스케줄링&#39;이라고 한다. 
<img src="https://velog.velcdn.com/images/yh_lee/post/6760f99c-8963-4649-914f-dabab503bb66/image.png" alt=""></p>
<p>ElevatorManager 클래스는 이동 요청을 처리하는 클래스로 엘리베이터를 스케줄링하기 위한 ThroughputScheduler 객체를 갖는다. 그리고 각 엘리베이터의 이동을 책임지는 ElevatorController 객체를 복수 개 갖는다. ElevatorManager 클래스의 requestElevator 메서드는 요청을 받았을 때 우선 ThroughputScheduler의 selectElevator 메서드를 호출해 엘리베이터를 선택한다. 그리고 ElevatorController 객체의 gotoFloor 메서드를 호출해 엘리베이터를 이동시킨다. </p>
<pre><code class="language-java">public class ElevatorManager{
    private List&lt;ElevatorController&gt; controllers;
    private ThroughputScheduler throughputScheduler;

    //주어진 수만큼 ElevatorController를 생성
    public ElevatorManager(int controllerCount){
        controllers = new ArrayList&lt;ElevatorController&gt;(controllerCount);
        for (int i=0;i&lt;controllerCount;i++){
            ElevatorController controller = new ElevatorController(1);
            controllers.add(controller);
        }    
        scheduler = new ThroughputScheduler();
    }

    void requestElevator(int destination, Direction direction){
        //ThroughputScheduler를 이용해 엘리베이터 선택
        int selectedElevator = scheduler.selectElevator(this, distination, direction);

        //선택된 엘리베이터 이동시킴
        controllers.get(selectedElevator).gotoFloor(destination);
    }
}

public class ElvatorController{
    private int id;
    private int curFloor;

    public ElevatorController(int id){
        this.id=id;
        curFloor=1;
    }    

    public void gotoFloor(int destination){
        System.out.println(&quot;Elevator [&quot;+id+&quot;] Floor: &quot;+curFloor);

        //목적지 층으로 엘리베이터 이동
        curFloor = destination;
        System.out.println(&quot;==&gt;&quot;+curFloor);
    }
}

public ThroughputScheduler{
    public int selectElevator(ElevatorManager manager, int destination, Direction direction){
        return 0;//임의로 선택
    }
}</code></pre>
<h2 id="문제점">문제점</h2>
<h3 id="현재-throughputscheduler-전략을-사용한다-만약-다른-스케줄-전략을-사용해야-한다면">현재 ThroughputScheduler 전략을 사용한다. 만약 다른 스케줄 전략을 사용해야 한다면?</h3>
<p>예를 들어 사용자의 대기 시간을 최소화하는 엘리베이터 선택 전략 사용해야 한다면? 실행 중에 스케줄링 전략을 변경, 즉 동적 스케줄링을 지원해야 한다면?</p>
<p>동적 스케줄링을 지원하도록 ElevatorManager 클래스를 수정한다.</p>
<pre><code class="language-java">publi class ElevatorManger{
    ...
    void requestElevator(int destination, Direction direction){
        ...
        if(hour&lt;12)//오전에는 ResponseTimeScheduler를 이용함
            scheduler=new ResponseTimeScheduler();
        else//오후에는 ThroughputScheduler를 이용함
            scheduler=new ThroughputScheduler();
        ...
    }
}</code></pre>
<p>ThroughputScheduler, ResponseTimeScheduler 객체를 생성 후 이를 각각의 클래스가 아니라 ElevatorScheduler라는 인터페이스를 이용한다. 이는 결과적으로 스트래티지 패턴을 활용해 엘리베이터 스케줄링 전략을 설계한 것이다. </p>
<p>즉, ElevatorScheduler 인터페이스는 AbstractStrategy에 해당되고 ThroughputScheduler와 ResponseTimeScheduler 클래스는 ConcreteStrategy에 해당된다. </p>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/43214a9f-b752-44b5-98ae-a6c6e4ad8bdd/image.png" alt=""></p>
<p>엘리베이터 스케줄링 전략이 추가되거나 동적 스케줄링 방식으로 전략을 선택하도록 변경되면, 해당 스케줄링 전략을 지원하는 구체적인 클래스를 생성해야 할 뿐만 아니라 requestElevator 메서드도 수정할 수밖에 없다. 그런데 requestElevator 메서드는 엘리베이터 선택과 선택된 엘리베이터를 이동시키는 것이 근본 책임이다. 따라서 엘리베이터를 선택하는 전략의 변경에 따라 requestElevator가 변경되는 것은 바람직하지 않다. </p>
<h2 id="해결책">해결책</h2>
<p>주어진 기능을 실제로 제공하는 적절한 클래스 생성 작업을 별도의 클래스/메서드로 분리시키는 것이 좋다. ElevatorManager 클래스가 지금처럼 직접 ThroughputScheduler 객체와 ResponseTimeScheduler 객체를 생성하는 대신 SchedulerFactory 클래스가 이들 객체를 생성하도록 설계를 변경한다.<img src="https://velog.velcdn.com/images/yh_lee/post/57fe64d9-8eb2-484e-a1ae-576afd121caa/image.png" alt=""></p>
<h2 id="팩토리-메서드-패턴factory-method-pattern">팩토리 메서드 패턴(Factory Method Pattern)</h2>
<p>팩토리 메서드 패턴은 객체의 생성 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성의 변화에 대비하는데 유용하다. <img src="https://velog.velcdn.com/images/yh_lee/post/b763716a-6a37-4ce3-bc59-92e2a1b065f5/image.png" alt="">
왼쪽 그림에서는 A,Z 클래스가 필요에 따라 X1,X2 객체를 생성해 사용한다. 만약 X1,X2 생성 방식이 달라지거나 X3같은 새로운 객체를 생성해야 하는 경우에는 X1,X2를 생성하는 모든 코드 부분을 변경해야 한다.</p>
<p>오른쪽 그림 같이 팩토리 메서드 패턴을 사용하면 객체 생성 기능을 제공하는 Factory 클래스를 정의하고 이를 활용하는 방식으로 설계하면 된다. 이렇게 설계하면 변경이 필요할 때 Factory 클래스만 변경하고 A,Z 클래스는 변경이 필요 없다. 
<img src="https://velog.velcdn.com/images/yh_lee/post/5ae51353-f566-42ce-a05f-36dcded7e1f7/image.png" alt=""></p>
<h3 id="product">Product</h3>
<p>팩토리 메서드로 생성될 객체의 공통 인터페이스</p>
<h3 id="concreteproduct">ConcreteProduct</h3>
<p>구체적으로 객체가 생성되는 클래스</p>
<h3 id="creater">Creater</h3>
<p>팩토리 메서드를 갖는 클래스</p>
<h3 id="concretecreator">ConcreteCreator</h3>
<p>팩토리 메서드를 구현하는 클래스로 ConcreteProduct 객체를 생성한다.</p>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/c5e4a92f-6140-4d59-8ddf-1548e53fc7f3/image.png" alt=""></p>
<ul>
<li>ElevatorScheduler 인터페이스는 Product 역할을 한다.</li>
<li>ResponseTimerScheduler 클래스와 ThroughputScheduler 클래스는 ConcreteProduct 역할을 한다.</li>
<li>ElevatorManager 클래스는 Creater 역할을 한다.</li>
<li>ElevatorManagerWithThroughputScheduling, ElevatorManagerWithDynamicScheduling, ElevatorManagerWithResponseTimeScheduling 클래스는 ConcreteCreator 역할을 한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Thymeleaf - 기본 기능]]></title>
            <link>https://velog.io/@yh_lee/Thymeleaf-%EA%B8%B0%EB%B3%B8-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@yh_lee/Thymeleaf-%EA%B8%B0%EB%B3%B8-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Wed, 27 Jul 2022 14:13:10 GMT</pubDate>
            <description><![CDATA[<h1 id="thymeleaf">Thymeleaf</h1>
<ol>
<li>서버사이드 HTML 렌더링(SSR)</li>
</ol>
<ul>
<li>타임리프는 백엔드 서버에서 HTML을 동적으로 렌더링 하는 용도</li>
</ul>
<ol start="2">
<li>네츄럴템플릿</li>
</ol>
<ul>
<li>타임리프는 순수 HTML을 최대한 유지하는 특징이 있다. </li>
<li>타임리프로 작성한 파일은 HTML을 유지하기 때문에 웹 브라우저에서 파일을 직접 열어도 내용물을 확인할 수 있고, 서버를 통해 뷰템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다.</li>
<li>JSP를 포함한 다른 뷰 템플릿은 파일을 열면 JSP와 HTML이 섞여 정상적인 HTML을 확인할 수 없다. 오직 서버를 통해 JSP가 렌더링 되고 HTML 응답 결과를 받아야 화면을 확인할 수 있다.</li>
<li>순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿이라 한다.</li>
</ul>
<ol start="3">
<li>스프링 통합 지원</li>
</ol>
<ul>
<li>타임리프는 스프링과 자연스럽게 통합되고, 스프링의 다양한 기능을 편리하게 사용할 수 있게 지원한다.</li>
</ul>
<h2 id="텍스트---text-utext">텍스트 - text, utext</h2>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/basic&quot;)
public class BasicController {
    @GetMapping(&quot;text-basic&quot;)
    public String textBasic(Model model) {
        model.addAttribute(&quot;data&quot;, &quot;hello spring!&quot;);
        return &quot;basic/text-basic&quot;;
    }
}</code></pre>
<pre><code class="language-html">&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;title&gt;Title&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;컨텐츠에 데이터 출력하기&lt;/h1&gt;
&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;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/3026cfaa-86a3-4422-801e-d8914daa7225/image.png" alt=""></p>
<h3 id="escape">Escape</h3>
<p>&#39;&lt;&#39;, &#39;&gt;&#39;같은 부분을 모델에 담아서 넘기면 &#39;&amp;lt&#39;로 변경되어 나온다. </p>
<pre><code class="language-java">@Controller
@RequestMapping(&quot;/basic&quot;)
public class BasicController {
    @GetMapping(&quot;text-basic&quot;)
    public String textBasic(Model model) {
        model.addAttribute(&quot;data&quot;, &quot;&lt;b&gt;hello spring!&lt;/b&gt;&quot;);
        return &quot;basic/text-basic&quot;;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/692402a8-2835-4f64-a416-4f4ff822bf4c/image.png" alt=""></p>
<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;th:utext = &lt;span th:utext=&quot;${data}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;span th:inline=&quot;none&quot;&gt;[[...]] vs [(...)]&lt;/span&gt;&lt;/h1&gt;
&lt;ul&gt;
    &lt;li&gt;&lt;span th:inline=&quot;none&quot;&gt;[[...]] = &lt;/span&gt;[[${data}]]&lt;/li&gt;
    &lt;li&gt;&lt;span th:inline=&quot;none&quot;&gt;[(...)] = &lt;/span&gt;[(${data})]&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/cd7c13fe-6492-4beb-bbba-ba835e12123b/image.png" alt=""></p>
<h2 id="변수---springel">변수 - SpringEL</h2>
<pre><code class="language-java">@GetMapping(&quot;/variable&quot;)
public String variable(Model model) {
    User userA = new User(&quot;kim&quot;, 20);
    User userB = new User(&quot;Lee&quot;, 25);

    ArrayList&lt;User&gt; list = new ArrayList&lt;&gt;();
    list.add(userA);
    list.add(userB);

    Map&lt;String, User&gt; map = new HashMap&lt;&gt;();
    map.put(&quot;userA&quot;, userA);
    map.put(&quot;userB&quot;, userB);

    model.addAttribute(&quot;user&quot;, userA);
    model.addAttribute(&quot;users&quot;, list);
    model.addAttribute(&quot;userMap&quot;, map);

    return &quot;basic/variable&quot;;
}

@Data
static class User{
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }
}</code></pre>
<pre><code class="language-html">&lt;h1&gt;SpringEL 표현식&lt;/h1&gt; &lt;ul&gt;Object
    &lt;li&gt;${user.username} =    &lt;span th:text=&quot;${user.username}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${user[&#39;username&#39;]} = &lt;span th:text=&quot;${user[&#39;username&#39;]}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${user.getUsername()} = &lt;span th:text=&quot;${user.getUsername()}&quot;&gt;&lt;/span&gt;&lt;/
    li&gt;
&lt;/ul&gt;
&lt;ul&gt;List
    &lt;li&gt;${users[0].username}    = &lt;span th:text=&quot;${users[0].username}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${users[0][&#39;username&#39;]} = &lt;span th:text=&quot;${users[0][&#39;username&#39;]}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${users[0].getUsername()} = &lt;span th:text=&quot;${users[0].getUsername()}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;Map
    &lt;li&gt;${userMap[&#39;userA&#39;].username} =  &lt;span th:text=&quot;${userMap[&#39;userA&#39;].username}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${userMap[&#39;userA&#39;][&#39;username&#39;]} = &lt;span th:text=&quot;${userMap[&#39;userA&#39;][&#39;username&#39;]}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${userMap[&#39;userA&#39;].getUsername()} = &lt;span th:text=&quot;${userMap[&#39;userA&#39;].getUsername()}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/c93772c2-43c8-4165-ba8c-6330d797b636/image.png" alt=""></p>
<h2 id="기본-객체들">기본 객체들</h2>
<p>Session: 유저가 웹브라우저를 종료하기 전까지 세션이 유지하게 함
<img src="https://velog.velcdn.com/images/yh_lee/post/ee69756e-c2b4-4f3f-b309-e1a8e07997b4/image.png" alt=""></p>
<h3 id="기본객체">기본객체</h3>
<ul>
<li>${#request}</li>
<li>${#response}</li>
<li>${#session} </li>
<li>${#servletContext} </li>
<li>${#locale}<h3 id="편의객체">편의객체</h3>
</li>
<li>HTTP 요청 파라미터 접근: param 
예) ${param.paramData}</li>
<li>HTTP 세션 접근: session
예) ${session.sessionData}</li>
<li>스프링 빈 접근: @
예) ${@helloBean.hello(&#39;Spring!&#39;)}</li>
</ul>
<h2 id="유틸리티-객체와-날짜">유틸리티 객체와 날짜</h2>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/79b96224-2151-4a35-b814-7a8a48b25898/image.png" alt=""></p>
<pre><code class="language-html">&lt;h1&gt;LocalDateTime&lt;/h1&gt;
&lt;ul&gt;
    &lt;li&gt;default = &lt;span th:text=&quot;${localDateTime}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;yyyy-MM-dd HH:mm:ss = &lt;span th:text=&quot;${#temporals.format(localDateTime,&#39;yyyy-MM-dd HH:mm:ss&#39;)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;LocalDateTime - Utils&lt;/h1&gt;
&lt;ul&gt;
    &lt;li&gt;${#temporals.day(localDateTime)} = &lt;span th:text=&quot;${#temporals.day(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.month(localDateTime)} = &lt;span th:text=&quot;${#temporals.month(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.monthName(localDateTime)} = &lt;span th:text=&quot;${#temporals.monthName(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.monthNameShort(localDateTime)} = &lt;span th:text=&quot;${#temporals.monthNameShort(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.year(localDateTime)} = &lt;span th:text=&quot;${#temporals.year(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.dayOfWeek(localDateTime)} = &lt;span th:text=&quot;${#temporals.dayOfWeek(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.dayOfWeekName(localDateTime)} = &lt;span th:text=&quot;${#temporals.dayOfWeekName(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.dayOfWeekNameShort(localDateTime)} = &lt;span th:text=&quot;${#temporals.dayOfWeekNameShort(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.hour(localDateTime)} = &lt;span th:text=&quot;${#temporals.hour(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.minute(localDateTime)} = &lt;span th:text=&quot;${#temporals.minute(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.second(localDateTime)} = &lt;span th:text=&quot;${#temporals.second(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
    &lt;li&gt;${#temporals.nanosecond(localDateTime)} = &lt;span th:text=&quot;${#temporals.nanosecond(localDateTime)}&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</code></pre>
<h2 id="url-링크">URL 링크</h2>
<pre><code class="language-html">&lt;h1&gt;URL 링크&lt;/h1&gt;
&lt;ul&gt;
    &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;
&lt;/ul&gt;</code></pre>
<pre><code>http://localhost:8080/hello
http://localhost:8080/hello?param1=data1&amp;param2=data2
http://localhost:8080/hello/data1/data2
http://localhost:8080/hello/data1?param2=data2</code></pre><h2 id="리터럴">리터럴</h2>
<p>리터럴을 소스 코드 상에서 고정된 값을 말하는 용어다. </p>
<ul>
<li>문자: &#39;hello&#39;</li>
<li>숫자: &#39;10&#39;</li>
<li>불린: &#39;true&#39;,&#39;false&#39;</li>
<li>null: &#39;null&#39;</li>
</ul>
<p><strong>타임리프에서 문자 리터럴은 항상 &#39;&#39;(작은 따옴표)로 감싸야 한다.</strong></p>
<pre><code>&lt;span th:text=&quot;&#39;hello&#39;&quot;&gt;</code></pre><p>그런데문자를항상 &#39;로감싸는것은너무귀찮은일이다.공백없이쭉이어진다면하나의의미있는 토큰으로 인지해서 다음과 같이 작은 따옴표를 생략할 수 있다.
룰: A-Z, a-z, 0-9, [], ., -, _</p>
<pre><code>&lt;span th:text=&quot;hello&quot;&gt;</code></pre><h4 id="주의">주의</h4>
<p><span th:text="hello world!"></span>
문자 리터럴은 원칙상 &#39; 로 감싸야 한다. 중간에 공백이 있어서 하나의 의미있는 토큰으로도 인식되지 않는다.</p>
<pre><code class="language-html">&lt;!--주의! 다음 주석을 풀면 예외가 발생함--&gt;
&lt;!--    &lt;li&gt;&quot;hello world!&quot; = &lt;span th:text=&quot;hello world!&quot;&gt;&lt;/span&gt;&lt;/li&gt;--&gt;
&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>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/504a81e6-6093-42b9-b3d8-f47c7436bce6/image.png" alt=""></p>
<h2 id="연산">연산</h2>
<pre><code class="language-html">&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;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/ddec1aca-4fa8-4916-9d4c-e87f36f13df0/image.png" alt=""></p>
<ul>
<li>비교연산: HTML 엔티티를 사용해야 하는 부분을 주의하자,
&gt; (gt), &lt; (lt), &gt;= (ge), &lt;= (le), ! (not), == (eq), != (neq, ne)</li>
<li>조건식: 자바의 조건식과 유사하다.</li>
<li>Elvis 연산자: 조건식의 편의 버전</li>
<li>No-Operation: _ 인 경우 마치 타임리프가 실행되지 않는 것 처럼 동작한다. 이것을 잘 사용하면 HTML 의 내용 그대로 활용할 수 있다. 마지막 예를 보면 데이터가 없습니다. 부분이 그대로 출력된다.</li>
</ul>
<h2 id="속성-값-설정">속성 값 설정</h2>
<pre><code class="language-html">&lt;h1&gt;속성 설정&lt;/h1&gt;
&lt;input type=&quot;text&quot; name=&quot;mock&quot; th:name=&quot;userA&quot; /&gt;
&lt;h1&gt;속성 추가&lt;/h1&gt;
- 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;
&lt;h1&gt;checked 처리&lt;/h1&gt;
- checked o &lt;input type=&quot;checkbox&quot; name=&quot;active&quot; th:checked=&quot;true&quot; /&gt;&lt;br/&gt;
- checked x &lt;input type=&quot;checkbox&quot; name=&quot;active&quot; th:checked=&quot;false&quot; /&gt;&lt;br/&gt;
- checked=false &lt;input type=&quot;checkbox&quot; name=&quot;active&quot; checked=&quot;false&quot; /&gt;&lt;br/&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/d223b77d-3efd-4a19-b280-775a48f61b86/image.png" alt=""><img src="https://velog.velcdn.com/images/yh_lee/post/b6d27a0e-5b27-493d-95d9-b8699bb3f855/image.png" alt=""></p>
<p>checked 처리
HTML에서는 &lt;input type=&quot;checkbox&quot; name=&quot;active&quot; checked=&quot;false&quot; /&gt; checked 속성이 있기 때문에 checked 처리가 되어버린다. HTML에서 checked 속성은 checked 속성의 값과 상관없이 checked 라는 속성만 있어도 체크가 된다. 이런 부분이 true , false 값을 주로 사용하는 개발자 입장에서는 불편하다.</p>
<p>타임리프의 th:checked 는 값이 false 인 경우 checked 속성 자체를 제거한다. </p>
<pre><code class="language-html">&lt;input type=&quot;checkbox&quot; name=&quot;active&quot; th:checked=&quot;false&quot; /&gt;</code></pre>
<p>타임리프 렌더링 후 </p>
<pre><code>&lt;input type=&quot;checkbox&quot; name=&quot;active&quot; /&gt;</code></pre><h2 id="반복">반복</h2>
<p>타임리프에서 반복은 th:each를 사용한다.</p>
<pre><code class="language-html">&lt;tr th:each=&quot;user : ${users}&quot;&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;/tr&gt;</code></pre>
<pre><code class="language-html">&lt;tr&gt;
    &lt;th&gt;count&lt;/th&gt;
    &lt;th&gt;username&lt;/th&gt;
    &lt;th&gt;age&lt;/th&gt;
    &lt;th&gt;etc&lt;/th&gt;
&lt;/tr&gt;
&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>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/4b7a35ce-d690-4b4f-8a99-89c8c50ec9de/image.png" alt=""></p>
<p>반복 상태 유지</p>
<ul>
<li>반복의 두번째 파라미터를 설정해서 반복의 상태를 확인 할 수 있다.</li>
<li>두번째 파라미터는 생략 가능한데, 생략하면 지정한 변수명( user ) + Stat 가 된다. 여기서는 user + Stat = userStat 이므로 생략 가능하다.</li>
</ul>
<p>반복 상태 유지 기능</p>
<ul>
<li>index : 0부터 시작하는 값</li>
<li>count : 1부터 시작하는 값</li>
<li>size : 전체 사이즈</li>
<li>even , odd : 홀수, 짝수 여부( boolean ) </li>
<li>first , last :처음, 마지막 여부( boolean ) </li>
<li>current : 현재 객체</li>
</ul>
<h2 id="조건부-평가">조건부 평가</h2>
<pre><code class="language-html">&lt;h1&gt;if, unless&lt;/h1&gt;
&lt;table border=&quot;1&quot;&gt;
    &lt;tr&gt;
        &lt;th&gt;count&lt;/th&gt;
        &lt;th&gt;username&lt;/th&gt;
        &lt;th&gt;age&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr th:each=&quot;user, userStat : ${users}&quot;&gt;
        &lt;td th:text=&quot;${userStat.count}&quot;&gt;1&lt;/td&gt;
        &lt;td th:text=&quot;${user.username}&quot;&gt;username&lt;/td&gt;
        &lt;td&gt;
            &lt;span th:text=&quot;${user.age}&quot;&gt;0&lt;/span&gt;
            &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&gt; &lt;/tr&gt;
&lt;/table&gt;
&lt;h1&gt;switch&lt;/h1&gt;
&lt;table border=&quot;1&quot;&gt;
    &lt;tr&gt;
        &lt;th&gt;count&lt;/th&gt;
        &lt;th&gt;username&lt;/th&gt;
        &lt;th&gt;age&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr th:each=&quot;user, userStat : ${users}&quot;&gt;
        &lt;td th:text=&quot;${userStat.count}&quot;&gt;1&lt;/td&gt;
        &lt;td th:text=&quot;${user.username}&quot;&gt;username&lt;/td&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; &lt;/tr&gt;
</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/f28055c3-0816-473c-ace9-3e3d1d12b1f7/image.png" alt=""></p>
<h2 id="주석">주석</h2>
<pre><code class="language-html">&lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
&lt;h1&gt;1. 표준 HTML 주석&lt;/h1&gt;
&lt;!--
&lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
 --&gt;
&lt;h1&gt;2. 타임리프 파서 주석&lt;/h1&gt; &lt;!--/* [[${data}]] */--&gt;
&lt;!--/*--&gt;
&lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt;
&lt;!--*/--&gt;
&lt;h1&gt;3. 타임리프 프로토타입 주석&lt;/h1&gt;
&lt;!--/*/
&lt;span th:text=&quot;${data}&quot;&gt;html data&lt;/span&gt; /*/--&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/d5b14a3f-90bc-4e08-89fc-92526dd1389b/image.png" alt=""><img src="https://velog.velcdn.com/images/yh_lee/post/9ce07f25-559c-4b6e-8e3d-3e601f9ddb17/image.png" alt=""></p>
<ol>
<li>표준 HTML 주석
자바스크립트의 표준 HTML 주석은 타임리프가 렌더링 하지 않고, 그대로 남겨둔다.</li>
<li>타임리프 파서 주석
타임리프 파서 주석은 타임리프의 진짜 주석이다. 렌더링에서 주석 부분을 제거한다.</li>
<li>타임리프 프로토타입 주석
타임리프 프로토타입은 약간 특이한데, HTML 주석에 약간의 구문을 더했다.
HTML 파일을 웹 브라우저에서 그대로 열어보면 HTML 주석이기 때문에 이 부분이 웹 브라우저가 렌더링하지 않는다. 타임리프 렌더링을 거치면 이 부분이 정상 렌더링 된다. HTML 파일을 그대로 열어보면 주석처리가 되지만, 타임리프를 렌더링 한 경우에만 보이는 기능이다.</li>
</ol>
<h2 id="블록">블록</h2>
<p>ht:block은 HTML 태그가 아닌 타임리프의 유일한 자체 태그이다.</p>
<p>유저당 하나씩 요약을 반복하기 위해</p>
<pre><code class="language-html">&lt;th:block th:each=&quot;user : ${users}&quot;&gt;
    &lt;div&gt;
        사용자 이름1 &lt;span th:text=&quot;${user.username}&quot;&gt;&lt;/span&gt;
        사용자 나이1 &lt;span th:text=&quot;${user.age}&quot;&gt;&lt;/span&gt; &lt;/div&gt;
    &lt;div&gt;
        요약 &lt;span th:text=&quot;${user.username} + &#39; / &#39; + ${user.age}&quot;&gt;&lt;/span&gt;
    &lt;/div&gt;
&lt;/th:block&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/6089dfdf-d5dd-472e-b53e-ea2724615a5c/image.png" alt=""><img src="https://velog.velcdn.com/images/yh_lee/post/b8b0c3fe-8b5d-49bf-8b03-6581132abc1c/image.png" alt=""></p>
<h2 id="자바스크립트-인라인">자바스크립트 인라인</h2>
<pre><code class="language-html">&lt;!-- 자바스크립트 인라인 사용 전 --&gt;
&lt;script&gt;
    var username = [[${user.username}]];
    var age = [[${user.age}]];
    //자바스크립트 내추럴 템플릿
    var username2 = /*[[${user.username}]]*/ &quot;test username&quot;;
    //객체
    var user = [[${user}]];
&lt;/script&gt;
&lt;!-- 자바스크립트 인라인 사용 후 --&gt;
&lt;script th:inline=&quot;javascript&quot;&gt;
    var username = [[${user.username}]];
    var age = [[${user.age}]];
    //자바스크립트 내추럴 템플릿
    var username2 = /*[[${user.username}]]*/ &quot;test username&quot;;
    //객체
    var user = [[${user}]];
&lt;/script&gt;</code></pre>
<h4 id="자바스크립트-인라인-사용-전---결과">자바스크립트 인라인 사용 전 - 결과</h4>
<pre><code>&lt;script&gt;
  var username = userA;
  var age = 10;
  //자바스크립트 내추럴 템플릿
  var username2 = /*userA*/ &quot;test username&quot;;
  //객체
  var user = BasicController.User(username=userA, age=10);
&lt;/script&gt;
</code></pre><h4 id="자바스크립트-인라인-사용-후---결과">자바스크립트 인라인 사용 후 - 결과</h4>
<pre><code>&lt;script&gt;
  var username = &quot;userA&quot;;
   var age = 10; //자바스크립트 내추럴 템플릿
  var username2 = &quot;userA&quot;; //객체
  var user = {&quot;username&quot;:&quot;userA&quot;,&quot;age&quot;:10};
&lt;/script&gt;</code></pre><h3 id="each">each</h3>
<pre><code>&lt;!-- 자바스크립트 인라인 each --&gt; &lt;script th:inline=&quot;javascript&quot;&gt;
      [# th:each=&quot;user, stat : ${users}&quot;]
      var user[[${stat.count}]] = [[${user}]];
      [/]
&lt;/script&gt;</code></pre><h4 id="자바스크립트-인라인-each-결과">자바스크립트 인라인 each 결과</h4>
<pre><code>&lt;script&gt;
  var user1 = {&quot;username&quot;:&quot;userA&quot;,&quot;age&quot;:10};
  var user2 = {&quot;username&quot;:&quot;userB&quot;,&quot;age&quot;:20};
  var user3 = {&quot;username&quot;:&quot;userC&quot;,&quot;age&quot;:30};
&lt;/script&gt;</code></pre><h2 id="템플릿-조각">템플릿 조각</h2>
<p>웹 페이지를 개발할 때는 공통 영역이 많다. 상단 영역, 하단 영역 등 여러 페이지들에서 함께 사용하는 영역들이 있다. 타임리프를 이를 위해 템플릿 조각과 레이아웃 기능을 지원한다.</p>
<pre><code>&lt;footer th:fragment=&quot;copy&quot;&gt; 푸터 자리 입니다.
&lt;/footer&gt;
&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><pre><code>&lt;body&gt;
&lt;h1&gt;부분 포함&lt;/h1&gt;
&lt;h2&gt;부분 포함 insert&lt;/h2&gt;
&lt;div th:insert=&quot;~{template/fragment/footer :: copy}&quot;&gt;&lt;/div&gt;
&lt;h2&gt;부분 포함 replace&lt;/h2&gt;
&lt;div th:replace=&quot;~{template/fragment/footer :: copy}&quot;&gt;&lt;/div&gt;
&lt;h2&gt;부분 포함 단순 표현식&lt;/h2&gt;
&lt;div th:replace=&quot;template/fragment/footer :: copy&quot;&gt;&lt;/div&gt;
&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><p><img src="https://velog.velcdn.com/images/yh_lee/post/a3c10164-190c-438a-bac0-d18712c3abaf/image.png" alt=""></p>
<h2 id="템플릿-레이아웃1">템플릿 레이아웃1</h2>
<p>base.html</p>
<pre><code>&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><p>layoutMain.html</p>
<pre><code>&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><p><img src="https://velog.velcdn.com/images/yh_lee/post/482c16f6-94ab-4e92-883e-962893b6bb4b/image.png" alt="">
<img src="https://velog.velcdn.com/images/yh_lee/post/b3393958-28be-4000-b85d-be5e858cda9f/image.png" alt=""></p>
<h2 id="템플릿-레이아웃2">템플릿 레이아웃2</h2>
<p>layoutFile.html</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html th:fragment=&quot;layout (title, content)&quot; xmlns:th=&quot;http://
  www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;title th:replace=&quot;${title}&quot;&gt;레이아웃 타이틀&lt;/title&gt; &lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;레이아웃 H1&lt;/h1&gt;
&lt;div th:replace=&quot;${content}&quot;&gt;
    &lt;p&gt;레이아웃 컨텐츠&lt;/p&gt; &lt;/div&gt;
&lt;footer&gt; 레이아웃 푸터
&lt;/footer&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>layoutExtendMain.html</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html th:replace=&quot;~{template/layoutExtend/layoutFile :: layout(~{::title},~{::section})}&quot;
      xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;title&gt;메인 페이지 타이틀&lt;/title&gt; &lt;/head&gt;
&lt;body&gt;
&lt;section&gt;
    &lt;p&gt;메인 페이지 컨텐츠&lt;/p&gt;
    &lt;div&gt;메인 페이지 포함 내용&lt;/div&gt; &lt;/section&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre><p><img src="https://velog.velcdn.com/images/yh_lee/post/eb9e1df5-0da4-41dc-8677-16cc54b956f1/image.png" alt="">
<img src="https://velog.velcdn.com/images/yh_lee/post/01ad6ee6-6442-4661-8768-d9b94b239e5d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVC 프레임워크 동작 방식]]></title>
            <link>https://velog.io/@yh_lee/MVC-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@yh_lee/MVC-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%8F%99%EC%9E%91-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Tue, 26 Jul 2022 13:20:16 GMT</pubDate>
            <description><![CDATA[<h1 id="chap10-스프링-mvc-프레임워크--동작-방식">Chap10 스프링 MVC 프레임워크  동작 방식</h1>
<h2 id="스프링-mvc-핵심-구성-요소">스프링 MVC 핵심 구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/32d37cb6-78ae-456b-b1c2-aeee53ac472a/image.png" alt="">
&lt;&lt;spring bean&gt;&gt;이라고 표시한 것은 스프링 빈으로 등록해야 하는 것을 의미한다. 회색 배경을 가진 요소는 개발자가 구현해햐 하는 요소이다. </p>
<p>DispatcherServlet은 모든 연결을 담당한다. 웹 브라우저로부터 요청이 들어오면 DispatcherServlet은 요청을 처리하기 위한 컨트롤러 객체를 검색한다. 이때 직접 검색하지 않고 HandlerMapping 빈 객체에게 컨트롤러 검색을 요청(2)한다. </p>
<p>HandlerMapping은 클라이언트의 요청 경로를 이용해서 컨트롤러 빈 객체를 dispatcherServlet에 전달한다. 예를 들어 웹 요청 경로가 &#39;/hello&#39;라면 등록된 컨트롤러 빈 중에서 &#39;/hello&#39; 요청 경로를 처리할 컨트롤러를 리턴한다.</p>
<p>@Controller, Controller 인터페이스, HttpRequestHandler 인터페이스를 동일한 방식으로 처리하기 위해 중간에 사용되는 것이 HandlerApdapter이다. </p>
<p>DispatcherServlet은 HandlerMapping이 찾아준 컨트롤러 객체를 처리할 수 있는 HandlerAdapter 빈에게 요청 처리를 위임(3)한다. HandlerAdapter는 컨트롤러의 알맞은 메서드를 호출해서 요청을 처리(4,5)하고 결과를 DispatcherServlet에 리턴(6)한다. 이때 HandlerAdapter는 컨트롤러의 처리 결과를 ModelAndView 객체로 변환해서 DispatcherServlet에 리턴한다. </p>
<p>DispatcherServlet은 결과를 보여줄 뷰를 찾기 위해 ViewResolver 빈 객체를 사용(7)한다. </p>
<p>DispatcherServlet은 ViewResolver가 리턴한 View 객체에게 응답 결과 생성을 요청(8)한다. JSP를 사용하는 경우 View 객체는 JSP를 실행함으로써 웹 브라우저에게 전송할 응답 결과를 생성하고 끝난다.</p>
<h2 id="컨트롤러와-핸들러">컨트롤러와 핸들러</h2>
<p>클라이언트의 요청을 실제로 처리하는 것은 컨트롤러이다. 컨트롤러를 찾아주는 객체는 ControllerMapping이 아니라 HandlerMapping인 이유는 무엇일까?</p>
<p>DispatcherServlet 입장에서 클라이언트 요청을 처리하는 객체의 타입이 반드시 @Controller를 적용한 클래스일 필요 없다. 스프링이 제공하는 타입 중 HttpRequestHandler 타입도 있다. 이런 이유로 스프링 MVC는 웹 요청을 처리하는 객체를 Handler라고 표현한다.</p>
<h2 id="dispatcherservlet과-스프링-컨테이너">DispatcherServlet과 스프링 컨테이너</h2>
<p>DispatcherServlet은 전달받은 설정 파일을 이용해서 스프링 컨테이너를 생성한다. HandlerMapping, HandlerAdapter, 컨트롤러, ViewResolver 등의 빈을 DispatcherServlet이 생성한 스프링 컨테이너에서 구한다.</p>
<h2 id="controller를-위한-handlermapping과-handleradapter">@Controller를 위한 HandlerMapping과 HandlerAdapter</h2>
<p>DispatcherServlet은 웹 브라우저의 요청을 처리할 핸들러 객체를 찾기 위해 HandlerMapping을 사용하고, 핸들러를 실행하기 위해 HandlerAdpater를 사용한다.</p>
<pre><code class="language-java">@Controller 
public class HelloController{
    @GetMapping(&quot;/hello&quot;)
    public String hello(Model model, @RequestParam(value=&quot;name&quot;,required=false) String name){
        model.addAttribute(&quot;greeting&quot;,&quot;HI, &quot;+name);
        return &quot;hello&quot;;
    }
}    </code></pre>
<p>RequestMappingHandlerAdapter는 컨트롤러 메서드 결과 값이 String 타입이면 해당 값을 뷰 이름으로 갖는 ModelAndView 객체를 생성해서 DispatcherServlet에 리턴한다. </p>
<h2 id="webmvcvonfigurer-인터페이스와-설정">WebMvcVonfigurer 인터페이스와 설정</h2>
<p>@EnableWebMvc 어노테이션을 사용하면 @Controller 어노테이션을 붙인 컨트롤러를 위한 설정을 생성한다. </p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
    ...
}</code></pre>
<p>@EnableWebMvc 어노테이션을 사용하면 WebMvcConfigurer 타입인 빈 객체의 메서드를 호출해서 MVC 설정을 추가한다. 예를 들어 ViewResolver 설정을 추가하기 위해 WebMvcConfigurer 타입인 빈 객체의 configureViewResolvers() 메서드를 호출한다. 따라서 WebMvcConfigurer 인터페이스를 구현한 설정 클래스는 configureViewResolvers() 메서드를 재정의해서 뷰 설정을 추가하면 된다. 스프링5는 자바8부터 지원하는 디폴트 메서드를 사용해서 WebMvcConfigurer 인터페이스의 메서드에 기본 구현을 제공하고 있다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 MVC 시작하기]]></title>
            <link>https://velog.io/@yh_lee/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yh_lee/%EC%8A%A4%ED%94%84%EB%A7%81-MVC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 26 Jul 2022 11:33:11 GMT</pubDate>
            <description><![CDATA[<h1 id="chap9-스프링-mvc-시작하기">Chap9 스프링 MVC 시작하기</h1>
<h2 id="컨트롤러-구현">컨트롤러 구현</h2>
<pre><code class="language-java">@Controller 
public class HelloController{
    @GetMapping(&quot;/hello&quot;)
    public String hello(Model model, @RequestParam(value=&quot;name&quot;,required=false) String name){
        model.addAttribute(&quot;greeting&quot;,&quot;HI, &quot;+name);
        return &quot;hello&quot;;
    }
}    </code></pre>
<ul>
<li><strong>@Controller</strong> 어노테이션을 적용한 클래스는 스프링 MVC에서 컨트롤러로 사용한다.</li>
<li><strong>@GetMapping</strong> 어노테이션은 메서드가 처리할 요청 경로를 지정한다. &quot;/hello&quot; 경로로 들어온 요청을 처리한다. </li>
<li><strong>Model</strong> 파라미터는 컨트롤러의 처리 결과를 뷰에 전달할 때 사용한다.</li>
<li><strong>@RequestParam</strong> 어노테이션은 HTTP 요청 파라미터의 값을 메서드의 파라미터로 전달할 때 사용된다. name 요청 파라미터 값을 name 파라미터에 전달한다.</li>
<li><strong>&quot;greeting&quot; 이라는 모델 속성</strong>에 값을 설정한다. 값으로는 &quot;HI, {name}&quot;의 문자열을 사용한다. 뒤에 작성할 JSP 코드에서 이 속성을 이용해서 값을 출력한다.</li>
<li>컨트롤러 처리 결과를 보유줄 뷰 이름으로 <strong>&quot;hello&quot;</strong>를 사용한다.</li>
</ul>
<blockquote>
<p>스프링 MVC 프레임워크에서 컨트롤러란 웹 요청을 처리하고 그 결과를 뷰에 전달하는 스프링 빈 객체이다. 스프링 컨트롤러로 사용될 클래스는 @Controller 어노테이션을 붙여야 하고, @GetMapping이나 @PostMapping 같은 요청 매핑 어노테이션을 이용해서 처리할 경로를 지정해줘야 한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yh_lee/post/13720885-e286-4f98-8091-50b982c7a70a/image.png" alt=""></p>
<p>컨트롤러를 구현했으므로 컨트롤러를 스프링 빈으로 등록할 단계이다. 컨트롤러 클래스를 빈으로 등록할 때 사용할 설정 파일은 다음과 같다.</p>
<pre><code class="language-java">@Configuration
public class ControllerConfig{
    @Bean
    public HelloController helloCotroller(){
        return new HelloController();
    }
}</code></pre>
<h2 id="jsp-구현">JSP 구현</h2>
<p>컨트롤러가 생성한 결과를 보여줄 뷰 코드를 만들어보자. 
<strong>WEB-INF/view/hello.jsp</strong></p>
<pre><code class="language-jsp">...
&lt;body&gt;
 인사말: ${greeting}
&lt;/body&gt;
...</code></pre>
<p>뷰 이름과 JSP 파일과의 연결은 MvcConfig 클래스의 다음 설정을 통해서 이루어진다.</p>
<pre><code class="language-java">@Override
public void configureViewResolvers(ViewResolverRegistry registry){
    registry.jsp(&quot;/WEB-INF/view/&quot;,&quot;.jsp&quot;);

}</code></pre>
<p>registry.jsp() 코드는 JSP를 뷰 구현으로 사용할 수 있게 한다. <img src="https://velog.velcdn.com/images/yh_lee/post/20272fd9-9ba4-4c0a-a787-f2fa7f56314e/image.png" alt="">
&quot;greeting&quot;은 컨트롤러에서 설정한 속성을 뷰 JSP 코드에서 접근할 수 있는 이유는 스프링 MVC 프레임워크가 아래 그림처럼 모델에 추가한 속성을 JSP 코드에서 접근할 수 있게 HttpServletRequest에 옮겨주기 때문이다.<img src="https://velog.velcdn.com/images/yh_lee/post/04d7f806-4dc5-46a9-9321-27fea02effd5/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>