<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>D.I.Labs</title>
        <link>https://velog.io/</link>
        <description>어떠한 가치를 창출할 수 있을까를 고민하는 개발자. 주로 Spring으로 개발해요.</description>
        <lastBuildDate>Fri, 23 Sep 2022 11:20:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. D.I.Labs. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/do_ng_iill" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[부하 테스트 회고]]></title>
            <link>https://velog.io/@do_ng_iill/%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@do_ng_iill/%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 23 Sep 2022 11:20:42 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>항해99 실전프로젝트를 6주간 진행하면서 부하 테스트를 진행했다. </li>
<li>처음에 1주일이라는 기간을 잡았는데 사실상 2주라는 시간이 할애되었고 핵심 기능인 만큼 시간이 걸리더라도 목표를 달성하는 것에 초점을 맞췄다.</li>
</ul>
<h2 id="❓-부하-테스트-왜-했어">❓ 부하 테스트 &quot;왜&quot; 했어?</h2>
<ol>
<li>여러 사례를 토대로 각 시스템의 응답 성능을 예측</li>
<li>부하가 많이 발생하면 성능을 개선</li>
<li>원하는 성능을 완성하는 데 필요한 하드웨어를 미리 선정</li>
<li>시스템 확장성을 가졌는지 확인</li>
<li>시스템 확장성에 대한 특성을 파악</li>
</ol>
<h2 id="부하-테스트-전제-조건">부하 테스트 전제 조건</h2>
<ul>
<li><p>테스트 대상 시스템 범위</p>
<ul>
<li>t2.micro EC2 * 2(개)</li>
<li>db.t3.micro RDS * 1(1개)</li>
<li>Classic Load Balance</li>
</ul>
</li>
<li><p>데이터 양</p>
<ul>
<li>유저 450만<ul>
<li>무신사 회원 수 600만, 브랜디 회원 가입자수 305만 ⇒ 평균 450만</li>
</ul>
</li>
<li>상품 100만<ul>
<li>브랜디의 경우 약 100만개의 상품 데이터 보유하고 있으며 패션 플랫폼 중에서 가장 많은 데이터를 보유하고 있음.</li>
</ul>
</li>
<li>주문내역 1670만<ul>
<li>[각 패션 플랫폼 1년 매출 / 객단가] = 1년 주문건수</li>
<li>여러 패션 플랫폼의 1년 주문 건수를 기반으로 평균값 추출</li>
<li>1년 주문 건수의 평균값 * 5년(주문데이터 최대 보관 기간) = <strong>1670만</strong></li>
<li>&quot;정보통신망법 제29조에 의거 보관기간 <strong>5년을 초과한 주문데이터는 개인정보보호차원에서 파기</strong>해야 합니다. - [보관 기간 5년 초과시, 삭제]로 설정할 경우 설정한 날로 부터 5년 초과된 주문서는 익일부터 오전 6시에 일괄 삭제처리 됩니다.&quot;</li>
</ul>
</li>
</ul>
</li>
<li><p>지속적인 성능 유지 기간</p>
</li>
<li><p>부하를 주는 방법</p>
<ul>
<li>어떤 네트워크에서 부하?</li>
<li>HTTPS는 사용 ❌</li>
</ul>
</li>
</ul>
<h2 id="부하-테스트-목표값-설정">부하 테스트 목표값 설정</h2>
<h3 id="latency-목표값-설정">Latency 목표값 설정</h3>
<blockquote>
<p>📢 KISSmetrics는 고객의 47%가 2초 이내의 시간에 로딩이 되는 웹 페이지를 원하고 있으며, 40%는 로딩에 3초 이상 걸리는 페이지를 바로 떠난다고 설명했습니다.</p>
</blockquote>
<ul>
<li>일반적인 경우 : 0.05~0.1초</li>
<li>복잡한 트랜잭션이 필요한 경우 : 2초이내</li>
</ul>
<h3 id="throughput-목표값-설정">Throughput 목표값 설정</h3>
<blockquote>
<p>📢 News1 자료(2021년 기준)에서 쇼핑 플랫폼별 MAU(Monthly Active User, 월간 순수 이용자) 추이는 평균 약 400만 명이다.</p>
</blockquote>
<ul>
<li>MAU : 400만(단위 : 명)</li>
<li>DAU : 13만(단위 : 명)</li>
<li>안전계수 : 3</li>
<li>1일 평균 접속 수에 대한 최대 피크 때 배율 : 2배</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/75340923-76f1-44ef-a799-35d2ba8729ea/image.png" alt=""></p>
<ul>
<li>1명당 평균 접속 수 : 20회</li>
</ul>
<p>⇒ 130,000(명) * 20(회) / 86,400(초) * 3(안전계수) * 2(1일 평균 접속 수에 대한 최대 피크 때 배율) = 약 180 rps</p>
<ul>
<li>130,000(명) / 86,400(초) = 1.5명(1초에 접속하는 사용자 수)
⇒ 평균 : 1.5(명) * 3(안전계수) = 4.5(명) → 5명
⇒ 최대 : 5(명) * 2(1일 평균 접속 수에 대한 최대 피크 때 배율) = 10(명)</li>
</ul>
<h2 id="부하-테스트-시나리오-결정">부하 테스트 시나리오 결정</h2>
<ol>
<li>정적 페이지 테스트</li>
<li>Hello World 페이지 테스트</li>
<li>참조계 페이지 테스트 (단순 DB 조회)<ul>
<li>메인페이지</li>
<li>검색 페이지<ul>
<li>DB 병목 ⇒ DB 스케일 UP
<img src="https://velog.velcdn.com/images/do_ng_iill/post/3dc195af-bf3a-4605-bb3e-b5a5bfa5b0b6/image.png" alt="">    </li>
</ul>
</li>
</ul>
<hr>
<ul>
<li>상품 데이터 가져오기</li>
<li>주문 내역 가져오기</li>
<li>재입고 가능한 상품 데이터 가져오기</li>
</ul>
</li>
<li>갱신계 페이지 테스트 (데이터 등록, 수정, 삭제)<ul>
<li>회원가입</li>
<li>주문하기</li>
<li><del>주문 취소</del></li>
<li>재입고 알림 등록</li>
<li>재입고 알림 삭제</li>
<li><del>재입고 수량 등록</del></li>
</ul>
</li>
<li>시나리오 테스트<ul>
<li><strong>일반 유저</strong><ol>
<li>주문 관련 시나리오<ul>
<li>로그인 → 검색 → 페이지 넘기기 → 상세 페이지 → 주문하기</li>
<li><del>로그인 → 주문 내역 가져오기 → 주문 취소</del></li>
</ul>
</li>
<li>재입고 알림 관련 시나리오<ul>
<li>로그인 → 검색 → 상세 페이지 → 재입고 알림 등록</li>
<li>로그인 → 검색 → 상세 페이지 → 재입고 등록 삭제</li>
</ul>
</li>
</ol>
</li>
<li><strong>셀러</strong><ul>
<li><del>로그인 → 재입고 가능한 상품 데이터 가져오기→ 재입고 수량 등록</del></li>
</ul>
</li>
</ul>
</li>
<li>스케일 업/아웃 테스트 준비 및 테스트</li>
</ol>
<h2 id="부하-테스트-도구-결정">부하 테스트 도구 결정</h2>
<ul>
<li>JMeter</li>
<li>Locust</li>
<li>Artillery</li>
<li>NGrinder</li>
</ul>
<h2 id="테스트-결과-기록">테스트 결과 기록</h2>
<p><a href="https://docs.google.com/spreadsheets/d/1TI6gnlM-oUQtTpSN-fTS1uW9YMFFbqm6MCS30mMAXBU/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1TI6gnlM-oUQtTpSN-fTS1uW9YMFFbqm6MCS30mMAXBU/edit?usp=sharing</a></p>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>[아마존 웹 서비스 부하 테스트 입문] 도서</li>
</ul>
<h2 id="회고">회고</h2>
<ul>
<li>최종발표 일주일을 남겨두고 부하테스트를 시작하게 되었다. 부하 테스트는 이번 항해99를 하면서 처음 알게되어서 어떻게 시작을 해야할지 참 막막했다. 매주 멘토링을 받는데 멘토님께서 [아마존 웹 서비스 부하 테스트 입문]이라는 책을 추천해주셔서 Yes24 E-book을 구매했다. 책에서 전반적으로 부하 테스트가 무엇이고 부하 테스트를 할 때 주의해야할 점들을 잘 설명해줘서 갈피를 좀 잡을 수 있었다. 부하 테스트 도구는 여러개 있었지만 실전 프로젝트 초반에 다른 팀의 팀원 분께서 Artillery로 간단하게 부하 테스트하는 세미나를 해주셔서 프로젝트 기간을 고려해 Artillery로 일단 진행했다. 다음에 기회가 된다면 다른 도구들도 고려해보면 좋을 것 같다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[검색 성능 개선 회고]]></title>
            <link>https://velog.io/@do_ng_iill/%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@do_ng_iill/%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 19 Sep 2022 09:23:48 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>항해99 실전프로젝트를 6주간 진행하면서 검색 및 필터 기능에 대해 성능 개선을 진행했다. </li>
<li>처음에 1주일이라는 기간을 잡았는데 사실상 2주라는 시간이 할애되었고 핵심 기능인 만큼 시간이 걸리더라도 목표를 달성하는 것에 초점을 맞췄다.</li>
</ul>
<h2 id="❓-검색-성능-개선-왜-했어">❓ 검색 성능 개선 &quot;왜&quot; 했어?</h2>
<ul>
<li>KISSmetrics에 따르면 고객의 47%가 2초 이내의 시간에 로딩이 되는 웹 페이지를 원하고 있으며, 40%는 로딩이 3초 이상 걸리는 페이지를 바로 떠난다고 한다.</li>
<li>👉🏻 비즈니스적인 관점에서 이탈률 40%는 매우 치명적이다. 그렇기 때문에 검색 성능 개선의 필요성을 느꼈다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/4eba1f4f-2eb6-4b97-a5e5-85736c0b9b28/image.png" alt=""></li>
</ul>
<h2 id="📑-진행-단계">📑 진행 단계</h2>
<ul>
<li>진행단계는 모두 7단계다.</li>
<li>Index 생성, Cross Join 제거, 불필요한 Join이 발생하지 않도록 분기 처리해 Count Query 개선, No Offset 방식 적용, Covering Index 생성, Full-Text Index 생성 및 Match() Against() 사용, 검색 로징 변경이다.</li>
<li>그 중에서 No Offset 방식과 Full-Text Index는 적용하지 않았는데 그 이유도 아래에 설명하겠다.</li>
</ul>
<h3 id="1-index-생성">1. Index 생성</h3>
<ul>
<li>적용 계기<ul>
<li>기존 쿼리 실행 시 Order By 부분에서 많은 시간이 소요되는 것을 알 수 있었다. 그래서 Index를 적용하기로 했다.</li>
</ul>
</li>
<li>인덱스 항목<ul>
<li>리뷰수(Default 정렬값)<ul>
<li>👉🏻 실제 쇼핑몰의 경우 변동이 많은 값이므로 인덱스로 설정하지 않는 것이 좋지만 현재 Mucosa 프로젝트의 경우 리뷰수 변동이 없으므로 인덱스로 설정하기로 결정했다.</li>
</ul>
</li>
<li>가격(고가순, 저가순)</li>
</ul>
</li>
<li>결과 분석<ul>
<li>개선된 부분<ul>
<li>대부분의 첫 페이지 로딩이 개선되었으며 최대 2880%까지 개선되었다.</li>
</ul>
</li>
<li>추가 개선이 필요한 부분<ul>
<li>일부 항목의 경우 Count Query에 성능 저하 발생했다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-cross-join-제거">2. Cross Join 제거</h3>
<ul>
<li>적용 계기<ul>
<li>Querydsl로 작성한 쿼리가 Cross Join을 발생시키고 있다는 것을 알게 되었다.</li>
<li>👉🏻 Cross Join 대신 Inner Join이 발생하도록 코드를 수정했다.</li>
</ul>
</li>
<li>Cross Join 제거 방법<ul>
<li>Cross Join이 발생하는 Brand 테이블을 Inner Join이 발생하도록 쿼리 수정했다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/f4bd2086-03c8-45b8-bd70-457e1fd4c2f3/image.png" alt=""></li>
</ul>
</li>
<li>결과 분석<ul>
<li>개선된 부분<ul>
<li>키워드 검색 부분의 경우 기존에 발생하던 Cross Join이 Inner Join으로 변경되면서 최대 232%까지 개선되었다.</li>
</ul>
</li>
<li>추가 개선이 필요한 부분<ul>
<li>단순 메인페이지 로딩의 경우 Join을 강제하면서 Count Query에 성능 저하가 발생</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-불필요한-join이-발생하지-않도록-분기-처리해-count-query-개선">3. 불필요한 Join이 발생하지 않도록 분기 처리해 Count Query 개선</h3>
<ul>
<li>적용 계기<ul>
<li>Inner Join을 명시적 처리하면서  Brand 테이블 필요하지 않은 Count Query에서도 Join이 발생하여 성능이 떨어진 사실을  발견했다.</li>
<li>불필요한 Join이 발생하지 않도록 분기 처리했다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/9ab69a0d-8249-413d-aafd-660ddef33a29/image.png" alt=""></li>
</ul>
</li>
<li>결과 분석<ul>
<li>개선된 부분<ul>
<li>Inner Join을 강제하면서 발생했던 count query 성능 저하에 대해서는 최대 1127%까지 개선되었다.</li>
</ul>
</li>
<li>추가 개선이 필요한 부분<ul>
<li>마지막 페이지에 대한 Query 속도는 여전히 느렸다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="4-no-offset-방식-적용"><del>4. No Offset 방식 적용</del></h3>
<ul>
<li>적용 계기<ul>
<li>페이징 처리 시 Offset이 성능 저하를 발생한다는 사실을 알게 되었다.</li>
<li>👉🏻 No Offset 방식 적용하기로 했다.</li>
</ul>
</li>
<li>Offset이 성능 저하를 발생시키는 이유<ul>
<li>Offset을 사용하게 되면 Offset + Limit만큼의 데이터를 읽은 후 Offset만큼의 데이터를 버린다.</li>
<li>👉🏻 마지막 페이지로 갈수록 읽어야하는 데이터 수가 비약적으로 증가한다.</li>
</ul>
</li>
<li>No Offset 방식을 적용하지 않은 이유<ol>
<li>No Offset 방식의 경우 Offset 사용 대신 where절에 조회 시작 부분을 판단하도록 한다.
👉🏻 하지만 MUCOSA의 경우 Where절에 사용될 조회 시작 부분을 판단하도록 하는 기준 key가 중복이 가능한 값(리뷰수, 가격)이기 때문에 No Offset 방식을 적용할 수 없다고 판단했다.</li>
<li>No Offset 방식의 경우 페이징버튼이 아닌 ‘more(더보기)’ 버튼을 사용해야 한다.
👉🏻 순차적 페이지 이동만 가능한데 MUCOSA의 경우 전체 상품 수가 100만개이므로 페이지 이동이 자유로운 페이징 버튼을 사용하는 것이 좋을 것으로 판단했다.</li>
</ol>
</li>
</ul>
<h3 id="5-covering-index-생성">5. Covering Index 생성</h3>
<ul>
<li>적용 계기<ul>
<li>No Offset 방식을 적용할 수 없는 상황에서 성능을 개선하기 위해 Full Scan이 발생하는 Product 테이블 개션의 필요성을 알게 되었다.</li>
<li>👉🏻 Covering Index를 통해 &#39;where, order by, offset ~ limit&#39;를 인덱스 검색으로 빠르게 처리하고 걸러진 데이터를 통해서만 데이터 블록에 접근하기 때문에 성능 개선이 가능하다고 판단했다.</li>
</ul>
</li>
<li>Querydsl에서의 Covering Index 적용<ul>
<li>Querydsl의 경우 from절의 서브쿼리를 지원하지 않는다.</li>
<li>👉🏻 커버링 인덱스를 활용하여 조회 대상의 PK를 조회하는 부분과 해당 PK로 필요한 컬럼 항목들을 조회하는 부분을 나누어 구현
<img src="https://velog.velcdn.com/images/do_ng_iill/post/f31f1ceb-7f67-4af9-829b-dbae329f638e/image.png" alt=""></li>
</ul>
</li>
<li>결과 분석<ul>
<li>개선된 부분<ul>
<li>마지막 페이지 속도가 최대 236%까지 개선되었다.</li>
</ul>
</li>
<li>추가 개선이 필요한 부분<ul>
<li>첫페이지에 대해 목표했던 2초 이내의 결과는 달성하지 못했다.</li>
<li>키워드 검색에 대한 성능 개선이 다른 항목들에 비해 많이 되지 않았다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="6-full-text-index-생성-및-match-against"><del>6. Full-Text Index 생성 및 Match()-Against()</del></h3>
<ul>
<li>적용 계기<ul>
<li>마지막 페이지 부분이 개선되었으나 처음 목표했던 2초 이내는 달성하지 못함. 그래서 엘라스틱서치 적용도 고려했지만 엘라스틱서치의 역인덱싱 방식과 같은 원리로 동작하는 MySQL의 Full-Text 인덱스를 알게 되었다.</li>
<li>👉🏻 굳이 엘라스틱 서치를 이용할 필요 없이 Full-Text Index를 적용하고 Match()-Against() 쿼리문을 사용했다.</li>
</ul>
</li>
<li>Full-Text Index 방식을 적용하지 않은 이유<ul>
<li>Full-Text 인덱스를 적용하여 실제 테스트를 해 본 결과 성능 개선이 되지 않았다.</li>
<li>👉🏻 MUCOSA의 경우 검색 시 Join이 많이 발생하기 때문이라고 판단했다.</li>
</ul>
</li>
</ul>
<h3 id="7-검색-로직-변경">7. 검색 로직 변경</h3>
<ul>
<li>적용 계기<ul>
<li>Full-Text Index 방식을 시도하면서 Join에 의해 성능 저하가 많이 된다는 것을 인지했다.</li>
<li>👉🏻 상품명과 브랜드명을 동시에 검색할 수 있는 로직상 더 이상 Join을 줄일 수 없어 Search Type을 지정해 검색하는 로직을 반영하여 Join을 줄이고자 했다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/6ae63e25-52ff-4803-92e9-01142789b4a0/image.png" alt=""></li>
</ul>
</li>
<li>결과 분석<ul>
<li>개선된 부분<ul>
<li>검색 기능 성능이 최대 308%까지 개선되었다.</li>
<li>👉🏻 첫페이지의 경우 상품명 검색, 브랜드명 검색 모두 1초 이내로 목표 달성했다.</li>
<li>👉🏻 브랜드명 검색의 경우 마지막페이지까지 1초 이내로 목표 달성했다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/a7a2e837-578d-4231-97d1-6b5bfe792eb7/image.png" alt=""></p>
<h3 id="테스트-결과">테스트 결과</h3>
<ul>
<li>자세한 테스트 결과는 아래 구글 Docs에 저장했다.</li>
<li>테스트의 정확성을 높이기 위해 No Cache를 적용했고 10번씩 쿼리를 실행해서 평균값을 냈다.</li>
<li><a href="https://docs.google.com/spreadsheets/d/1TI6gnlM-oUQtTpSN-fTS1uW9YMFFbqm6MCS30mMAXBU/edit?usp=sharing">https://docs.google.com/spreadsheets/d/1TI6gnlM-oUQtTpSN-fTS1uW9YMFFbqm6MCS30mMAXBU/edit?usp=sharing</a></li>
</ul>
<h2 id="회고">회고</h2>
<ul>
<li>실전 프로젝트를 하기 전에는 이러한 대량의 데이터 환경에서의 성능을 고려하지 않았다. 채용공고를 들여다보니 대용량 트래픽에서의 경험을 중요시하다는 것을 알 수 있어서 이번 프로젝트에서도 중요하게 다뤘다.</li>
<li>검색 성능 개선을 하면서 다른 서비스에서는 어떻게 진행하는지 살펴봤다. 확실히 무한스크롤을 통해 로딩하는 서비스가 많았다. 가장 인상적이었던 서비스는 네이버 쇼핑이다. 정말 대량의 데이터가 있었는데 MUCOSA처럼 페이징을 사용했다. 차이점은 1페이지 20개씩~80개씩 보여주는데 1페이지 안에서는 무한스크롤로 되고 무거운 이미지 데이터들은 따로 불러오는듯 했다.</li>
<li>아직 백엔드 개발자로써 갈길이 멀다고 생각한다. 그리고 공부를 하면 할수록 CS전공 지식이 많이 필요하구나 뼈저리게 느끼게 되었다. 그래서 비록 비전공자이지만 틈틈이 CS전공 지식을 쌓아야겠다고 생각했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[회고]]></title>
            <link>https://velog.io/@do_ng_iill/%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@do_ng_iill/%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 19 Sep 2022 09:21:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/c6652f60-a18e-4324-a2f3-f6d349b4f9e1/image.png" alt=""></p>
<ul>
<li>회고는 중요하다.</li>
<li>👉🏻 시리즈로 만들어서 정리해볼까한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[부하테스트 (1/2)]]></title>
            <link>https://velog.io/@do_ng_iill/%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@do_ng_iill/%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Mon, 05 Sep 2022 16:10:46 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li><p>실전 프로젝트도 이제 추석을 제외하면 7일 가량 남은 것 같다. 생각보다 쿼리 성능 개선에서 시간을 많이 잡아 먹었고 남은 7일 동안은 부하테스트를 진행해보고자 한다. 살면서 처음으로 부하테스트를 진행하기 때문에 아무것도 모른다. 그래서 멘토님이 추천해주신 책 <strong>&quot;아마존 웹 서비스 부하 테스트 입문&quot;</strong>으로 공부하면서 진행해보려고 한다.</p>
</li>
<li><p>해당 글은 책을 읽으면서 정리한 내용들인데 더 자세한 내용을 알고싶다면 책을 구매해서 공부하시는 것을 추천한다!</p>
</li>
</ul>
<h2 id="1-웹-시스템-설계-방법">1. 웹 시스템 설계 방법</h2>
<h3 id="1-1-웹-시스템의-가용성">1-1. 웹 시스템의 가용성</h3>
<blockquote>
<p><strong>가용성(Availability) : 시스템이 서비스를 정상적으로 제공할 수 있는 상태를 말한다.</strong></p>
</blockquote>
<p><strong>📌 서비스 다운의 원인</strong></p>
<blockquote>
<ul>
<li>광역 네트워크 장애</li>
</ul>
</blockquote>
<ul>
<li><p>광역 전원 장애</p>
</li>
<li><p>하드웨어 장애</p>
<ul>
<li>네트워크 기기 장애</li>
<li>서버 물리 장애(전원, CPU, 메모리, 디스크, 메인보드 등...)</li>
</ul>
</li>
<li><p>소프트웨어 장애</p>
<ul>
<li>보안에 문제가 있어 시스템을 이용할 수 없는 경우</li>
<li>그 외의 소프트웨어의 버그</li>
</ul>
</li>
<li><p>점검 기간</p>
<ul>
<li>하드웨어 교체</li>
<li>소프트웨어 업데이트</li>
<li>미들웨어 업데이트</li>
</ul>
</li>
<li><p>고부하에 따른 요청 타임아웃</p>
</li>
<li><p>가용성이 높고 낮음은 가동률의 퍼센티지(%)로 표시된다. 가용성이 99.99%라는 경우 99.99% 시간을 정상적으로 이용 가능한 시스템을 말함. =&gt; 1년에 53분 정도는 서비스가 다운된다는 의미.</p>
<ul>
<li>1년(365) : 100% = x : 99.99% =&gt; x = (365 * 99.99) / 100 = 364.9635일</li>
<li>다운되는 시간 0.0365(일) =&gt; 0.0365 * 24시간 = 0.876(시간) =&gt; 0.876 * 60(분) = 52.56분</li>
</ul>
</li>
<li><p>모놀리식이 아닌 만약 마이크로 서비스의 경우라면?</p>
<ul>
<li>어떠한 마이크로 서비스의 A시스템의 가용성이 a%, B시스템의 가용성이 b%이라면 =&gt; a% * b%</li>
<li>서로 독립적으로 99.99%의 가용성을 가진 5대의 시스템을 연동한 경우 =&gt; 99.99% * 99.99% * 99.99% * 99.99% * 99.99% = 99.95%</li>
</ul>
</li>
</ul>
<h3 id="1-2-높은-가용성을-가진-시스템-설계-방법">1-2. 높은 가용성을 가진 시스템 설계 방법</h3>
<ul>
<li>시스템을 이중화한다.<ul>
<li>시스템 이중화 : 단일 장애점(SPOF: Single Poin Of Failure)을 없애는 방법!</li>
<li>단일 장애점 : 그 지점에 장애가 발생하면 서비스르 제공할 수 없는 지점</li>
<li>결론 👉🏻 시스템 이중화는 시스템의 일부분을 사용할 수 없게 되어도 다른 시스템을 이용해 서비스를 계속 제공하는 것을 의미 =&gt; 가용성 높음. 👆🏻</li>
<li>ex) 백업 시스템</li>
<li>리던던시(Redundancy) : 같은 장비를 병렬로 배열한 대수</li>
</ul>
</li>
<li>시스템을 확장한다.<ul>
<li>스케일 업/스케일 다운한다.<ul>
<li>시스템의 리소스 처리 능력을 올리거나 내리는 방법을 말함.</li>
<li>AWS RDS instance를 large =&gt; 8xlarge로 변경</li>
<li>각각의 장 단점이 있음.</li>
</ul>
</li>
<li>스케일 아웃/스케일 인 한다.<ul>
<li>시스템 성능을 올리기 위해 시스템 리소스 수를 늘리거나 줄이는 것을 말함.</li>
<li>AWS의 로드 밸런서 서비스인 ELB(Elastic Load Balancing)을 이용해 애플리케이션 서버 대수를 동적으로 늘리거나 줄이는 것이 가능.</li>
<li>해당 방법도 각각의 장 단점이 있음.</li>
</ul>
</li>
<li>클라우드 사업자가 확장성을 보증하는 서비스를 사용한다.<ul>
<li>각 서비스를 제공하는 리소스에는 접속할 수 없으며, 서비스로 기능을 이용하는 형태다.</li>
<li>Rout 53(DNS 제공 서비스)</li>
<li>CloudFront(CDN 제공 서비스)</li>
<li>Elastc Load Balancing(로드 밸런서 제공 서비스)</li>
<li>S3(스토리지 제공 서비스)</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2-부하-테스트-기본-지식">2. 부하 테스트 기본 지식</h2>
<h3 id="2-1-부하-테스트-목적">2-1. 부하 테스트 목적</h3>
<ul>
<li>시스템 처리 능력을 계산해 가용성을 높이는 것!!</li>
<li>온프레미스 시스템과 클라우드 시스템의 설계 방법이 다르고 부하 테스트 목적도 서로 다르다. 책에서는 2가지 내용을 모두 다루지만 해당 글에서는 필요한 클라우드 시스템에 대해서만 다루기로 한다.</li>
</ul>
<h3 id="2-2-클라우드에서의-부하-테스트-목적">2-2. 클라우드에서의 부하 테스트 목적</h3>
<ul>
<li>여러 사례를 토대로 각 시스템의 응답 성능을 예측</li>
<li>부하가 많이 발생하면 성능을 개선</li>
<li>원하는 성능을 완성하는 데 필요한 하드웨어를 미리 선정</li>
<li>시<strong>스템 확장성을 가졌는지 확인</strong></li>
<li>시스템 확장성에 대한 특성을 파악<ul>
<li>해당 내용을 파악하기 위해 사전에 파악해야 할 것들<ul>
<li>몇 가지 Throughput 레벨(ex. 100rps,  500rps, 1000rps, 2000rps, 5000rps 등)을 처리하기 위한 최적의 인프라 구성</li>
<li>실제 웹 시스템은 목표치를 넘어 최대 어디까지 Throughput을 처리할 수 있는지(제한 성능)의 기준</li>
<li>WHY? =&gt; 예를 들어 Throughput을 지금의 2배로 하고 싶을 때 웹 서버만 올리면 되는지, DB 서버도 같이 올려야 하는지, 혹은 반대의 상황을 가정한다면 미리 위 2가지 내용을 파악해야 한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="2-3-부하-테스트에서의-시스템-성능-지표">2-3. 부하 테스트에서의 시스템 성능 지표</h3>
<ul>
<li><p>Throughput : 시간당 처리량</p>
<ul>
<li>RPS(Request Per Second) : 1초에 처리하는 HTTP 요청 수</li>
<li>시간당 처리량과는 다른 개념으로 네이트워크로 전송되는 데이터 전송 속도를 의미하는 경우도 있음. =&gt; Throughput을 네트워크 대역, 최대 Throughput을 네트워크 대역폭이라고 한다.</li>
</ul>
</li>
<li><p>Latency : 처리 시간</p>
<ul>
<li>사용자가 본 처리 시간 : 사용자가 요청을 보내고 응답을 받을 때까지의 시간 = 시스템에서 본 처리 시간 + 네트워크를 통한 데이터 왕복 시간 + 브라우저가 데이터를 받고 화면에 표시하기 까지의 시간(브라우저 사용을 할 경우)</li>
<li>시스템에서 본 처리 시간 : 웹 시스템이 요청을 받고 응답을 줄 때까지의 시간</li>
</ul>
</li>
<li><p>여러 하위 시스템으로 구성된 환경에서의 Throughput과 Latency</p>
<ul>
<li>시스템 전체 Throughput : 각 하위 시스템 Throughput 중 최솟값</li>
<li>시스템 전체 Latency : 각 하위 시스템의 Latency 합계 값</li>
</ul>
</li>
</ul>
<h3 id="2-4-시스템-성능-개선-기본-지식">2-4. 시스템 성능 개선 기본 지식</h3>
<ul>
<li><p>Throughput 개선</p>
<ul>
<li>병목구간 : Throughput이 가장 낮은 하위 시스템</li>
<li>병목구간을 찾아 개선해서 최대 Throughput을 올릴 수 있음.</li>
<li>하지만 병목을 개선하더라도 다른 쪽에서 병목을 발견 될 수 있음 =&gt; 즉, 병목은 이동한다!라고 판단이 가능</li>
<li>병목 구간이라고 생각해서 그 구간을 개선했는데 Throughput에 영향을 주지 않았다면? =&gt; 병목 확인이 잘못되었으므로 다른 구간을 확인해야 한다. =&gt; 해당 과정들을 반복해서 Throughput을 개선해 나가는 것이 중요하다.</li>
</ul>
</li>
<li><p>Latency 개선</p>
<ul>
<li>&#39;긴 처리 시간이 필요한 처리&#39;에서는 순차적으로 개선 사항이 없는지 확인해 나가는 방법을 사용하는 것이 좋다.</li>
<li>각 처리 시간이 적정한 범위 내에 있다면 더 이상 개선하는 것은 어렵다.</li>
<li><strong>비효율 적인 알고리즘, 필요없는 I/O</strong>, 데이터베이스 사용 시 <strong>인덱스 부족 현상</strong>을 확인해야 한다.</li>
<li>처리 시간은 애플리케이션 내부라면 프로파일러라는 도구를 사용하면 비교적 쉽게 파악 가능.</li>
<li>Throughput 개선과 달리 일부 구간의 Latency 개선은 전체 Latency 개선과 연결된다. =&gt; 병목 구간에 관계없이 가장 시간이 오래 걸린 구간을 개선하는 것이 전체 Latency를 크게 개선하는 방법이다.</li>
</ul>
</li>
</ul>
<h3 id="2-5-좋은-부하-테스트에-대한-지표">2-5. 좋은 부하 테스트에 대한 지표</h3>
<ul>
<li><p><strong>테스트 대상 시스템은 부하가 집중되고 있는 상태</strong></p>
<ul>
<li>부하 테스트 : 테스트 대상 시스템에 부하를 준 상태에서 실행하는 테스트.</li>
<li>여러 하위 시스템으로 구성된 테스트일 때 각각의 하위 시스템에 개별적으로 부하를 주고 조사해야한다.</li>
<li>부하 테스트 중 특정 하드웨어 리소스가 과부하 상태가 되는 것은 나쁜 것이 아니라 좋은 부하 테스트가 진행 중임을 의미한다.</li>
</ul>
</li>
<li><p><strong>병목 지점을 확인한 상태</strong></p>
<ul>
<li>시스템에 많은 요청을 보낸 부하 테스트인 경우 일반적으로 시스템 어느 한 부분이 과부하 상태가 되어 이 부분이 전체 Throughput을 결정하게 된다.</li>
<li>부하 테스트 실행 중에는 항상 이 테스트에서 병목 구간이 어디인지를 의식하고 확인해야 한다.</li>
<li><strong>병목 구간을 찾기 쉽게 테스트 방법을 준비</strong>하면 보다 효율적으로 테스트할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="3-부하-테스트-도구">3. 부하 테스트 도구</h2>
<blockquote>
<p><strong>부하 테스트 도구 : 웹 시스템뿐만 아니라 특정 시스템을 사용하는 입장에서 시뮬레이션하여 대상 시스템의 상태를 고부하로 만들어준다. 시스템에 많은 요청을 일이크기 DoS 공격이나 DDoS 공격이 들어온 것과 같은 상태를 만든다. 부하 테스트 도구를 가동한 서버를 부하 테스트 서버라고 한다.</strong></p>
</blockquote>
<h3 id="3-1-부하-테스트에서-사용하는-3가지-도구">3-1. 부하 테스트에서 사용하는 3가지 도구</h3>
<ul>
<li>부하 테스트 도구 : 시스템에 부하를 주는 도구<ul>
<li>많은 Http(s) 요청으로 인해 시스템에 부하를 준다.</li>
</ul>
</li>
<li>모니터링 도구 : 시스템 리소스 사용률을 가시화해주는 도구<ul>
<li>시스템 리소스(CPU, 메모리, 스토리지 등등..) 감시</li>
</ul>
</li>
<li>프로파일링 도구 : 미들웨어나 애플리케이션 내부 동작을 분석하고 가시화해주는 도구<ul>
<li>애플리케이션(Spring, MySQL 등등..) 감시</li>
</ul>
</li>
</ul>
<h3 id="3-2-부하-테스트-도구-선택-기준">3-2. 부하 테스트 도구 선택 기준</h3>
<ul>
<li><strong>조건1</strong> 요청을 정확하게 시뮬레이션한다.<ul>
<li>Apache Bench를 사용하면 시나리오 기반 테스트 불가능 =&gt; 시나리오가 필요한 부하 테스트는 이러한 도구를 사용할 수 없음.</li>
</ul>
</li>
<li><strong>조건2</strong> 부하 정도를 조정 가능해야 한다.<ul>
<li>클라이언트 동시 접속자 수, 요청 간격, 최대 Throughput 등을 조정하여 공격 강도를 조절해야 한다. 이러한 설정은 대부분의 테스트 도구에서 가능.</li>
</ul>
</li>
<li><strong>조건3</strong> 대상 시스템에 충분한 부하를 발생시켜야 한다.<ul>
<li>대상 시스템의 성능 지표에 따라 사용할 수 있는 테스트 도구가 달라짐.</li>
</ul>
</li>
<li><strong>조건4</strong> 부하 테스트 도구, 설치, 장소 및 가동 장소를 선택할 수 있어야 한다.<ul>
<li>부하 테스트 도구에 따라 기동할 수 있는 서버에 제약이 있을 수 있어 부하 테스트 서버를 설치하는 장소 제약이 있을 수 있다.</li>
<li>SaaS 서비스로 원격에 설치된 서버에서 부하를 주는 도구도 있지만 원격에서 부하를 주는 형태는 대상 시스템에만 부하를 주는 것이 어려워서 효율적인 부하 테스트를 하기엔 맞지 않다.</li>
</ul>
</li>
</ul>
<p><strong>📌 부하 테스트 도구 공통 개념</strong></p>
<table>
<thead>
<tr>
<th align="center">용어</th>
<th align="center">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="center">클라이언트</td>
<td align="center">HTTP 요청을 동시에 1개만 줄 수 있는 요청 생성기</td>
</tr>
<tr>
<td align="center">클라이언트 동시 가동 수</td>
<td align="center">테스트 시작 후에 테스트 도구에서 사용할 수 있는 클라이언트 수</td>
</tr>
<tr>
<td align="center">Ramp-up 기간</td>
<td align="center">테스트 시작 후 모든 클라이언트를 기동하기까지의 준비 기간</td>
</tr>
<tr>
<td align="center">시나리오</td>
<td align="center">클라이언트별로 설정된 HTTP 요청 생성 패턴</td>
</tr>
<tr>
<td align="center">시나리오 실행 횟수</td>
<td align="center">클라이언트가 시나리오에 따라 요청을 보내는 횟수</td>
</tr>
<tr>
<td align="center">Throughput</td>
<td align="center">시스템이 시간당 처리할 수 있는 요청 수</td>
</tr>
<tr>
<td align="center">Latency</td>
<td align="center">테스트 도구가 요청을 보내고 응답을 받을 때까지의 시간</td>
</tr>
</tbody></table>
<p><strong>📌 클라이언트가 실제 부하 테스트를 하는 이미지</strong>
이미지1 클라이언트별 HTTP 요청 라이프 사이클
(1) 부하 테스트 서버에서 HTTP 요청 생성
(2) HTTP 요청이 네트워크로 이동
(3) 로드 밸런서가 요청을 서버에 전달
(4) HTTP 요청을 웹 서버가 받음
(5) HTTP 응답을 웹 서버가 보냄
(6) 로드 밸런서를 통해 외부로 나감
(7) HTTP 응답이 네트워크로 이동
(8) 부하 테스트 서버가 HTTP 응답 받음. 그 이후로 다시 (1)로 돌아감</p>
<p><strong>⚠ 주의점 3가지</strong></p>
<ul>
<li>부하 테스트 도구에서 보이는 Latency ≠ 부하 테스트 대상 시스템의 Latency<ul>
<li>부하 테스트 도구에서 보이는 Latency = 네트워크로 전송되는 동안 발생하는 Latency나 SSL 디코드에 대한 Latency가 포함된 값 =&gt; 부하 테스트 대상 시스템이 빨리 응답해도 이 값이 반영되었는지 모름 👉🏻 각 시스템의 실제 Latency는 서버 로그를 보거나 로드 밸런서에서 보인 Latency를 확인해야만 한다.</li>
</ul>
</li>
<li>부하 테스트 도구의 클라이언트 동시 기동 수 ≠ 시스템에서 처리되는 동시 처리 수<ul>
<li>부하 테스트 도구에서 생성된 요청 대부분은 네트워크나 서버에 있으며 실제 부하를 줘야 하는 대상 시스템에서 처리 중인 요청은 전체 요청 중 일부분일 뿐이다. 특히 네트워크 Latency가 클 때는 부하 테스트 도구에서 설정한 클라이언트의 동시 가동 수와 비교하여 실제 시스템에서 처리 중인 요청 비율은 낮다.</li>
</ul>
</li>
<li>부하 테스트 도구의 클라이언트는 앞의 요청이 완료되지 않으면 다음 요청을 생성하지 않는다.<ul>
<li>서버나 네트워크 어딘가에서 일부 요청에 대한 응답을 처리하지 못하게 되면 전체 Throughpu에 많은 영향을 준다. 그러나 이 현상은 부하 테스트 특유의 현상!! 실제 사용자가 접속했을 때의 현상과는 다르다. 아래의 부하 테스트 도구상의 부하와 실 운영환경의 차이에서 자세히 알아보자!</li>
</ul>
</li>
</ul>
<p><strong>📌 부하 테스트 도구상의 부하와 실 운영환경의 차이</strong></p>
<ul>
<li><p>요청을 생성하는 서버 대수, 네트워크의 차이</p>
<ul>
<li>부하 테스트 환경에서 부하 테스트 서버는 1~N대의 범위 BUT, 서비스 환경에서는 요청을 한 수만큼 사용자가 존재 =&gt; SSL을 사용한 사이트의 부하 테스트 환경에서는 SSL 접속과 계산 처리 부하가 부하 테스트 서버에 집중되지만, 서비스 환경에서는 한 대의 서버에 집중되는 문제가 없어 큰 문제는 되지 않는다.</li>
<li>SSL 접속을 하지 않을 경우도 HTTP 요청 때마다 통신을 끊고 다시 접속하게 되면 부하 테스트 서버에 과부하가 발생! =&gt; 시스템에 효율적으로 부하를 주기 위해 Keep-Alive 설정에 대한 테스트도 필요.</li>
<li>네트워크도 마찬가지로 부하 테스트 환경에서는 집중 되지만 실제 서비스 환경에서는 분산된다. 그래서 테스트 서버의 사양이 아무리 높아도 네트워크 대역이 충분하지 않으면 부하 테스트를 실행할 수 없음.</li>
<li>환경에 따라 같은 IP에서 연속적인 접속을 차단하는 구성도 있으므로 테스트 시 주의가 필요.</li>
</ul>
</li>
<li><p>요청을 보내는 서버와 엔드포인트의 차이</p>
<ul>
<li>엔드포인트가 되는 서버가 부하 테스트 환경에서는 부하 테스트 서버별로 일정 시간동안 DNS 정보를 캐시할 때가 있다. 그래서 부하를 받는 서버가 스케일 아웃 되더라도 성능을 내지 못할 때가 많다. 그러나 서비스 환경에서는 요청을 보내는 사용자가 하나의 서버가 아닌 여러 곳으로 분산되어 있어 이러한 문제가 발생하지 않는다.</li>
<li>이러한 문제를 해결하기 위해 테스트 대상 시스템의 가까운 지점에서 부하를 주어 네트워크 영향을 최소화시킬 필요가 있고 또 각 엔드포인트에 같은 양의 요청이 있는지를 항상 확인해야 한다.</li>
</ul>
</li>
<li><p>동시 요청 수의 차이</p>
<ul>
<li>부하 테스트 환경에서는 먼저 보내진 요청 결과가 부하 테스트 서버에 돌아올 때까지 기다리고 다음 요청을 보내게 된다. 이와 같은 동작으로 시스템의 응답이 아무리 늦어도! 지정한 클라이언트 수의 요청밖에 동시에 발생하지 않는다.</li>
<li>하지만 실제 서비스 환경에서는 특정 사용자 응답이 늦어져 그 요청에 대한 결과를 기다리는 중에도 새로운 사용자가 새로운 요청을 계속 보내게 된다. 그래서 시스템 응답 시간이 늦더라도 처리해야 할 동시 요청 수는 점점 증가하게 된다.</li>
<li>동시 요청 수가 늘어난다는 것은 해당 서버에 메모리 리소스 사용과 외부 서버와의 접속 수 등에 영향을 주기 때문에 주의해야 한다.</li>
</ul>
</li>
<li><p>일부 느린 처리가 전체 Throughput에 미치는 영향의 차이</p>
<ul>
<li>부하 테스트 환경에서 시스템 리소스 사용량이 적음에도 실행 시간이 길어지는 요청이 조금이라도 섞여 있다면 다음 요청을 보낼 수 없게 되고 결과적으로 전체 Throughput이 저하되는 경우가 있따.</li>
<li>하지만 실제 서비스 환경에서는 실행 시간이 긴 요청이 전체 처리에 있어 병목이 발생하지 않으며, 부하 테스트 결과와 실제 환경에서 확인되는 결과와는 많이 다를 때도 있다.</li>
<li>이런 경우 <strong>시간이 소요되는 요청은 별도 스레드로 테스트를 하거나 시나리오에서 일단 빼고 테스트를 진행하는 등 전체 조정이 필요!</strong></li>
</ul>
</li>
</ul>
<h3 id="3-3-부하-테스트-도구-비교">3-3. 부하 테스트 도구 비교</h3>
<table>
<thead>
<tr>
<th align="center">이름</th>
<th align="left">특징</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><strong>Apache Bench</strong></td>
<td align="left">- 단일 URL 부하 테스트는 간단히 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- POST/PUT 부하 테스트 가능(DELETE 불가능)</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 요청별로 파라미터 변경 불가</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 시나리오 테스트 불가</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">부하 테스트 서버의 CPU 코어 1개만 사용</td>
</tr>
<tr>
<td align="center"><strong>Apache JMeter</strong></td>
<td align="left">- Apache Bench에서 할 수 없는 DELETE 테스트 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 요청 별로 동적 파라미터 변경 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 복수의 URL에 시나리오 기반의 복잡한 부하 테스트 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 시나리오는 XML을 사용하지만, GUI로 사용할 수 있고 비교적 직관적인 시나리오 작성 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- Proxy Recorder를 사용한 시나리오 작성도 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 부하 테스트 결과 출력 기능이 다양</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 복수의 서버를 연계하여 비교적 고부하 테스트 가능</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- HTML 콘텐츠는 콘텐츠 내에서 필요한 여러 가지 정적 리소스를 동시에 확인할 수 있는 테스트 가능</td>
</tr>
<tr>
<td align="center"><strong>Locust</strong></td>
<td align="left">- 시나리오를 파이썬 스크립트로 작성할 수 있어 유연한 시나리오를 만들 수 있다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 테스트 시나리오가 스크립트로 되어 있어 소스 코드로 관리할 수 있다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 필요한 서버 리소스가 작으므로 작은 크기의 부하 테스트 서버로 테스트할 수 있다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 테스트 결과 리포트가 간단하다.</td>
</tr>
<tr>
<td align="center"><strong>Tsung</strong></td>
<td align="left">- 시나리오를 XML로 작성하는 것은 JMeter와 같지만, 작성 방식이 JMeter보다 비교적 간단하고 이해하기 쉽다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 그러나 GUI로 시나리오를 작성하거나 볼 수 없어 복잡한 시나리오에는 조금 맞지 않는다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- JMeter와 같이 Proxy Recorder를 사용하고 시나리오 생성이 가능하다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 적은 부하 테스트 서버로 고부하 테스트에 적합하다.</td>
</tr>
<tr>
<td align="center"></td>
<td align="left">- 결과는 JSON으로 표시되고 결과를 보기 위한 웹 화면이 준비되어 있다.</td>
</tr>
</tbody></table>
<h3 id="3-4-모니터링-도구와-프로파일링-도구">3-4. 모니터링 도구와 프로파일링 도구</h3>
<ul>
<li>3-3에서의 부하 테스트 도구에는 Throughput 모니터링 도구, 시각화 도구가 포함된 것도 있지만, 부하테스트 도구에서 볼 수 있는 Latency는 전체 Latency뿐이며 개별 하위 시스템에 대한 Latency는 볼 수 없다.</li>
<li>부하 테스트 도구에서는 각 시스템이 사용하고 있는 리소스를 모니터링하거나 프로파일링 할 수 없다.</li>
<li>프로파일링 도구는 시스템 외부에서는 볼 수 없는 시스템의 병목 구간을 확인할 수 있는 유요한 것이지만 프로파일링 도구를 활성화하고 부하 테스트를 진행하게 된다면 프로파일링 도구의 움직임 자체가 시스템 리소스 사용 상태에 영향을 주므로 정확한 프로파일링을 할 수 없게 된다.</li>
<li>이 문제를 해결하기 위해 프로파일링 도구를 비활성화한 상태에서 부하 테스트를 하는 동시에! 프로파일링 도구를 활성화한 상태에서 별도의 테스트를 하여 <strong>고부하 상태의 프로파일링 결과</strong>를 확인할 수 있다.<ul>
<li>저부하 상태와 고부하 상태의 시스템은 전혀 다른 상태를 가진다. 저부하 상태 시스템 프로파일링의 결과를 가지고 고부하 상태 시스템을 프로파일링하는 것은 무의미한 경우가 많기 때문에 꼭 부하를 준 상태의 결과를 확인해야 한다.</li>
</ul>
</li>
</ul>
<p><strong>📌 각 하위 시스템의 상세 모니터링과 시스템 프로파일링을 하기 위한 도구</strong></p>
<ul>
<li>top 명령어<ul>
<li>많은 Linux계 OS에는 기본으로 제공되는 명령어로 시스템 전체와 프로세스별 CPU와 메모리 사용량 등을 볼 수 있다. 실시간으로 볼 수 있어 부하 테스트 시에 유용하게 사용된다.</li>
</ul>
</li>
<li>netstat 명령어</li>
<li>AWS 관리 콘솔<ul>
<li>CloudWatch 활용</li>
<li>주의점은 1분 간격의 RDS나 5분 간격의 EC2 인스턴스 모니터링때문에 부하가 없는 동안의 리소스 사용량과 부하가 있을 때의 사용량이 평균값으로 보이기 때문에 정상적인 모니터링이 불가 =&gt; 추가 과금이 되더라도 부하 테스트를 할 때는 CloudWatch 메트릭스의 상세 모니터링을 최소 1대의 서버에는 적용하도록 한다.</li>
</ul>
</li>
<li>Xhprof<ul>
<li>PHP 애플리케이션 전용 프로파일링 도구 </li>
</ul>
</li>
<li>New Relic<ul>
<li>프로파일링과 모니터링을 할 수 있는 SaaS 제품.</li>
<li>Java, Ruby on Rails, PHP, Python, Node.js 등 여러 언어와 웹 프레임워크를 지원</li>
<li>유료 플랜과 무료 플랜이 있고 무료 플랜도 많은 도움이 된다.</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th align="left">하위 시스템</th>
<th align="left">WATCH 해야 하거나, 방법을 알아야 하는 항목</th>
</tr>
</thead>
<tbody><tr>
<td align="left">네트워크</td>
<td align="left">전송량</td>
</tr>
<tr>
<td align="left">하드웨어 및 OS</td>
<td align="left">CPU, 메모리, 프로세스 수, SWAP, Load Average</td>
</tr>
<tr>
<td align="left">TCP</td>
<td align="left">외부 커넥션 상태(ESTABLISH나 FIN WAIT 등)</td>
</tr>
<tr>
<td align="left">디스크</td>
<td align="left">IOPS, R/W 전송 데이터 양</td>
</tr>
<tr>
<td align="left">미들웨어</td>
<td align="left">커넥션 수(&lt;-&gt;Max Connection)</td>
</tr>
<tr>
<td align="left">애플리케이션</td>
<td align="left">프로파일러로 모니터링</td>
</tr>
<tr>
<td align="left">MySQL</td>
<td align="left">Slow Query, Process List</td>
</tr>
</tbody></table>
<h2 id="마무리">마무리</h2>
<ul>
<li>[아마존 웹 서비스 부하 테스트 입문]의 1장-4장까지 정리한 내용이다. 웹 시스템 설계 방법부터 시작해서 부하 테스트 기본 지식 그리고 부하 테스트 도구와 선택 기준에 대한 내용들이었다. 5장-9장은 부하 테스트의 큰 PDCA(Plan Do Check Action) 사이클에 대한 설명이다. 그래서 한 번 끊고 가야겠다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>아마존 웹 서비스 부하 테스트 입문(도서)<ul>
<li>Yes24 : <a href="http://www.yes24.com/Product/Goods/102911927">http://www.yes24.com/Product/Goods/102911927</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-15 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-15-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-15-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Mon, 15 Aug 2022 11:39:37 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li><p>더미 데이터를 생성하는 과정중에서 다음과 같은 문제점이 발생했다.</p>
<ol>
<li>Brand 클래스의 name(브랜드명)과 Prodcut클래스의 name(상품명)은 임의로 faker.name()으로 했음. 추후 변경 필요!</li>
</ol>
<p><del>2. Order 클래스의 createdAt도 임의의 날짜 데이터인데 최근 날짜로 변경이 필요</del></p>
<p><del>3. Product 클래스의 가격 범위가 1,000원</del>10,000,000원인데 범위가 너무 넓어서 수정이 좀 필요하지 않을까 싶음.~~</p>
<p><del>4. User 클래스 role에 대한 비율 조절이 필요함.</del></p>
</li>
<li><p>해당 문제점들을 각각 해결해보자..!</p>
</li>
</ul>
<h2 id="1-brand-클래스의-name브랜드명을-조금-그럴싸하게-바꿔보자">1. Brand 클래스의 name(브랜드명)을 조금 그럴싸하게 바꿔보자!</h2>
<h3 id="🔍-문제점-파악">🔍 문제점 파악</h3>
<h3 id="❗-문제-해결">❗ 문제 해결</h3>
<h2 id="2-user-클래스의-일반-사용자-셀러의-확률-조정">2. User 클래스의 일반 사용자, 셀러의 확률 조정</h2>
<h3 id="🔍-문제점-파악-1">🔍 문제점 파악</h3>
<ul>
<li><p>현재 코드 분석</p>
<pre><code class="language-python"># -*- coding: utf-8 -*-
from table.User import User
from faker import Faker
from faker.providers import DynamicProvider

fake = Faker(&#39;ko_KR&#39;)

def create_user_dataset(num):
    user_class_list = []

    user_role_provider = DynamicProvider(
        provider_name=&quot;set_user_role&quot;,
        elements=[&quot;ADMIN&quot;, &quot;USER&quot;],
    )

    # 동적 프로바이더 추가
    fake.add_provider(user_role_provider)

    # num개의 User 생성
    for i in range(1, num + 1):
        email = fake.unique.ascii_free_email()
        password = fake.password()
        role = fake.set_user_role()
        user = User(i, email, password, role)
        user_class_list.append(user)

    return user_class_list

if __name__ == &quot;__main__&quot;:
    user_class_list = create_user_dataset(1000)

    user_cnt = 0
    admin_cnt = 0
    for i in range(1000):
        if user_class_list[i].role == &quot;ADMIN&quot;:
            admin_cnt += 1
        if user_class_list[i].role == &quot;USER&quot;:
            user_cnt += 1

    print(&quot;일반 사용자 수 : &quot;, user_cnt)
    print(&quot;셀러 수 : &quot;, admin_cnt)</code></pre>
<ul>
<li>코드에서 확률을 정해주는 부분이 없어서 결과값이 아래와 같이 나온다. 심지어 셀러가 더 많이 나온다.. 문제가 있다..<pre><code>일반 사용자 수 :  469
셀러 수 :  531
</code></pre></li>
</ul>
<pre><code></code></pre></li>
<li><p>faker 패키지 공식문서에서 random_element라는 메서드를 발견했다. 사용법은 아래와 같다.</p>
<pre><code class="language-python">&gt;&gt;&gt; Faker.seed(0)
&gt;&gt;&gt; for _ in range(10):
...     fake.random_element(elements=OrderedDict([(&quot;a&quot;, 0.45), (&quot;b&quot;, 0.35), (&quot;c&quot;, 0.15), (&quot;d&quot;, 0.05), ]))
...
&#39;c&#39;
&#39;b&#39;
&#39;a&#39;
&#39;a&#39;
&#39;b&#39;
&#39;a&#39;
&#39;b&#39;
&#39;a&#39;
&#39;b&#39;
&#39;b&#39;</code></pre>
</li>
<li><p>적용 후 코드</p>
<pre><code class="language-python"># -*- coding: utf-8 -*-
from table.User import User
from faker import Faker
from collections import OrderedDict

fake = Faker(&#39;ko_KR&#39;)

def create_user_dataset(num):
    user_class_list = []

    # num개의 User 생성
    for i in range(1, num + 1):
        email = fake.unique.ascii_free_email()
        password = fake.sha256()
        role = fake.random_element(elements=OrderedDict([(&quot;USER&quot;, 0.9), (&quot;ADMIN&quot;, 0.1)]))
        user = User(i, email, password, role)
        user_class_list.append(user)

    return user_class_list

if __name__ == &quot;__main__&quot;:
    user_class_list = create_user_dataset(1000)

    user_cnt = 0
    admin_cnt = 0
    for i in range(1000):
        if user_class_list[i].role == &quot;ADMIN&quot;:
            admin_cnt += 1
        if user_class_list[i].role == &quot;USER&quot;:
            user_cnt += 1

    print(&quot;일반 사용자 수 : &quot;, user_cnt)
    print(&quot;셀러 수 : &quot;, admin_cnt)</code></pre>
</li>
<li><p>결과값은 아래와 같다.</p>
<pre><code>일반 사용자 수 :  901
 셀러 수 :  99</code></pre></li>
</ul>
<h3 id="❗-문제-해결-1">❗ 문제 해결</h3>
<ul>
<li>random_element를 통해 일반 사용자 수와 셀러 수의 비율을 조절해줬다. 이를 활용해 Product 클래스의 상품가격 문제도 해결할 수 있지 않을까...?</li>
</ul>
<h2 id="3-product-클래스의-가격-범위-수정">3. Product 클래스의 가격 범위 수정!</h2>
<h3 id="🔍-문제점-파악-2">🔍 문제점 파악</h3>
<ul>
<li><p>현재 코드 분석</p>
<pre><code class="language-python">def create_product_dataset(brand_class_list, categroy_class_list, num):

    product_class_list = []

    ...

    for i in range(1, num + 1):
        ...
        price = fake.pyint(min_value=1000, max_value=10000000, step=100)
        ...

        product = Product(i, brand_id, name, thumbnail, category_id, price, amount, review_num, review_avg)
        product_class_list.append(product)

    return product_class_list</code></pre>
<ul>
<li>price 범위가 1000원~10,000,000원이여서 실제 생성된 데이터를 보니 중구난방이었다. 또한 어느정도 분배로 랜덤이 되는지 10,000,000원에 가까운 값들도 많이 나와서 비율 조절의 필요성을 느꼈다.</li>
</ul>
</li>
<li><p>문제해결시도!</p>
<pre><code class="language-python">from faker import Faker
from collections import OrderedDict

fake = Faker(&#39;ko_KR&#39;)

max_value_100000_cnt = 0
max_value_500000_cnt = 0
max_value_1000000_cnt = 0
max_value_5000000_cnt = 0
max_value_10000000_cnt = 0

for i in range(1, 1000):
    # max value 확률 지정
    max_value = fake.random_element(
        elements=OrderedDict([
            (&quot;100000&quot;, 0.7),
            (&quot;500000&quot;, 0.2),
            (&quot;1000000&quot;, 0.05),
            (&quot;5000000&quot;, 0.03),
            (&quot;10000000&quot;, 0.02)
        ])
    )
    if max_value == &quot;100000&quot;:
        price = fake.pyint(min_value=1000, max_value=int(max_value), step=100)
        max_value_100000_cnt += 1
    if max_value == &quot;500000&quot;:
        price = fake.pyint(min_value=100001, max_value=int(max_value), step=1000)
        max_value_500000_cnt += 1
    if max_value == &quot;1000000&quot;:
        price = fake.pyint(min_value=500001, max_value=int(max_value), step=1000)
        max_value_1000000_cnt += 1
    if max_value == &quot;5000000&quot;:
        price = fake.pyint(min_value=1000001, max_value=int(max_value), step=1000)
        max_value_5000000_cnt += 1
    if max_value == &quot;10000000&quot;:
        price = fake.pyint(min_value=5000001, max_value=int(max_value), step=1000)
        max_value_10000000_cnt += 1

print(&quot;max_value_100000_cnt&quot;, max_value_100000_cnt)
print(&quot;max_value_500000_cnt&quot;, max_value_500000_cnt)
print(&quot;max_value_1000000_cnt&quot;, max_value_1000000_cnt)
print(&quot;max_value_5000000_cnt&quot;, max_value_5000000_cnt)
print(&quot;max_value_10000000_cnt&quot;, max_value_10000000_cnt)</code></pre>
<ul>
<li>random_element를 사용하면 되지 않을까라는 발상으로 시도를 했는데 실제로 위 코드(price_test.py)에서는 정상적으로 비율대로 잘 작동이 되었다. 하지만 이를 적용한 실제 product_faker.py 파일에서는 가격대 분포가 거의 동일하게 나왔다. ex) 1000개면 각각 200개씩. 이유를 정확하게 분석은 못했다...! 안되는 것만 확인을 해서 다른 방법을 모색했다.  </li>
</ul>
</li>
</ul>
<h3 id="❗-문제-해결-2">❗ 문제 해결</h3>
<ul>
<li><p>또 다른 시도!</p>
<pre><code class="language-python">def create_product_dataset(brand_class_list, categroy_class_list, num):

    product_class_list = []

    ...

    for i in range(1, num + 1):
        ...

        # 약 70% 20% 5% 3% 2% 비율로 max_value 지정
        if i &lt;= num * 0.6:
            price = fake.pyint(min_value=1000, max_value=100000, step=100)
        if num * 0.6 &lt; i &lt;= num * (0.6 + 0.3) + 1:
            price = fake.pyint(min_value=100000, max_value=500000, step=1000)
        if num * (0.6 + 0.3) + 1 &lt; i &lt;= num * (0.6 + 0.3 + 0.05):
            price = fake.pyint(min_value=500000, max_value=1000000, step=1000)
        if num * (0.6 + 0.3 + 0.05) &lt; i &lt;= num * (0.6 + 0.3 + 0.05 + 0.03):
            price = fake.pyint(min_value=1000000, max_value=5000000, step=1000)
        if num * (0.6 + 0.3 + 0.05 + 0.03) &lt; i &lt;= num * (0.6 + 0.3 + 0.05 + 0.03 + 0.02):
            price = fake.pyint(min_value=5000000, max_value=10000000, step=1000)

        ...

        product = Product(i, brand_id, name, thumbnail, category_id, price, amount, review_num, review_avg)
        product_class_list.append(product)

    return product_class_list

if __name__ == &quot;__main__&quot;:

    user_class_list = user_faker.create_user_dataset(50)

    brand_class_list = brand_faker.create_brand_dataset(user_class_list, 100)

    categroy_class_list = category_faker.create_catogory_dataset()

    product_class_list = create_product_dataset(brand_class_list, categroy_class_list, 1000)

    range_1000_to_100000_cnt = 0
    range_100000_to_500000_cnt = 0
    range_500000_to_1000000_cnt = 0
    range_1000000_to_5000000_cnt = 0
    range_5000000_to_10000000_cnt = 0
    for product in product_class_list:
        print(product)
        price = product.price
        if 1000 &lt;= price &lt;= 100000:
            range_1000_to_100000_cnt += 1
        if 100000 &lt; price &lt;= 500000:
            range_100000_to_500000_cnt += 1
        if 500000 &lt; price &lt;= 1000000:
            range_500000_to_1000000_cnt += 1
        if 1000000 &lt; price &lt;= 5000000:
            range_1000000_to_5000000_cnt += 1
        if 5000000 &lt; price &lt;= 10000000:
            range_5000000_to_10000000_cnt += 1

    print(&quot;range_1000_to_100000_cnt : &quot;, range_1000_to_100000_cnt)
    print(&quot;range_100000_to_500000_cnt : &quot;, range_100000_to_500000_cnt)
    print(&quot;range_500000_to_1000000_cnt : &quot;, range_500000_to_1000000_cnt)
    print(&quot;range_1000000_to_5000000_cnt : &quot;, range_1000000_to_5000000_cnt)
    print(&quot;range_5000000_to_10000000_cnt : &quot;, range_5000000_to_10000000_cnt)

    print(len(product_class_list))</code></pre>
<ul>
<li>해당 코드는 인위적으로 i를 기준으로 비율을
1000원 ~ 100,000원은 60%
100,000원 ~ 500,000원 30%
500,000 ~ 1,000,000원 5%
1,000,000원 ~ 5,000,000원 3%
5,000,0000원 ~ 10,000,000원 2%
이렇게 나눠서 구현했다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-14 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-14-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-14-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Sun, 14 Aug 2022 15:04:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li><p>더미 데이터를 생성하는 과정중에서 다음과 같은 문제점이 발생했다.</p>
<ol>
<li>Brand 클래스의 name(브랜드명)과 Prodcut클래스의 name(상품명)은 임의로 faker.name()으로 했음. 추후 변경 필요!
<del>2. Order 클래스의 createdAt도 임의의 날짜 데이터인데 최근 날짜로 변경이 필요</del></li>
<li>Product 클래스의 가격 범위가 1,000원~10,000,000원인데 범위가 너무 넓어서 수정이 좀 필요하지 않을까 싶음.</li>
</ol>
</li>
<li><p>해당 문제점들을 각각 해결해보자..!</p>
</li>
</ul>
<h2 id="1-order-클래스-createdat-최근-날짜로-변경-필요">1. Order 클래스 createdAt 최근 날짜로 변경 필요</h2>
<h3 id="🔍-문제점-파악">🔍 문제점 파악</h3>
<ul>
<li><p>문제가 되는(?) 코드</p>
<pre><code class="language-python">    for i in range(1, num + 1):
        user_class = fake.set_user_in_order()
        user_id = user_class.user_id
        product_class = fake.set_product_in_order()
        product_id = product_class.product_id
        amount = fake.pyint(min_value=1, max_value=10)
        product_price = product_class.price
        totalPrice = amount * product_price
        createdAt = fake.iso8601()</code></pre>
<ul>
<li>위 코드에서 createA에서 fake의 iso8601()메서드를 통해 생성하는데 생성하면 임의의 값이라서 1980년대까 등장한다..그래서 최근 1년 혹은 2년..? 정도의 데이터들만 생성하고 싶은 것이 문제다.</li>
</ul>
</li>
<li><p>faker 패키지 까보기</p>
<pre><code class="language-python">def iso8601(
        self,
        tzinfo: Optional[TzInfo] = None,
        end_datetime: Optional[DateParseType] = None,
        sep: str = &quot;T&quot;,
        timespec: str = &quot;auto&quot;,
    ) -&gt; str:
        &quot;&quot;&quot;
        Get a timestamp in ISO 8601 format (or one of its profiles).
        :param tzinfo: timezone, instance of datetime.tzinfo subclass
        :param sep: separator between date and time, defaults to &#39;T&#39;
        :param timespec: format specifier for the time part, defaults to &#39;auto&#39; - see datetime.isoformat() documentation
        :example: &#39;2003-10-21T16:05:52+0000&#39;
        &quot;&quot;&quot;
        return self.date_time(tzinfo, end_datetime=end_datetime).isoformat(sep, timespec)</code></pre>
<ul>
<li>위 코드는 faker패키지의 date_time관련 함수 중 내가 사용한 iso8601부분이다. return 값을 보면 date_time에서 isoformat(sep, timespec)이 있다. 즉, date_time 메서드를 단지 isoformat으로 변환하는 것일 뿐이다. 그러니 date_time도 까보자!</li>
<li><a href="https://github.com/joke2k/faker/blob/master/faker/providers/date_time/__init__.py">https://github.com/joke2k/faker/blob/master/faker/providers/date_time/__init__.py</a></li>
<li>헷갈릴법한 UTC와 ISO8601 관련 자료<ul>
<li><a href="https://ohgyun.com/416">https://ohgyun.com/416</a></li>
</ul>
</li>
</ul>
</li>
<li><p>date_time 메서드 까보기</p>
<pre><code class="language-python">def date_time(
        self,
        tzinfo: Optional[TzInfo] = None,
        end_datetime: Optional[DateParseType] = None,
    ) -&gt; datetime:
        &quot;&quot;&quot;
        Get a datetime object for a date between January 1, 1970 and now
        :param tzinfo: timezone, instance of datetime.tzinfo subclass
        :example: datetime(&#39;2005-08-16 20:39:21&#39;)
        :return: datetime
        &quot;&quot;&quot;
        # NOTE: On windows, the lowest value you can get from windows is 86400
        #       on the first day. Known python issue:
        #       https://bugs.python.org/issue30684
        return datetime(1970, 1, 1, tzinfo=tzinfo) + timedelta(seconds=self.unix_time(end_datetime=end_datetime))</code></pre>
<ul>
<li>까보니깐 결국 import한 date타임을 사용하고 있었는데 return값을 보아하니 1970-01-01이 시작날짜인 듯하다. 하지만 내부 함수를 내가 못건드리니..다른 함수를 봤는데 date_time_between 함수를 발견했다.</li>
</ul>
</li>
<li><p>date_time_between 사용법</p>
<pre><code class="language-python">def date_time_between(
        self,
        start_date: DateParseType = &quot;-30y&quot;,
        end_date: DateParseType = &quot;now&quot;,
        tzinfo: Optional[TzInfo] = None,
    ) -&gt; datetime:
        &quot;&quot;&quot;
        Get a datetime object based on a random date between two given dates.
        Accepts date strings that can be recognized by strtotime().
        :param start_date: Defaults to 30 years ago
        :param end_date: Defaults to &quot;now&quot;
        :param tzinfo: timezone, instance of datetime.tzinfo subclass
        :example: datetime(&#39;1999-02-02 11:42:52&#39;)
        :return: datetime
        &quot;&quot;&quot;
        start_date = self._parse_date_time(start_date, tzinfo=tzinfo)
        end_date = self._parse_date_time(end_date, tzinfo=tzinfo)
        if end_date - start_date &lt;= 1:
            ts = start_date + self.generator.random.random()
        else:
            ts = self.generator.random.randint(start_date, end_date)
        if tzinfo is None:
            return datetime(1970, 1, 1, tzinfo=tzinfo) + timedelta(seconds=ts)
        else:
            return (datetime(1970, 1, 1, tzinfo=tzutc()) + timedelta(seconds=ts)).astimezone(tzinfo)</code></pre>
<ul>
<li>파라미터에 String으로 &quot;-30y&quot;를 보니 2년 전부터 지금까지의 날짜를 생성하려면 &quot;-2y&quot;를 하면 되는 것 같다. 그래서 이를 isoformat으로 변경해주면 내가 원하는 값을 얻을 수 있을듯 하다.</li>
</ul>
</li>
</ul>
<h3 id="❗-문제-해결">❗ 문제 해결</h3>
<ul>
<li>기존 코드를 <code>fake.iso8601()</code>에서 <code>fake.date_time_between(start_date=&quot;-2y&quot;).isoformat(&quot;T&quot;, &quot;auto&quot;)</code>로 변경해줌으로써 해결!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[ Python Faker로 더미 데이터 생성 (2/2)]]></title>
            <link>https://velog.io/@do_ng_iill/Python-Faker%EB%A1%9C-%EB%8D%94%EB%AF%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-23</link>
            <guid>https://velog.io/@do_ng_iill/Python-Faker%EB%A1%9C-%EB%8D%94%EB%AF%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1-23</guid>
            <pubDate>Sat, 13 Aug 2022 13:22:04 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>더미 데이터를 어떻게 만드냐도 문제인데 연관관계까지 생각해서 저장을 해야하는데...</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/2d26aa93-7b0b-4b52-b6aa-317397e40b5c/image.png" alt=""></p>
<ul>
<li>생성해야하는 테이블의 순서 user -&gt; brand , category -&gt; product -&gt; order, restock_notification</li>
</ul>
<h2 id="1-user-테이블">1. user 테이블</h2>
<ul>
<li>user_id : 랜덤값 ❌, 1부터 증가</li>
<li>emial : 이메일 관련 랜덤값 =&gt; fake.ascii_free_email(), fake.free_email() 차이는?!</li>
<li>password : 랜덤값 ❌, 1234를 암호화</li>
<li>role : USER, ADMIN 랜덤인데 USER이 비중이 더 많이</li>
</ul>
<h2 id="2-brand-테이블">2. brand 테이블</h2>
<ul>
<li>brand_id : 랜덤값 ❌, 1부터 증가</li>
<li>name : 브랜드명 관련 랜덤값</li>
<li>user_id : 랜덤값 ❌, user 테이블에 있는 id 값 + role이 Admin인 경우!</li>
</ul>
<h2 id="3-category-테이블">3. category 테이블</h2>
<ul>
<li>category_id : 랜덤값 ❌, 1부터 증가</li>
<li>category : 랜덤값 ❌</li>
<li>parent_cateogry : 랜덤값 ❌, category 테이블에서 상위 category에 해당하는 ID값 + 최상위 category일 때는 null값</li>
</ul>
<p>👉🏻 결론적으로, category테이블은 미리 카테고리를 정해서 테이블을 생성하고 생성된 category의 id값을 랜덤하게 product 테이블에 배정해야겠다. 
<strong>ex)</strong></p>
<pre><code>{
    &quot;id&quot; : 1
    &quot;category&quot; : &quot;상의&quot;,
    &quot;parent_cateogry&quot; : null
}

{
    &quot;id&quot; : 2
    &quot;category&quot; : &quot;반팔 티셔츠&quot;,
    &quot;parent_cateogry&quot; : 1
}

...</code></pre><h2 id="4-product-테이블">4. product 테이블</h2>
<ul>
<li>product_id : 랜덤값 ❌, 1부터 증가</li>
<li>brand_id : 랜덤값 ❌, 브랜드 테이블에 있는 id 값</li>
<li>name : 옷 관련 랜덤값</li>
<li>thumbnail : 저작권 없는 이미지 주소 더미 데이터<ul>
<li>&quot;<a href="http://localhost:8080&quot;">http://localhost:8080&quot;</a> + fake.file_path(category=&#39;image&#39;, extension=&#39;png&#39;)</li>
</ul>
</li>
<li>category_id : 랜덤값 ❌, 카테고리 테이블에 있는 id 값</li>
<li>price : 1000원부터 10,000,000원까지 랜덤 값(단, 100원 단위 ex, 92,010원 ❌ 92,100원 ⭕)</li>
<li>amount : 0개 ~ 999개 랜덤값</li>
<li>review_num : 0개 ~ 50,000개 랜덤값</li>
<li>review_avg : 0점 ~ 5점(0.1단위) 랜덤값</li>
</ul>
<h2 id="5-order-테이블">5. order 테이블</h2>
<ul>
<li>order_id : 랜덤값 ❌, 1부터 증가</li>
<li>user_id : 랜덤값 ❌, user 테이블에 있는 id 값</li>
<li>product_id : 랜덤값 ❌, product 테이블에 있는 id 값</li>
<li>amount : 0개 ~ 10개 랜덤값</li>
<li>totalPrice : 랜덤값 ❌, product_id의 price의 값 * amount 값!</li>
<li>createdAt : LocalDateTime 랜덤값 =&gt; fake.iso8601()</li>
</ul>
<h2 id="6-restock_notification-테이블">6. restock_notification 테이블</h2>
<ul>
<li>restock_id : 랜덤값 ❌, 1부터 증가</li>
<li>user_id : 랜덤값 ❌, user 테이블에 있는 id 값</li>
<li>product_id : 랜덤값 ❌, product 테이블에 있는 id 값</li>
<li>alarm_flag : 랜덤값 ⭕, product_id의 amount값이 0일 경우에서 False, True 랜덤값</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-12~13 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-1213-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-1213-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Sat, 13 Aug 2022 13:21:48 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>더미 데이터를 만들기 위해 공부중이다. 진행하면서 문제점 어려운 점을 정리하고자 한다.</li>
</ul>
<h2 id="1-uniquenessexception">1. UniquenessException</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><pre><code>Traceback (most recent call last):
File &quot;user-data.py&quot;, line 32, in &lt;module&gt;
email = fake.unique.ascii_free_email()
File &quot;C:\Users\21528463\AppData\Local\Programs\Python\Python36\lib\site-packages\faker\proxy.py&quot;, line 320, in wrapper
raise UniquenessException(f&quot;Got duplicated values after {_UNIQUE_ATTEMPTS:,} iterations.&quot;)     
faker.exceptions.UniquenessException: Got duplicated values after 1,000 iterations.</code></pre></li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<pre><code class="language-python">from faker import Faker
from faker.providers import DynamicProvider
import time
</code></pre>
</li>
</ul>
<p>fake = Faker(&#39;ko_KR&#39;)</p>
<p>class User:
    &quot;&quot;&quot;
    User Class
    &quot;&quot;&quot;</p>
<pre><code>def __init__(self, id, email, role):
    self.user_id = id
    self.email = email
    self.role = role

def __str__(self):
    return &#39;User(id={0}, email={1}, role={2})&#39;.format(self.user_id, self.email, self.role)</code></pre><p>user_role_provider = DynamicProvider(
     provider_name=&quot;set_user_role&quot;,
     elements=[&quot;ADMIN&quot;, &quot;USER&quot;],
)</p>
<p>fake.add_provider(user_role_provider)</p>
<p>user_list = []</p>
<p>start = time.time()
for i in range(1, 10000000):
    email = fake.unique.ascii_free_email()
    role = fake.set_user_role()
    user = User(i,email, role)
    user_list.append(user)</p>
<p>print(&quot;time :&quot;, time.time() - start)  # 현재시각 - 시작시간 = 실행 시간
print(&quot;User 객체 생성 완료!&quot;) # 1000만 개</p>
<p>```</p>
<ul>
<li>오류 발생 이유<ul>
<li>email이 곧 아이디값이기 때문에 유니크한 값이어야만 한다. 그래서 코드에 보면 <code>fake.unique.ascii_free_email()</code>에 unique가 들어간다. 하지만 지금 1000만개를 돌다보니 <code>fake.unique.ascii_free_email()</code>에 더 이상 유니크한 값이 없어서 나는 에러인 것 같다.</li>
</ul>
</li>
<li>해결책<ul>
<li>아직 해결책은 없고...한 번 고민해봐야겠다...</li>
<li>1000만 개, 100만 개, 20만 개 에러 </li>
<li>10만 개 👉🏻 30.160502672195435초</li>
<li>15만 개 👉🏻 72.7302782535553초</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python Faker로 더미 데이터 생성 (1/2)]]></title>
            <link>https://velog.io/@do_ng_iill/Python-Faker%EB%A1%9C-%EB%8D%94%EB%AF%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@do_ng_iill/Python-Faker%EB%A1%9C-%EB%8D%94%EB%AF%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 11 Aug 2022 12:47:56 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li><p>항해99 프로젝트에서 대규모 데이터를 가지고 성능이 얼마나 나오고 이것을 얼마나 개선했는지를 보여주기로 팀원들과 계획했다. 프로젝트 주제는 의류 데이터였는데 무신O를 크롤링할까 봤는데 robots.txt를 보니 막혀있다... 포트폴리오에 괜히 문제가 생길까봐 일단 크롤링은 패스! 주제를 바꾸는 것도 고려하면서 공공 데이터도 뒤져봤지만 우리가 원하는 정도의 데이터는 없었다... 그래서 결국 파이썬 Faker를 사용해 더미데이터를 만드는 것으로 결론이 났다.</p>
</li>
<li><p>그래서 이번 포스팅은 Faker 공식 문서를 보고 번역(?)도하고 정리해보고자 한다! 사실 번역은 파파고가 한다.</p>
</li>
</ul>
<h2 id="faker">Faker</h2>
<blockquote>
<p>Faker is a Python package that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill-in your persistence to stress test it, or anonymize data taken from a production service, Faker is for you.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
Faker는 당신을 위해 가짜 데이터를 생성하는 파이썬 패키지입니다. 데이터베이스를 부트스트랩하거나, 보기 좋은 XML 문서를 만들거나, 끈기 있게 테스트하거나, 프로덕션 서비스에서 가져온 데이터를 익명화하거나, Faker가 좋습니다.</p>
</blockquote>
<ul>
<li>version : v13.15.1</li>
<li>Python Tests : passing</li>
<li>coverage : 99%</li>
<li>license : MIT</li>
</ul>
<h3 id="compatibility호환성">Compatibility(호환성)</h3>
<blockquote>
<p>tarting from version 4.0.0, Faker dropped support for Python 2 and from version 5.0.0 only supports Python 3.6 and above. If you still need Python 2 compatibility, please install version 3.0.1 in the meantime, and please consider updating your codebase to support Python 3 so you can enjoy the latest features Faker has to offer. Please see the extended docs for more details, especially if you are upgrading from version 2.0.4 and below as there might be breaking changes.</p>
</blockquote>
<blockquote>
<p>This package was also previously called fake-factory which was already deprecated by the end of 2016, and much has changed since then, so please ensure that your project and its dependencies do not depend on the old package.</p>
</blockquote>
<p><strong>번역</strong></p>
<blockquote>
<p>버전 4.0.0부터 페이커는 파이썬 2에 대한 지원을 중단했으며 버전 5.0.0부터는 파이썬 3.6 이상만 지원한다. 아직 파이썬 2 호환성이 필요하다면 버전 3.0.1을 설치하고, 페이커가 제공하는 최신 기능을 즐길 수 있도록 코드베이스를 업데이트해 파이썬 3을 지원하는 방안을 고려해 주시기 바랍니다. 특히 버전 2.0.4 이하에서 업그레이드하는 경우 변경 사항이 발생할 수 있으므로 자세한 내용은 확장 문서를 참조하십시오.</p>
</blockquote>
<blockquote>
<p>이 패키지는 2016년 말에 이미 폐지된 fake-factory라고도 불렸고, 그 이후로 많은 것이 바뀌었으니, 당신의 프로젝트와 그 의존성이 이전 패키지에 의존하지 않도록 확실히 해주시기 바랍니다.</p>
</blockquote>
<h3 id="basic-usage기본-사용법">Basic Usage(기본 사용법)</h3>
<ol>
<li><code>pip install Faker</code> : Faker 패키지 설치</li>
<li>기본적인 코드<pre><code class="language-python">from faker import Faker
fake = Faker()
</code></pre>
</li>
</ol>
<p>fake.name()</p>
<h1 id="lucy-cechtelar">&#39;Lucy Cechtelar&#39;</h1>
<p>fake.address()</p>
<h1 id="426-jordy-lodge">&#39;426 Jordy Lodge</h1>
<h1 id="cartwrightshire-sc-88120-6700">Cartwrightshire, SC 88120-6700&#39;</h1>
<p>fake.text()</p>
<h1 id="sint-velit-eveniet-rerum-atque-repellat-voluptatem-quia-rerum-numquam-excepturi">&#39;Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi</h1>
<h1 id="beatae-sint-laudantium-consequatur-magni-occaecati-itaque-sint-et-sit-tempore-nesciunt">beatae sint laudantium consequatur. Magni occaecati itaque sint et sit tempore. Nesciunt</h1>
<p>...</p>
<pre><code>
&gt; Each call to method fake.name() yields a different (random) result. This is because faker forwards faker.Generator.method_name() calls to faker.Generator.format(method_name).


&gt; **번역**
fake.name() 메서드에 호출할 때마다 다른(추적) 결과가 생성됩니다. faker가 faker를 포워드하기 때문이다.발전기.method_name은 페이커에 호출합니다.발전기.형식(https_name)을 지정합니다.

### Provider(제공자)
&gt; Each of the generator properties (like name, address, and lorem) are called “fake”. A faker generator has many of them, packaged in “providers”.

&gt; **번역**
각 제너레이터 속성(예: 이름, 주소 및 lorem???)은 &quot;fake&quot;라고 합니다. faker 제너레이터에는 &quot;제공자&quot;로 패키지된 많은 것들이 있다.

```python
from faker import Faker
from faker.providers import internet

fake = Faker()
fake.add_provider(internet)

print(fake.ipv4_private())
</code></pre><p><strong>결과값</strong></p>
<pre><code class="language-shell">192.168.232.8</code></pre>
<h3 id="localization">Localization</h3>
<blockquote>
<p>faker.Faker can take a locale as an argument, to return localized data. If no localized provider is found, the factory falls back to the default LCID string for US english, ie: en_US.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
faker.faker는 locale을 인수로 사용하여 지역화된 데이터를 반환할 수 있습니다. 현지화된 공급자가 없는 경우 공장에서는 미국 영어의 기본 LCID 문자열(즉, en_US)로 돌아갑니다.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker

fake = Faker(&#39;ko_KR&#39;)

for _ in range(10):
    print(fake.name())
</code></pre>
<p><strong>결과값</strong></p>
<pre><code class="language-shell">이준서
이서영
배우진
김지우
박성호
강상호
윤영일
박건우
성영철
백경숙</code></pre>
<blockquote>
<p>faker.Faker also supports multiple locales. New in v3.0.0.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
fake = Faker([&#39;it_IT&#39;, &#39;en_US&#39;, &#39;ja_JP&#39;])
for _ in range(10):
    print(fake.name())</code></pre>
<p><strong>결과값</strong></p>
<pre><code class="language-shell">鈴木 陽一
Leslie Moreno
Emma Williams
渡辺 裕美子
Marcantonio Galuppi
Martha Davis
Kristen Turner
中津川 春香
Ashley Castillo
山田 桃子</code></pre>
<h3 id="optimizations최적화">Optimizations(최적화)</h3>
<blockquote>
<p>The Faker constructor takes a performance-related argument called use_weighting. It specifies whether to attempt to have the frequency of values match real-world frequencies (e.g. the English name Gary would be much more frequent than the name Lorimer). If use_weighting is False, then all items have an equal chance of being selected, and the selection process is much faster. The default is True.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
Faker constructor는 use_weighting이라는 성능 관련 인수를 사용합니다. 값의 빈도가 실제 빈도와 일치하는지 여부를 지정합니다(예: 영어 이름 Gary라는 영어 이름이 Lorimer라는 이름보다 훨씬 더 자주 사용된다.) use_weighting이 False이면 모든 항목이 선택될 확률이 동일하고 선택 프로세스가 훨씬 빠릅니다. 기본값은 True입니다.</p>
</blockquote>
<h3 id="how-to-create-a-provider프로바이더-생성-방법">How to create a Provider(프로바이더 생성 방법)</h3>
<pre><code class="language-python">from faker import Faker
from faker.providers import BaseProvider

fake = Faker()

class MyProvider(BaseProvider):
    def foo(self) -&gt; str:
        return &#39;bar&#39;

fake.add_provider(MyProvider)

print(fake.foo())</code></pre>
<p><strong>결과값</strong></p>
<pre><code>bar</code></pre><h3 id="how-to-create-a-dynamic-provider동적-프로바이더-생성-방법">How to create a Dynamic Provider(동적 프로바이더 생성 방법)</h3>
<blockquote>
<p>Dynamic providers can read elements from an external source.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
동적 제공자들은 외부 자원에서 요소를 읽을 수 있습니다.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
from faker.providers import DynamicProvider

medical_professions_provider = DynamicProvider(
     provider_name=&quot;medical_profession&quot;,
     elements=[&quot;dr.&quot;, &quot;doctor&quot;, &quot;nurse&quot;, &quot;surgeon&quot;, &quot;clerk&quot;],
)

fake = Faker()

fake.add_provider(medical_professions_provider)

for i in range(10):
    print(fake.medical_profession())</code></pre>
<p><strong>결과값</strong></p>
<pre><code>surgeon
dr.
doctor
clerk
clerk
surgeon
dr.
dr.
nurse
clerk</code></pre><ul>
<li>BaseProvider와의 차이점은 class를 생성하지 않고 바로 사용할 수 있다는 점인 것 같다.</li>
<li>사용법 정리<ol>
<li><code>from faker.providers import DynamicProvider</code> : DynamicProvider 임포트</li>
<li>provider_name으로 프로바이더이름 설정 후 elements에 리스트로 값 할당 =&gt; 변수에 저장</li>
<li><code>fake.add_provider(Dynamic 프로바이더를 저장한 변수)</code>로 프로바이더 추가</li>
<li><code>fake.프로바이더명</code> 실제로 사용할 때는 DynamicProvider 내부에 설정한 프로바이더 명으로 사용한다.</li>
</ol>
</li>
</ul>
<h3 id="how-to-customize-the-lorem-provider로렘-제공자-커스텀-방법">How to customize the Lorem Provider(로렘 제공자 커스텀 방법)</h3>
<blockquote>
<p>You can provide your own sets of words if you don’t want to use the default lorem ipsum one. The following example shows how to do it with a list of words picked from cakeipsum</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
기본 로렘 입숨을 사용하지 않으려는 경우 사용자 고유의 단어 집합을 제공할 수 있습니다. 다음 예는 케이키썸에서 고른 단어 목록을 사용하여 그것을 하는 방법을 보여준다.</p>
</blockquote>
<ul>
<li><strong>로렘 입숨(lorem ipsum; 줄여서 립숨, lipsum)</strong>은 출판이나 그래픽 디자인 분야에서 폰트, 타이포그래피, 레이아웃 같은 그래픽 요소나 시각적 연출을 보여줄 때 사용하는 표준 채우기 텍스트로, 최종 결과물에 들어가는 실제적인 문장 내용이 채워지기 전에 시각 디자인 프로젝트 모형의 채움 글로도 이용된다. 때로 로렘 입숨은 <strong>공간만 차지하는 무언가</strong>를 지칭하는 용어로도 사용된다.</li>
</ul>
<pre><code class="language-python">from faker import Faker
fake = Faker()

my_word_list = [
&#39;danish&#39;,&#39;cheesecake&#39;,&#39;sugar&#39;,
&#39;Lollipop&#39;,&#39;wafer&#39;,&#39;Gummies&#39;,
&#39;sesame&#39;,&#39;Jelly&#39;,&#39;beans&#39;,
&#39;pie&#39;,&#39;bar&#39;,&#39;Ice&#39;,&#39;oat&#39; ]

fake.sentence()

for i in range(10):
    print(fake.sentence(ext_word_list=my_word_list))</code></pre>
<p><strong>결과값</strong></p>
<pre><code>Oat beans Lollipop Jelly.
Cheesecake Jelly pie.
Lollipop oat Jelly sesame cheesecake sesame wafer.
Beans sesame Ice sugar Jelly Lollipop cheesecake.
Gummies pie wafer bar Gummies Ice sugar.
Sesame cheesecake cheesecake oat.
Jelly Ice bar sugar Jelly.
Oat cheesecake Lollipop Lollipop Ice.
Ice Lollipop Gummies Gummies.
Pie wafer oat Ice Lollipop Lollipop bar Jelly.</code></pre><h3 id="how-to-use-with-factory-boyfactory-boy와-함께-사용하는-방법">How to use with Factory Boy(Factory Boy와 함께 사용하는 방법)</h3>
<blockquote>
<p>Factory Boy already ships with integration with Faker. Simply use the factory.Faker method of factory_boy:</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
Factory Boy는 이미 Faker와 통합된 상태로 출하됩니다. 그냥 <code>factory_boy</code>의 메서드인 <code>factory.Faker</code>를 이용하세요.</p>
</blockquote>
<pre><code>import factory
from myapp.models import Book

class BookFactory(factory.Factory):
    class Meta:
        model = Book

    title = factory.Faker(&#39;sentence&#39;, nb_words=4)
    author_name = factory.Faker(&#39;name&#39;)</code></pre><p><em><strong>- 이건 어떻게 쓰는지 잘 모르겠다..</strong></em></p>
<h3 id="accessing-the-random-instance랜덤-인스턴스-접근중">Accessing the random instance(랜덤 인스턴스 접근중..?)</h3>
<blockquote>
<p>The .random property on the generator returns the instance of random.Random used to generate the values:</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
제네레이터의 <code>.random</code> 프로퍼티는 값을 생성하는 데 사용되는 <code>random.Random</code> 인스턴스를 반환합니다. :</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
fake = Faker()
fake.random
fake.random.getstate()</code></pre>
<p><strong>결과값</strong></p>
<pre><code></code></pre><blockquote>
<p>By default all generators share the same instance of random.Random, which can be accessed with from faker.generator import random. Using this may be useful for plugins that want to affect all faker instances.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
기본적으로 모든 제네레이터들은 동일한 <code>random.Random</code> 인스턴스를 공유합니다. <code>from faker.generator import random.</code>로 액세스할 수 있습니다. 이 기능을 사용하면 모든 faker 인스턴스에 영향을 미치려는 플러그인에 유용할 수 있습니다.</p>
</blockquote>
<p><em><strong>- 이건 어떻게 쓰는지 잘 모르겠다..</strong></em></p>
<h3 id="unique-values유니크한-값들">Unique values(유니크한 값들)</h3>
<blockquote>
<p>Through use of the .unique property on the generator, you can guarantee that any generated values are unique for this specific instance.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
제네레이터에서 <code>.unique</code> 프로퍼티를 사용하면 생성된 값이 이 특정 인스턴스에 대해 고유함을 보장할 수 있습니다.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
fake = Faker()
names = [fake.unique.first_name() for i in range(500)]
assert len(set(names)) == len(names)</code></pre>
<ul>
<li>unique가 중간에 들어가면 유니크한 값들이기 때문에 중복되는 값이 없다.</li>
</ul>
<blockquote>
<p>Calling fake.unique.clear() clears the already seen values. Note, to avoid infinite loops, after a number of attempts to find a unique value, Faker will throw a UniquenessException. Beware of the birthday paradox, collisions are more likely than you’d think.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
<code>fake.unique.clear()</code>를 호출하면 이미 표시된 값이 지워집니다. 참고: 무한 루프를 피하기 위해 고유 값을 찾으려고 여러 번 시도한 후 Faker는 <code>UniquenessException</code>을 던집니다. 생일 역설에 주의하세요, 충돌은 여러분이 생각하는 것보다 더 가능성이 높습니다.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker

fake = Faker()
for i in range(3):
     # Raises a UniquenessException
     fake.unique.boolean()</code></pre>
<blockquote>
<p>In addition, only hashable arguments and return values can be used with .unique.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
또한 hashable 인수와 반환 값만 <code>.unique</code>와 함께 사용할 수 있습니다.</p>
</blockquote>
<ul>
<li>boolean은 true, false 2개만 있으니 unique로 3개를 출력하면 <code>UniquenessException</code>이 발생한다.</li>
</ul>
<h3 id="seeding-the-generator">Seeding the Generator</h3>
<blockquote>
<p>When using Faker for unit testing, you will often want to generate the same data set. For convenience, the generator also provide a seed() method, which seeds the shared random number generator. Calling the same methods with the same version of faker and seed produces the same results.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
단위 테스트에 Faker를 사용할 때 종종 동일한 데이터 셋을 생성하려고 할 수 있습니다. 또한 편의를 위해 제너레이터는 공유 난수 제너레이트를 심어주는(?) seed() 메서드를 제공합니다. 동일한 버전의 faker와 시드로 동일한 메서드를 호출하면 동일한 결과가 생성됩니다.</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
fake = Faker()
Faker.seed(4321)

print(fake.name())
# &#39;Margaret Boehm&#39;</code></pre>
<p><strong>결과값</strong></p>
<pre><code>Jason Brown</code></pre><ul>
<li>seed는 공유 난수를 심어준다는데 계속 print해보면 같은 값이 나온다. 공식문서에는 &#39;Margaret Boehm&#39;라고 주석되어있는데 실제 결과값은 &#39;Jason Brown&#39;이다.</li>
</ul>
<blockquote>
<p>Each generator can also be switched to its own instance of random.Random, separate to the shared one, by using the seed_instance() method, which acts the same way. For example:</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
각 제너레이터는 자체 <code>random.Random</code> 인스턴스로 전환할 수도 있습니다. 동일한 방식으로 작동하는 seed_instance() 메서드를 사용하여 공유된 것과 별개로 임의입니다. 예:</p>
</blockquote>
<pre><code class="language-python">from faker import Faker
fake = Faker()
fake.seed_instance(4321)

print(fake.name())
# &#39;Margaret Boehm&#39;</code></pre>
<p><strong>결과값</strong></p>
<pre><code>Jason Brown</code></pre><blockquote>
<p>Please note that as we keep updating datasets, results are not guaranteed to be consistent across patch versions. If you hardcode results in your test, make sure you pinned the version of Faker down to the patch number.</p>
</blockquote>
<blockquote>
<p>If you are using pytest, you can seed the faker fixture by defining a faker_seed fixture. Please check out the pytest fixture docs to learn more.</p>
</blockquote>
<blockquote>
<p><strong>번역</strong>
데이터 셋을 계속 업데이트하므로 패치 버전 간에 결과가 일관되게 유지되지는 않습니다. 테스트 결과를 하드코드한 경우 Faker 버전을 패치 번호에 고정했는지 확인하십시오.</p>
</blockquote>
<blockquote>
<p>pytest를 사용하는 경우 faker_seed 고정장치를 정의하여 페이커 고정장치를 시드할 수 있습니다. 자세한 내용을 보려면 pytest 고정 장치 문서를 확인하십시오.</p>
</blockquote>
<h2 id="standard-providers표준-제공자들">Standard Providers(표준 제공자들)</h2>
<ul>
<li>faker.providers</li>
<li>faker.providers.address</li>
<li>faker.providers.automotive</li>
<li>faker.providers.bank</li>
<li>faker.providers.barcode</li>
<li>faker.providers.color</li>
<li>faker.providers.company</li>
<li>faker.providers.credit_card</li>
<li>faker.providers.currency</li>
<li>faker.providers.date_time</li>
<li>faker.providers.file</li>
<li>faker.providers.geo</li>
<li>faker.providers.internet</li>
<li>faker.providers.isbn</li>
<li>faker.providers.job</li>
<li>faker.providers.lorem</li>
<li>faker.providers.misc</li>
<li>faker.providers.person</li>
<li>faker.providers.phone_number</li>
<li>faker.providers.profile</li>
<li>faker.providers.python</li>
<li>faker.providers.ssn</li>
<li>faker.providers.user_agent</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-11 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-11-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-11-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Thu, 11 Aug 2022 10:28:01 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>항해99 실전 프로젝트를 진행하면서 실전 프로젝트팀이 아닌 스프링팀에 지원해서 프로젝트 진행중이다. CI/CD에 대해 이야기만 들었었는데 다른 팀의 한 분이 Jenkins를 할 줄 아셔서 특강을 해주셨다. 2번이나 해주셨는데 1번은 일이 생겨 잠시 못따라가다가 못하고 다른 1번은 서울에 물난리때문에 또 못들었다. 정말 다행인게 그 분이 노션에 정리한 내용을 공유해주어서 노션을 보고 따라했다.</li>
</ul>
<h2 id="❗-error">❗ Error</h2>
<ul>
<li><p>다 따라했는데 배포가 되질 않아서 문제점들을 분석했다. 가장 처음에 발생한 문제는 Jenkins의 아이템 설정부분 Build 탭이에서 <code>Send files or execute commands over SSH</code> 설정에서 폴더명과 파일명이 맞지 않았다.</p>
<ul>
<li><code>Send files or execute commands over SSH</code> : ssh 방법으로 빌드 스크립트를 Spring 서버에 보내주겠다는 설정</li>
</ul>
</li>
<li><p>설정 부분을 내 폴더와 파일명에 맞게 수정했음에도 오류가 나서 Test 서버인 EC2서버에 연결해서 내부를 확인해봤다. 확인 결과 Jenkins 서버와 Test 서버 모두 연동이 잘 되어있었고 git pull까지는 잘 되어있는 상황이었다.</p>
</li>
<li><p>문제는 <code>sudo: ./gradlew: command not found</code>에러로 빌드가 진행되지 않았다. 여러 해결책을 찾아봤지만 잘 나오지 않았고 같은 팀원분께 도움을 요청했다. 👉🏻 <code>sudo chmod 777 ./gradlew</code>를 통해 권한 설정을 해주었고 <code>./gradlew</code>명령어를 사용할 수 있게 되었다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-09~10 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-09-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-09-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Wed, 10 Aug 2022 10:12:14 GMT</pubDate>
            <description><![CDATA[<h2 id="📍-intro">📍 Intro</h2>
<ul>
<li>어제부터 TDD를 진행하는데 쉽지않다...일단 다른 조 상황을 보니 통합테스트 코드만 짰길래 나도 우선적으로 통합테스트 코드를 작성 후에 &quot;망나니 개발자&quot;님의 &quot;[Spring] TDD로 멤버십 등록 API 구현 예제 - (3/5)&quot; 포스팅을 보면서 진행중이다.</li>
</ul>
<h2 id="1-repository-test-코드-작성하다가-발생한-오류">1. Repository Test 코드 작성하다가 발생한 오류</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><pre><code class="language-shell">Failed to load ApplicationContext
java.lang.IllegalStateException: Failed to load ApplicationContext
...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name &#39;productRepositoryImpl&#39; defined in file [C:\Users\21528463\IdeaProjects\mocosa\build\classes\java\main\com\hanghae99\mocosa\layer\repository\ProductRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type &#39;com.querydsl.jpa.impl.JPAQueryFactory&#39; available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type &#39;com.querydsl.jpa.impl.JPAQueryFactory&#39; available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
...</code></pre>
</li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class ProductRepositoryImpl implements ProductRepositoryCustom {

  private final JPAQueryFactory queryFactory;
}</code></pre>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>사실 정확하게 이유를 모르겠지만 오류창을 보고 대충(?) ProductRepositoryImpl의 JPAQueryFactory 때문이라는 정도만 알겠다.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li><code>private final JPAQueryFactory queryFactory;</code> 지워보자!</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">@RequiredArgsConstructor
public class ProductRepositoryImpl implements ProductRepositoryCustom {
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>![](https://velog.velcdn.com/images/do_ng_iill/post/eae6af4c-3430-492b-8c7a-f825529561a1/image.png)

## 2. 
- 발생한 오류
  + `java.lang.IllegalStateException: No primary or single unique constructor found for interface java.awt.print.Pageable`
- 오류가 발생한 코드
```java
    @GetMapping(&quot;/api/search&quot;)
    @ResponseBody
    public Slice&lt;SearchResponseDto&gt; searchProduct(Pageable pageable,
                                                  @RequestParam(required = false, defaultValue = &quot;리뷰순&quot;) String sort,
                                                  @RequestParam(required = false, defaultValue = &quot;전체&quot;) String categoryFilter,
                                                  @RequestParam(required = false, defaultValue = &quot;0&quot;) int minPriceFilter,
                                                  @RequestParam(required = false, defaultValue = &quot;2147483647&quot;) int maxPriceFilter,
                                                  @RequestParam(required = false, defaultValue = &quot;0&quot;) int reviewFilter,
                                                  @RequestParam String keyword) {
        return productService.searchProduct(pageable, sort, categoryFilter, minPriceFilter, maxPriceFilter, reviewFilter);
    }</code></pre><ul>
<li>오류 발생 이유<ul>
<li>sort가 &quot;리뷰순&quot;이라서?</li>
</ul>
</li>
<li>해결책<ul>
<li>Pageable대신 그냥 page만 받아서 내부적으로 처리하기</li>
</ul>
</li>
<li>오류 해결 후 코드<pre><code class="language-java">  @GetMapping(&quot;/api/search&quot;)
  @ResponseBody
  public Slice&lt;SearchResponseDto&gt; searchProduct(@RequestParam(required = false, defaultValue = &quot;0&quot;) int page,
                                                @RequestParam(required = false, defaultValue = &quot;리뷰순&quot;) String sort,
                                                @RequestParam(required = false, defaultValue = &quot;전체&quot;) String categoryFilter,
                                                @RequestParam(required = false, defaultValue = &quot;0&quot;) int minPriceFilter,
                                                @RequestParam(required = false, defaultValue = &quot;2147483647&quot;) int maxPriceFilter,
                                                @RequestParam(required = false, defaultValue = &quot;0&quot;) int reviewFilter,
                                                @RequestParam String keyword) {
      return productService.searchProduct(page, sort, categoryFilter, minPriceFilter, maxPriceFilter, reviewFilter);
  }</code></pre>
</li>
<li>오류를 해결하면서 참고한 자료<ul>
<li><a href="https://www.javafixing.com/2022/05/fixed-no-primary-or-default-constructor.html">https://www.javafixing.com/2022/05/fixed-no-primary-or-default-constructor.html</a></li>
</ul>
</li>
</ul>
<h2 id="3-통합테스트에서-실제-서버-구동한-후-결과와-다르게-401-unauthorized-에러-발생">3. 통합테스트에서 실제 서버 구동한 후 결과와 다르게 &lt;401 UNAUTHORIZED&gt; 에러 발생</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><code>expected: &lt;400 BAD_REQUEST&gt; but was: &lt;401 UNAUTHORIZED&gt;</code></li>
</ul>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>Spring Security를 프로젝트 시작할 때 gradle에 추가해줘서 발생하는 오류 같다.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li>gradle에서 Spring Security관련</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">dependencies {
  implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
//    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
  implementation &#39;org.springframework.boot:spring-boot-starter-thymeleaf&#39;
  implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
  implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity5&#39;
  compileOnly &#39;org.projectlombok:lombok&#39;
  runtimeOnly &#39;com.h2database:h2&#39;
  runtimeOnly &#39;mysql:mysql-connector-java&#39;
  annotationProcessor &#39;org.projectlombok:lombok&#39;
  testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
  testImplementation &#39;org.springframework.security:spring-security-test&#39;

  // 테스트 코드를 위한 Lombok 라이브러리
  testCompileOnly &#39;org.projectlombok:lombok:1.18.12&#39;
  testAnnotationProcessor &#39;org.projectlombok:lombok:1.18.12&#39;

  // 3. querydsl dependencies 추가
  implementation &quot;com.querydsl:querydsl-jpa:${queryDslVersion}&quot;
  implementation &quot;com.querydsl:querydsl-apt:${queryDslVersion}&quot;
}</code></pre>
</li>
<li><p>오류를 해결하면서 참고한 자료</p>
<ul>
<li>[Error]SpringBoot에서 401 UNAUTHORIZED<ul>
<li><a href="https://hihellosuah.tistory.com/92">https://hihellosuah.tistory.com/92</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="4-type-definition-error">4. Type definition error</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li>```
Type definition error: [simple type, class com.hanghae99.mocosa.integration.ProductIntegrationTest$SearchResponseDtos]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of <code>com.hanghae99.mocosa.integration.ProductIntegrationTest$SearchResponseDtos</code> (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)</li>
</ul>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>콘솔 창을 보고 잘 이해는 안갔지만 해결해나가는 과정에서 에러 코드의 의미가 Dto 설정 문제였던 것 같음.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li>@Setter를 추가해준다.</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(&quot;test&quot;)
public class ProductIntegrationTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  @DisplayName(&quot;검색에 성공한 케이스 - 필터 제외&quot;)
  public void case1(){
      //given
      String keyword = &quot;무탠다드&quot;;

      //when
      ResponseEntity&lt;SearchResponseDtoList&gt; response = restTemplate
              .getForEntity(
                      &quot;/api/search?keyword=&quot;+keyword,
                      SearchResponseDtoList.class
              );

      //then
      assertEquals(HttpStatus.OK, response.getStatusCode());
      SearchResponseDtoList responseBody = response.getBody();
      assertNotNull(responseBody);

      assertEquals(
              1L
              ,responseBody.content.get(0).productId);
      assertEquals(
              &quot;릴렉스 핏 크루 넥 반팔 티셔츠&quot;
              ,responseBody.content.get(0).name);
      assertEquals(
              &quot;image.png&quot;
              ,responseBody.content.get(0).thumbnail);
      assertEquals(
              &quot;무신사 스탠다드&quot;
              ,responseBody.content.get(0).brandName);
      assertEquals(
              &quot;상의&quot;
              ,responseBody.content.get(0).category);
      assertEquals(
              10690
              ,responseBody.content.get(0).price);
      assertEquals(
              100
              ,responseBody.content.get(0).amount);
      assertEquals(
              69058
              ,responseBody.content.get(0).reviewNum);
      assertEquals(
              4.8
              ,responseBody.content.get(0).reviewAvg);
  }

  @Getter
  @Setter
  @Builder
  static class SearchResponseDtoList {
      private List&lt;SearchResponseDto&gt; content;
  }

</code></pre>
</li>
</ul>
<pre><code>@Getter
@Setter
@Builder
static class SearchResponseDto{
    private Long productId;
    private String name;
    private String thumbnail;
    private String brandName;
    private String category;
    private int price;
    private int amount;
    private int reviewNum;
    private float reviewAvg;
}</code></pre><p>}</p>
<pre><code>- 오류 해결 후 코드
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(&quot;test&quot;)
public class ProductIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @DisplayName(&quot;검색에 성공한 케이스 - 필터 제외&quot;)
    public void case1(){
        //given
        String keyword = &quot;무탠다드&quot;;

        //when
        ResponseEntity&lt;SearchResponseDtoList&gt; response = restTemplate
                .getForEntity(
                        &quot;/api/search?keyword=&quot; + keyword,
                        SearchResponseDtoList.class
                );

        //then
        assertEquals(HttpStatus.OK, response.getStatusCode());
        List&lt;SearchResponseDto&gt; responseBody = response.getBody().content;
        assertNotNull(responseBody);

        assertEquals(
                1L
                ,responseBody.content.get(0).productId);
        assertEquals(
                &quot;릴렉스 핏 크루 넥 반팔 티셔츠&quot;
                ,responseBody.content.get(0).name);
        assertEquals(
                &quot;image.png&quot;
                ,responseBody.content.get(0).thumbnail);
        assertEquals(
                &quot;무신사 스탠다드&quot;
                ,responseBody.content.get(0).brandName);
        assertEquals(
                &quot;상의&quot;
                ,responseBody.content.get(0).category);
        assertEquals(
                10690
                ,responseBody.content.get(0).price);
        assertEquals(
                100
                ,responseBody.content.get(0).amount);
        assertEquals(
                69058
                ,responseBody.content.get(0).reviewNum);
        assertEquals(
                4.8
                ,responseBody.content.get(0).reviewAvg);
    }

    @Getter
    @Setter
    @Builder
    static class SearchResponseDtoList {
        private List&lt;SearchResponseDto&gt; content;
    }


    @Getter
    @Setter
    @Builder
    static class SearchResponseDto{
        private Long productId;
        private String name;
        private String thumbnail;
        private String brandName;
        private String category;
        private int price;
        private int amount;
        private int reviewNum;
        private float reviewAvg;
    }

}</code></pre><ul>
<li>다른 부분이 //then 아래 2번째줄이다. 오류코드는 <code>SearchResponseDtoList responseBody = response.getBody();</code>이고 해결한 코드는 <code>List&lt;SearchResponseDto&gt; responseBody = response.getBody().content;</code> 이렇게 반환 타입을 잘못 작성해서 난 오류였다. 다음부터는 반환타입을 잘 생각해서 코드를 작성하도록 하자!</li>
<li>오류를 해결하면서 참고한 자료<ul>
<li>팀원의 다른 코드</li>
</ul>
</li>
</ul>
<h2 id="5-통합테스트에서-부동소수점-문제">5. 통합테스트에서 부동소수점 문제</h2>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/3452d32f-3819-47cb-b855-630e7e1d9614/image.png" alt=""></p>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><pre><code>org.opentest4j.AssertionFailedError:
Expected :4.8
Actual : 4.800000190734863</code></pre></li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<pre><code class="language-java">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(&quot;test&quot;)
public class ProductIntegrationTest {

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  @DisplayName(&quot;검색에 성공한 케이스 - 필터 제외&quot;)
  public void case1(){
      //given
      String keyword = &quot;무탠다드&quot;;

      //when
      ResponseEntity&lt;SearchResponseDtoList&gt; response = restTemplate
              .getForEntity(
                      &quot;/api/search?keyword=&quot; + keyword,
                      SearchResponseDtoList.class
              );

      //then
      assertEquals(HttpStatus.OK, response.getStatusCode());
      List&lt;SearchResponseDto&gt; responseBody = response.getBody().content;
      assertNotNull(responseBody);

      assertEquals(
              1L
              ,responseBody.content.get(0).productId);
      assertEquals(
              &quot;릴렉스 핏 크루 넥 반팔 티셔츠&quot;
              ,responseBody.content.get(0).name);
      assertEquals(
              &quot;image.png&quot;
              ,responseBody.content.get(0).thumbnail);
      assertEquals(
              &quot;무신사 스탠다드&quot;
              ,responseBody.content.get(0).brandName);
      assertEquals(
              &quot;상의&quot;
              ,responseBody.content.get(0).category);
      assertEquals(
              10690
              ,responseBody.content.get(0).price);
      assertEquals(
              100
              ,responseBody.content.get(0).amount);
      assertEquals(
              69058
              ,responseBody.content.get(0).reviewNum);
      assertEquals(
              4.8
              ,responseBody.content.get(0).reviewAvg);
  }
}</code></pre>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>기대값은 4.8인데 response값이 4.800000190734863이라서 부동소수점 문제를 가지고 있음.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li>테스트진행 할 때 TOLERRANCE 상수를 둬서 assertEquals에 해당 상수를 추가해주면 테스트에 통과할 수 있다.</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(&quot;test&quot;)
public class ProductIntegrationTest {

  static final double TOLERANCE = 0.001;

  @Autowired
  private TestRestTemplate restTemplate;

  @Test
  @DisplayName(&quot;검색에 성공한 케이스 - 필터 제외&quot;)
  public void case1(){
      //given
      String keyword = &quot;무탠다드&quot;;

      //when
      ResponseEntity&lt;SearchResponseDtoList&gt; response = restTemplate
              .getForEntity(
                      &quot;/api/search?keyword=&quot; + keyword,
                      SearchResponseDtoList.class
              );

      //then
      assertEquals(HttpStatus.OK, response.getStatusCode());
      List&lt;SearchResponseDto&gt; responseBody = response.getBody().content;
      assertNotNull(responseBody);

      assertEquals(
              1L
              ,responseBody.content.get(0).productId);
      assertEquals(
              &quot;릴렉스 핏 크루 넥 반팔 티셔츠&quot;
              ,responseBody.content.get(0).name);
      assertEquals(
              &quot;image.png&quot;
              ,responseBody.content.get(0).thumbnail);
      assertEquals(
              &quot;무신사 스탠다드&quot;
              ,responseBody.content.get(0).brandName);
      assertEquals(
              &quot;상의&quot;
              ,responseBody.content.get(0).category);
      assertEquals(
              10690
              ,responseBody.content.get(0).price);
      assertEquals(
              100
              ,responseBody.content.get(0).amount);
      assertEquals(
              69058
              ,responseBody.content.get(0).reviewNum);
      assertEquals(
              4.8
              ,responseBody.content.get(0).reviewAvg, TOLERANCE);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/c6f342b0-f646-4957-9956-6608e07d5247/image.png" alt=""></p>
</li>
<li><p>오류를 해결하면서 참고한 자료</p>
<ul>
<li>[자바 코딩의 기술] 6장 : 올바르게 드러내기<ul>
<li><a href="https://yunanp.tistory.com/34">https://yunanp.tistory.com/34</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>[Spring] TDD로 멤버십 등록 API 구현 예제 - (3/5)<ul>
<li><a href="https://mangkyu.tistory.com/184">https://mangkyu.tistory.com/184</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDsl 사용법]]></title>
            <link>https://velog.io/@do_ng_iill/QueryDsl-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@do_ng_iill/QueryDsl-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Mon, 08 Aug 2022 08:41:08 GMT</pubDate>
            <description><![CDATA[<h1 id="querydsl">QueryDsl?</h1>
<h2 id="querydsl-사용하는-이점">QueryDsl 사용하는 이점</h2>
<ul>
<li>IDE를 통한 자동완성 기능</li>
<li>컴파일 에러가 발생</li>
<li>조건문을 사용한 동적 쿼리문 작성이 간편</li>
<li>코드의 재사용성 증가</li>
</ul>
<h2 id="querydsl-사용하는-단점">QueryDsl 사용하는 단점</h2>
<ul>
<li>까다롭다</li>
</ul>
<h3 id="간단한-사용예시">간단한 사용예시</h3>
<blockquote>
<p>사용 예시</p>
</blockquote>
<p>1) query문을 작성하려면 JpaQuery 인스턴스가 필요합니다. 이를 위해 JpaQueryFactory를 통해 인스턴스를 생성해야 합니다. 먼저 JpaQueryFactory를 영속성 컨텍스트를 파라미터로 넘겨서 생성합니다.(Querydsl은 JPA API를 사용하며 JPA를 지원하는 모듈입니다.)
<code>JpaQueryFactory queryFactory = new JpaQueryFactory(em);</code>
2) 사용하려는 QEntity를 생성합니다. 
<code>QItem qItem = QItem.item;</code>  -&gt; QItem은 정적 메소드로 만들어져 있는 인스턴스를 가져옵니다.
3) 쿼리문 작성
<code>JpaQuery&lt;Item&gt; query = queryFacotory.selectFrom(qItem)......where....조건문;</code>
4) 쿼리 결과 반환
<code>List&lt;Item&gt; items = query.fetch();</code></p>
<p>출처 : 쿼리 메소드, JPQL, Querydsl 요약 - <a href="https://velog.io/@simgyuhwan/%EC%BF%BC%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C-JPQL-Querydsl-%EC%9A%94%EC%95%BD">https://velog.io/@simgyuhwan/%EC%BF%BC%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C-JPQL-Querydsl-%EC%9A%94%EC%95%BD</a></p>
<h2 id="get-strated">Get Strated</h2>
<h3 id="1-buildgradle에서-querydsl-설정-방법">1. build.gradle에서 Querydsl 설정 방법</h3>
<ul>
<li><a href="https://data-make.tistory.com/728">https://data-make.tistory.com/728</a></li>
</ul>
<h3 id="2">2.</h3>
<ul>
<li>Spring Boot Data Jpa 프로젝트에 Querydsl 적용하기<ul>
<li><a href="https://jojoldu.tistory.com/372">https://jojoldu.tistory.com/372</a></li>
</ul>
</li>
</ul>
<h1 id="예제를-통해-querydsl-공부">예제를 통해 QueryDsl 공부</h1>
<h2 id="projection-사용하기">Projection 사용하기</h2>
<blockquote>
<p><strong>프로젝션(Projection)은 select 절에서 어떤 컬럼들을 조회할지 대상을 지정하는 것을 말한다.</strong></p>
</blockquote>
<ul>
<li>[Querydsl] Projection &amp; 결과 매핑<ul>
<li><a href="https://jaime-note.tistory.com/75">https://jaime-note.tistory.com/75</a></li>
</ul>
</li>
<li>20201009 [jpa] querydsl 내, query projection 사용해보기<ul>
<li><a href="https://pasudo123.tistory.com/431">https://pasudo123.tistory.com/431</a></li>
</ul>
</li>
<li>[querydsl] querydsl에서 projection 다루기<ul>
<li><a href="https://devkingdom.tistory.com/253">https://devkingdom.tistory.com/253</a></li>
</ul>
</li>
</ul>
<h2 id="서브쿼리-사용하기">서브쿼리 사용하기</h2>
<ul>
<li>[Database]서브 쿼리(MySQL)<ul>
<li><a href="https://sskl660.tistory.com/69">https://sskl660.tistory.com/69</a></li>
</ul>
</li>
<li>[Querydsl] 서브 쿼리(Subquery), Case(when, then), 상수(Constant), concat<ul>
<li><a href="https://jaime-note.tistory.com/74">https://jaime-note.tistory.com/74</a></li>
</ul>
</li>
<li>QueryDSL 서브 쿼리 사용법<ul>
<li><a href="https://icarus8050.tistory.com/6">https://icarus8050.tistory.com/6</a></li>
</ul>
</li>
<li>[Querydsl] 서브쿼리 사용하기<ul>
<li><a href="https://jojoldu.tistory.com/379">https://jojoldu.tistory.com/379</a></li>
</ul>
</li>
</ul>
<h2 id="case-when-사용하기">Case When 사용하기</h2>
<ul>
<li>[Querydsl] Case When 사용하기<ul>
<li><a href="https://jojoldu.tistory.com/401">https://jojoldu.tistory.com/401</a></li>
</ul>
</li>
<li>CASE 기초 문법 뽀개기<ul>
<li><a href="https://z-hwan.tistory.com/entry/CASE-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95-%EB%BD%80%EA%B0%9C%EA%B8%B0">https://z-hwan.tistory.com/entry/CASE-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95-%EB%BD%80%EA%B0%9C%EA%B8%B0</a></li>
</ul>
</li>
</ul>
<h2 id="n1-문제-해">N+1 문제 해</h2>
<ul>
<li>[JPA] 관심 카테고리 게시글 + 좋아요 Querydsl로 한방 쿼리 만들기 (N+1 문제 해결)<ul>
<li><a href="https://loosie.tistory.com/792">https://loosie.tistory.com/792</a></li>
</ul>
</li>
</ul>
<h2 id="좋아요-기능-구현">좋아요 기능 구현...</h2>
<ul>
<li>게시글 리스트에서 &#39;좋아요&#39; 버튼을 눌렀는지 판별<ul>
<li><a href="https://hashcode.co.kr/questions/1697/%EA%B2%8C%EC%8B%9C%EA%B8%80-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B2%84%ED%8A%BC%EC%9D%84-%EB%88%8C%EB%A0%80%EB%8A%94%EC%A7%80-%ED%8C%90%EB%B3%84">https://hashcode.co.kr/questions/1697/%EA%B2%8C%EC%8B%9C%EA%B8%80-%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B2%84%ED%8A%BC%EC%9D%84-%EB%88%8C%EB%A0%80%EB%8A%94%EC%A7%80-%ED%8C%90%EB%B3%84</a></li>
</ul>
</li>
<li>Springboot + JPA + Querydsl로 좋아요 기능 만들기 1 - 등록<ul>
<li><a href="https://coco-log.tistory.com/133">https://coco-log.tistory.com/133</a></li>
</ul>
</li>
</ul>
<h2 id="use-dynamic-query">Use Dynamic Query</h2>
<ul>
<li>QueryDSL(2) - 쿼리 생성 방법, 기본 문법<ul>
<li><a href="https://ykh6242.tistory.com/107">https://ykh6242.tistory.com/107</a></li>
</ul>
</li>
<li>Querydsl 동적 쿼리<ul>
<li><a href="https://velog.io/@aidenshin/Querydsl-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC">https://velog.io/@aidenshin/Querydsl-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC</a></li>
</ul>
</li>
<li>Querydsl 동적쿼리<ul>
<li><a href="https://escapefromcoding.tistory.com/620">https://escapefromcoding.tistory.com/620</a></li>
</ul>
</li>
<li>[Querydsl] 다이나믹 쿼리 사용하기<ul>
<li><a href="https://jojoldu.tistory.com/394">https://jojoldu.tistory.com/394</a></li>
</ul>
</li>
<li>Spring querydsl 동적쿼리 Or 처리 할때<ul>
<li><a href="https://webstorage.tistory.com/2">https://webstorage.tistory.com/2</a></li>
</ul>
</li>
<li>Querydsl - where절을 이용한 동적 쿼리와 성능 최적화<ul>
<li><a href="https://jddng.tistory.com/343">https://jddng.tistory.com/343</a></li>
</ul>
</li>
</ul>
<h2 id="use-join">Use Join</h2>
<ul>
<li>querydsl join<ul>
<li><a href="https://hjhng125.github.io/querydsl/querydsl-join/">https://hjhng125.github.io/querydsl/querydsl-join/</a></li>
</ul>
</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>[QueryDSL] QueryDSL 시작하기<ul>
<li><a href="https://heekng.tistory.com/159">https://heekng.tistory.com/159</a></li>
</ul>
</li>
<li>[QueryDSL] QueryDSL 기본문법<ul>
<li><a href="https://heekng.tistory.com/160?category=1070077">https://heekng.tistory.com/160?category=1070077</a></li>
</ul>
</li>
<li>[Querydsl] 기본문법 학습하기<ul>
<li><a href="https://velog.io/@shlee327/Querydsl-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95-%ED%95%99%EC%8A%B5%ED%95%98%EA%B8%B0">https://velog.io/@shlee327/Querydsl-%EA%B8%B0%EB%B3%B8%EB%AC%B8%EB%B2%95-%ED%95%99%EC%8A%B5%ED%95%98%EA%B8%B0</a></li>
</ul>
</li>
<li>[SPRING] JPA의 영속성 컨텍스트<ul>
<li><a href="https://jaeho214.tistory.com/73">https://jaeho214.tistory.com/73</a></li>
</ul>
</li>
<li>Querydsl - 레퍼런스 문서<ul>
<li><a href="http://querydsl.com/static/querydsl/3.4.3/reference/ko-KR/html_single/#d0e1913">http://querydsl.com/static/querydsl/3.4.3/reference/ko-KR/html_single/#d0e1913</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[항해99] 49일이 지난 후 회고]]></title>
            <link>https://velog.io/@do_ng_iill/%ED%95%AD%ED%95%B499-49%EC%9D%BC%EC%9D%B4-%EC%A7%80%EB%82%9C-%ED%9B%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@do_ng_iill/%ED%95%AD%ED%95%B499-49%EC%9D%BC%EC%9D%B4-%EC%A7%80%EB%82%9C-%ED%9B%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 07 Aug 2022 16:19:08 GMT</pubDate>
            <description><![CDATA[<h1 id="📍-intro">📍 Intro</h1>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/68278911-5020-4dcb-8424-fbfb7ce59157/image.png" alt=""></p>
<ul>
<li>항해99를 시작한지 벌써 49일차..! 내일이면 50일차고 그럼 99일의 반을 지나는 시점이다. 1주차때 Flask로 미니 프로젝트를 하던 때가 엊그제같은데 벌써 Spring으로 프로젝트를 미니 프로젝트 주차에 1개, 클론코딩 주차에 1개 총 2개나 진행했다. 저번주에 정신이 없어서 미니프로젝트 주차 회고를 못했는데 이번에 클론코딩 주차 회고와 같이 이어서 회고해야겠다.</li>
</ul>
<h2 id="1-미니-프로젝트-주차-회고">1. 미니 프로젝트 주차 회고</h2>
<ul>
<li><p>주특기 입문, 숙련, 심화 3주가 지나고 처음으로 팀 프로젝트인 미니 프로젝트 주차가 시작되었다. 리액트를 사용하는 프론트엔드 2명과 나 포함 백엔드 3명이 한 팀을 이뤘다. 1주차 미니프로젝트를 했던 것 처럼 첫날 와이어프레임 작성, API 설계, DB 설계를 진행했다. 항상 첫날이 가장 바쁜 것 같다.</p>
<ul>
<li><a href="https://www.notion.so/HeyYo-a60e126e39594243b9839e14398e3f76#f34ce2cb86994be788869c61038295ab">https://www.notion.so/HeyYo-a60e126e39594243b9839e14398e3f76#f34ce2cb86994be788869c61038295ab</a></li>
</ul>
</li>
<li><p>이번에 배정된 팀에서는 모두 항해99를 하면서 처음 만나는 팀원들이었다. 역시 처음엔 낯설어서 오전에는 서로 자기소개를 하면서 어색함을 풀어나갔고 간단하게 어떤 프로젝트를 진행할지에 대해 이야기를 나눴다. 프로젝트는 민주주의적으로 여러 의견을 내고 카카오톡 단톡방을 설계해서 투표로 정해졌다. 최종적으로 결정된 프로젝트는 &quot;공동구매, 배달, 공연/전시회 등 같이 할 사람을 모집하는 서비스&quot;였다.</p>
</li>
<li><p>오후에는 API설계부터 DB설계까지 진행했는데 API 설계를 오랜만에 하다보니 response에는 어떤게 들어가고 request에는 어떤 값들을 던져줘야하지..막막했다. 그래도 팀원들과 같이 부족한 부분들을 채워가다보니 나름 그럴싸해졌다. 그리고 1주일치 프로젝트라서 DB는 크게 복잡하지 않아서 수월하게 해나갔다.</p>
</li>
<li><p>2일차부터는 프로젝트를 생성하고 초기에 셋팅해야하는 Model, Repository, Controller, Service, Dto같은 패키지와 로그인, 회원가입 기능을 백엔드 팀원들과 같이 코드를 작성했다. 주말을 쉬고 월요일부터 각자 브랜치를 만들고 맡은 기능들을 구현했다.</p>
</li>
<li><p>내가 맡은 기능들은 상세페이지의 게시글 불러오기, 수정하기, 삭제하기와 마이페이지 기능이다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/af44ed35-41fd-4eae-b1ad-eb38b41cf0f3/image.png" alt="">
<img src="https://velog.velcdn.com/images/do_ng_iill/post/467159b6-a1dd-42ad-a094-e9369fdc7c7c/image.png" alt=""></p>
</li>
<li><p>사실상 주특기 주차에서 사용했던 CRUD를 복습하는 정도였다. 난이도가 어려운 기능들은 아니라서 복습하는 기분으로 구현을 완료했다. 구현단계에서는 어려움이 없었고 프론트엔드와 통신할 때 약간의 에러가 발생했다.</p>
<ul>
<li>발생했던 에러중에 기억에 남는 것은 게시글과 댓글이 1:N관계여서 여러개의 댓글이 달린 게시물을 삭제할 때 참조 에러가 발생했다. 기존 코드가 단방향으로만 설정을 해서 오류가 났던 것이다. 그래서 양방향 관계를 해주면서 <code>cascade = CascadeType.ALL,orphanRemoval = true</code>을 해주면서 트러블슈팅을 했던 기억이 있다.</li>
</ul>
</li>
<li><p>마이페이지는 사실 모든 팀원들이 까먹고 있다가 프로젝트 제출 하루 전에 생각이 나서 빠르게 구현했었다. 다행히 다른 팀원분이 구현한 메인페이지와 너무나 비슷해서 내가 한 번 구현해보겠다고 했다. 이 기능을 구현하면서 무한스크롤 구현하는 방법과 QueryDsl 사용하는 방법을 조금 알게 되었다.</p>
</li>
<li><p>주특기 주차에서는 간단하게 부트스트랩으로 화면을 제작하거나 화면없이 PostMan으로 API통신 테스트만 했었다. 이번에 팀 프로젝트를 프론트엔드 분들과 하다보니 화면이 너무 이쁘게 나와서 정말 있어보는 프로젝트로 보여서 좋았다. 그리고 처음으로 React와 Spring을 연결하는 작업을 하다보니 통신관련 CS지식이 많이 필요하다고 느꼈다. 마지막으로 팀원들과 커뮤니케이셔이 잘되서 프로젝트 마무리를 너무 잘 할 수 있어서 좋았고 그 덕분에 예비군 훈련도 마음 편히 갔다올 수 있었다.</p>
</li>
</ul>
<h2 id="2-클론코딩-주차-회고">2. 클론코딩 주차 회고</h2>
<ul>
<li><p>미니 프로젝트 주차를 마치고 또다시 팀이 배정되었다. 2주 연속으로 항해99를 하면서 처음 만난 팀원들과 팀을 이루게 되었다. 저번주와 비슷하게 오전에는 간단하게 자기소개와 프로젝트를 정했다. 최종적으로는 인프런 클론코딩으로 주제를 정했지만 점심을 먹고나서 갑작스럽게 에어비앤비로 정해졌다.</p>
<ul>
<li><a href="https://busy-chili-2bd.notion.site/SA-113e0d07b3dc419c8fc92e79ee1c7797">https://busy-chili-2bd.notion.site/SA-113e0d07b3dc419c8fc92e79ee1c7797</a></li>
</ul>
</li>
<li><p>저번 미니 프로젝트 주차보다 진행이 조금 더딘 상황이었지만 그래도 한 번 해봤다고 API설계, DB 설계가 조금은 수월했다. DB는 확실히 미니 프로젝트 주차보다는 복잡했다. 
<img src="https://velog.velcdn.com/images/do_ng_iill/post/e09f5d25-b2a3-4655-afca-a7fe48cd8417/image.png" alt=""></p>
</li>
<li><p>이번 프로젝트에서는 조금 핵심적인 기능들을 구현해보고 싶어서 메인페이지, 필터, 마이페이지-예약현황, 마이페이지 - 위시리스트를 맡았다. 그리고 팀원 중 한 분이 코로나에 걸려서 추가적으로 호스트 숙소 등록까지 꽤 많은 기능들을 구현했다. 조금 아쉬웠던 점은 너무 많으 기능들을 구현하다보니 테스트 코드를 하나도 못짰다는 점이다... 그래서 실전 프로젝트에서는 정말 무조건 테스트코드를 짜기로 결심했다.
<img src="https://velog.velcdn.com/images/do_ng_iill/post/3295b9ba-7791-44e6-a5c8-188ca26ec3e3/image.png" alt="">
<img src="https://velog.velcdn.com/images/do_ng_iill/post/a0af170d-5b36-4f3c-9ba9-6825496541b2/image.png" alt="">
<img src="https://velog.velcdn.com/images/do_ng_iill/post/61e52a9b-55d2-4132-8cb4-6d0f37b3d0a8/image.png" alt=""></p>
</li>
<li><p>프로젝트를 진행하면서 생겼던 트러블 슈팅들은 Velog에 정리해뒀다.</p>
<ul>
<li>2022-07-30 개발일지<ul>
<li><a href="https://velog.io/@do_ng_iill/2022-07-30-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80">https://velog.io/@do_ng_iill/2022-07-30-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</a></li>
</ul>
</li>
<li>2022-08-01 개발일지<ul>
<li><a href="https://velog.io/@do_ng_iill/2022-08-01-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80">https://velog.io/@do_ng_iill/2022-08-01-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</a></li>
</ul>
</li>
</ul>
</li>
<li><p>팀장님이 너무 지쳐보여서 프로젝트 회고 발표는 내가 진행했다.(발표는 역시 어려워...) 다른 조들 발표도 듣는데 다들 너무 잘했다. 우리 조 회고 발표 피드백으로는 Response를 ResponseDto를 사용해 통일성이 있어 깔끔했다. QueryDsl사용하면서 발생한 트러블슈팅을 잘 들었고 이것을 깃허브 리드미에 추가해주면 좋게다. 단지 아쉽다면 테스트 코드 작성을 하지 않았다.</p>
<ul>
<li>저번 미니 프로젝트 주차에서도 테스트 코드 작성을 못한 점에 대해 피드백을 받았는데 또...진짜 실전 프로젝트에서는 테스트 코드를 꼭 작성해야겠다.</li>
</ul>
</li>
</ul>
<h2 id="3-실전-프로젝트-시작">3. 실전 프로젝트 시작...!</h2>
<ul>
<li><p>클론코딩 주차도 잘 마무리한 후에 드디어 대망의 6주짜리 실전 프로젝트가 시작되었다. 원래 항해99 실전 프로젝트는 프론트엔드, 백엔드 그리고 추가적으로 디자이너 1명을 붙여주고 매주 현업 개발자인 튜터분들에게 멘토링을 받는다. 그리고 마케팅비용도 지원을 받아 서비스를 런칭해 운영까지 진행한다. 사실 항해99를 참여한 이유도 6주짜리 실전 프로젝트 때문이었다. 하지만 이번 이노베이션 캠프에서 최초로 스프링 실습팀이라는 새로운 것을 진행하는데 스프링에 대해 더욱 깊이 공부하고 싶어서 신청했다.</p>
</li>
<li><p>스프링 실습팀은 따로 게더에 모여서 진행했다. 첫날엔 역시 프로젝트 주제 선정과 API설계, DB설계를 진행했다. 한 팀당 3명의 팀원으로 이루어졌다. 이번엔 항해99를 하면서 같은 팀이었던 팀원들과 팀을 이루었다. 그래서 빠르게 자기소개를 하고 프로젝트에 대해 이야기를 나눴다.</p>
</li>
<li><p>프로젝트 설계를 하면서 가장 난감했던 부분은 어디서부터 어디까지 스코프를 정해야할까...그리고 데이터는 어디서 구하지...?였다. 그래서 총괄 매니저님에게도 이것저것 물어보기도하고 다른 팀들은 어떻게 했는지 참고하면서 만들어나갔다.</p>
</li>
<li><p>토요일 오전에는 다른 스프링 실습팀들과 서로의 프로젝트에 대해 이야기를 나누고 상호 피드백하는 시간을 가졌다. 그리고 오후에는 처음으로 멘토님과 다같이 미팅을 했다. 멘토님과 여러 이야기를 나누다보니 스프링 실습팀 기간동안 어떻게 진행을 해야할지도 조금 감이 잡혔고 프로젝트의 스코프에 대해서도 감이 잡혔다. 그리고 데이터를 어떻게 구할지는 조금 더 팀원들과 상의해서 정하기로 했다.</p>
</li>
<li><p>내일부터 팀원들과 기능구현에 들어가는데 정말 서로 배울점도 많고 우리 팀원들 뿐만아니라 스프링 실습팀에 계시는 다른 분들에게도 정말 배울것들이 많아서 좋은 시너지가 날 것 같다. 앞으로 남은 6주 화이팅이다! </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-08-01 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-08-01-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-08-01-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Thu, 04 Aug 2022 08:01:09 GMT</pubDate>
            <description><![CDATA[<h1 id="📂-error-list">📂 Error List</h1>
<h2 id="1-scalar-subquery-contains-more-than-one-row-sql-statement">1. Scalar subquery contains more than one row; SQL statement:</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><code>Scalar subquery contains more than one row; SQL statement:</code></li>
<li>```sql
select room0_.room_id as col_0_0_,<pre><code>  room0_.title as col_1_0_,
    room0_.price as col_2_0_,
    (select image1_.img_url from image
    image1_ where room0_.room_id=image1_.room_id)as col_3_0_</code></pre>from room room0_
where room0_.category=?
limit ?
offset ? [90053-200]</li>
<li>위 SQL문은 QueryDsl을 통해 만들어진 SQL문이다. 아래 오류 발생한 코드에서 보면 서브쿼리 안에 limit(1)을 넣었지만 실제 서브쿼리 안에는 limit에 대한 내용이 적용되지 않았고 where절 밑에 적용되어있다...</li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<pre><code class="language-java">  @Override
  public Slice&lt;GetRoomsResponseDto&gt; findAllByCategoryOrderByCreatedAt(String category, Pageable pageable, Long userId) {
      List&lt;GetRoomsResponseDto&gt; returnRoom = queryFactory.select(Projections.fields(
                      GetRoomsResponseDto.class,
                      room.id,
                      room.title,
                      room.price,
                      // imgUrl 1개만 가져오기  room.imageList.get(0) 오류남
                      ExpressionUtils.as(
                              JPAExpressions
                                      .select(image.imgUrl)
                                      .from(image)
                                      .limit(1)
                                      .where(room.id.eq(image.room.id))
                                      ,&quot;imgUrl&quot;
                      )
                  )
              ))
              .from(room)
              .where(room.category.eq(category))
              .offset(pageable.getOffset())
              .limit(pageable.getPageSize())
              .fetch();
      return new SliceImpl&lt;&gt;(returnRoom, pageable, returnRoom.iterator().hasNext());
  }</code></pre>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>QueryDsl에서 서브쿼리안에 limit가 먹히질 않아서 2개의 row를 가져서 <code>Scalar subquery contains more than one row; SQL statement:</code>오류가 났음. 구글링을 해보니 아래의 글과같이 이유를 모른다...</li>
<li>비슷한 에러 질문 : <a href="https://www.inflearn.com/questions/532719">https://www.inflearn.com/questions/532719</a></li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li>limit을 직접적으로 사용하지 않고 서브쿼리 안에 또 서브쿼리로 Room테이블의 ROOM_ID와 Image테이블의 ROOM_ID가 같은 것들 중(한 숙소 정보의 이미지 URL List)에서 image_id가 가장 작은 것 즉, 가장 먼저 생성된 Image Id 1개만 가져와서 ImageUrl을 검색하는 방식으로 해결했다.</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">  @Override
  public Slice&lt;GetRoomsResponseDto&gt; findAllByCategoryOrderByCreatedAt(String category, Pageable pageable, Long userId) {
      List&lt;GetRoomsResponseDto&gt; returnRoom = queryFactory.select(Projections.fields(
                      GetRoomsResponseDto.class,
                      room.id.as(&quot;roomId&quot;),
                      room.title,
                      room.price,
                      room.location,
                      // imgUrl 1개만 가져오기  room.imageList.get(0) 오류남
                      ExpressionUtils.as(
                              JPAExpressions
                                      .select(image.imgUrl)
                                      .from(image)
                                      .where(image.id.eq(
                                              JPAExpressions
                                                      .select(image.id.min())
                                                      .from(image)
                                                      .where(room.id.eq(image.room.id))
                                      ))
                                      ,&quot;imgUrl&quot;
                      ),
                      // wish 여부
                      new CaseBuilder()
                              .when(wish.id.isNull())
                              .then((ComparableExpression&lt;Boolean&gt;) Expressions.asBoolean(false))
                              .otherwise(Expressions.asBoolean(true)).as(&quot;isWish&quot;)
              ))
              .distinct()
              .from(room)
              .leftJoin(wish)
              .on(room.id.eq(wish.room.id), wish.user.id.eq(userId))
              .where(getCategory(category))
              .offset(pageable.getOffset())
              .limit(pageable.getPageSize())
              .fetch();

      return new SliceImpl&lt;&gt;(returnRoom, pageable, returnRoom.iterator().hasNext());
  }</code></pre>
</li>
<li><p>오류를 해결하면서 참고한 자료</p>
<ul>
<li>[QueryDsl] 스칼라 서브쿼리 (Select 서브쿼리) 에서 limit 처럼 사용하는 방법<ul>
<li><a href="https://velog.io/@hellonewtry/QueryDsl-%EC%8A%A4%EC%B9%BC%EB%9D%BC-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-Select-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-%EC%97%90%EC%84%9C-limit-%EC%B2%98%EB%9F%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95">https://velog.io/@hellonewtry/QueryDsl-%EC%8A%A4%EC%B9%BC%EB%9D%BC-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-Select-%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC-%EC%97%90%EC%84%9C-limit-%EC%B2%98%EB%9F%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2-distinct-no-working">2. Distinct no working</h2>
<ul>
<li>add Fetch Type Lazy in Entity Class</li>
</ul>
<h2 id="3-use-booleanbuilder">3. Use BooleanBuilder</h2>
<ul>
<li><p>우아한 형제들의 Querydsl 사용법</p>
<ul>
<li><a href="https://velog.io/@youngerjesus/%EC%9A%B0%EC%95%84%ED%95%9C-%ED%98%95%EC%A0%9C%EB%93%A4%EC%9D%98-Querydsl-%ED%99%9C%EC%9A%A9%EB%B2%95#2-%EB%8F%99%EC%A0%81%EC%BF%BC%EB%A6%AC%EB%8A%94-booleanexpression-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">https://velog.io/@youngerjesus/%EC%9A%B0%EC%95%84%ED%95%9C-%ED%98%95%EC%A0%9C%EB%93%A4%EC%9D%98-Querydsl-%ED%99%9C%EC%9A%A9%EB%B2%95#2-%EB%8F%99%EC%A0%81%EC%BF%BC%EB%A6%AC%EB%8A%94-booleanexpression-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</a></li>
</ul>
</li>
<li><p>[Querydsl] 동적 쿼리</p>
<ul>
<li><a href="https://jaime-note.tistory.com/76">https://jaime-note.tistory.com/76</a></li>
</ul>
</li>
<li><p>BooleanBuilder와 BooleanExpression의 차이</p>
<ul>
<li><a href="https://www.inflearn.com/questions/161280">https://www.inflearn.com/questions/161280</a></li>
</ul>
</li>
<li><p><a href="https://www.inflearn.com/questions/161280">https://www.inflearn.com/questions/161280</a></p>
<ul>
<li><a href="https://whitepro.tistory.com/374">https://whitepro.tistory.com/374</a></li>
</ul>
</li>
<li><p>Querydsl and, or 연산이 적용된 동적 쿼리 페이징 처리</p>
<ul>
<li><a href="https://kukekyakya.tistory.com/entry/Querydsl-and-or-%EC%97%B0%EC%82%B0%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%9C-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC">https://kukekyakya.tistory.com/entry/Querydsl-and-or-%EC%97%B0%EC%82%B0%EC%9D%B4-%EC%A0%81%EC%9A%A9%EB%90%9C-%EB%8F%99%EC%A0%81-%EC%BF%BC%EB%A6%AC-%ED%8E%98%EC%9D%B4%EC%A7%95-%EC%B2%98%EB%A6%AC</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022-07-30 개발일지]]></title>
            <link>https://velog.io/@do_ng_iill/2022-07-30-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@do_ng_iill/2022-07-30-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Sat, 30 Jul 2022 16:56:29 GMT</pubDate>
            <description><![CDATA[<h1 id="📂-error-list">📂 Error List</h1>
<h2 id="1-transactionrequiredexception-해결">1. TransactionRequiredException 해결</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><code>TransactionRequiredException</code></li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<pre><code class="language-java">  public void deleteWish(User user, Long roomId) {
      Room room = roomRepository.findById(roomId).orElseThrow(
              () -&gt; new IllegalArgumentException(&quot;해당 숙소 정보를 찾을 수 없습니다.&quot;)
      );

      Wish wish = wishRepository.findByUserAndRoom(user, room)
              .orElseThrow(() -&gt; new IllegalArgumentException(&quot;찾을 수 없는 wish 항목입니다.&quot;));

      wishRepository.deleteById(wish.getId());
  }</code></pre>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>Service단에 deleteById()를 사용할 때 @Transactional을 걸어주지 않았다.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li>해당 메서드 위에 @Transactional 추가해준다.</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<pre><code class="language-java">  @Transactional
  public void deleteWish(User user, Long roomId) {
      Room room = roomRepository.findById(roomId).orElseThrow(
              () -&gt; new IllegalArgumentException(&quot;해당 숙소 정보를 찾을 수 없습니다.&quot;)
      );

      Wish wish = wishRepository.findByUserAndRoom(user, room)
              .orElseThrow(() -&gt; new IllegalArgumentException(&quot;찾을 수 없는 wish 항목입니다.&quot;));

      wishRepository.deleteById(wish.getId());
  }</code></pre>
</li>
<li><p>오류를 해결하면서 참고한 자료</p>
<ul>
<li>TransactionRequiredException: Executing an update/delete query 오류의 해결 방법은 @Transaction<ul>
<li><a href="https://wildeveloperetrain.tistory.com/17">https://wildeveloperetrain.tistory.com/17</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="2-javalangclasscastexception-해결">2. java.lang.ClassCastException 해결</h2>
<ul>
<li><p>발생한 오류</p>
<ul>
<li><code>java.lang.ClassCastException</code></li>
</ul>
</li>
<li><p>오류가 발생한 코드</p>
<p>** WishController.java**</p>
<pre><code class="language-java">public interface WishRepository extends JpaRepository&lt;Wish, Long&gt; {
    Optional&lt;Long&gt; findByUserAndRoom(User user, Room room);
}</code></pre>
<p><strong>WishService.java</strong></p>
<pre><code class="language-java">    @Transactional
    public void deleteWish(User user, Long roomId) {
        ...생략

        Long wishId = wishRepository.findByUserAndRoom(user, room)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;찾을 수 없는 wish 항목입니다.&quot;));

        wishRepository.deleteById(wish.getId());
    }</code></pre>
</li>
<li><p>오류 발생 이유</p>
<ul>
<li>findByUserAndRoom을 하면 사실상 Wish 객체가 반환되는데 wish의 id값이 반환된다고 생각하고 <code>Optional&lt;Long&gt;</code>을 사용했다. 그래서 Service단에서 에러를 던져주다가 반환타입이 맞지 않아서 ClassCastException 오류가 발생했다.</li>
</ul>
</li>
<li><p>해결책</p>
<ul>
<li><code>Optional&lt;Long&gt;</code>에서 <code>Optional&lt;Wish&gt;</code>로 코드 변경 후 서비스단에서는 <code>wish.getId()</code>로 wish의 id값을 반환받아서 사용했다.</li>
</ul>
</li>
<li><p>오류 해결 후 코드</p>
<p>** WishController.java**</p>
<pre><code class="language-java">public interface WishRepository extends JpaRepository&lt;Wish, Long&gt; {
    Optional&lt;Wish&gt; findByUserAndRoom(User user, Room room);
}</code></pre>
<p><strong>WishService.java</strong></p>
<pre><code class="language-java">    @Transactional
    public void deleteWish(User user, Long roomId) {
        ...생략

        Wish wish = wishRepository.findByUserAndRoom(user, room)
                .orElseThrow(() -&gt; new IllegalArgumentException(&quot;찾을 수 없는 wish 항목입니다.&quot;));

        wishRepository.deleteById(wish.getId());
    }</code></pre>
</li>
<li><p>오류를 해결하면서 참고한 자료</p>
<ul>
<li>java.lang.ClassCastException<ul>
<li><a href="https://shs2810.tistory.com/15">https://shs2810.tistory.com/15</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="3-h2-console에서-column-not-found">3. H2 console에서 Column not Found</h2>
<ul>
<li>저번에도 이것때문에 많이 헤맸는데 또 헤맸다...</li>
<li>발생한 오류<ul>
<li><code>Column not Found</code></li>
</ul>
</li>
<li>오류가 발생한 코드<pre><code class="language-sql">INSERT INTO ROOM (CREATED_AT,CATEGORY, INFORMATION, LOCATION, MAX_GUEST, PRICE, TITLE, USER_ID) VALUES (&quot;2022-07-30&quot;,&quot;test 카테고리&quot;, &quot;test 소개글&quot;, &quot;test 위치&quot;, 6, 70000, &quot;test 숙소&quot;, 1)</code></pre>
</li>
<li>오류 발생 이유<ul>
<li>VALUES ()안에 쌍따옴표(&quot;)를 사용했기 때문</li>
</ul>
</li>
<li>해결책<ul>
<li>쌍따옴표(&quot;)를 그냥 따옴표(&#39;)로 변경</li>
</ul>
</li>
<li>오류 해결 후 코드<pre><code class="language-sql">INSERT INTO ROOM (CREATED_AT,CATEGORY, INFORMATION, LOCATION, MAX_GUEST, PRICE, TITLE, USER_ID) VALUES (&#39;2022-07-30&#39;,&#39;test 카테고리&#39;, &#39;test 소개글&#39;, &#39;test 위치&#39;, 6, 70000, &#39;test 숙소&#39;, 1)</code></pre>
</li>
<li>오류를 해결하면서 참고한 자료<ul>
<li>[Spring Boot] H2를 사용하면서 자주 나오는 오류들<ul>
<li><a href="https://shinsunyoung.tistory.com/80">https://shinsunyoung.tistory.com/80</a></li>
</ul>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 간단한 사용방법]]></title>
            <link>https://velog.io/@do_ng_iill/Spring-Security-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@do_ng_iill/Spring-Security-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 26 Jul 2022 07:09:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-intro">📌 Intro</h2>
<ul>
<li>항해99 주특기 숙련주차에서 처음 Spring Security를 접하고 미니프로젝트 주차에서도 Spring Security를 접했다. 물론 숙련주차에서 작성한 코드를 기반으로 했지만 Spring Security는 너무 어렵다...그리고 너무 내용이 방대하다..</li>
<li>이번 포스팅에서는 간단하게 Spring Security 사용법과 메소드들을 쭉 나열하고 어떤 기능인지 적어봤다. 일단 이론적인 부분들로만 채웠는데 포스팅을 하면서 느낀 것은(포스팅 다하고 Intro 적는중..) Spring Security 프로젝트를 하나 만들어서 내가 직접 이것 저것 해봐야겠다..!는 것이다.</li>
<li>Spring Security 프로젝트를 하나 파서 나중에 에러나는 부분들이나 에러를 해결하는 부분들을 잘 정리해보려고 한다!</li>
</ul>
<h1 id="spring-security란">Spring Security란?</h1>
<blockquote>
<p><strong>Spring Security(스프링 시큐리티) : 스프링 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공하는 프레임워크</strong></p>
</blockquote>
<h2 id="📍-간단한-사용방법">📍 간단한 사용방법</h2>
<h3 id="1-spring-initializr에서-추가-or-buildgradle에-추가">1. Spring initializr에서 추가 or build.gradle에 추가</h3>
<ul>
<li><p>Spring initializr에서 &quot;Spring Security&quot;추가
<img src="https://velog.velcdn.com/images/do_ng_iill/post/7aafa9a0-7f36-41f8-94ae-f7f95980d08c/image.png" alt=""></p>
</li>
<li><p>build.gradle에 아래 코드 추가</p>
<pre><code class="language-java">  // 스프링 시큐리티
  implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/5b54dbce-8dad-4cbf-8ad6-5fceadf62546/image.png" alt=""></p>
</li>
</ul>
<h3 id="2-security-config-설정">2. Security Config 설정</h3>
<pre><code class="language-java">@Configuration
@EnableWebSecurity // 웹보안 활성화를위한 annotation 스프링 시큐리티 지원을 가능하게 함
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); // 해당 코드가 없으면 403 에러가 남.

        http.authorizeRequests()
                // 어떤 요청이든 &#39;인증&#39;
                .anyRequest().authenticated()
                .and()
                    // 로그인 기능 허용
                    .formLogin()
                    .defaultSuccessUrl(&quot;/&quot;)
                    .permitAll()
                .and()
                    // 로그아웃 기능 허용
                    .logout()
                    .permitAll();
    }
}</code></pre>
<ul>
<li>Spring security 5.7버전 이상부터 WebSecurityConfigurerAdapter가 deprecated 되었습니다. 스프링 부트 버전을 Spring security가 5.7 아래 버전을 사용하는 버전(2.6.x)으로 변경해주세요!</li>
</ul>
<h3 id="3-h2-console을-사용할-경우">3. h2-console을 사용할 경우</h3>
<ul>
<li>h2-console 사용에 대한 허용 (CSRF, FrameOptions 무시)<pre><code class="language-java">  @Override
  public void configure(WebSecurity web) {
      web
              .ignoring()
              .antMatchers(&quot;/h2-console/**&quot;);
  }</code></pre>
</li>
<li>Springboot 2.7.1버전 사용하기 때문에 WebSecurityConfigurerAdapter가 deprecated된 경우 해결법!<ul>
<li><a href="https://honeywater97.tistory.com/264">https://honeywater97.tistory.com/264</a></li>
</ul>
</li>
</ul>
<h3 id="4-비밀번호-암호화를-위해-bean-등록">4. 비밀번호 암호화를 위해 Bean 등록</h3>
<pre><code class="language-java">public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Bean
    public BCryptPasswordEncoder encodePwd() {
        return new BCryptPasswordEncoder();
    }</code></pre>
<h2 id="📍-websecurityconfig-메소드-및-어노테이션-정리">📍 WebSecurityConfig 메소드 및 어노테이션 정리</h2>
<h3 id="1-인증관련-메소드">1. 인증관련 메소드</h3>
<ul>
<li>앞에 http.authorizeRequests()가 붙는다.<ul>
<li>ex) access() -&gt; http.authorizeRequests().access()라는 의미</li>
<li>authorizeRequests() : 시큐리티 처리에 <strong>HttpServletRequest</strong>를 이용한다는 것을 의미</li>
<li>HttpServletRequest란?<ul>
<li>http프로토콜의 request정보를 서블릿에게 전달하기 위해 사용</li>
<li>헤더정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어 들이는 메소드 포함</li>
<li>Body의 Stream을 읽어 들이는 메소드 포함</li>
<li>[출처] HttpServletRequest, HttpServletResponse 객체란 : <a href="https://velog.io/@oliviarla/HttpServletRequest-HttpServletResponse-%EA%B0%9D%EC%B2%B4%EB%9E%80">https://velog.io/@oliviarla/HttpServletRequest-HttpServletResponse-%EA%B0%9D%EC%B2%B4%EB%9E%80</a></li>
</ul>
</li>
</ul>
</li>
<li>access(hasRole(USER)) : 주어진 SpEL 표현식의 평가 결과가 true이면 접근을 허용</li>
<li>anonymous() : 익명의 사용자의 접근을 허용</li>
<li>antMatchers(antPatterns 경로) : <ul>
<li>ant : 아래 <strong>번외1)</strong>에서 설명.</li>
<li>Matchers : URL 또는 대상 문자열에서 패턴이 일치하는 부분을 찾거나 전체 일치 여부 등을 판단</li>
</ul>
</li>
<li>authenticated() : 인증된 사용자의 접근을 허용</li>
<li>denyAll() : 무조건 접근을 허용하지 않음</li>
<li>fullyAuthenticated() : 사용자가 완전히 인증되면 접근을 허용(기억되지 않음)</li>
<li>hasAnyAuthority(String...) : 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용</li>
<li>hasAnyRole(String...) : 사용자가 주어진 역할 중 어떤 것이라도 있다면 접근을 허용</li>
<li>hasAuthority(String) : 사용자가 주어진 권한이 있다면 접근을 허용</li>
<li>hasIpAddress(String) : 주어진 IP로부터 요청이 왔다면 접근을 허용</li>
<li>hasRole(USER) : 사용자가 주어진 역할이 있다면 접근을 허용</li>
<li>not() : 다른 접근 방식의 효과를 무효화</li>
<li>permitAll() : 무조건 접근을 허용</li>
<li>rememberMe() : 기억하기를 통해 인증된 사용자의 접근을 허용</li>
</ul>
<h3 id="2-인가관련-메소드">2. 인가관련 메소드</h3>
<ul>
<li>http.formLogin() : 로그인 기능<ul>
<li>.loginPage(URL) : 로그인 페이지 URL 설정</li>
<li>.loginProcessingUrl(URL) : 로그인 처리(로그인 버튼 눌렀을 때) URL 설정 =&gt; POST URL</li>
<li>.successHandler(new CustomAuthenticationSuccessHandler())</li>
<li>.failureHandler(new CustomAuthenticationFailureHandler())</li>
<li>.defaultSuccessUrl(URL) : 로그인 처리 후 성공 시 URL</li>
<li>.permitAll() : 로그인 처리 후 실패 시 URL, 로그인 처리 후 실패 시 이전 경로로 간다.(이해 잘 안감.)</li>
</ul>
</li>
<li>http.logout() : 로그아웃 기능<ul>
<li>.logoutUrl(URL) : 로그아웃 요청 URL </li>
</ul>
</li>
<li>http.csrf().disable() : csrf() 설정을 안해도 된다는 의미<ul>
<li>csrf() : csrf를 막기 위한 설정</li>
<li>csrf란 무엇인가? 👉🏻 <strong>번외2)</strong>에서 설명</li>
<li>왜 csrf()를 disable()하는 것일까?는 아래의 블로그에 너무 정리가 잘 되어있다.<ul>
<li>Spring Security :: CSRF protection disable option 대한 생각 정리 : <a href="https://wave1994.tistory.com/150">https://wave1994.tistory.com/150</a></li>
<li>Spring security - csrf란? : <a href="https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80">https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80</a></li>
</ul>
</li>
</ul>
</li>
<li>http.httpBasic() : </li>
<li>http.sessionManagement() : 세션 관리 기능 설정<ul>
<li>.invalidSessionUrl(URL) : 세션이 유효하지 않을 때 이동 할 페이지</li>
<li>.maximumSessions(int타입) : 최대 허용 가능 세션 수, -1은 무제한</li>
<li>.maxSessionsPreventsLongin(불린 타입) : 동시 로그인 차단, default는 false(기존 세션 만료)</li>
<li>.sessionFixation().changeSessionId() : 기본값 -&gt; 세션은 유지하되 세션 아이디는 계속 새로 발급(servlet 3.1이상 기본 값) 살짝 이해 안감..<ul>
<li>none() : 세션이 새로 생성되지 않고 그대로 유지되기 때문에 세션 고정 공격에 취약하다.</li>
<li>migrateSession() : 새로운 세션도 생성되고 세션 아이디도 발급된다. (sevlet3.1 이하 기본 값) + 이전 세션의 속성값들도 유지된다. </li>
<li>newSession() : 세션이 새롭게 생성되고, 세션 아이디도 발급되지만, 이전 세션의 속성값들을 유지할 수 없다.</li>
</ul>
</li>
<li>.expireUrl(URL) : 세션이 만료된 경우 이동할 페이지</li>
</ul>
</li>
<li>http.rememberMe() : rememberMe 기능 작동 설정<ul>
<li>remember me?<ul>
<li>SessionID 쿠키를 삭제하더라도 Remember-Me가 있다면 해당 쿠키를 decoding한 다음 로그인 상태를 유지할 수 있도록 한다.</li>
</ul>
</li>
<li>.rememberMeParameter(파라미터명) : 파라미터 설정, default는 &quot;remember-me&quot;</li>
<li>.tokenValiditySeconds(초) : 토큰 유효기간 설정, default는 14일</li>
<li>.alwaysRemember(불린타입) : remember me 기능이 활성화되지 않아도 항상 실행, default는 false</li>
<li>.userDetailsService(userDetailsService) : remember me에서 시스템에 있는 사용자 계정을 조회할 때 사용</li>
<li>.deleteCookies(파라미터명) : 쿠키 삭제 기능, 설정해준 파라미터명을 넣어준다.</li>
</ul>
</li>
<li>http.ExceptionHandling() : 예외처리 기능이 작동<ul>
<li>.authenticationEntryPoint(new AuthenicationEntryPoint()) : 인증예외 발생시 수행 메서드(commence) 오버라이딩. 해당 코드에서는 login페이지로 이동시키지만 다른 로직을 수행할 수도 있다.</li>
<li>.accessDeniedHandler(new AccessDeniedHandler() : 인가예외 발생시 처리 로직 수행 해당 코드에서는 denied페이지로 이동하지만 별도로 다른 로직을 수행할 수 있다. </li>
<li>.onAuthenticationSuccess : savedRequest객체에 RequestCache객체가 담고 있는 사용자가 원래 가려던(요청하려던) 자원의 요청정보를 가져와 활용할 수 있도록 한다.</li>
</ul>
</li>
<li>http.addFilter() : </li>
</ul>
<h3 id="3-어노테이션">3. 어노테이션</h3>
<ul>
<li>@EnableWebSecurity : 웹보안 활성화를위한 어노테이션 스프링 시큐리티 지원을 가능하게 한다.<ul>
<li>@EnableWebSecurity 어노테이션을 WebSecurityconfigurerAdapter를 상속하는 설정 객체를 붙혀주면 SpringSecurityFilterChain에 등록된다.  </li>
</ul>
</li>
</ul>
<p><strong>번외1)</strong> antMatchers에서 ant란?</p>
<blockquote>
<p>&quot;ant&quot; 의 어원은 <strong>Apache Ant Project</strong> 에서 가져온거다.
Apache Ant Project 는 xml 스크립트 언어를 사용하는 <strong>자바 빌드 시스템</strong>이다.
Apache Ant Home 또는 Spring 공식문서에서 말하기를
<strong>매핑되는 부분 (resource-path)은 Apache Ant 의 기술을 빌려</strong>쓴? 것이다.
간단히 정리해서 말하자면 &quot;antMatcher&quot; 는 <strong>url패턴과 자원을 매핑할 때 사용하는 하나의 Style(기법)</strong>이라고 생각하면 된다.</p>
</blockquote>
<ul>
<li><p>Ant-style path patterns</p>
<ul>
<li>? : 1 개의 문자와 매칭</li>
<li>* : 0개 이상의 문자와 매칭</li>
<li>** : 0개 이상의 디렉터리와 파일 매칭</li>
<li>출처: <a href="https://hmjkor.tistory.com/493">https://hmjkor.tistory.com/493</a> </li>
</ul>
</li>
<li><p>블로그 참고</p>
<ul>
<li><a href="https://velog.io/@codren/AntMatchers">https://velog.io/@codren/AntMatchers</a></li>
</ul>
</li>
</ul>
<p><strong>번외2) CSRF관련</strong></p>
<ul>
<li><p>CSRF란?</p>
<blockquote>
<p><strong>CSRF (Cross Site Request Forgery) : 다른 사이트에서 유저가 보내는 요청을 조작하는 공격. 예시로는 이메일에 첨부된 링크를 누르면 내 은행계좌의 돈이 빠져나가는 방식의 해킹 등이 있다.</strong></p>
</blockquote>
</li>
<li><p>CSRF 토큰이란?</p>
<blockquote>
<p>*<em>CSRF TOKEN : 회원가입시 페이지를 바꿔치기 하는등의 방법으로 해킹을 하기도 하기 때문에 서버에 들어온 요청이 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 것이 CSRF 토큰이다. *</em></p>
</blockquote>
</li>
<li><p>CSRF 토큰 로직
<img src="https://velog.velcdn.com/images/do_ng_iill/post/b58a8648-23c5-4c98-9047-5cbb1255f2f4/image.PNG" alt=""></p>
</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>CSRF토큰이란?<ul>
<li><a href="https://velog.io/@jupiter-j/CSRF%ED%86%A0%ED%81%B0%EC%9D%B4%EB%9E%80">https://velog.io/@jupiter-j/CSRF%ED%86%A0%ED%81%B0%EC%9D%B4%EB%9E%80</a></li>
</ul>
</li>
<li>[스프링프레임워크] 스프링 시큐리티 -3. 요청 가로채기<ul>
<li><a href="https://m.blog.naver.com/kimnx9006/220638156019">https://m.blog.naver.com/kimnx9006/220638156019</a></li>
</ul>
</li>
<li>Spring Security :: CSRF protection disable option 대한 생각 정리<ul>
<li><a href="https://wave1994.tistory.com/150">https://wave1994.tistory.com/150</a></li>
</ul>
</li>
<li>Spring security - csrf란?<ul>
<li><a href="https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80">https://velog.io/@woohobi/Spring-security-csrf%EB%9E%80</a></li>
</ul>
</li>
<li>Ant-style path patterns<ul>
<li><a href="https://hmjkor.tistory.com/493">https://hmjkor.tistory.com/493</a></li>
</ul>
</li>
<li>&quot;Ant&quot;Matchers<ul>
<li><a href="https://velog.io/@codren/AntMatchers">https://velog.io/@codren/AntMatchers</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Pagination으로 무한스크롤 구현]]></title>
            <link>https://velog.io/@do_ng_iill/Spring-Pagination</link>
            <guid>https://velog.io/@do_ng_iill/Spring-Pagination</guid>
            <pubDate>Mon, 25 Jul 2022 16:26:58 GMT</pubDate>
            <description><![CDATA[<h2 id="pagination-하는법">Pagination 하는법</h2>
<h3 id="1-파라미터를-pageable-객체로-받거나-sort-page-size를-따로-받기">1. 파라미터를 Pageable 객체로 받거나 sort, page, size를 따로 받기.</h3>
<p><strong>1-1. 파라미터를 Pageable 객체로 받기</strong></p>
<pre><code class="language-java">    @GetMapping(&quot;/api/posts&quot;)
    public Page&lt;Post&gt; getPosts(@RequestParam String category ,
                               Pageable pageable) {
        ...생략
    }</code></pre>
<ul>
<li>Pageable 객체로 받을 때 주의할 점은 <code>@RequestParam</code> 없이 받아야한다.<ul>
<li>@RequestParam을 넣었더니 status code 400 Bad Request 에러발생!</li>
</ul>
</li>
<li>pageable을 출력하면 어떻게 출력될까?<ul>
<li><strong>?category=ALL&amp;page=0&amp;size=5&amp;sort=default</strong><ul>
<li>참고로 controller단에서 sort를 따로 받는 코드였는데 아래와 같이 sort : default로 담긴다.</li>
<li>```java
@GetMapping(&quot;/api/posts&quot;)
public Page<Post> getPosts(@RequestParam String sort,<pre><code>                     @RequestParam String category ,
                     Pageable pageable) {</code></pre>  ...생략
}</li>
<li><code>Page request [number: 0, size 5, sort: default: ASC]</code></li>
</ul>
</li>
<li>*<em>?category=ALL&amp;page=0&amp;size=5 *</em><ul>
<li>sort 파라미터가 없다면 UNSORTED로 담긴다.</li>
<li><code>Page request [number: 0, size 5, sort: UNSORTED]</code></li>
</ul>
</li>
<li><strong>?category=ALL&amp;page=0&amp;size=5&amp;sort=createdAt</strong><ul>
<li>만약 정렬기준이 없이 sort만 파라미터에 있다면 Default값은 ASC 오름차순이다.</li>
<li><code>Page request [number: 0, size 5, sort: createdAt: ASC]</code></li>
</ul>
</li>
<li><code>?category=ALL&amp;page=0&amp;size=5&amp;sort=CREATED_AT,DESC</code><ul>
<li>만약 정렬기준을 DESC로 설정하고 싶다면 ,DESC로 추가해준다.</li>
<li><code>Page request [number: 0, size 5, sort: CREATED_AT: DESC]</code>
<img src="https://velog.velcdn.com/images/do_ng_iill/post/b04890a5-92a0-43a7-aeec-ce3c1777c29d/image.png" alt=""></li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>1-2. 파라미터를 sort, page, size를 따로 받기.</strong></p>
<ul>
<li>postController.java<pre><code class="language-java">    @GetMapping(&quot;/api/posts&quot;)
    public Page&lt;GetPostsResponseDto&gt; getPosts(@RequestParam String sort,
                                              @RequestParam String category ,
                                              @RequestParam int page,
                                              @RequestParam int size) {
        return postService.getPosts(sort, category, page, size);
    }</code></pre>
</li>
<li>postrepository를 커스텀할 때 sort와 category가 필요하다. 그래서 sort부분을 따로 받아주기위해 sort, page, size를 따로 받았다.</li>
</ul>
<h3 id="2-service단에서-pagerequestof메소드로-pageable객체-생성">2. Service단에서 PageRequest.of메소드로 Pageable객체 생성</h3>
<ul>
<li>postService.java<pre><code class="language-java">public Page&lt;GetPostsResponseDto&gt; getPosts(@RequestParam String sort,
                                          @RequestParam String category,
                                          @RequestParam int page,
                                          @RequestParam int size) {
        Pageable pageable = PageRequest.of(page,size);
        return postRepositoryImpl.findAllByCategoryOrderBySort(sort, category, pageable);
    }</code></pre>
</li>
<li>Pageabele 객체를 생성한 pageable을 출력해봤다.<ul>
<li><code>Page request [number: 0, size 5, sort: UNSORTED]</code></li>
<li>page와 size만 들어가서 sort는 UNSORTED가 담긴다.</li>
</ul>
</li>
<li>참고로 반환타입은 Page다. 반환타입은 Slice, Page, List 3가지가 있다.</li>
</ul>
<h3 id="3-postrepository-커스텀-클래스에서-return해줄-때-new-pageimpl해서-반환">3. postRepository 커스텀 클래스에서 return해줄 때 new PageImpl&lt;&gt;()해서 반환!</h3>
<ul>
<li>postRepositoryImpl.java<pre><code class="language-java">    @Override
    public Page&lt;GetPostsResponseDto&gt; findAllByCategoryOrderBySort(String sort, String category, Pageable pageable) {
        List&lt;GetPostsResponseDto&gt; returnPost = queryFactory.select(Projections.fields(
                GetPostsResponseDto.class,
                        post.title,
                        post.category,
                        post.deadline,
                        post.numberPeople,
                        post.currentNumberPeople,
                        post.contactMethod,
                        post.viewCount,
                        post.user.nickname,
                        post.imageUrl
                        ))
                .from(post)
                .where(categoryContains(category))
                .orderBy(orderByValidDeadline(),getOrderSpecifier(sort))
                .fetch();
        return new PageImpl&lt;&gt;(returnPost,pageable,returnPost.size());
    }</code></pre>
</li>
<li>여기서 가장 중요한 부분은 return부분이다.<ul>
<li><code>return new PageImpl&lt;&gt;(returnPost,pageable,returnPost.size());</code></li>
<li>PageImpl&lt;&gt;(select 결과값, 생성된 pageable 객체, select 결과값의 크기);</li>
</ul>
</li>
</ul>
<h3 id="결과값">결과값</h3>
<pre><code class="language-json">{
    &quot;content&quot;: [
        {
            &quot;title&quot;: &quot;amo&quot;,
            &quot;category&quot;: &quot;DELIVERY&quot;,
            &quot;deadline&quot;: 1663243762507,
            &quot;numberPeople&quot;: 9,
            &quot;currentNumberPeople&quot;: 4,
            &quot;viewCount&quot;: 65,
            &quot;nickname&quot;: &quot;123&quot;,
        },
        {
            &quot;title&quot;: &quot;dpu&quot;,
            &quot;category&quot;: &quot;EXHIBITION&quot;,
            &quot;deadline&quot;: 1660593585499,
            &quot;numberPeople&quot;: 4,
            &quot;currentNumberPeople&quot;: 3,
            &quot;viewCount&quot;: 40,
            &quot;nickname&quot;: &quot;123&quot;,
        },
      ...
    ],
    &quot;pageable&quot;: {
        &quot;sort&quot;: {
            &quot;empty&quot;: true,
            &quot;sorted&quot;: false,
            &quot;unsorted&quot;: true
        },
        &quot;offset&quot;: 0,
        &quot;pageNumber&quot;: 0,
        &quot;pageSize&quot;: 5,
        &quot;paged&quot;: true,
        &quot;unpaged&quot;: false
    },
    &quot;last&quot;: false,
    &quot;totalElements&quot;: 99,
    &quot;totalPages&quot;: 20,
    &quot;size&quot;: 5,
    &quot;number&quot;: 0,
    &quot;sort&quot;: {
        &quot;empty&quot;: true,
        &quot;sorted&quot;: false,
        &quot;unsorted&quot;: true
    },
    &quot;first&quot;: true,
    &quot;numberOfElements&quot;: 99,
    &quot;empty&quot;: false
}</code></pre>
<ul>
<li>결과값을 보면 자동으로 select결과값은 contents로 싸여지고 자동으로 &quot;pagealbe&quot;, &quot;last&quot;, &quot;totalElements&quot;..등이 생성된다.<ul>
<li>last(boolean타입) : 이 페이지가 마지막인가?</li>
<li>totalElements(int타입) : 요소의 총 수, 즉 contents의 크기 혹은 길이</li>
<li>totalPages : 만들 수 있는 페이지 총 수</li>
<li>size(int타입) : 페이지 당 나타낼 수 있는 요소 수(참고로 <strong>default : 20</strong>)</li>
<li>number(int타입) : 현재 페이지번호</li>
<li>first(boolean타입) : 첫 번째 페이지인가?</li>
<li>numberOfElements(int타입) : 실제 데이터 개수</li>
<li>empty(boolean타입) : 리스트가 비어있가?</li>
</ul>
</li>
<li>참고로 페이지 시작은 0부터 시작이다!</li>
</ul>
<h2 id="slice-page-list-차이점은">Slice, Page, List 차이점은?</h2>
<pre><code class="language-java">Slice&lt;User&gt; findByLastname(String lastname, Pageable pageable);

Page&lt;User&gt; findByLastname(String lastname, Pageable pageable);

List&lt;User&gt; findByLastname(String lastname, Pageable pageable);

List&lt;User&gt; findByLastname(String lastname, Sort sort);</code></pre>
<h3 id="slice">Slice</h3>
<ul>
<li>Slice는 Page에서 카운트 쿼리에 많은 비용이 발생하는 경우에 Slice를 사용하면 된다.</li>
<li>Slice는 다음 Slice가 존재하는지 여부만 알기 때문에 전체 데이터의 셋의 크기가 큰 경우에는 Slice를 사용하는 것이 성능상 유리하다.</li>
<li>무한스크롤에 적합하다.</li>
</ul>
<h3 id="page">Page</h3>
<ul>
<li>page는 사용 가능한 데이터의 총 개수 및 전체 페이지 수를 알 수 있다.</li>
<li>총 개수를 알아내기 위해 추가적으로 카운트 쿼리가 실행된다.</li>
<li>기본적으로 카운트 쿼리는 실제로 실행되는 쿼리에서 파생된다.</li>
</ul>
<h3 id="list">List</h3>
<ul>
<li>Pageable을 통해서 정렬을 할 수 있지만, 정렬만 하는 경우 Sort를 사용하는 것이 좋다.</li>
<li>결과를 단순히 List로 받을 수 있다.</li>
<li>이 경우 Page 인스턴스를 생성하기 위한 메타데이터가 생성되지 않기 때문에 카운트 쿼리가 실행되지 않는다.</li>
<li>단순히 주어진 범위내의 엔티티를 검색하기 위한 쿼리만 실행된다.</li>
</ul>
<h3 id="참고-블로그">참고 블로그</h3>
<ul>
<li><a href="https://velog.io/@dltkdgns3435/SpringBoot-Spring-Data-JPA-%EC%97%90%EC%84%9C-Page%EC%99%80-Slice">https://velog.io/@dltkdgns3435/SpringBoot-Spring-Data-JPA-%EC%97%90%EC%84%9C-Page%EC%99%80-Slice</a><ul>
<li>성능비교하는 것이 인상적이었다.</li>
</ul>
</li>
<li><a href="https://n1tjrgns.tistory.com/263">https://n1tjrgns.tistory.com/263</a></li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li><strong>Pageable을 이용한 Pagination을 처리하는 다양한 방법</strong><ul>
<li>기본적인 내용들이 잘 나타나있음.</li>
<li><a href="https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/">https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/</a></li>
</ul>
</li>
<li><strong>Pageable 사용하기</strong><ul>
<li>기본적인 내용들이 잘 나타나있음.</li>
<li><a href="https://jiyongpark-dev.tistory.com/15">https://jiyongpark-dev.tistory.com/15</a></li>
</ul>
</li>
<li><strong>[JPA] Paging</strong><ul>
<li>기본적인 내용들이 잘 나타나있음.</li>
<li><a href="https://tmdrl5779.tistory.com/61">https://tmdrl5779.tistory.com/61</a></li>
</ul>
</li>
<li><strong>[JPA] Querydsl에 pageable을 적용하며... 2가지 방법을</strong><ul>
<li>Page객체뿐만 아니라 Slice 객체도 사용하는 예제가 있음.</li>
<li><a href="https://jessyt.tistory.com/55">https://jessyt.tistory.com/55</a></li>
</ul>
</li>
<li><strong>[일지] List 를 pageable 과 PageImpl 로 구현하기 ( List to pageImpl )</strong><ul>
<li><a href="https://onejunu.tistory.com/76">https://onejunu.tistory.com/76</a></li>
</ul>
</li>
<li><strong>[Spring Boot] JPA + Pageable 을 이용한 페이징 처리</strong><ul>
<li>PagingAndSortingRepository의 상속관계를 잘 알 수 있음.</li>
<li><a href="https://ibks-platform.tistory.com/277">https://ibks-platform.tistory.com/277</a></li>
</ul>
</li>
<li><strong>[QueryDSL] Page와 Slice</strong><ul>
<li><a href="https://zzerosouth.tistory.com/32">https://zzerosouth.tistory.com/32</a></li>
</ul>
</li>
<li><strong>[Querydsl, pageable] slice를 이용한 무한 스크롤</strong><ul>
<li><a href="https://earth-95.tistory.com/116">https://earth-95.tistory.com/116</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIL] CORS란?]]></title>
            <link>https://velog.io/@do_ng_iill/WIL-CORS%EB%9E%80</link>
            <guid>https://velog.io/@do_ng_iill/WIL-CORS%EB%9E%80</guid>
            <pubDate>Fri, 22 Jul 2022 13:56:39 GMT</pubDate>
            <description><![CDATA[<h2 id="cors">CORS?</h2>
<blockquote>
<p><strong>CORS(Cross Origin Resource Sharing) : 클라이언트 애플리케이션과 다른 origin을 가진 서버 애플리케이션이 서로 통신할 수 있도록 허용하는 프로토콜</strong></p>
</blockquote>
<ul>
<li>교차 출처 리소스 공유 혹은 교차 출처 자원 공유라고 한다.</li>
<li>여기서 다른 orgin은 뭘까?<ul>
<li>다른 origin이란 예를 들어 클라이언트 애플리케이션은 <a href="http://www.mysite.com">www.mysite.com</a> 으로 올라가 있는 반면 서버 애플리케이션은 <a href="http://www.serverapi.com">www.serverapi.com</a> 으로 올라가 있다고 했을 때 클라이언트와 서버 애플리케이션은 다른 origin을 가졌다고 말합니다.</li>
</ul>
</li>
<li>사실 클라이언트와 서버는 같은 origin을 가진 경우에만 서로 통신을 할 수 있다. 이를 Same Origin Policy 라고도 한다.<ul>
<li>그 이유는 클라이언트와 서버의 origin이 달라도 통신을 무조건 허용하면 클라이언트에서 악의적으로 서버에 접근할 가능성이 너무 높기 때문이다.</li>
</ul>
</li>
<li>즉, 보안을 위해서 같은 origin일 때만 통신을 할 수 있는 것이다.</li>
<li>그래서 같은 origin을 가진 경우에만 통신을 허용하여 웹 브라우저에서는 다른 origin을 가진 서버에 API를 호출할려고 하면 요청 자체를 막아버린다.</li>
<li>그런데 클라이언트와 서버가 같은 origin을 가진 경우는 그렇게 많지 않다.<ul>
<li>여러가지 이유가 있지만 그 중 프로젝트에서 클라이언트의 크기가 커지면서
클라이언트는 클라이언트데로 별도로 관리하기 위해 별도의 origin을 가지게 되었고
서버는 서버데로 origin을 가지게 된 경우가 그렇다.</li>
</ul>
</li>
<li>물론 보안적인 이슈로 인해 클라이언트와 서버 애플리케이션을 다른 origin에 넣는 경우도 많다.</li>
<li>유저에게는 서버에 직접적으로 접근할 수 있는 모든 방향을 차단하는 것이 좋기 때문이다.
👉🏻 그래서 CORS가 필요한 것이다. 서로 다른 origin에서도 통신이 가능해야하는 상황이 온 것이기 때문이다.</li>
</ul>
<h2 id="react와-spring-boot-cors통신-방법">React와 Spring Boot CORS통신 방법</h2>
<ul>
<li>Spring과 React 통신 시에 CORS 에러 해결<ul>
<li><a href="https://l4279625.tistory.com/26">https://l4279625.tistory.com/26</a> 해당블로그 글 참고.</li>
</ul>
</li>
<li>[Spring] Spring Security, React를 사용하면서 CORS 허용하는 방법<ul>
<li><a href="https://devlog-wjdrbs96.tistory.com/429">https://devlog-wjdrbs96.tistory.com/429</a></li>
</ul>
</li>
<li>리액트 - 스프링부트 연동 CORS 이슈 해결!<ul>
<li><a href="https://velog.io/@pjj186/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%97%B0%EB%8F%99-CORS-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0">https://velog.io/@pjj186/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%97%B0%EB%8F%99-CORS-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0</a></li>
</ul>
</li>
<li>[Spring Security] CORS<ul>
<li><a href="https://velog.io/@chullll/Spring-Security-CORS">https://velog.io/@chullll/Spring-Security-CORS</a></li>
</ul>
</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<ul>
<li>CORS란 무엇인가: CORS는 브라우저 하는 일<ul>
<li><a href="https://valuefactory.tistory.com/1141">https://valuefactory.tistory.com/1141</a></li>
</ul>
</li>
<li>[Browser] CORS란?<ul>
<li><a href="https://beomy.github.io/tech/browser/cors/">https://beomy.github.io/tech/browser/cors/</a></li>
</ul>
</li>
<li>CORS란 무엇인가?<ul>
<li><a href="https://hannut91.github.io/blogs/infra/cors">https://hannut91.github.io/blogs/infra/cors</a></li>
</ul>
</li>
<li>CORS란?<ul>
<li><a href="https://medium.com/@su_bak/cors%EB%9E%80-f7e1447e97d8">https://medium.com/@su_bak/cors%EB%9E%80-f7e1447e97d8</a></li>
</ul>
</li>
<li>교차 출처 리소스 공유<ul>
<li><a href="https://ko.wikipedia.org/wiki/%EA%B5%90%EC%B0%A8_%EC%B6%9C%EC%B2%98_%EB%A6%AC%EC%86%8C%EC%8A%A4_%EA%B3%B5%EC%9C%A0">https://ko.wikipedia.org/wiki/%EA%B5%90%EC%B0%A8_%EC%B6%9C%EC%B2%98_%EB%A6%AC%EC%86%8C%EC%8A%A4_%EA%B3%B5%EC%9C%A0</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIL] ORM, SQL, MVC]]></title>
            <link>https://velog.io/@do_ng_iill/WIL-ORM-SQL-MVC</link>
            <guid>https://velog.io/@do_ng_iill/WIL-ORM-SQL-MVC</guid>
            <pubDate>Fri, 15 Jul 2022 02:52:19 GMT</pubDate>
            <description><![CDATA[<h2 id="📌-intro">📌 Intro</h2>
<ul>
<li>오늘부터 항해99 이제 5주차 주특기 심화가 시작되었다. 4주차 주특기 숙련때 과제가 너무 힘들었다. Spring Security, JWT, JPA... 아직 Spring이 이숙해지기도 전에 더 많은 기술들로 과제를 진행하다보니 정말 힘들었다. 처음으로 개인과제 구현을 다 못끝냈다. 그나마 위안이 되는 것은 모두가 힘들어했다는 것.. 그리고 5주차~7주차를 진행하면서 해당 기술들을 계속 사용해보고 익힐 수 있다는 것이다.</li>
<li>4주차를 진행하면서 Spring을 사용하는 것에 부족함이 많다고 많이 느꼈다. 그래서 인프런에서 Spring강의를 로드맵채로 구매했다. 단지 아쉬운 것은 JPA 로드맵도 통채로 구매하고 싶었지만...그것까지는 너무 부담이 되었고 일단 JPA는 책을 사서 공부를 진행해야하나 싶다.</li>
<li>이번 5주차 과제는 프론트엔드단에서 해야할 일은 없고 오로지 API설계대로 구현하는 것인데 이것도 쉽지않아 보인다. 그래도 끝까지 해봐야겠다.</li>
</ul>
<h2 id="1-ormobject-relational-mapping">1. ORM(Object-Relational Mapping)</h2>
<h3 id="1-orm이란">1. ORM이란</h3>
<ul>
<li>객체 지향 프로그래밍의 객체와 관계형 데이터베이스의 데이터를 매핑하는 기술</li>
<li>객체 지향 프로그래밍에서 사용할 수 있는 가상의 객체 지향 데이터베이스를 만들어 프로그래밍 코드와 데이터 연결</li>
<li>SQL을 사용하지 않고도 DB의 데이터를 쉽게 객체로 만들어줌</li>
<li>ORM이 필요한 이유는 객체 지향 언어과 관계형 데이터베이스사이의 패러다임 불일치가 있기때문이다.</li>
<li>이 둘 간의 <strong>패러다임 불일치</strong> 때문에 개발자는 더 많은 코드를 작성해야 하며, 이는 반복적이고 실수하기 쉬운 작업이 된다.<ul>
<li><strong>객체 지향</strong><ul>
<li>필드와 메서드 등을 묶어서 객체로 잘 만들어 사용하는 것이 목표</li>
<li>객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다.</li>
</ul>
</li>
<li><strong>관계형 데이터베이스</strong><ul>
<li>데이터를 잘 정규화해서 보관하는 것이 목표</li>
</ul>
</li>
</ul>
</li>
<li>그렇기 때문에 개발자는 <strong>객체지향적인 설계에 집중</strong>할 수 없게 된다. ORM이 바로 이러한 문제를 해결해 준다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/5303d5a8-c5fa-4e44-8d53-fe335dea379b/image.png" alt="">
<img src="https://velog.velcdn.com/images/do_ng_iill/post/1077f884-e451-4f4f-b5f9-26229286fc48/image.png" alt=""></p>
<h3 id="2-orm-장점">2. ORM 장점</h3>
<ul>
<li>SQL을 사용하지 않고 객체 지향 프로그래밍 언어를 사용하여 그대로 사용이 가능<ul>
<li>SQL query를 쓰지 않고 객체 간의 관계를 풀어낼 수 있기 때문에 개발자가 좀 더 직관적으로 객체 간의 관계 파악이 가능</li>
</ul>
</li>
<li>재사용 및 유지보수의 편리성</li>
<li>DBMS에 대한 종속성이 감소</li>
</ul>
<h3 id="3-orm-단점">3. ORM 단점</h3>
<ul>
<li>완벽하게 ORM만을 사용해서 서비스를 구현하기 힘듦</li>
<li>프로시저가 많은 시스템에서는 ORM의 객체 지향적인 장점 활용이 어려움</li>
<li>프레임워크가 자동으로 작성하기 때문에 의도대로 작성이 되었는지 확인할 필요가 있음</li>
<li>객체지향적 사용을 고려, 설계한 데이터베이스가 아닌 경우 프로젝트가 크고 복잡할수록 ORM 기술 적용이 어려움</li>
<li>기존의 기업들의 경우 ORM을 고려하지 않은 데이터베이스를 사용하고 있어 ORM에 적합하게 변환하기 위해선 많은 시간과 노력이 필요함</li>
</ul>
<h2 id="2-sql">2. SQL</h2>
<blockquote>
<p>SQL( Structured Query Language, 구조화 질의어 ) : 관계형 데이터베이스 관리 시스템(RDBMS)의 데이터를 관리하기 위해 설계된 특수 목적의 프로그래밍 언어</p>
</blockquote>
<h3 id="sql-명령어-종류">SQL 명령어 종류</h3>
<ul>
<li>데이터 정의 언어 ( <strong>D</strong>ata <strong>D</strong>efinition <strong>L</strong>anguage, <strong>DDL</strong> )<ul>
<li><blockquote>
<p><strong>컴퓨터 사용자 또는 응용 프로그램 소프트웨어가 컴퓨터의 데이터를 정의</strong>하는 컴퓨터 언어 또는 컴퓨터 언어 요소</p>
</blockquote>
</li>
<li>대표적인 명령어<ul>
<li>CREATE : 새로운 데이터베이스 관계 (테이블), VIEW, 인덱스, 저장 프로시저 만들기</li>
<li>DROP : 이미 존재하는 데이터베이스 관계(테이블), 뷰, 인덱스, 저장 프로시저를 제거</li>
<li>ALTER : 이미 존재하는 데이터베이스 개체에 대한 변경, RENAME의 역할</li>
<li>TRUNCATE : 관계 (테이블)에서 데이터를 돌이킬 수 없는 제거</li>
</ul>
</li>
</ul>
</li>
<li>데이터 조작 언어 ( <strong>D</strong>ata <strong>M</strong>anipulation <strong>L</strong>anguage, <strong>DML</strong> )<ul>
<li><blockquote>
<p>데이터베이스 사용자 또는 응용 프로그램 소프트웨어가 컴퓨터 <strong>데이터베이스에 대해 데이터 검색, 등록, 삭제, 갱신을 위한, 데이터베이스 언어</strong> 또는 데이터베이스 언어 요소</p>
</blockquote>
</li>
<li>대표적인 명령어<ul>
<li>SELECT : 검색(질의)</li>
<li>INSERT : 삽입(등록)</li>
<li>UPDATE : 업데이트(수정)</li>
<li>DELETE : 삭제</li>
</ul>
</li>
</ul>
</li>
<li>데이터 제어 언어 ( <strong>D</strong>ata <strong>C</strong>ontrol <strong>L</strong>anguage, <strong>DCL</strong> )<ul>
<li><blockquote>
<p>데이터베이스에서 <strong>데이터에 대한 액세스를 제어하기 위한 데이터베이스 언어</strong> 또는 데이터베이스 언어 요소</p>
</blockquote>
</li>
<li>대표적인 명령어<ul>
<li>GRANT : 특정 데이터베이스 사용자에게 특정 작업을 수행 권한을 부여</li>
<li>REVOKE : 특정 데이터베이스 이용자에게 부여한 특정 권한을 박탈</li>
<li>GRANT </li>
<li>CONNECT: 데이터베이스 또는 스키마에 연결하는 권한</li>
<li>SELECT : 데이터베이스에서 데이터를 검색하는 권한</li>
<li>INSERT : 데이터베이스에 데이터를 등록할 수 있는 권한</li>
<li>UPDATE : 데이터베이스의 데이터를 업데이트 할 수 있는 권한</li>
<li>DELETE : 데이터베이스의 데이터를 삭제할 수 있는 권한</li>
<li>USAGE : 스키마 또는 함수와 같은 데이터베이스 개체를 사용할 수 있는 권한</li>
</ul>
</li>
<li>오라클 데이터베이스에서는 데이터 제어 언어의 명령 실행은 암시적 커밋을 수반한다. PostgreSQL에서는 데이터 제어 언어의 명령 실행은 트랜잭션의 맥락에서 이루어지므로 롤백을 할 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="3-mvc">3. MVC</h2>
<blockquote>
<p><strong>MVC( Model–View–Controller, MVC )는 소프트웨어 공학에서 사용되는 소프트웨어 디자인 패턴이다.</strong></p>
</blockquote>
<h3 id="구성요소">구성요소</h3>
<ul>
<li>컨트롤러 : 데이터와 비즈니스 로직 사이의 상호 동작을 관리한다. 즉, 모델과 뷰를 통제한다. MVC 패턴에서 View와 Model이 직접적인 상호 소통을 하지 않도록 관리한다.</li>
<li>모델 :  어플리케이션의 정보나 데이터, DB 등을 말한다.</li>
<li>뷰 : 사용자가 볼 결과물을 생성하기 위해 모델로부터 값을 받아오고  컨트롤 정보를 얻어 온다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/0ef89c7b-2a7b-4dbb-af6c-342d1afd1983/image.png" alt=""></p>
<ul>
<li>참고로 MVC패턴은 크게 MVC 1패턴, MVC 2패턴 2가지가 있다. 그 중에서 스프링이 채택한 패턴은 MVC 2패턴이고 MVC1과 어떻게 다른지 알아보자.  </li>
</ul>
<h3 id="mvc1">MVC1</h3>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/de38378b-8b4c-4b71-9528-c0b9118fb87f/image.png" alt=""></p>
<ul>
<li>View와 Controller를 모두 JSP가 담당하는 형태를 가진다.</li>
<li>즉, JSP 하나로 유저의 요청을 받고 응답을 처리하므로 구현 난이도는 쉽다.</li>
<li>JSP 하나에서 MVC 가 모두 이루어지다보니 재사용성도 매우 떨어지고, 읽기도 힘들어진다.</li>
<li>즉, 유지보수에 있어서 문제가 발생하다보니 대규모 프로젝트에서는 적합하지 않다.</li>
</ul>
<h3 id="mvc2">MVC2</h3>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/ed105422-f00d-4a84-988a-fb052c68307a/image.png" alt=""></p>
<ul>
<li>MVC2 패턴은 널리 표준으로 사용되는 패턴이다.</li>
<li>요청을 하나의 컨트롤러(Servlet)가 먼저 받는다. 즉, MVC1과는 다르게 Controller, View가 분리되어 있습니다.</li>
<li>따라서 역할이 분리되어 MVC1패턴에서의 단점을 보완할 수 있다.</li>
<li>개발자는 M, V, C 중에서 수정해야 할 부분이 있다면, 그것만 꺼내어 수정하면 된다. 따라서 <strong>유지보수에 있어서도 큰 이점을 가진다.</strong></li>
<li>MV2는 MVC1 패턴보다 구조가 복잡해질 수 있지만, 개발자가 이러한 세부적인 구성까지 신경쓰지 않을 수 있도록 각종 프레임워크들이 지금까지 잘 발전되어 왔다.</li>
<li>그 중에서 대표적인 것이 바로 스프링 프레임워크입니다.</li>
</ul>
<h3 id="spring-mvc2">Spring MVC2</h3>
<p><img src="https://velog.velcdn.com/images/do_ng_iill/post/06f82d13-ea1b-46f0-8c5b-2ba6df751a62/image.png" alt=""></p>
<ul>
<li>스프링에서는 유저의 요청을 받는 DispathcerServlet이 핵심이다. 이것이 Front Controller의 역할을 맡는다.</li>
<li>Front Controller(프런트 컨트롤러)란, 우선적으로 유저(클라이언트)의 모든 요청을 받고, 그 요청을 분석하여 세부 컨트롤러들에게 필요한 작업을 나눠주게 된다.</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<h3 id="orm">ORM</h3>
<ul>
<li>[DATABASE] ORM(Object-Relational Mapping)<ul>
<li><a href="https://rosypark.tistory.com/296">https://rosypark.tistory.com/296</a></li>
</ul>
</li>
<li>[Spring] Spring Data JPA 이해하기 (feat ORM, JPA)<ul>
<li><a href="https://doing7.tistory.com/105">https://doing7.tistory.com/105</a></li>
</ul>
</li>
</ul>
<h3 id="sql">SQL</h3>
<ul>
<li>SQL<ul>
<li><a href="https://ko.wikipedia.org/wiki/SQL">https://ko.wikipedia.org/wiki/SQL</a></li>
</ul>
</li>
<li>데이터 정의 언어<ul>
<li><a href="https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%95%EC%9D%98_%EC%96%B8%EC%96%B4">https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%95%EC%9D%98_%EC%96%B8%EC%96%B4</a></li>
</ul>
</li>
<li>데이터 조작 언어<ul>
<li><a href="https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A1%B0%EC%9E%91_%EC%96%B8%EC%96%B4">https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A1%B0%EC%9E%91_%EC%96%B8%EC%96%B4</a><ul>
<li>데이터 제어 언어</li>
</ul>
</li>
<li><a href="https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%9C%EC%96%B4_%EC%96%B8%EC%96%B4">https://ko.wikipedia.org/wiki/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EC%A0%9C%EC%96%B4_%EC%96%B8%EC%96%B4</a></li>
</ul>
</li>
</ul>
<h3 id="mvc">MVC</h3>
<ul>
<li>[Spring] Spring의 MVC 패턴과 MVC1과 MVC2 비교<ul>
<li><a href="https://chanhuiseok.github.io/posts/spring-3/">https://chanhuiseok.github.io/posts/spring-3/</a></li>
</ul>
</li>
<li>[Spring] MVC 패턴 &amp; Spring Framework MVC<ul>
<li><a href="https://aridom.tistory.com/61">https://aridom.tistory.com/61</a></li>
</ul>
</li>
<li>[Spring] 스프링 MVC 모델<ul>
<li><a href="https://emongfactory.tistory.com/121">https://emongfactory.tistory.com/121</a></li>
</ul>
</li>
<li>모델-뷰-컨트롤러<ul>
<li><a href="https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC">https://ko.wikipedia.org/wiki/%EB%AA%A8%EB%8D%B8-%EB%B7%B0-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC</a></li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[WIL] 🍃 의존성 주입(DI)과 제어의 역전(IoC) 그리고 스프링 빈(Spring Bean)]]></title>
            <link>https://velog.io/@do_ng_iill/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI%EA%B3%BC-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84IoC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88Spring-Bean</link>
            <guid>https://velog.io/@do_ng_iill/%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85DI%EA%B3%BC-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84IoC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88Spring-Bean</guid>
            <pubDate>Sat, 09 Jul 2022 02:26:25 GMT</pubDate>
            <description><![CDATA[<h1 id="📌-intro">📌 Intro</h1>
<ul>
<li><p>항해99를 시작한지 이제 4주차가 된다. 3주차 주특기 입문에서 처음으로 Spring을 사용해봤다. Spring이 어렵다고 이야기만 들었는데 생각보다 너무 편하다. 하지만 편한만큼 내부적으로 어떻게 동작하는지 잘 이해가가지 않는 단점이 있다. 이제 Spring을 제대로 공부할 시간이 4주차 5주차 2주간의 시간이 남았는데 인프런의 Spring강의를 구매해서 들어볼까 고민중이다.</p>
</li>
<li><p>Spring에서 중요한 개념 3가지를 한 번 정리해볼까한다.</p>
</li>
</ul>
<h1 id="1-di의존성-주입">1. DI(의존성 주입)</h1>
<blockquote>
<p><strong>의존성 주입( Dependency Injection ) : 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다.</strong></p>
</blockquote>
<ul>
<li>의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것이다. 이는 가독성과 코드 재사용을 높혀준다.</li>
<li>의존성 주입은 아래와 같은 문제를 해결한다.<ul>
<li>어떻게 애플리케이션이나 클래스가 객체의 생성 방식과 독립적일 수 있는가?</li>
<li>어떻게 객체의 생성 방식을 분리된 구성 파일에서 지정할 수 있는가?</li>
<li>어떻게 애플리케이션이 다른 구성을 지원할 수 있는가?</li>
</ul>
</li>
<li>프로그램 디자인이 결합도를 느슨하게 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트(수신객체)의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것이다.</li>
<li>의존성 주입의 기본 단위인 주입은 새롭거나 관습적인 메커니즘이 아니다. &quot;매개변수 전달&quot;과 동일하게 동작한다.</li>
</ul>
<hr>
<ul>
<li>서비스를 사용하려는 클라이언트(수신 객체)가 해당 서비스의 구성방법을 알 필요가 없게 하는 것.</li>
<li>의존성 주입은 프레임워크가 아닌 디자인 패턴, IoC 방법 중 하나.</li>
<li>의존성 주입은 컴파일타임이 아닌 런타임에 종속성을 해결하는 데 사용됨.</li>
<li>무언가를 쉽게 교체할 수 있게 만들어준다.</li>
</ul>
<h2 id="장점">장점</h2>
<ul>
<li>모듈들을 쉽게 교체할 수 있는 구조 -&gt; 테스팅이 쉽고 마이그레이션하기도 쉽다.</li>
<li>구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어주기 때문에 어플리케이션 의존성 방향이 일관되고, 어플리케이션을 쉽게 추론 가능. 👉🏻 모듈 간 관계가 조금 더 명확해짐.</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li>모듈들의 분리가 증가 -&gt; 클래스 수 증가 -&gt; 복잡성이 증가됨.</li>
<li>복잡성이 증가됨에 따라 약간의 런타임 페널티가 생기기도 한다.</li>
<li>종속성은 런타임에서 해결되기 때문에 종속성 오류는 컴파일 시간에 포착할 수 없다.</li>
</ul>
<h2 id="의존성-주입-사용법">의존성 주입 사용법</h2>
<h3 id="lombok없이">Lombok없이</h3>
<pre><code class="language-java">@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @PostMapping(&quot;members/new&quot;)
    public String create(MemberForm form){
        Member member = new Member();
        member.setName(form.getName());
        memberService.join(member);
        return &quot;redirect:/&quot;;
    }
}</code></pre>
<ul>
<li>클라이언트(수신객체)에 사용할 memberService를 private final 키워드를 사용해 선언.</li>
<li>생성자로 의존성 주입 후 해당 메소드 위에 <code>@Autowired</code> 어노테이션 추가<pre><code class="language-java">    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }</code></pre>
</li>
</ul>
<h3 id="lombok활용">Lombok활용</h3>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostRepository postRepository;
    private final PostService postService;

    @PostMapping(&quot;/posts&quot;)
    public Post createPost(@RequestBody PostRequestDto requestDto){
        Post post = new Post(requestDto);
        return postRepository.save(post);
    }
}</code></pre>
<ul>
<li>클라이언트(수신객체)에 사용할 postRepository, postService를 private final 키워드를 사용해 선언.</li>
<li>Controller 클래스 위에 <code>@RequiedArgsConstructor</code> 어노테이션 추가만 해주면 됨.</li>
</ul>
<h1 id="2-ioc제어의-역전">2. IoC(제어의 역전)</h1>
<blockquote>
<p><strong>제어의 역전( Inversion of Control ) : 프로그래머가 작성한 프로그램이 재사용 라이브러리의 흐름 제어를 받게 되는 소프트웨어 디자인 패턴을 말한다.</strong></p>
</blockquote>
<ul>
<li>제어의 역전은 의존성 주입의 상위 개념이다.</li>
<li>Spring Container가 필요에 따라 개발자 대신 Bean들을 관리(제어)해주는 행위<ul>
<li>Spring Container는 기본적으로 싱글톤 패턴의 객체 생성을 지향한다.</li>
<li>별도로 설정하지 않으면 모든 컨테이너 안의 빈(Bean) 객체는 딱 하나만 생성되어 계속 재사용된다. <strong>Why?</strong> 내부 데이터가 바뀌지 않는 객체들을 여러 개 생성해서 사용하는 것은 자원 낭비이기 때문이다.</li>
<li>그래서 스프링 컨테이너를 <strong>싱글톤 관리 컨테이너</strong>라고 부르는 사람들도 있는 것 같다. </li>
</ul>
</li>
<li>일반적인 상황에서는 개발자가 직접 객체를 제어해야 했다. new 연산자를 통해 객체를 생성하고, 객체의 의존성을 맺어주고, 초기화를 해주고 등등..</li>
<li>Spring 에서는 xml파일 또는 어노테이션 방식으로 스프링 컨테이너에 Bean(객체)를 등록하기만 하면, 스프링 컨테이너에서 Bean의 생명주기(생성 -&gt; 의존성 설정 -&gt; 초기화 -&gt; 소멸)를 전부 관리해준다.</li>
<li>즉, 객체에 대한 제어권이 컨테이너로 역전되기 때문에 제어의 역전이라고 하는 것</li>
</ul>
<hr>
<blockquote>
<ul>
<li>일반적으로 처음에 배우는 자바 프로그램에서는 각 객체들이 프로그램의 흐름을 결정하고 각 객체를 직접 생성하고 조작하는 작업(객체를 직접 생성하여 메소드 호출)을 했다. 즉, 모든 작업을 사용자가 제어하는 구조였다. </li>
</ul>
</blockquote>
<ul>
<li>예를 들어 A 객체에서 B 객체에 있는 메소드를 사용하고 싶으면, B 객체를 직접 A 객체 내에서 생성하고 메소드를 호출한다.</li>
<li>하지만 IOC가 적용된 경우, 객체의 생성을 특별한 관리 위임 주체에게 맡긴다. 이 경우 사용자는 객체를 직접 생성하지 않고, 객체의 생명주기를 컨트롤하는 주체는 다른 주체가 된다.</li>
<li>즉, 사용자의 제어권을 다른 주체에게 넘기는 것을 IOC(제어의 역전) 라고 합니다.</li>
</ul>
<h2 id="장점-1">장점</h2>
<ul>
<li>개발자는 객체 관리에 덜 신경쓸 수 있게 된다.</li>
<li>약한 결합을 이용하여 객체 간 의존 관계를 쉽게 변경할 수 있다.</li>
<li>결과적으로 코드의 재사용성과 유지보수성을 높인다.</li>
</ul>
<h1 id="3-spring-bean스프링-빈">3. Spring Bean(스프링 빈)</h1>
<blockquote>
<p><strong>스프링 빈(Spring Bean) : Spring IoC 컨테이너가 관리하는 자바 객체를 말한다.</strong></p>
</blockquote>
<ul>
<li>우리가 알던 기존의 Java Programming 에서는 Class를 생성하고 new를 입력하여 원하는 객체를 직접 생성한 후에 사용했었다.</li>
<li>하지만 Spring에서는 직접 new를 이용하여 생성한 객체가 아니라, Spring에 의하여 관리당하는 자바 객체를 사용한다. <strong>이렇게 Spring에 의하여 생성되고 관리되는 자바 객체를 Bean</strong>이라고 한다.</li>
<li>Spring Framework 에서는 Spring Bean 을 얻기 위하여 <strong>ApplicationContext.getBean() 와 같은 메소드를 사용하여 Spring 에서 직접 자바 객체를 얻어서 사용</strong>합니다.</li>
</ul>
<h2 id="3-1-spring-bean을-spring-ioc-container에-등록하는-방법">3-1 Spring Bean을 Spring IoC Container에 등록하는 방법</h2>
<ul>
<li>Spring Bean을 Spring IoC Container에 등록하는 방법은 2가지가 있다.</li>
</ul>
<h3 id="3-1-1-자바-어노테이션java-annotation을-사용하는-방법">3-1-1 자바 어노테이션(Java Annotation)을 사용하는 방법</h3>
<ul>
<li><p>자바 어노테이션 참고!</p>
<ul>
<li>JAVA에서 어노테이션 이라는 기능이 있다. <strong>사전상으로는 주석</strong>의 의미이지만 Java 에서는 <strong>주석 이상의 기능을 가지고 있다.</strong></li>
<li>어노테이션은 자바 소스 코드에 추가하여 사용할 수 있는 <strong>메타데이터의 일종</strong>이다.</li>
<li>소스코드에 추가하면 단순 주석의 기능을 하는 것이 아니라 특별한 기능을 사용할 수 있다.</li>
</ul>
</li>
<li><p>어노테이션 예제</p>
<ul>
<li>Java에서는 @Override, @Deprecated 와 같은 기본적인 어노테이션을 제공한다. 아래의 상속 예제에서는 @Override 를 이용하여 상속임을 명시해준다.<pre><code class="language-java">public class Parent { 
  public void doSomething() { 
      System.out.println(&quot;This is Parent&quot;); 
  } 
} 
</code></pre>
</li>
</ul>
<p>public class Son extends Parent{ </p>
<pre><code>@Override 
public void doSomething() { 
    System.out.println(&quot;This is Son&quot;); 
} </code></pre><p>}</p>
<pre><code></code></pre></li>
<li><p>그럼 실제 Spring에서 어떤 어노테이션을 사용해 Bean 등록할까?</p>
<ul>
<li>@Component 어노테이션을 사용해 Bean을 등록한다.</li>
<li>@Component 어노테이션이 등록되어 있는 경우에는 Spring이 Annotation을 확인하고 자체적으로 Bean으로 등록한다.</li>
</ul>
</li>
<li><p>Spring @Component 예제</p>
<pre><code class="language-java">@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping(&quot;/members/new&quot;)
    public String createForm(){
        return &quot;/members/createMemberForm&quot;;
    }
}</code></pre>
<ul>
<li><p>왜 @Component가 없지? 라고 생각할 수 있다. 해당 부분을 확인하기 위해서 인텔리제이를 사용한다면 Ctrl을 누른 상태로 @Controller를 클릭해본다. 그럼 아래와 같은 코드를 확인할 수 있다.</p>
<pre><code class="language-java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

  /**
   * The value may indicate a suggestion for a logical component name,
   * to be turned into a Spring bean in case of an autodetected component.
   * @return the suggested component name, if any (or empty String otherwise)
   */
  @AliasFor(annotation = Component.class)
  String value() default &quot;&quot;;

}</code></pre>
<ul>
<li>위 @Controller 어노테이션 코드에서 @Component가 있음을 확인할 수 있다.</li>
<li>@Component 어노테이션을 통해 Spring은 해당 Controller를 Bean으로 등록한다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="3-1-2-bean-configuration-file에-직접-bean-등록하는-방법">3-1-2. Bean Configuration File에 직접 Bean 등록하는 방법</h3>
<ul>
<li><p>@Configuration과 @Bean 어노테이션을 이용하여 Bean을 등록할 수 있다.</p>
</li>
<li><p>아래의 예제와 같이 @Configuration을 이용하면 Spring 프로젝트에서의 Configuration 역할을 하는 Class를 지정할 수 있다.</p>
</li>
<li><p>해당 파일 하위에 Bean으로 등록하고자 하는 Class에 @Bean 어노테이션을 사용해주면 간단하게 Bean을 등록할 수 있다.</p>
<pre><code class="language-java">@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    private final EntityManager em;
    @Autowired
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new JpaMemberRepository(em);
    }
}</code></pre>
</li>
</ul>
<h2 id="참고자료">참고자료</h2>
<h3 id="의존성-주입">의존성 주입</h3>
<ul>
<li><a href="https://devlog-wjdrbs96.tistory.com/165">https://devlog-wjdrbs96.tistory.com/165</a></li>
<li><a href="https://esoongan.tistory.com/90">https://esoongan.tistory.com/90</a><h3 id="제어의-역전">제어의 역전</h3>
</li>
<li><a href="https://velog.io/@damiano1027/Spring-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84">https://velog.io/@damiano1027/Spring-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EC%A0%9C%EC%96%B4%EC%9D%98-%EC%97%AD%EC%A0%84</a></li>
<li><a href="https://codevang.tistory.com/241">https://codevang.tistory.com/241</a><h3 id="스프링-빈">스프링 빈</h3>
</li>
<li><a href="https://melonicedlatte.com/2021/07/11/232800.html">https://melonicedlatte.com/2021/07/11/232800.html</a></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>