macaronics.net http://macaronics.net/index.php/feed KR braverokmc79@gmail.com Copyright 2024 js sort 정렬 을 이용한 섞기, 자바스크립트 랜덤 섞기 http://macaronics.net/index.php/m04/javascript/view/2185 2185 <p>&nbsp;</p><p>JavaScript의&nbsp;sort()&nbsp;함수는 배열의 요소들을 정렬하는 데 사용됩니다. 기본적으로, 이 함수는 문자열로 변환된 요소들을 유니코드 값에 따라 오름차순으로 정렬합니다. 예를 들어, 다음과 같이 사용할 수 있습니다:</p><p>JavaScript</p><p>&nbsp;</p><pre>const fruits = [&#39;바나나&#39;, &#39;체리&#39;, &#39;사과&#39;]; fruits.sort(); console.log(fruits); // [&#39;바나나&#39;, &#39;사과&#39;, &#39;체리&#39;] </pre><p>AI가 생성한 코드입니다. 신중하게 검토하고 사용하세요.&nbsp;<a target="_blank" appearance="system-link" is="cib-link" href="https://www.bing.com/new#faq">FAQ의 자세한 정보</a>.</p><p>만약 다른 기준으로 정렬하고 싶다면,&nbsp;sort()&nbsp;함수에 비교 함수(compareFunction)를 인자로 제공할 수 있습니다. 비교 함수는 두 요소를 비교하여 정렬 순서를 결정합니다. 예를 들어, 숫자 배열을 숫자의 크기에 따라 정렬하려면 다음과 같이 할 수 있습니다:</p><p>JavaScript</p><p>&nbsp;</p><pre>const numbers = [4, 2, 5, 1, 3]; numbers.sort((a, b) =&gt; a - b); console.log(numbers); // [1, 2, 3, 4, 5] </pre><p>AI가 생성한 코드입니다. 신중하게 검토하고 사용하세요.&nbsp;<a target="_blank" appearance="system-link" is="cib-link" href="https://www.bing.com/new#faq">FAQ의 자세한 정보</a>.</p><p>이 비교 함수는 두 요소&nbsp;a와&nbsp;b를 비교하여,&nbsp;a가&nbsp;b보다 작으면 음수를, 크면 양수를, 같으면 0을 반환합니다. 이 반환 값에 따라 배열이 정렬됩니다.</p><p>sort()&nbsp;함수는 원본 배열을 직접 변경하며, 변경된 배열을 반환합니다.&nbsp;<a data-citationid="62e95ad6-cc66-4c8d-2464-1c28aa259c81-44-group" h="ID=SERP,5043.1" target="_blank" href="https://www.freecodecamp.org/news/how-does-the-javascript-sort-function-work/">원본 배열을 변경하지 않고 정렬된 새 배열을 얻고 싶다면,&nbsp;sort()&nbsp;함수를 사용하기 전에 배열을 복사해야 합니다</a>.</p><p>&nbsp;</p><p>&nbsp;</p><p>자세한 정보</p><p><a aria-labelledby="learn-more-header" target="_blank" h="ID=SERP,5043.1" aria-label="1: freecodecamp.org-JavaScript Sort() – How to Use the Sort Function in JS - freeCodeCamp.org" data-citationid="639b05ab-897e-f63d-1e59-054d92a3de52" href="https://www.freecodecamp.org/news/how-does-the-javascript-sort-function-work/">1<img alt="" src="blob:https://edgeservices.bing.com/afd306eb-b9f7-4230-a953-652abadd5b3a" />freecodecamp.org</a></p><p>&nbsp;</p><p><a aria-labelledby="learn-more-header" target="_blank" h="ID=SERP,5043.1" aria-label="2: developer.mozilla.org-Array.prototype.sort() - JavaScript | MDN - MDN Web Docs" data-citationid="0d5b8582-bcc4-c5b7-5484-4b4f68df250e" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">2<img alt="" src="blob:https://edgeservices.bing.com/551e0603-9e89-4f85-a7a6-2f6306020fc3" />developer.mozilla.org</a></p><p>&nbsp;</p><p><a aria-labelledby="learn-more-header" target="_blank" h="ID=SERP,5043.1" aria-label="3: geeksforgeeks.org-JavaScript Array sort() Method - GeeksforGeeks" data-citationid="6f0d5911-e9e8-b5fe-af3d-eb9138b41925" href="https://www.geeksforgeeks.org/javascript-array-sort-method/">3<img alt="" src="blob:https://edgeservices.bing.com/ab88f3a2-3656-43b1-b316-bc89ec72f398" />geeksforgeeks.org</a></p><p>&nbsp;</p><p><a aria-labelledby="learn-more-header" target="_blank" h="ID=SERP,5043.1" aria-label="4: developer.mozilla.org-Array.prototype.sort() - JavaScript | MDN - MDN Web Docs" data-citationid="d793e375-608e-7d4e-19c2-7e5e2cd2b46a" href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">developer.mozilla.org</a></p><p>&nbsp;</p><p>&nbsp;</p><p>무작위 정</p><p>shuffledAnswers.sort((a, b) =&gt; Math.random() - 0.5);&nbsp;코드는 JavaScript에서 배열을 무작위로 섞으려는 의도로 사용되곤 합니다. 그러나 이 방법은 완벽하게 무작위가 아니며, 일부 요소가 다른 요소보다 정렬된 결과에서 더 자주 나타날 수 있는&nbsp;<strong>편향</strong>을 가질 수 있습니다.</p><p>더 나은 방법은&nbsp;<strong>Fisher-Yates (또는 Knuth) Shuffle</strong>&nbsp;알고리즘을 사용하는 것입니다. 이 알고리즘은 배열의 각 요소에 대해 무작위 인덱스를 생성하고, 해당 인덱스의 요소와 현재 요소를 교환하는 방식으로 작동합니다. 다음은 Fisher-Yates Shuffle을 구현한 예시입니다:</p><p>JavaScript</p><p>&nbsp;</p><pre>function shuffle(array) { let currentIndex = array.length, randomIndex; // 배열에 남아 있는 요소가 있을 때까지 반복 while (currentIndex != 0) { // 남아 있는 요소 중에서 무작위 인덱스 선택 randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; // 현재 요소와 무작위 인덱스의 요소 교환 [array[currentIndex], array[randomIndex]] = [ array[randomIndex], array[currentIndex]]; } return array; } // 사용 예시 const arr = [2, 11, 37, 42]; shuffle(arr); console.log(arr); </pre><p>AI가 생성한 코드입니다. 신중하게 검토하고 사용하세요.&nbsp;<a target="_blank" appearance="system-link" is="cib-link" href="https://www.bing.com/new#faq">FAQ의 자세한 정보</a>.</p><p><a data-citationid="ba973fda-a232-a74a-9840-00be2368bb88-34-group" h="ID=SERP,5043.1" target="_blank" href="https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array">이 함수는 배열의 요소를 효과적으로 섞어서, 각 요소가 배열 내의 어떤 위치에도 동등한 확률로 나타날 수 있도록 합니다</a><a target="_blank" data-citationid="ba973fda-a232-a74a-9840-00be2368bb88-34" aria-label="1: " h="ID=SERP,5043.1" href="https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array"><sup>1</sup></a>.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-03-18 21:09:53 ★★★스프링부트 & 리액트 3 - oauth2 소셜 로그인 개발 github http://macaronics.net/index.php/m01/spring/view/2184 2184 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>■ 환경</p><p>이 프로젝트는&nbsp; (세션방식과 +&nbsp; JW T 방식)&nbsp; 두가지를&nbsp;복합적으로 적용한 프로젝트이다.</p><p>- spring boot 3.2.3</p><p>- JPA, gradle</p><p>-&nbsp;&nbsp;h2, mariadb , redis</p><p>-&nbsp; 프론트엔드 :&nbsp; thymelef,&nbsp; react 18.2</p><p>&nbsp;</p><p>&nbsp;</p><p>■ 소스</p><p><a target="_blank" href="https://github.com/braverokmc79/spring-boot-react-oauth2">https://github.com/braverokmc79/spring-boot-react-oauth2</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>코드 보기&nbsp;</strong></p><p>스프링부트&nbsp; &amp;&amp;&nbsp;리액트</p><p><a target="_blank" href="https://github.dev/braverokmc79/spring-boot-react-oauth2">https://github.dev/braverokmc79/spring-boot-react-oauth2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:16px"><strong>[Spring Boot] OAuth2 소셜 로그인 가이드 (구글, 페이스북, 네이버, 카카오)</strong></span></span></p><p><strong>=&gt;</strong></p><p><a target="_blank" href="https://deeplify.dev/back-end/spring/oauth2-social-login"><strong>https://deeplify.dev/back-end/spring/oauth2-social-login</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:16px"><span style="color:#c0392b">Spring Boot Github 소셜 로그인 구현하기 (RestTemplate &middot; WebClient)</span></span></strong></p><p><a target="_blank" href="https://inkyu-yoon.github.io/docs/Language/SpringBoot/GithubLogin">https://inkyu-yoon.github.io/docs/Language/SpringBoot/GithubLogin</a></p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:22px"><strong>※ 1.</strong></span></span>&nbsp;<span style="font-size:22px"><span style="color:#8e44ad"><strong>Github OAuth 인증 흐름과 사전 준비</strong></span></span></p><pre>1. https://github.com/login/oauth/authorize?client_id={발급받은 client_ID} 주소를 사용자에게 띄워준다. 2. 사용자는 깃허브 로그인을 통해서 인증을 한다. 3. 인증을 성공할 시, 깃허브는 {우리가 설정한 콜백 URL}?code={인증코드} 로 code값을 쿼리 파라미터 형태로 보내준다. 4. 받은 code를 이용해서 https://github.com/login/oauth/access_token 로 {client_ID,client_Secret,code}를 POST요청으로 전송한다. 5. 깃허브는 access_token을 응답해서 서버측으로 보내준다. 6. 서버는 access_token을 https://api.github.com/user 로 담아서 GET 요청을 보낸다. 7. 깃허브는 로그인한 사용자의 정보를 서버측으로 보내준다. 8. 받은 사용자 정보를 사용한다.(회원가입 혹은 로그인)</pre><p>&nbsp;</p><p>Settings&nbsp;&gt;&nbsp;Developer settings&nbsp;&gt;&nbsp;New GitHub App&nbsp;으로 OAuth 인증 기능 구현을 위한&nbsp;Client Id&nbsp;와&nbsp;Client Secret&nbsp;을 발급받아야 한다.</p><p><img alt="image-20230325171139842" src="https://raw.githubusercontent.com/buinq/imageServer/main/img/image-20230325171139842.png" /></p><p>Homepage URL은 일단 로컬 환경에서 구현해볼것이기 때문에&nbsp;localhost:8080&nbsp;으로 해두었다.</p><p>Callback URL 은 어떤 사용자가 깃허브 로그인을 성공하면 깃허브 측에서&nbsp;Code&nbsp;를 쿼리 파라미터로 보내주는데, 그 파라미터를 받을 주소이다.</p><p>나는&nbsp;http://localhost:8080/oauth2/redirect&nbsp;로 설정하였다.</p><p>http://localhost:8080/oauth2/redirect?code={코드~~}&nbsp;이런식으로 리다이렉트 될 것이다.</p><p>정상적으로 등록을 마치면&nbsp;Client ID와&nbsp;Client Secret정보를 얻을 수 있다.</p><p>Client Secret은 민감정보 이므로 노출되지 않도록 주의하자.</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:22px"><strong>※ 2. 흐름</strong></span></span></p><pre>1) oauth2 로그인 페이지 -&gt; oauth2 github 에서 인증후 로그인되면 callback 호출</pre><p>&nbsp;</p><pre>2)oauth2 callback 로 호출 .redirectionEndpoint(rE -&gt;rE.baseUri(&quot;/oauth2/callback/*&quot;))</pre><p>&nbsp;</p><pre>3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다 .userInfoEndpoint(userInfoEndpointConfig -&gt; userInfoEndpointConfig.userService(oAuth2UserService))</pre><p>&nbsp;</p><pre>4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다. .successHandler(oAuth2SuccessHandler)</pre><p>&nbsp;</p><pre>5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면 //http://localhost:8080/oauth2/authorization/github url 주소와 동일하게 //http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다. .authorizationEndpoint(anf -&gt; anf.baseUri(&quot;/api/auth/authorize&quot;))</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:24px"><span style="color:#c0392b"><strong>1. 라이브러리 추가</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:24px"><span style="color:#c0392b"><strong>2.properties&nbsp;&nbsp;설정</strong></span></span></p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:18px">1) application.properties</span></span></strong></p><p>개발 로컬에&nbsp; 적용</p><pre class="brush:as3;">spring.profiles.active=dev </pre><p>&nbsp;</p><p>배포 실질 운영시&nbsp; 보통&nbsp; prod 를 사용하나 여기서는 real 이라는 이름으로 사용했다.&nbsp;</p><pre class="brush:as3;">spring.profiles.active=real</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:18px">2)개발 properties</span></span></strong></p><p><strong>application-dev.properties</strong></p><p>&nbsp;</p><pre class="brush:as3;">#애플리케이션 포트 설정 server.port = 5000 spring.output.ansi.enabled=always spring.jpa.open-in-view=false #redis 설정 spring.data.redis.lettuce.pool.max-active=10 spring.data.redis.lettuce.pool.max-idle=10 spring.data.redis.lettuce.pool.min-idle=2 spring.data.redis.host=localhost spring.data.redis.port=6379 spring.data.redis.password=test1234 #MariaDB server spring.datasource.driver-class-name=org.mariadb.jdbc.Driver spring.datasource.url=jdbc:mariadb://localhost:3306/shop?serverTimezone=UTF-8&amp;allowPublicKeyRetrieval=true&amp;useSSL=false spring.datasource.username=shop spring.datasource.password=1111 #접근토큰시간: 5시간 #60*60*5*1000 =18000000 #샘플테스트 :30초 ==&gt;30*1000=30000 spring.jwt.token.access-expiration-time=60000 #갱신토큰시간: 7일 #60*60*24*7*1000=604800000 #샘플테스트: 1분 ===&gt;60000 spring.jwt.token.refresh-expiration-time=120000 #oauth2 frontend 주소 serverRedirectFrontUrl=http://localhost:3000 #깃허브 #http://localhost:5000/oauth2/authorization/github spring.security.oauth2.client.registration.github.client-id=752037935070eb6d2dc3 spring.security.oauth2.client.registration.github.client-secret=e0f5cde1030674aa98308a8ca9475692e310514a spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/oauth2/callback/{registrationId} spring.security.oauth2.client.registration.github.scope=user:email, read:user #provider 는 리소스 제공자인 github 에 대한 정보를 명시한 것, 따라서 Todo 애플리케이션이 소셜 로그인요청을 할때 해당 주소로 리다이렉트 한다. spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize #token-uri 는 깃허브에 액세스 가능한 엑세스 토큰을 받아오기 위한 주소 spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token #유저의 정보를 가져오기 위해서는 액세스 토큰이필요하므로 우리는 token-uri 를 이용해 먼저 액세스 토큰을 받은후, user-info-uri 로 사용자의 정보를 요청할때 토큰을 함께 보낸다. spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user #구글 spring.security.oauth2.client.registration.google.client-id=dsa spring.security.oauth2.client.registration.google.client-secret=313123 spring.security.oauth2.client.registration.google.scope=profile,email #네이버 # registration spring.security.oauth2.client.registration.naver.client-id=클라이언트아이디 spring.security.oauth2.client.registration.naver.client-secret=클라이언트시크릿 spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth/code/naver spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.naver.scope=name,email.profile_image spring.security.oauth2.client.registration.naver.client-name=Naver # provider spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me spring.security.oauth2.client.provider.naver.user-name-attribute=response ## 카카오 ## # registration spring.security.oauth2.client.registration.kakao.client-id=클라이언트아이디 spring.security.oauth2.client.registration.kakao.client-secret=클라이언트시크릿 spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.kakao.scope=profile_nickname,account_email,profile_image spring.security.oauth2.client.registration.kakao.client-name=Kakao spring.security.oauth2.client.registration.kakao.client-authentication-method=POST # provider spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me spring.security.oauth2.client.provider.kakao.user-name-attribute=id #실행되는 쿼리 콘솔 출력 spring.jpa.properties.hibernate.show_sql=true #콘솔창에 출력되는 쿼리를 가독성이 좋게 포맷팅 spring.jpa.properties.hibernate.format_sql=true #쿼리에 물음표로 출력되는 바인드 파라미터 출력 logging.level.org.hibernate.type.descriptor.sql=trace #create , update , none spring.jpa.hibernate.ddl-auto=update #파일 한 개당 최대 사이즈 spring.servlet.multipart.maxFileSize=20MB #요청당 최대 파일 크기 spring.servlet.multipart.maxRequestSize=100MB #상품 이미지 업로드 경로 itemImgLocation=/uploads/shop/item #리소스 업로드 경로 uploadPath=file:/uploads/shop/ #기본 batch size 설정 spring.jpa.properties.hibernate.default_batch_fetch_size=1000 </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:18px">3)운영 properties</span></span></strong></p><p><strong>application-real.properties</strong></p><p>&nbsp;</p><p>&nbsp;리다이렉트할 주소&nbsp; 적어준다.</p><p>serverRedirectFrontUrl=https://ma7front.p-e.kr</p><p>&nbsp;</p><p>spring.security.oauth2.client.registration.github.redirect-uri 에서 콜백할 주소도 직접 적어준다.</p><p>로컬에서는&nbsp; 다음과 같이설정해도 적용이 되나, 운영서버에서는 작동이 안된다.</p><p>spring.security.oauth2.client.registration.github.redirect-uri={baseUrl}/oauth2/callback/{registrationId}</p><p>&nbsp;</p><p>{baseUrl}/oauth2/callback/{registrationId} 과 같이 설정할경우 nginx 와 연동되&nbsp; 실질적인 운영에서는&nbsp;</p><p>invalid_request 에러가 발생한다.&nbsp;</p><p>참조 : &nbsp;<a target="_blank" href="https://velog.io/@da_na/OAuth2-%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%97%B0%EB%8F%99-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0%EC%B1%85"> [OAuth2] 구글 로그인 400 오류 : invalid_request 에러 해결책&nbsp;</a></p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>따라서, 직접적으로 frontend 주소를&nbsp; 적도록 하자</strong></span></p><p><span style="color:#c0392b"><strong>spring.security.oauth2.client.registration.github.redirect-uri=https://ma7server.p-e.kr/oauth2/callback/{registrationId}</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">#oauth2 frontend 주소 serverRedirectFrontUrl=https://ma7front.p-e.kr #깃허브 #http://localhost:5000/oauth2/authorization/github spring.security.oauth2.client.registration.github.client-id=깃허브Client_ID spring.security.oauth2.client.registration.github.client-secret=깃허브Client_SECRET spring.security.oauth2.client.registration.github.redirect-uri=https://ma7server.p-e.kr/oauth2/callback/{registrationId} spring.security.oauth2.client.registration.github.scope=user:email, read:user #provider 는 리소스 제공자인 github 에 대한 정보를 명시한 것, 따라서 Todo 애플리케이션이 소셜 로그인요청을 할때 해당 주소로 리다이렉트 한다. spring.security.oauth2.client.provider.github.authorization-uri=https://github.com/login/oauth/authorize #token-uri 는 깃허브에 액세스 가능한 엑세스 토큰을 받아오기 위한 주소 spring.security.oauth2.client.provider.github.token-uri=https://github.com/login/oauth/access_token #유저의 정보를 가져오기 위해서는 액세스 토큰이필요하므로 우리는 token-uri 를 이용해 먼저 액세스 토큰을 받은후, user-info-uri 로 사용자의 정보를 요청할때 토큰을 함께 보낸다. spring.security.oauth2.client.provider.github.user-info-uri=https://api.github.com/user </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:24px"><span style="color:#c0392b"><strong>3.SecurityConfig 설정</strong></span></span></p><p>&nbsp;</p><p>securityCofig 에 다음 코드를 추가해 준다.</p><p>1~ 5번&nbsp; 코드 순서 처럼&nbsp;&nbsp;&nbsp;&nbsp;oauth2&nbsp; &nbsp;인증처리 된다.</p><pre class="brush:as3;"> //oauth2Login 로그인 처리 http.oauth2Login(oauth2Configurer -&gt; oauth2Configurer //1) oauth2 로그인 페이지 -&gt; oauth2 github 에서 인증후 로그인되면 callback 호출 //.loginPage(&quot;/oauth2/login&quot;) //2)oauth2 callback 로 호출 .redirectionEndpoint(rE -&gt;rE.baseUri(&quot;/oauth2/callback/*&quot;)) //3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다 .userInfoEndpoint(userInfoEndpointConfig -&gt; userInfoEndpointConfig.userService(oAuth2UserService)) //4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다. .successHandler(oAuth2SuccessHandler) //5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면 //http://localhost:8080/oauth2/authorization/github url 주소와 동일하게 //http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다. .authorizationEndpoint(anf -&gt; anf.baseUri(&quot;/api/auth/authorize&quot;)) ) //인증 실패시 Oauth2 흐름으로 넘어가는 것을 막고 응답코드 403을 반환처리 .exceptionHandling(exceptionConfig-&gt;exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));</pre><p>&nbsp;</p><p>전체 securityCofig&nbsp;</p><pre class="brush:as3;">package com.shop.config; import com.querydsl.jpa.impl.JPAQueryFactory; import com.shop.config.filter.JwtAuthenticationFilter; import com.shop.config.oauth2.OAuth2SuccessHandler; import com.shop.exception.CustomAuthenticationEntryPoint; import com.shop.service.api.oauth2.OAuth2UserService; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @RequiredArgsConstructor //@EnableGlobalMethodSecurity(prePostEnabled = true)//spring boot 3 이상 부터 기본값 true 업데이트 됩 public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final OAuth2UserService oAuth2UserService; //OAuth2 로그인 성공시 토큰 생성 private final OAuth2SuccessHandler oAuth2SuccessHandler; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public JPAQueryFactory queryFactory(EntityManager em){ return new JPAQueryFactory(em); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //JWT 를 사용하면 csrf 보안이 설정 필요없다. 그러나 여기 프로젝트에서는 세션방식과 jwt 방식을 둘다적용 중이라 특정 페이지만 제외 처리 http.csrf(c -&gt; { c.ignoringRequestMatchers(&quot;/admin/**&quot;,&quot;/api/**&quot;, &quot;/oauth2/**&quot; ,&quot;/error/**&quot;); }); //1.csrf 사용하지 않을 경우 다음과 같이 설정 //http.csrf(AbstractHttpConfigurer::disable); //2. h2 접근 및 iframe 접근을 위한 SameOrigin (프레임 허용) &amp; 보안 강화 설정 할경우 //http.headers((headers) -&gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); http.headers(headers -&gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); //3.HTTP 기본 인증 만 할경우 다음과 같이 처리 //httpSecurity.formLogin(AbstractHttpConfigurer::disable); //3.jwt token 만 인증 처리 할경우 basic 인증을 disable 한다. 그러나 이 프로젝트는 세션+jwt 이라 disable 설정은 하지 않는다. //httpSecurity.httpBasic(AbstractHttpConfigurer::disable); //4.JWT 만 사용할경우 세션 관리 상태 없음 설정 그러나, 이 프로젝트는 세션 + JWT 를 사용하지 때문에 주석 //http.sessionManagement(sessionManagementConfigurer -&gt;sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); //세션방식의 form 로그인 처리 http.formLogin(login-&gt; login .loginPage(&quot;/members/login&quot;) .defaultSuccessUrl(&quot;/&quot;, true) .usernameParameter(&quot;email&quot;) .failureUrl(&quot;/members/login/error&quot;)) .logout(logoutConfig -&gt;logoutConfig.logoutRequestMatcher(new AntPathRequestMatcher(&quot;/members/logout&quot;)).logoutSuccessUrl(&quot;/&quot;)) .exceptionHandling(exceptionConfig-&gt; exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint())); http.authorizeHttpRequests(request-&gt;request .requestMatchers(&quot;/css/**&quot;, &quot;/js/**&quot;, &quot;/img/**&quot;,&quot;/images/**&quot;).permitAll() .requestMatchers(&quot;/&quot;, &quot;/members/**&quot;, &quot;/item/**&quot;, &quot;/main/**&quot;, &quot;/error/**&quot; ).permitAll() //JWT 일반 접속 설정 .requestMatchers(&quot;/auth/**&quot;, &quot;/oauth2/**&quot;).permitAll() .requestMatchers(&quot;/api/todo/**&quot; ,&quot;/api/auth/**&quot;, &quot;/api/oauth2/**&quot; ).permitAll() //JWT 관리자 페이지 설정 .requestMatchers( &quot;/api/admin/**&quot;).hasAuthority(&quot;ADMIN&quot;) //세션방식 --&gt; 관리자 페이지는 설정 .requestMatchers(&quot;/admin/**&quot;).hasAuthority(&quot;ADMIN&quot;) .anyRequest().authenticated() ); //api 페이지만 JWT 필터 설정(jwtAuthenticationFilter 에서 shouldNotFilter 메서드로 세션 페이지는 필터를 제외 시켰다.) http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //에외 처리 .exceptionHandling(exceptionConfig-&gt;exceptionConfig.authenticationEntryPoint(new CustomAuthenticationEntryPoint()) ); //oauth2Login 로그인 처리 http.oauth2Login(oauth2Configurer -&gt; oauth2Configurer //1) oauth2 로그인 페이지 -&gt; oauth2 github 에서 인증후 로그인되면 callback 호출 //.loginPage(&quot;/oauth2/login&quot;) //2)oauth2 callback 로 호출 .redirectionEndpoint(rE -&gt;rE.baseUri(&quot;/oauth2/callback/*&quot;)) //3)oAuth2UserService 에서 회원이 존재 하지 않으면 가입처리되고, 존재하면 해당 아이디를 호출하여 정보를 가져온다 .userInfoEndpoint(userInfoEndpointConfig -&gt; userInfoEndpointConfig.userService(oAuth2UserService)) //4)성공시 oAuth2SuccessHandler 호출 되며 토큰 생성및 쿠키에 저장 시킨다. .successHandler(oAuth2SuccessHandler) //5)OAuth2.0 흐름 시작을 위한 엔드 포인트 임의 주소 /api/auth/authorize 추가하면 //http://localhost:8080/oauth2/authorization/github url 주소와 동일하게 //http://localhost:8080/api/auth/authorize/github url 주소를 입력시 OAuth2 처리 페이지로 이동 처리된다. .authorizationEndpoint(anf -&gt; anf.baseUri(&quot;/api/auth/authorize&quot;)) ) //인증 실패시 Oauth2 흐름으로 넘어가는 것을 막고 응답코드 403을 반환처리 .exceptionHandling(exceptionConfig-&gt;exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint())); return http.build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>위와 같이 시큐리티를 설정했다면&nbsp; &nbsp;http://localhost:포토번호/oauth2/authorization/github 로 입력해서 들어가보면&nbsp;</strong></p><p><strong>http://localhost:포토번호 으로 정상적으로 디라이렉트 하는 것을 확인할 수 있다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:24px"><span style="color:#c0392b"><strong>4.OAuth2UserService</strong></span></span></p><p>&nbsp;</p><p><strong>Member</strong></p><pre class="brush:as3;">package com.shop.entity; import com.shop.constant.Role; import com.shop.dto.MemberFormDto; import com.shop.entity.base.BaseEntity; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.security.crypto.password.PasswordEncoder; @Entity @Table(name = &quot;member&quot;) @Getter @Setter @ToString public class Member extends BaseEntity { @Id @Column(name = &quot;member_id&quot;) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String name; @Column(unique = true) private String email; private String password; private String address; @Enumerated(EnumType.STRING) private Role role; private String authProvider; //이후 OAuth 에서 사용할 유저 정보 제공자 : github private String authProviderId; public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) { Member member = new Member(); member.setName(memberFormDto.getName()); member.setUsername(memberFormDto.getUsername()); member.setEmail(memberFormDto.getEmail()); member.setAddress(memberFormDto.getAddress()); String password = passwordEncoder.encode(memberFormDto.getPassword()); member.setPassword(password); member.setRole(Role.USER); //member.setRole(Role.ADMIN); return member; } public static Member createMember(MemberFormDto memberFormDto) { Member member = new Member(); member.setName(memberFormDto.getName()); member.setUsername(memberFormDto.getUsername()); member.setEmail(memberFormDto.getEmail()); member.setAddress(memberFormDto.getAddress()); member.setPassword(memberFormDto.getPassword()); member.setRole(Role.USER); return member; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>OAuth2UserService</strong></p><pre class="brush:as3;">package com.shop.service.api.oauth2; import com.fasterxml.jackson.databind.ObjectMapper; import com.shop.config.auth.PrincipalDetails; import com.shop.constant.Role; import com.shop.dto.MemberDto; import com.shop.entity.Member; import com.shop.repository.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service @Log4j2 @RequiredArgsConstructor @Transactional public class OAuth2UserService extends DefaultOAuth2UserService { private final MemberRepository memberRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { log.info(&quot;★★★★★★★★★★ OAuth2UserService loadUse&quot;); //1.DefaultOauth2UserService 의 기존 loadUser 를 호출한다. 이 메서드가 user-info-uri 를 이용해 사용자 정보를 가져오는 부분이다. final OAuth2User oAuth2User =super.loadUser(userRequest); try{ //2.디버깅을 돕기 위해 OAuth2User 사용자 정보를 출력 log.info(&quot;******** OAuth2User attributes : {}&quot;, new ObjectMapper().writeValueAsString(oAuth2User.getAttributes())) ; }catch (Exception e){ e.printStackTrace(); } log.info(&quot;******** OAuth2UserService loadUser : {} , getAttributes() : {}&quot;, oAuth2User , oAuth2User.getAttributes()); //3.login 필드를 가져온다. final String username = oAuth2User.getAttributes().get(&quot;login&quot;).toString(); final String authProviderId = oAuth2User.getAttributes().get(&quot;id&quot;).toString(); String email =null; if(oAuth2User.getAttributes().get(&quot;email&quot;)!=null){ email = oAuth2User.getAttributes().get(&quot;email&quot;).toString(); } final String authProvider=userRequest.getClientRegistration().getClientName(); //OAUTH2_ID = Oauth2(naver,github,google)_authProviderId ex) Github_1234 String OAUTH2_ID= authProvider+&quot;_&quot;+authProviderId; /** 각 소셜 로그인 제공자가 반환하는 유저 정보, 즉 attributes 에 들어 있는 내용은 제공자 마다 각각 다르다. email을 아이디로 사용하는 제공자는 email 필드가 있을 것이고, 깃허브의 경우에는 login 필드가 있다. 따라서 여러 소셜 로그인과 통홥하려면 이 부분을 알맞게 파싱해야 한다. * */ //authProvider :GitHub Member member=null; //1.기존에 등록된 OAUTH2 가 있을 경우 if(memberRepository.existsByAuthProviderId(OAUTH2_ID)){ member = memberRepository.findMemberByAuthProviderId(OAUTH2_ID); }else{ //2.기존에 등록된 OAUTH2 가 없을 경우 if(StringUtils.hasText(email)){ //3. OAUTH2 가져온 email 이 존재하고 널이 아닐경우, DB 에서 해당 이메일로 회원정보를 가져온다. Member memberEntity = memberRepository.findByEmail(email); if(memberEntity!=null){ //1)널이 아니면 DB 데이터에 업데이트 처리 memberEntity.setAuthProvider(authProvider); memberEntity.setAuthProviderId(OAUTH2_ID); member=memberEntity; }else{ //2)기존에 등록된 OAUTH2 정보없고, email 은 존재하지만 DB에 등록된 정보가 없기에 신규로 등록 처리 member=createOauth2Member( authProvider , username, OAUTH2_ID, email); } }else { //4.기존에 등록된 OAUTH2 정보없고, OAUTH2 에서 가져온 email 이 널이라서 당연히 신규로 등록 처리 member=createOauth2Member( authProvider , username, OAUTH2_ID, email); } } PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes()); log.info(&quot;========================2 Oauth2User 토큰 반환시 정보값 ================ {}&quot;, principalDetails.getMember().getId()); return principalDetails; } //oauth2 DB 등록 private Member createOauth2Member(String authProvider ,String username, String OAUTH2_ID, String email){ MemberDto memberDto = MemberDto.builder() .username(authProvider+&quot;_&quot;+username) .email(email) .authProvider(authProvider) .authProviderId(OAUTH2_ID) .role(Role.USER) .build(); Member member = MemberDto.oauth2CreateMember(memberDto); return memberRepository.save(member); } } </pre><p>&nbsp;</p><p><strong>1. application.properties 에 설정한&nbsp; user-info-uri 값들의 사용자 정보를 &nbsp;가져온다.</strong></p><p>&nbsp;</p><p>2. oAuth2User&nbsp; 에&nbsp; 로그인이 성공하면&nbsp; 다음과 같이&nbsp;oAuth2User.getAttributes().get(&quot;login&quot;) 과&nbsp;&nbsp;&nbsp; oAuth2User.getAttributes().get(&quot;id&quot;) 를 통해</p><p>정보를 가져올수 있다.&nbsp;</p><p>&nbsp;</p><p>여기서는</p><p>username 은&nbsp;github&nbsp; 가입시&nbsp; 아이디값이고,</p><p>&nbsp;authProvider&nbsp;은 github 등록된&nbsp; 123456 의&nbsp; pk 와 같은 아이디 값이다.&nbsp;&nbsp; &nbsp;그리고 authProvider은&nbsp;&nbsp;GitHub 값이다.</p><p>&nbsp;</p><pre class="brush:as3;"> final String username = oAuth2User.getAttributes().get(&quot;login&quot;).toString(); final String authProviderId = oAuth2User.getAttributes().get(&quot;id&quot;).toString(); String email =null; if(oAuth2User.getAttributes().get(&quot;email&quot;)!=null){ email = oAuth2User.getAttributes().get(&quot;email&quot;).toString(); } final String authProvider=userRequest.getClientRegistration().getClientName(); //OAUTH2_ID = Oauth2(naver,github,google)_authProviderId ex) Github_1234 String OAUTH2_ID= authProvider+&quot;_&quot;+authProviderId;</pre><p>ex)GitHub_18599682</p><p>OAUTH2_ID =GitHub+&quot;_&quot;+123456</p><p>따라서,&nbsp;OAUTH2_ID 값과</p><pre>username(authProvider+&quot;_&quot;+username)</pre><p>username =GitHub_깃허브등록시 아이디값으로 저장 처리 한다.</p><p>굳이 이와 같이&nbsp; 아이디값에 oauth2 를 넣어주면서 저장처리를 하지않아도 되지만 이프로젝트에서는 이렇게 진행하였다.</p><p>중요한것은 아이디값이 unique&nbsp; 한값으로 저장 되야 한다는 것이다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:24px">5. PrincipalDetails</span></span></strong></p><pre class="brush:as3;">package com.shop.config.auth; import com.shop.constant.Role; import com.shop.entity.Member; import lombok.Getter; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import java.io.Serial; import java.util.*; import java.util.stream.Collectors; //시큐리티가 login 주소 요청이 오면 낚아채서 로그인을 진행시킨다 //로그인을 진행이 완료가 되면 시큐리티 session 을 만들어 줍니다. (security ContectHolder) 세션 정보 저장 //오브젝트 =&gt; Authentication 타입 객체 //Authentication 안에 User 정보가 있어야 함. //User 오브젝트타입 =&gt; UserDetails 타입 객체 //Security Session =&gt; Authentication =&gt; UserDetails @Getter @Log4j2 public class PrincipalDetails implements UserDetails, OAuth2User { @Serial private static final long serialVersionUID = 1L; private final Member member; // 콤포지션 private final long id; private final String idStr; //아이디를 문자열로 반환 private final String email; private final String username; /** 다음은 OAuth2User 를 위한 필드 */ private String authProviderId; private Collection&lt;? extends GrantedAuthority&gt; authorities; private Map&lt;String, Object&gt; attributes; public PrincipalDetails(Member member, Map&lt;String, Object&gt; attributes){ this.authProviderId=member.getAuthProviderId(); this.attributes=attributes; this.authorities= Collections.singletonList(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;)); this.id=member.getId(); this.idStr=member.getId().toString(); this.username=member.getUsername(); this.email = member.getEmail(); this.member = member; } // 일반로그인 public PrincipalDetails(Member member) { this.id=member.getId(); this.idStr=member.getId().toString(); this.username=member.getUsername(); this.email = member.getEmail(); this.member = member; this.authorities=getAuthorities(); } /** * 사용자에게 부여된 권한을 반환합니다. null을 반환할 수 없습니다. */ // 해당 User 의 권한을 리턴하는 곳!! //권한:한개가 아닐 수 있음.(3개 이상의 권한) @Override public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() { Collection&lt;GrantedAuthority&gt; collector=new ArrayList&lt;&gt;(); log.info(&quot;********* 시큐리티 로그인 :&quot; +member.getRole().toString()); collector.add(()-&gt; member.getRole().toString()); return collector; } /** * 사용자를 인증하는 데 사용된 암호를 반환합니다. */ @Override public String getPassword() { return member.getPassword(); } /** * 사용자를 인증하는 데 사용된 사용자 이름을 반환합니다. null을 반환할 수 없습니다. */ @Override public String getUsername() { return member.getUsername(); } /** * 사용자의 계정이 만료되었는지 여부를 나타냅니다. 만료된 계정은 인증할 수 없습니다. */ @Override public boolean isAccountNonExpired() { return true; } /** * 사용자가 잠겨 있는지 또는 잠금 해제되어 있는지 나타냅니다. 잠긴 사용자는 인증할 수 없습니다. */ @Override public boolean isAccountNonLocked() { return true; } /** * 사용자의 자격 증명(암호)이 만료되었는지 여부를 나타냅니다. 만료된 자격 증명은 인증을 방지합니다. */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 사용자가 활성화되었는지 비활성화되었는지 여부를 나타냅니다. 비활성화된 사용자는 인증할 수 없습니다. */ @Override public boolean isEnabled() { // 우리 사이트 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 함. // 현재시간-로긴시간=&gt;1년을 초과하면 return false; return true; } public boolean isWriteEnabled() { if (member.getRole().equals(Role.USER)) return false; else return true; } public boolean isWriteAdminAndManagerEnabled() { if (member.getRole().equals(Role.ADMIN )|| member.getRole().equals(Role.USER)) return true; else return false; } /** * 다음 OAuth2User 를 위한 메소드 * @return */ @Override public Map&lt;String, Object&gt; getAttributes() { return this.attributes; } @Override public String getName() { return this.authProviderId; //name 대신 id를 리턴한다. } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:24px"><span style="color:#c0392b"><strong>6.&nbsp;&nbsp; OAuth2SuccessHandler OAuth2 로그인 성공시 토큰 생성&nbsp; 처리&nbsp;</strong></span></span><br />&nbsp;</p><pre class="brush:as3;">package com.shop.config.oauth2; import com.shop.dto.api.jwt.TokenDto; import com.shop.service.api.jwt.JwtTokenProviderService; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.checkerframework.checker.units.qual.A; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; /** * OAuth2 로그인 성공시 토큰 생성 * */ @Component @Log4j2 @RequiredArgsConstructor public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProviderService jwtTokenProviderService; @Value(&quot;${serverRedirectFrontUrl}&quot;) private String serverRedirectFrontUrl ; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { TokenDto tokenDto= jwtTokenProviderService.create(authentication); String redUrlPath=serverRedirectFrontUrl+&quot;/sociallogin?accessToken=&quot; +tokenDto.getAccessToken() +&quot;&amp;refreshToken=&quot;+tokenDto.getRefreshToken(); response.sendRedirect(redUrlPath); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>RedirectUrlCookieFilter &nbsp; 로 redirect 값을&nbsp; 쿠키에 저장해서&nbsp; 개발할경우&nbsp; </strong></span></span></p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>nginx 연동 운영서버에서는 에러가 나니 주의</strong></span></span></p><p>&nbsp;</p><p><strong>다음과 같이&nbsp; &nbsp;RedirectUrlCookieFilter 을 생성후</strong></p><pre class="brush:as3;">package com.shop.config.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Log4j2 @Component public class RedirectUrlCookieFilter extends OncePerRequestFilter { public static final String REDIRECT_URI_PARAM = &quot;redirect_url&quot;; private static final int MAX_AGE =180; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if(request.getRequestURI().startsWith(&quot;/api/auth/authorize&quot;)){ try{ log.info(&quot;*** RedirectUrlCookieFilter url {}&quot;, request.getRequestURI()); //프론트엔드에서 전송한 리퀘스트 파라미터에서 redirect_url 을 가져온다. String redirectUrl = request.getParameter(REDIRECT_URI_PARAM); Cookie cookie = new Cookie(REDIRECT_URI_PARAM, redirectUrl); cookie.setPath(&quot;/&quot;); cookie.setHttpOnly(true); cookie.setMaxAge(MAX_AGE); }catch(Exception ex){ log.error(&quot;Could not set user authentication in security context :&quot;, ex); log.info(&quot;unauthorized request&quot;); } } filterChain.doFilter(request, response); } } </pre><p><strong><span style="color:#c0392b">OAuth2SuccessHandler 설정 하면 로컬에서 는 정상적으로 작동되나&nbsp;</span></strong></p><p><strong><span style="color:#c0392b">nginx 로 실제 운영시에는&nbsp; &nbsp;redirect_uri 값을 가져오지 못한다.&nbsp;&nbsp;</span></strong></p><p><strong><span style="color:#c0392b">따라서.&nbsp; &nbsp;propertis 설정으로&nbsp; 할것.</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Component @Log4j2 @RequiredArgsConstructor public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private static final String LOCAL_REDIRECT_URL = &quot;http://localhost:3000&quot;; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { TokenDto tokenDto= jwtTokenProviderService.create(authentication); // response.getWriter().write(tokenDto.toString()); String redUrlPath=&quot;/sociallogin?accessToken=&quot; +tokenDto.getAccessToken() +&quot;&amp;refreshToken=&quot;+tokenDto.getRefreshToken(); //response.sendRedirect(&quot;http://localhost:3000&quot;+redUrlPath); //RedirectUrlCookieFilter 에서 리다렉트시에 쿠키에 저장한 redirectUrl 값 가져오기 Optional&lt;Cookie&gt; optionalCookie = Arrays.stream(request.getCookies()).filter(cookie -&gt; cookie.getName().equals(REDIRECT_URI_PARAM)).findFirst(); Optional&lt;String&gt; redirectUrl = optionalCookie.map(Cookie::getValue); log.info(&quot; 쿠키값 redirectUrl 저장 : {} &quot;, redirectUrl); response.sendRedirect( redirectUrl.orElseGet(()-&gt;LOCAL_REDIRECT_URL)+redUrlPath); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:24px"><strong>7.&nbsp;&nbsp; 리액트 프론트엔드 구현</strong></span></span></p><p><strong>Sociallogin</strong></p><pre class="brush:as3;">import { Navigate } from &quot;react-router-dom&quot;; const Sociallogin = (props) =&gt; { const getUrlParameters=(name)=&gt;{ let search=window.location.search; let params=new URLSearchParams(search); return params.get(name); }; const accessToken=getUrlParameters(&#39;accessToken&#39;); const refreshToken=getUrlParameters(&#39;refreshToken&#39;); console.log(&quot;토큰 파싱 :&quot;+ accessToken, refreshToken); if(accessToken &amp;&amp; refreshToken){ console.log(&quot;로컬스토리지에 저장 accessToken:&quot;, accessToken); console.log(&quot;로컬스토리지에 저장 refreshToken:&quot;, refreshToken); localStorage.setItem(&quot;ACCESS_TOKEN&quot;, accessToken); localStorage.setItem(&quot;REFRESH_TOKEN&quot;,refreshToken); return ( &lt;Navigate to={{pathname:&#39;/&#39;, state:{from: props.location}}} /&gt; ) }else{ return (&lt;Navigate to={{pathname:&#39;/login&#39;, state:{from: props.location}}} /&gt;) } } export default Sociallogin</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>ApiServie.js</strong></p><pre class="brush:as3;"> //oauth2 로그인 export function socialLogin(provider){ let frontedUrl; const hostname =window &amp;&amp; window.location &amp;&amp; window.location.hostname; if(hostname===&quot;localhost&quot;){ frontedUrl=&quot;http://localhost:3000&quot;; }else{ frontedUrl=&quot;https://ma7front.p-e.kr&quot;; } window.location.href=API_BASE_URL+&quot;/api/auth/authorize/&quot;+provider +&quot;?redirect_url=&quot;+frontedUrl; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-03-16 12:45:08 ★★★ 스프링부트 & 리액트 , jwt redis 갱신토큰 저장 , JWT refresh token 인증 구현 http://macaronics.net/index.php/m01/spring/view/2183 2183 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>다음 페이지에서&nbsp; jwt 설정 및 배포 처리를 진행 하였다.</strong></span></p><p>이어지는 내용이다.</p><p><a target="_blank" href="https://macaronics.net/m01/spring/view/2180">https://macaronics.net/m01/spring/view/2180</a>.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px">■ 환경</span></p><p><span style="font-size:18px">이 프로젝트는&nbsp; (세션방식과 +&nbsp; JW T 방식)&nbsp; 두가지를&nbsp;복합적으로 적용한 프로젝트이다.</span></p><p>- spring boot 3.2.3</p><p>- JPA, gradle</p><p>-&nbsp;&nbsp;h2, mariadb , redis</p><p>-&nbsp; 프론트엔드 :&nbsp; thymelef,&nbsp; react 18.2</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px">■ 소스</span></p><p><a target="_blank" href="https://github.com/braverokmc79/spring-boot-react-jwt-redis">https://github.com/braverokmc79/spring-boot-react-jwt-redis</a></p><p>&nbsp;</p><p><strong>코드 보기&nbsp;</strong></p><p>스프링부트&nbsp; &amp;&amp;&nbsp;리액트</p><p><a target="_blank" href="https://github.dev/braverokmc79/spring-boot-react-jwt-redis/tree/main/back-end">https://github.dev/braverokmc79/spring-boot-react-jwt-redis/tree/main/back-end</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>jwt 설정 프로젝트&nbsp;진행한 코드 목록&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOjw2vid3Oxa8qMB6-YdlN-cjAbkjZ0QSbWmC5jNOpMlzWp8vmSWqkyqLkntktyYNp1g_EDTz8dT8SEmcMJsXJS5WxDG2wfxv3cH_4RY-olnRBLoHuBJr88-djH7U673pBu7slqa9XeOkyonUilf6f7UMYFAP4LkTv36iJE53rrCZzuIH0CnUFJI7ifWs3/s2271/2024-03-12%2020%2011%2030.png">https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOjw2vid3Oxa8qMB6-YdlN-cjAbkjZ0QSbWmC5jNOpMlzWp8vmSWqkyqLkntktyYNp1g_EDTz8dT8SEmcMJsXJS5WxDG2wfxv3cH_4RY-olnRBLoHuBJr88-djH7U673pBu7slqa9XeOkyonUilf6f7UMYFAP4LkTv36iJE53rrCZzuIH0CnUFJI7ifWs3/s2271/2024-03-12%2020%2011%2030.png</a></p><p><img alt="" width="900" height="1043" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOjw2vid3Oxa8qMB6-YdlN-cjAbkjZ0QSbWmC5jNOpMlzWp8vmSWqkyqLkntktyYNp1g_EDTz8dT8SEmcMJsXJS5WxDG2wfxv3cH_4RY-olnRBLoHuBJr88-djH7U673pBu7slqa9XeOkyonUilf6f7UMYFAP4LkTv36iJE53rrCZzuIH0CnUFJI7ifWs3/s2271/2024-03-12%2020%2011%2030.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">◆&nbsp;</span><span style="color:#c0392b"><span style="font-size:20px"> 작업순서</span></span></strong></p><p><strong><span style="font-size:16px">1. redis&nbsp;설치</span></strong></p><p><strong><span style="font-size:16px">2.&nbsp; 벡인드&nbsp;</span>JWT access/refresh token 인증 구현</strong></p><p><strong><span style="font-size:16px">3&nbsp; 리액트 프론트 엔드&nbsp;</span>구현</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">◆</span><span style="color:#c0392b; font-size:20px"> 인증 순서</span></strong></p><p><strong><span style="font-size:16px">1. 프론트엔드에서&nbsp;&nbsp;로그인 요청</span></strong></p><p><strong><span style="font-size:16px">2.id/pw 검증 후 access/refresh token 발급 </span></strong></p><p><strong><span style="font-size:16px">3. refresh token은 redis에 저장하고,&nbsp; 프론트엔드로&nbsp;&nbsp;access/refresh 토큰 응답처리.</span></strong></p><p><strong><span style="font-size:16px">4.&nbsp;&nbsp;프로트엔드에서는 로컬스토리지에&nbsp;access/refresh token 저장.</span></strong></p><p><strong><span style="font-size:16px">5. 필터를 통해 access&nbsp; token으로만 통신을 하다가&nbsp;&nbsp;&nbsp;access token 만료되면&nbsp; 프론트엔드에 만료 메시지를 전송.</span></strong></p><p><strong><span style="font-size:16px">6.&nbsp; 프론트엔드에서는&nbsp;access token 만료메시지를 응답받고&nbsp; 로컬스토리지 저장된&nbsp;&nbsp; refresh token을&nbsp; 벡엔드에 전송함.</span></strong></p><p><strong><span style="font-size:16px">7. 벡인드에서는&nbsp;&nbsp;&nbsp;refresh token 에서&nbsp; id 값을 추출후&nbsp; redis 에 저장된&nbsp;&nbsp; refresh token&nbsp; 과 비교하여 값이 일히차면&nbsp;&nbsp; 재발급(reissue) (&nbsp;access /rfresh token 재발급) 처리</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#2980b9"><strong>1. redis&nbsp;설치</strong></span></span></p><p>&nbsp;</p><pre><span style="color:#e67e22"><strong>0. 우분트22 - Docker 설치방법</strong></span> <a target="_blank" href="https://haengsin.tistory.com/128">https://haengsin.tistory.com/128</a> <strong><span style="color:#e67e22">1. 도커 redis 데이터 외부 저장경로 생성</span></strong> $ mkdir /docker $ mkdir /docker/redis/ $ mkdir /docker/redis/data <span style="color:#e67e22"><strong>2. 도커 redis 설치 및 실행</strong></span> #1)윈도우 docker run -v c:/docker/redis/data:/data --name my-redis -p 6379:6379 redis redis-server --appendonly yes --requirepass test1234 #2)리눅스 docker run -v /docker/redis/data:/data --name my-redis -p 6379:6379 redis redis-server --appendonly yes --requirepass test1234 #docker inspect [container-name] 명령어로 확인하면, 볼륨 마운트 설정확인 $ docker inspect my-redis <strong><span style="color:#e67e22">3. container 접속 및 테스트</span></strong> $ docker exec -it my-redis redis-cli #비밀번호 인증 안된 상태에서 redis 명령어 &quot;keys * &quot;를 실해하면 NOAUTH 에러 발생 한다. # 비밀번호 인증 $ auth test1234 # 정상작동 확인 $ set a bbbb $ get a <span style="color:#e67e22"><strong>4.컨테이너를 중지후 재시작 데이터 보존되는 것을 확인</strong></span> $ docker stop my-redis $ docker start my-redis $ docker exec -it my-redis redis-cli $ auth test1234 $ get a <strong><span style="color:#e67e22">5.비밀번호 업데이트 ==&gt; </span><span style="color:#c0392b">그러나 도커를 정지하고 재구동 하면 원래비밀번호로 변경된다. 따라서 컨테이너 삭제후 docker run 을 통해 다시 컨테이너를 실행해야 한다. </span></strong> 127.0.0.1:6379&gt; config set requirepass 1234 종료후 재 접속 확인 $exit $ docker exec -it my-redis redis-cli $ auth 1234 $ get a </pre><p>&nbsp;</p><pre>#redis 전체 데이터 보기 127.0.0.1:6379&gt; keys * 1) &quot;refreshToken&quot; 2) &quot;logoutAccessToken:3&quot; 3) &quot;refreshToken:3&quot; 4) &quot;logoutAccessToken&quot; #redis 전체 삭제 : flushall #redis 상세보기 get , hash 타입은 hgetall 127.0.0.1:6379&gt; hgetall refreshToken:3 hgetall logoutAccessToken:3</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#2980b9"><strong>2. JWT refresh token 인증 구현</strong></span></span></p><p>&nbsp;</p><p><span style="color:null"><span style="font-size:20px"><strong>1) build.gradle</strong></span></span></p><pre class="brush:as3;"> implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39; //jwt version: &#39;0.12.4&#39; implementation group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-api&#39;, version: &#39;0.11.5&#39; runtimeOnly group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-impl&#39;, version: &#39;0.11.5&#39; implementation group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-jackson&#39;, version: &#39;0.11.5&#39;</pre><p>&nbsp;</p><p><span style="font-size:20px"><strong>2) application.properties</strong></span></p><pre class="brush:as3;">#레디스 설정 spring.data.redis.lettuce.pool.max-active=10 spring.data.redis.lettuce.pool.max-idle=10 spring.data.redis.lettuce.pool.min-idle=2 spring.data.redis.host=localhost spring.data.redis.port=6379 spring.data.redis.password=test1234 jwt.expiredMs=600000 #접근토큰시간: 5시간 #60*60*5*1000 =18000000 #샘플테스트 :30초 ==&gt;30*1000=30000 spring.jwt.token.access-expiration-time=60000 #갱신토큰시간: 7일 #60*60*24*7*1000=604800000 #샘플테스트: 1분 ===&gt;60000 spring.jwt.token.refresh-expiration-time=120000 </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>기본적으로 스프링 시큐리티를 설정하면&nbsp; 다음과 같이&nbsp; UserDetails 설정하도록 한다.</strong></span></p><p><span style="color:#8e44ad"><strong>1) PrincipalDetails</strong></span></p><pre class="brush:as3;">import com.shop.constant.Role; import com.shop.entity.Member; import lombok.Getter; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serial; import java.util.*; import java.util.stream.Collectors; //시큐리티가 login 주소 요청이 오면 낚아채서 로그인을 진행시킨다 //로그인을 진행이 완료가 되면 시큐리티 session 을 만들어 줍니다. (security ContectHolder) 세션 정보 저장 //오브젝트 =&gt; Authentication 타입 객체 //Authentication 안에 User 정보가 있어야 함. //User 오브젝트타입 =&gt; UserDetails 타입 객체 //Security Session =&gt; Authentication =&gt; UserDetails @Getter @Log4j2 public class PrincipalDetails implements UserDetails { @Serial private static final long serialVersionUID = 1L; private final Member member; // 콤포지션 private final long id; private final String idStr; //아이디를 문자열로 반환 private final String email; private final String username; /** 다음은 OAuth2User 를 위한 필드 */ private String authProviderId; private Collection&lt;? extends GrantedAuthority&gt; authorities; private Map&lt;String, Object&gt; attributes; // public PrincipalDetails(Member member, Map&lt;String, Object&gt; attributes){ // this.authProviderId=member.getAuthProviderId(); // this.attributes=attributes; // this.authorities= Collections.singletonList(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;)); // // this.id=member.getId(); // this.idStr=member.getId().toString(); // this.username=member.getUsername(); // this.email = member.getEmail(); // this.member = member; // } // // // 일반로그인 public PrincipalDetails(Member member) { this.id=member.getId(); this.idStr=member.getId().toString(); this.username=member.getUsername(); this.email = member.getEmail(); this.member = member; this.authorities=getAuthorities(); } // //OAuth 로그인 // public PrincipalDetails(UserVO user, Map&lt;String, Object&gt; attributes) { // this.user=user; // this.attributes=attributes; // } // /** * 사용자에게 부여된 권한을 반환합니다. null을 반환할 수 없습니다. */ // 해당 User 의 권한을 리턴하는 곳!! //권한:한개가 아닐 수 있음.(3개 이상의 권한) @Override public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() { Collection&lt;GrantedAuthority&gt; collector=new ArrayList&lt;&gt;(); // collector.add(new GrantedAuthority() { // // @Override // public String getAuthority() { // return member.getRole(); // } // }); log.info(&quot;********* 시큐리티 로그인 :&quot; +member.getRole().toString()); collector.add(()-&gt; member.getRole().toString()); return collector; } /** * 사용자를 인증하는 데 사용된 암호를 반환합니다. */ @Override public String getPassword() { return member.getPassword(); } /** * 사용자를 인증하는 데 사용된 사용자 이름을 반환합니다. null을 반환할 수 없습니다. */ @Override public String getUsername() { return member.getUsername(); } /** * 사용자의 계정이 만료되었는지 여부를 나타냅니다. 만료된 계정은 인증할 수 없습니다. */ @Override public boolean isAccountNonExpired() { return true; } /** * 사용자가 잠겨 있는지 또는 잠금 해제되어 있는지 나타냅니다. 잠긴 사용자는 인증할 수 없습니다. */ @Override public boolean isAccountNonLocked() { return true; } /** * 사용자의 자격 증명(암호)이 만료되었는지 여부를 나타냅니다. 만료된 자격 증명은 인증을 방지합니다. */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 사용자가 활성화되었는지 비활성화되었는지 여부를 나타냅니다. 비활성화된 사용자는 인증할 수 없습니다. */ @Override public boolean isEnabled() { // 우리 사이트 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 함. // 현재시간-로긴시간=&gt;1년을 초과하면 return false; return true; } public boolean isWriteEnabled() { if (member.getRole().equals(Role.USER)) return false; else return true; } public boolean isWriteAdminAndManagerEnabled() { if (member.getRole().equals(Role.ADMIN )|| member.getRole().equals(Role.USER)) return true; else return false; } /** * 다음 OAuth2User 를 위한 메소드 * @return */ // @Override // public Map&lt;String, Object&gt; getAttributes() { // return this.attributes; // } // // @Override // public String getName() { // return this.authProviderId; //name 대신 id를 리턴한다. // } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>2)PrincipalDetailsService</strong></span></p><pre class="brush:as3;">import com.shop.entity.Member; import com.shop.repository.MemberRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @RequiredArgsConstructor @Service @Slf4j @Transactional public class PrincipalDetailsService implements UserDetailsService { private final MemberRepository memberRepository; /* 1.패스워드는 알아서 체킹하니깐 신경쓸 필요 없다 2.리턴이 잘되면 자동으로 User 세션을 만든다. */ @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(email); if(member==null) throw new UsernameNotFoundException(email); return new PrincipalDetails(member); } //API 및 Oauth2 로그인시 memberId 로 public UserDetails loadUserApiByUsername(long memberId) throws UsernameNotFoundException { Member member = memberRepository.findById(memberId).orElse(null); if(member==null) throw new UsernameNotFoundException(String.valueOf(memberId)); return new PrincipalDetails(member); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">①&nbsp; redis 설정&nbsp;</span></strong></span></p><p><span style="font-size:20px"><strong>3) RedisConfiguration&nbsp;&nbsp;</strong></span></p><pre class="brush:as3;">package com.shop.config.redis; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * RedisRepository 를 사용하기 위해 @EnabledRedisRepositories 어노테이션을 붙여준다. */ @Configuration @EnableRedisRepositories public class RedisConfiguration { @Value(&quot;${spring.data.redis.host}&quot;) private String redisHost; @Value(&quot;${spring.data.redis.port}&quot;) private int redisPort; @Value(&quot;${spring.data.redis.password}&quot;) private String redisPassword; @Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort); //비밀번호 설정시 config.setPassword(redisPassword); return new LettuceConnectionFactory(config); } @Bean public RedisTemplate&lt;String, String&gt; redisTemplate() { RedisTemplate&lt;String, String&gt; redisTemplate = new RedisTemplate&lt;&gt;(); redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); return redisTemplate; } @Bean public StringRedisTemplate stringRedisTemplate() { StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(); stringRedisTemplate.setKeySerializer(new StringRedisSerializer()); stringRedisTemplate.setValueSerializer(new StringRedisSerializer()); stringRedisTemplate.setConnectionFactory(redisConnectionFactory()); return stringRedisTemplate; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>② redis Entity 생성 ,&nbsp; redis repositoy 생성&nbsp; , TonkenDto 생성</strong></span></span></p><p><span style="font-size:20px"><strong>4)RefreshToken</strong></span></p><p>&nbsp;</p><p><strong>여기서 주의할 점은 @Id 어노테이션입니다.</strong></p><p><strong>java.persistence.id가 아닌 org.springframework.data.annotation.Id 를 import 해야 됩니다. </strong></p><p><strong>Refresh Token은 Redis에 저장하기 때문에 JPA 의존성이 필요하지 않습니다. (persistence로 하면 에러납니다.) </strong></p><p><strong>&nbsp;또한 id 변수이름 그대로 사용해야지 CrudRepository 의 findById 를 사용할 수 있다.</strong></p><p>&nbsp;</p><pre class="brush:as3;">import lombok.*; import org.springframework.data.annotation.Id; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.TimeToLive; /** timeToLive는 유효시간을 값으로 초 단위를 의미합니다. 현재 14_400초 4시간으로 설정 , timeToLive = 14440 */ @Getter @RedisHash(value = &quot;refreshToken&quot;) @AllArgsConstructor @Builder @ToString @NoArgsConstructor public class RefreshToken { //여기서 주의할 점은 @Id 어노테이션입니다. //java.persistence.id가 아닌 org.springframework.data.annotation.Id 를 import 해야 됩니다. //Refresh Token은 Redis에 저장하기 때문에 JPA 의존성이 필요하지 않습니다. (persistence로 하면 에러납니다.) // 또한 id 변수이름 그대로 사용해야지 CrudRepository 의 findById 를 사용할 수 있다. @Id private Long id; private String refreshToken; @TimeToLive //기본값 무한 private Long expiration; public static RefreshToken createRefreshToken(Long memberId, String refreshToken, Long remainingMilliSeconds) { return RefreshToken.builder() .refreshToken(refreshToken) .id(memberId) .expiration(remainingMilliSeconds / 1000) .build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>②</strong></span></span></p><p><span style="font-size:20px"><strong>5)LogoutAccessToken</strong></span></p><pre class="brush:as3;">import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.TimeToLive; import jakarta.persistence.Id; @Getter @RedisHash(&quot;logoutAccessToken&quot;) @AllArgsConstructor @Builder public class LogoutAccessToken { @Id private Long id; private String refreshToken; //기본값 -1로 Redis 에 영구적으로 유지, 로그아웃 시간 Milliseconds 형식 @TimeToLive private Long logoutTimeMilliseconds; //로그아웃 시간 yyyy-MM-dd HH:mm:ss(EEE) 형식 private String logoutTime; public static LogoutAccessToken of(Long memberId, String refreshToken, Long logoutTimeMilliseconds, String logoutTime) { return LogoutAccessToken.builder() .id(memberId) .refreshToken(refreshToken) .logoutTimeMilliseconds(logoutTimeMilliseconds) .logoutTime(logoutTime) .build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>②</strong></span></span></p><p><span style="font-size:20px"><strong>6)TokenDto</strong></span></p><pre class="brush:as3;">import lombok.*; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class TokenDto { private String grantType; private String accessToken; private String refreshToken; public static TokenDto of(String accessToken, String refreshToken) { return TokenDto.builder() .grantType(&quot;Bearer &quot;) .accessToken(accessToken) .refreshToken(refreshToken) .build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>③&nbsp;JwtTokenUtil,&nbsp;&nbsp;JwtAuthenticationFilter ,&nbsp;&nbsp;JwtTokenProviderService</strong></span></span></p><p><span style="color:null"><span style="font-size:20px"><strong>7)&nbsp;JwtTokenUtil</strong></span></span></p><pre class="brush:as3;">package com.shop.config.jwt; import com.shop.config.auth.PrincipalDetails; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Base64; import java.util.Date; @Component public class JwtTokenUtil { // 시크릿 키를 담는 변수 private SecretKey cachedSecretKey; /** $ echo &#39;0-p7n#6s4l$ncdoui7+(^q(7^b^tt4@^i@6-q516f=aw-9%@fsdkj52423ksd9fs905235K$!$#49&#39;|base64 =&gt; git bash 에서 base64 로 인코딩한 값 private static final String SECRET_KEY = &quot;MC1wN24jNnM0bCRuY2RvdWk3KyhecSg3XmJedHQ0QF5pQDYtcTUxNmY9YXctOSVAZnNka2o1MjQyM2tzZDlmczkwNTIzNUskISQjNDkK&quot;; */ private final String SECRET_KEY= &quot;MC1wN24jNnM0bCRuY2RvdWk3KyhecSg3XmJedHQ0QF5pQDYtcTUxNmY9YXctOSVAZnNka2o1MjQyM2tzZDlmczkwNTIzNUskISQjNDkK&quot;; //1.인증 키생성 public SecretKey getSecretKey() { String keyBase64Encoded = Base64.getEncoder().encodeToString(SECRET_KEY.getBytes()); SecretKey secretKey1 = Keys.hmacShaKeyFor(keyBase64Encoded.getBytes()); if (cachedSecretKey == null) cachedSecretKey = secretKey1; return cachedSecretKey; } //2.토큰 정보 파싱 public Claims extractAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); } //3.토큰을 이용하여 유저아이디값 가져오기 반환값 id public String getUsername(String token) { return extractAllClaims(token).get(&quot;memberId&quot;, String.class); } /** * 4. 토큰 생성 * @param memberId * @return */ private String doGenerateToken(Long memberId, long expireTime) { Claims claim = Jwts.claims(); claim.put(&quot;memberId&quot;, memberId); //기한 지금으로부터 1일로 설정 // Date expiryDate =Date.from(Instant.now().plus(1, ChronoUnit.DAYS)); //Date now = new Date(); now.getTime() 으로 하면 오류 Date expireDate = new Date(System.currentTimeMillis() + expireTime); //JWT Token 생성 return Jwts.builder() .setClaims(claim) .signWith(getSecretKey()) //토큰 서명 설정 .setSubject(String.valueOf(memberId)) .setIssuer(&quot;macaronics app&quot;) .setIssuedAt(new Date()) .setExpiration(expireDate) .compact(); //문자열로 압축 } //5.접근 토큰 생성 public String generateAccessToken(Long memberId, long expireTime) { return doGenerateToken(memberId, expireTime); } //6.갱신 토큰 public String generateRefreshToken(Long memberId, long expireTime) { return doGenerateToken(memberId, expireTime); } //7.토큰 만료 여부 public Boolean isTokenExpired(String token) { Date expiration = extractAllClaims(token).getExpiration(); return expiration.before(new Date()); } //8.토큰 유효성 체크 public Boolean validateToken(String token, PrincipalDetails userDetails) { String memberId = getUsername(token); return memberId.equals(String.valueOf(userDetails.getId())) &amp;&amp; !isTokenExpired(token); } //9.토큰 남은시간 public long getRemainMilliSeconds(String token) { Date expiration = extractAllClaims(token).getExpiration(); Date now = new Date(); return expiration.getTime() - now.getTime(); } /** * 토큰 확인 * parseClaimsJws 메시드 Base 64로 디코딩 및 파싱. * 즉, 헤더와 페이로드를 setSigningKey 로 넘어온 시크릿을 이용해 서명 후, token 의 서명과 비교. * 위조되지 않았다면 페이로드(Claims) 리턴, 위조라면 예외를 날림 , 그중 우리는 memberId 가 필요하므로 getBody 를 부른다. * @param token * @return */ public String validateAndGetUserId(String token){ Claims claims=Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>③</strong></span></span></p><p><span style="color:null"><span style="font-size:20px"><strong>8)&nbsp;JwtAuthenticationFilter</strong></span></span><span style="color:#c0392b"><span style="font-size:20px"><strong> </strong></span></span></p><p>&nbsp;</p><pre><span style="font-size:16px"><span style="color:#16a085"><strong>doFilterInternal</strong></span></span></pre><p>a)&nbsp;parseBearerToken&nbsp; 으로&nbsp; &nbsp;Bearer 제거 후 토큰 가져오기 Bearer</p><p>b) 토큰 널값 확인후&nbsp; 접근 토큰 유효성 검사하기,</p><p>여기서는 유효성이&nbsp; 안맞는 경우는 무조건&nbsp;&nbsp;접근 토큰시간 만료라는 메시지를 보내도록 처리 하였다.</p><p>c)&nbsp;PrincipalDetails&nbsp; 통해 널 값 확인 체크&nbsp;</p><p>d) 최종적으로 정상적인 토큰일경우 에는&nbsp;authSetConfirm 메서드에서&nbsp; 인증확인된 데이터를&nbsp;</p><p>넣어주므로서&nbsp;&nbsp;시큐리티에 통과하도록 설정한다.</p><pre class="brush:as3;"> private void authSetConfirm(HttpServletRequest request, PrincipalDetails principalDetails){ //인증 완료; SecurityContextHolder 에 등록해야 인증된 사용자라고 생각한다. AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( principalDetails ,// userId,//인증된 사용자의 정보. 문자열이 아니어도 아무거나 넣을 수 있다. 보통 UserDetails 를 넣는다. null, principalDetails.getAuthorities() //권한 설정값을 넣어 준다. ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 정상 토큰이면 SecurityContext 에 저장 SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>중요) 프론트에서 처리 결과에 대한 &nbsp;값을 던져 주는 방법으로 다음과 같이 처리하였다.</strong></span></p><p>a) 필터 제외 페이지 설정을&nbsp;&nbsp;shouldNotFilter 을 오버라이딩 으로 해서 코딩했다는 점을 중요하게 살펴 보자.</p><p>b)예외 처리를 반환을&nbsp;&nbsp;&nbsp;catch 를 통해서&nbsp; 진행하였다.</p><p><strong><span style="color:#16a085">첫번째 catch는&nbsp;&nbsp;CustomAuthenticationException 일 경우 response.getWriter() 로 반환</span></strong> 처리</p><p><strong>그리고 두번째 catch 는&nbsp;throw&nbsp; 를 던져 주므로서&nbsp; &nbsp;CustomAuthenticationEntryPoint 에서&nbsp; 에러를 </strong>처리하도록 하였다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.shop.config.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.shop.config.auth.PrincipalDetails; import com.shop.config.auth.PrincipalDetailsService; import com.shop.exception.CustomAuthenticationException; import com.shop.dto.api.todo.ResDTO; import com.shop.service.api.jwt.JwtTokenProviderService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.io.PrintWriter; @Component @RequiredArgsConstructor @Log4j2 public class JwtAuthenticationFilter extends OncePerRequestFilter { private final PrincipalDetailsService principalDetailsService; private final JwtTokenProviderService tokenProvider; //JWT 필터 제외 페이지 설정 private static final String[] excludedUrlPatterns = { &quot;/&quot;, &quot;/static/**&quot;, &quot;/favicon.ico&quot;, &quot;/css/**&quot;,&quot;/js/**&quot;,&quot;/images/**&quot;, &quot;/main&quot;, &quot;/members&quot;,&quot;/members/**&quot;, &quot;/cart&quot;, &quot;/cart/**&quot;, &quot;/cartItem&quot;, &quot;/cartItem/**&quot;, &quot;/admin&quot;, &quot;/admin/**&quot;, &quot;/item&quot;, &quot;/item/**&quot;, &quot;/order&quot;, &quot;/order/**&quot;, &quot;/orders&quot;,&quot;/orders/**&quot;, &quot;/thymeleaf/**&quot;, &quot;/api/auth/signup&quot;,&quot;/api/auth/signin&quot; , &quot;/api/auth/reissue&quot;, &quot;/api/auth/logout&quot; }; // 필터 제외 페이지 설정 @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { return exclusionPages(request); } public static boolean exclusionPages(HttpServletRequest request){ String requestUrl = request.getRequestURI(); for (String pattern : excludedUrlPatterns) { if (!requestUrl.startsWith(&quot;/api&quot;)) { //api 시작 안되면 통과 //log.info(&quot;//api 시작되는 것은 통과 :{}&quot;, requestUrl); return true; }else if (pattern.contains(&quot;**&quot;)) { // URL 패턴에 **이 있으면, ** 앞부분만 비교하여 제외합니다. String patternBeforeDoubleStar = pattern.substring(0, pattern.indexOf(&quot;**&quot;)); if (requestUrl.startsWith(patternBeforeDoubleStar)) { return true; } } else { // URL 패턴에 **이 없으면 같음을 비교합니다. if (requestUrl.equals(pattern)) { return true; } } } return false; // 제외할 URL 패턴이 없는 경우 false를 반환합니다. } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { log.info(&quot;========= doFilterInternal getRequestURI : {}&quot;, request.getRequestURI()); try{ log.info(&quot;Filter running ..&quot;); //요청에서 토큰 가져오기 String token=parseBearerToken(request); //토큰 검사하기 . JWT 이므로 인가 서버에 요청하지 않고도 검증 가능. userId 가져오기. 위조된 경우 예외 처리된다. if(token!=null &amp;&amp; !token.equalsIgnoreCase(&quot;null&quot;)){ String memberId =null; try{ memberId = tokenProvider.validateAndGetUserId(token); }catch(Exception e){ //throw new CustomAuthenticationException(&quot;접근 토큰시간이 만료되었습니다.&quot;, &quot;TOKEN_EXPIRED&quot;); throw new CustomAuthenticationException(&quot;접근 토큰시간이 만료되었습니다.&quot;,&quot;TOKEN_EXPIRED&quot;); } //JWT 토큰로그인 인증에서는 API , oauth2 는 loadUserApiByUsername 커스텀으로 생성한 메서드로 id 값으로 인증처리 PrincipalDetails principalDetails =(PrincipalDetails)principalDetailsService.loadUserApiByUsername(Long.parseLong(memberId)); if(principalDetails!=null){ log.info(&quot;필터 ===principalDetails {}&quot;, principalDetails.getMember().getRole().toString()); authSetConfirm( request, principalDetails); filterChain.doFilter(request, response); }else throw new CustomAuthenticationException(&quot;해당하는 유저가 없습니다.&quot;, &quot;USER_NOT_FOUND&quot;); }else throw new CustomAuthenticationException(&quot;유효하지 않은 토큰 입니다.&quot;,&quot;INVALID_TOKEN&quot; ); }catch (CustomAuthenticationException e ){ log.info(&quot;JWT CustomAuthenticationException 에러 처리 &quot;); //1.에러처리 방법 :커스텀 에러 메시지는 다음과 같이 printWriter 전송 처리 한다. response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setCharacterEncoding(&quot;utf-8&quot;); response.setContentType(&quot;application/json&quot;); ResDTO&lt;Object&gt; errorRes = ResDTO.builder().code(-1).message(e.getMessage()).errorCode(e.getErrorCode()).build(); ObjectMapper objectMapper = new ObjectMapper(); String result = objectMapper.writeValueAsString(errorRes); PrintWriter printWriter=response.getWriter(); printWriter.print(result); printWriter.flush(); printWriter.close(); }catch (Exception e){ log.info(&quot;JWT 기타 에러 메시지처리 &quot;); //2.에러처리 방법 : 기타 에러 메시지는 다음과 같이 throw new 호출하여 JwtAuthenticationEntryPoint 클래스로 전송 처리 시킨다. request.setAttribute(&quot;message&quot;,&quot;JWT 토큰 필터 처리 에러입니다.&quot;); request.setAttribute(&quot;errorCode&quot;,e.getMessage()); throw new BadCredentialsException(&quot;Invalid&quot;); } } private void authSetConfirm(HttpServletRequest request, PrincipalDetails principalDetails){ //인증 완료; SecurityContextHolder 에 등록해야 인증된 사용자라고 생각한다. AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( principalDetails ,// userId,//인증된 사용자의 정보. 문자열이 아니어도 아무거나 넣을 수 있다. 보통 UserDetails 를 넣는다. null, principalDetails.getAuthorities() //권한 설정값을 넣어 준다. ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 정상 토큰이면 SecurityContext 에 저장 SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); securityContext.setAuthentication(authentication); SecurityContextHolder.setContext(securityContext); } /** * Http 요청의 헤더를 파싱해 Bearer 토큰을 리턴한다. * @param request * @return */ public static String parseBearerToken(HttpServletRequest request){ String bearerToken = request.getHeader(&quot;Authorization&quot;); if(StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)){ return bearerToken.substring(7); } return null; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>③</strong></span></span></p><p><span style="color:null"><span style="font-size:20px"><strong>9)&nbsp;JwtTokenProviderService</strong></span></span></p><pre class="brush:as3;">package com.shop.service.api.jwt; import com.shop.exception.CustomAuthenticationException; import com.shop.config.jwt.JwtTokenUtil; import com.shop.dto.MemberDto; import com.shop.dto.api.jwt.TokenDto; import com.shop.entity.Member; import com.shop.entity.api.jwt.LogoutAccessToken; import com.shop.entity.api.jwt.RefreshToken; import com.shop.repository.MemberRepository; import com.shop.repository.api.jwt.LogoutAccessTokenRedisRepository; import com.shop.repository.api.jwt.RefreshTokenRedisRepository; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.NoSuchElementException; /** * 추가된 라이브러리를 사용해서 JWT를 생성하고 검증하는 컴포넌트 */ @Log4j2 @Service @RequiredArgsConstructor @Transactional public class JwtTokenProviderService { private final RefreshTokenRedisRepository refreshTokenRedisRepository; private final MemberRepository memberRepository; private final LogoutAccessTokenRedisRepository logoutAccessTokenRedisRepository; private final JwtTokenUtil jwtTokenUtil; @Value(&quot;${spring.jwt.token.access-expiration-time}&quot;) private long accessExpirationTime; @Value(&quot;${spring.jwt.token.refresh-expiration-time}&quot;) private long refreshExpirationTime; /** * 1. 로그인시 접근 토큰 생성 * @param member * @return */ public TokenDto create(Member member){ //1.접근 토큰 생성 String accessToken = jwtTokenUtil.generateAccessToken(member.getId(), accessExpirationTime); //2.갱신토큰 생성후 redis 에 저장 RefreshToken refreshToken = saveRefreshToken(member.getId()); log.info(&quot;*********** 접근토큰 : {}&quot; ,accessToken); log.info(&quot;*********** 갱신토큰 : {}&quot;,refreshToken); return TokenDto.of(accessToken, refreshToken.getRefreshToken()); } //갱신토큰을 redis 에 저장 private RefreshToken saveRefreshToken(Long memberId) { RefreshToken refreshToken = RefreshToken.createRefreshToken(memberId, jwtTokenUtil.generateRefreshToken(memberId, refreshExpirationTime), refreshExpirationTime); refreshTokenRedisRepository.save(refreshToken); return refreshToken; } /** * 2. access token + refresh token 재발급 처리 * @param memberId * @return */ private TokenDto reissueRefreshToken( long memberId) { //access token + refresh token 재발급 String accessToken = jwtTokenUtil.generateAccessToken(memberId, accessExpirationTime); RefreshToken refreshToken = saveRefreshToken(memberId); return TokenDto.of(accessToken, refreshToken.getRefreshToken()); } /** * 3.토큰 재발행 * * * ★ * 현재 이 코드는 , 회원아이디 값을 키값을 설정 했기 때문에, 하나의 브라우저에서만 로그인 된다. * 정확이 말하면, 기존에 접속한 브라우저에서는 accessExpirationTime 만료시까지 유지 된다. * 즉, 새로운 브라우저로 로그인하면서 redis 에서 새로운 refresh token 값을 저장했기 때문이다. * * ★여러 브라우저에서 가능하도록 하려면, refresh token 을 키값을 설정하면 된다. * * * @return */ public MemberDto reissue(String refreshToken ) throws Exception{ //1.refreshToken 를 파싱해서 memberId 값을 가져온다. String memberId=validateAndGetUserId(refreshToken); log.info(&quot;1.refreshToken 를 파싱해서 memberId 값을 가져온다. {}&quot;,memberId); //2.Redis 저장된 토큰 정보를 가져온다. RefreshToken redisRefreshToken = refreshTokenRedisRepository.findById(memberId).orElseThrow(NoSuchElementException::new); log.info(&quot;2.Redis 저장된 토큰 정보를 가져온다. {}&quot;, redisRefreshToken.getRefreshToken()); //3.redis 에 저장된 토큰과 값과 파라미터의 갱신토크와 비교해서 같으면 갱신토큰 발급처리 if (refreshToken.equals(redisRefreshToken.getRefreshToken())) { TokenDto tokenDto = reissueRefreshToken( Long.parseLong(memberId)); Member member = memberRepository.findById(Long.parseLong(memberId)).orElse(null); if(member!=null){ MemberDto memberDto = MemberDto.of(member); memberDto.setToken(tokenDto); return memberDto; }throw new CustomAuthenticationException(&quot;해당하는 유저가 없습니다.&quot;, &quot;USER_NOT_FOUND&quot;); } throw new CustomAuthenticationException(&quot;유효하지 않은 토큰 입니다.&quot;,&quot;INVALID_TOKEN&quot; ); } /** * 토큰 확인 * @param token * @return */ public String validateAndGetUserId(String token) throws Exception{ return jwtTokenUtil.validateAndGetUserId(token); } public void logout(String refreshToken) throws Exception{ //1.refreshToken 를 파싱해서 memberId 값을 가져온다. String memberId=validateAndGetUserId(refreshToken); //2.redis 에서 삭제 처리 refreshTokenRedisRepository.deleteById(memberId); String logOutTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss(EEE)&quot;, Locale.ENGLISH)); //3.redis 에 로그아웃 날짜 저장 logoutAccessTokenRedisRepository.save(LogoutAccessToken.of(Long.valueOf(memberId), refreshToken, System.currentTimeMillis(), logOutTime)); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>④ 예외 처리 , SecurityConfig&nbsp; 설정</strong></span></span></p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#4e5f70">10)&nbsp;CustomAuthenticationException</span></span></strong></p><pre class="brush:as3;">package com.shop.exception; import lombok.*; import org.springframework.security.core.AuthenticationException; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class CustomAuthenticationException extends RuntimeException { private String message; private String errorCode; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#4e5f70">11)&nbsp;CustomAuthenticationEntryPoint</span></span></strong></p><pre class="brush:as3;">package com.shop.exception; import com.fasterxml.jackson.databind.ObjectMapper; import com.shop.config.filter.JwtAuthenticationFilter; import com.shop.dto.api.todo.ResDTO; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.io.IOException; /** * 토큰 필터 에러 처리 */ @Component @Log4j2 public class CustomAuthenticationEntryPoint extends RuntimeException implements AuthenticationEntryPoint { /** * 1.API 에러 설정 * @param request * @param response * @param authException * @throws IOException */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { log.info(&quot;========= 1.JwtAuthenticationEntryPoint getMessage : {}&quot;, authException.getMessage()); log.info(&quot;========= 2.JwtAuthenticationEntryPoint getMessage : {}&quot;, request.getContentType()); //예외 페이지가 아니고, json 타입이 아닌 경우 세션 페이지로 에러 페이지 이동처리 if (JwtAuthenticationFilter.exclusionPages(request) &amp;&amp; StringUtils.hasText(request.getContentType()) &amp;&amp; !request.getContentType().equals(&quot;application/json&quot;)) { response.sendRedirect(sessionErrorPage(authException.getMessage(), response)); return; } String errorCode =(String) request.getAttribute(&quot;errorCode&quot;); String message =(String) request.getAttribute(&quot;message&quot;); log.info(&quot;******* 3.JwtAuthenticationEntryPoint 토큰 필터 에러 처리 : errorCode :{} , message :{} &quot;,errorCode, message); if(StringUtils.hasText(errorCode) || ( StringUtils.hasText(authException.getMessage()) &amp;&amp; org.thymeleaf.util.StringUtils.contains(authException.getMessage(),&quot;authentication&quot;) )){ response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setCharacterEncoding(&quot;utf-8&quot;); response.setContentType(&quot;application/json&quot;); ResDTO&lt;Object&gt; errorRes ; if(org.thymeleaf.util.StringUtils.contains(authException.getMessage(),&quot;authentication&quot;)){ errorRes=ResDTO.builder().code(-1).message(&quot;권한이 없습니다. 해당 리소스에 접근할 수 없습니다.&quot;).errorCode(&quot;ACCESS_DENIED&quot;).build(); //response.sendError(HttpServletResponse.SC_UNAUTHORIZED, &quot;Unauthorized&quot;); }else{ errorRes=ResDTO.builder().code(-1).message(message).errorCode(errorCode).build(); } ObjectMapper objectMapper = new ObjectMapper(); String result = objectMapper.writeValueAsString(errorRes); response.getWriter().print(result); } } /** * 2.세션 페이지 에러 페이지 설정 * @param message * @param response * @return */ private String sessionErrorPage(String message, HttpServletResponse response){ String erroPage=&quot;/error/404&quot;; if(StringUtils.hasText(message)){ if(org.thymeleaf.util.StringUtils.contains(message,&quot;authentication&quot;)){ erroPage=&quot;/error/404&quot;; }else{ erroPage=&quot;/error/500&quot;; } } return erroPage; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#4e5f70">12)&nbsp;SecurityConfig</span></span></strong></p><pre class="brush:as3;">package com.shop.config; import com.querydsl.jpa.impl.JPAQueryFactory; import com.shop.exception.CustomAuthenticationEntryPoint; import com.shop.config.filter.JwtAuthenticationFilter; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @RequiredArgsConstructor //@EnableGlobalMethodSecurity(prePostEnabled = true)//기본값 true 업데이트 됩 public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public JPAQueryFactory queryFactory(EntityManager em){ return new JPAQueryFactory(em); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //JWT 를 사용하면 csrf 보안이 설정 필요없다. 그러나 여기 프로젝트에서는 세션방식과 jwt 방식을 둘다적용 중이라 특정 페이지만 제외 처리 http.csrf(c -&gt; { c.ignoringRequestMatchers(&quot;/admin/**&quot;,&quot;/api/**&quot;, &quot;/oauth2/**&quot; ,&quot;/error/**&quot;); }); //1.csrf 사용하지 않을 경우 다음과 같이 설정 //http.csrf(AbstractHttpConfigurer::disable); //2. h2 접근 및 iframe 접근을 위한 SameOrigin (프레임 허용) &amp; 보안 강화 설정 할경우 http.headers(headers -&gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)); // 다른 도메인 간에 프레임(Frame)을 허용하려면 X-Frame-Options 헤더를 비활성화(disable) //http.headers((headers) -&gt; headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); //3.HTTP 기본 인증 만 할경우 다음과 같이 처리 //httpSecurity.formLogin(AbstractHttpConfigurer::disable); //3.jwt token 만 인증 처리 할경우 basic 인증을 disable 한다. 그러나 이 프로젝트는 세션+jwt 이라 disable 설정은 하지 않는다. //httpSecurity.httpBasic(AbstractHttpConfigurer::disable); //4.JWT 만 사용할경우 세션 관리 상태 없음 설정 그러나, 이 프로젝트는 세션 + JWT 를 사용하지 때문에 주석 //http.sessionManagement(sessionManagementConfigurer -&gt;sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); //세션방식의 form 로그인 처리 http.formLogin(login-&gt; login .loginPage(&quot;/members/login&quot;) .defaultSuccessUrl(&quot;/&quot;, true) .usernameParameter(&quot;email&quot;) .failureUrl(&quot;/members/login/error&quot;)) .logout(logoutConfig -&gt;logoutConfig.logoutRequestMatcher(new AntPathRequestMatcher(&quot;/members/logout&quot;)).logoutSuccessUrl(&quot;/&quot;)) .exceptionHandling(exceptionConfig-&gt; exceptionConfig.authenticationEntryPoint(new Http403ForbiddenEntryPoint())); http.authorizeHttpRequests(request-&gt;request .requestMatchers(&quot;/css/**&quot;, &quot;/js/**&quot;, &quot;/img/**&quot;,&quot;/images/**&quot;).permitAll() .requestMatchers(&quot;/&quot;, &quot;/members/**&quot;, &quot;/item/**&quot;, &quot;/main/**&quot;, &quot;/error/**&quot; ).permitAll() //JWT 일반 접속 설정 .requestMatchers(&quot;/auth/**&quot;).permitAll() .requestMatchers(&quot;/api/todo/**&quot; ,&quot;/api/auth/**&quot;).permitAll() //JWT 관리자 페이지 설정 .requestMatchers( &quot;/api/admin/**&quot;).hasAuthority(&quot;ADMIN&quot;) //세션방식 --&gt; 관리자 페이지는 설정 .requestMatchers(&quot;/admin/**&quot;).hasAuthority(&quot;ADMIN&quot;) .anyRequest().authenticated() ); //=======api 페이지만 JWT 필터 설정(jwtAuthenticationFilter 에서 shouldNotFilter 메서드로 세션 페이지는 필터를 제외 시켰다.) http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //에외 처리 .exceptionHandling(exceptionConfig-&gt;exceptionConfig.authenticationEntryPoint(new CustomAuthenticationEntryPoint()) ); return http.build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#4e5f70">13)&nbsp;ApiMemberController</span></span></strong></p><pre class="brush:as3;">package com.shop.controller.api; import com.shop.dto.MemberDto; import com.shop.dto.MemberFormDto; import com.shop.dto.api.jwt.TokenDto; import com.shop.dto.api.todo.ResDTO; import com.shop.entity.Member; import com.shop.service.MemberService; import com.shop.service.api.jwt.JwtTokenProviderService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(&quot;/api/auth&quot;) @RequiredArgsConstructor @Log4j2 public class ApiMemberController { private final MemberService memberService; private final PasswordEncoder passwordEncoder; private final JwtTokenProviderService tokenProvider; /** * API 회원 가입 처리 * @param memberFormDto * @return */ @PostMapping(&quot;/signup&quot;) public ResponseEntity&lt;?&gt; registerUser(@RequestBody MemberFormDto memberFormDto){ try{ if(memberFormDto==null || memberFormDto.getPassword()==null){ throw new RuntimeException(&quot;비밀번호가 유효하지 않습니다.&quot;); } //1.요청을 이용해 저장할 유저 만들기 memberFormDto.setPassword(passwordEncoder.encode(memberFormDto.getPassword())); Member member = Member.createMember(memberFormDto); //2.서비스를 이용해 리포지터리에 유저 저장 MemberDto memberDto = MemberDto.of(memberService.createMember(member)); return ResponseEntity.ok(memberDto); }catch (Exception e){ return ResponseEntity.badRequest().body(ResDTO.builder().code(-1).errorCode(e.getMessage()).build()); } } /** * 로그인 * @param memberFormDto * @return */ @PostMapping(&quot;/signin&quot;) public ResponseEntity&lt;?&gt; authenticate(@RequestBody MemberFormDto memberFormDto){ Member memberEntity = memberService.getMemberUsername(memberFormDto.getUsername()); if(memberEntity!=null &amp;&amp; passwordEncoder.matches(memberFormDto.getPassword(), memberEntity.getPassword())){ log.info(&quot;3. signIn================&gt;{}&quot; ,memberFormDto.getUsername()); //토큰 생성 final TokenDto tokenDto = tokenProvider.create(memberEntity); MemberDto memberDto = MemberDto.of(memberEntity); memberDto.setToken(tokenDto); return ResponseEntity.ok(memberDto); }else{ return ResponseEntity.badRequest().body(ResDTO.builder().code(-1).message(&quot;아이디 또는 비밀번호가 일치하지 않습니다.&quot;).errorCode(&quot;not match&quot;).build()); } } /**확인 사항 * 1.갱신토큰값 과 재발행 토큰값과 비교시 갱신토큰값이 작으면 접근토큰 및 갱신토큰 발행 * 2. * 갱신토큰 재발행 * @param refreshToken * @return */ @PostMapping(&quot;/reissue&quot;) public ResponseEntity&lt;?&gt; reissue(@RequestHeader(value = &quot;RefreshToken&quot;) String refreshToken) { log.info(&quot;갱신 토큰 발행 =======================&gt;&quot;); try{ return ResponseEntity.ok(tokenProvider.reissue(refreshToken)); }catch (Exception e){ //갱신토큰이 유요하지 않을 경우 code 값을 -1로 주고, 프론트에서 &quot;refreshToken is invalid&quot; 메시지 확인후 로그아웃 처리한다. return ResponseEntity.badRequest().body(ResDTO.builder().code(-1).message(&quot;갱신 토큰이 유요하지 않습니다.&quot;).errorCode(&quot;INVALID_REFRESH_TOKEN&quot;).build()); } } /** * 로그 아웃 처리 * redis 에서 저장된 memberId + refresh token 삭제 처리 한다. * */ @PostMapping(&quot;/logout&quot;) public ResponseEntity&lt;?&gt; logout(@RequestHeader(value = &quot;RefreshToken&quot;) String refreshToken) { log.info(&quot;로그 아웃 처리 =======================&gt;&quot;); try { tokenProvider.logout(refreshToken); return ResponseEntity.ok(ResDTO.builder().code(1).message(&quot;success&quot;).build()); }catch (Exception e) { return ResponseEntity.ok(ResDTO.builder().code(-1).message(&quot;로그아웃 처리 오류&quot;).errorCode(e.getMessage()).build()); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#2980b9"><strong>3. React 프론트 엔드 구현</strong></span></span></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1)api-config.js</strong></span></p><pre class="brush:as3;">let backendHost=&quot;&quot;; const hostname= window &amp;&amp; window.location &amp;&amp;window.location.hostname; if(hostname ===&quot;localhost&quot;){ backendHost=&quot;http://localhost:8080&quot;; }else{ backendHost=&quot;http://localhost:5050&quot;; } export const API_BASE_URL =backendHost;</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>2)ApiServie.js</strong></span></p><p>&nbsp;</p><p>Promise-값이 반화 처리되는데,</p><p>다음 사이트를 참조해서 살펴 보도록 한다.</p><p><a target="_blank" href="https://squirmm.tistory.com/entry/ReactJavaScript-Promise-값-가져오기">https://squirmm.tistory.com/entry/ReactJavaScript-Promise-값-가져오기</a></p><p>&nbsp;</p><pre class="brush:as3;">//https://squirmm.tistory.com/entry/ReactJavaScript-Promise-값-가져오기 //promiseresult 데이터값 반환 async function getData(resData){ return await resData.then((promiseResult)=&gt;{ return promiseResult; }); }</pre><p>&nbsp;</p><pre class="brush:as3;">import {API_BASE_URL} from &#39;./api-config&#39;; //1.데이터 목록 불러오기 , 2.추가, 3.삭제, 4.수정 export async function call(url, mehtod, request){ console.log(&quot;call &quot;, url, mehtod, request); let headers=new Headers({&#39;Content-Type&#39;: &#39;application/json&#39;}); //로컬 스토리지에서 ACCESS TOKEN 가져오기 const accessToken =localStorage.getItem(&#39;ACCESS_TOKEN&#39;); if(accessToken &amp;&amp; accessToken!=null){ headers.append(&#39;Authorization&#39;, &#39;Bearer &#39;+ accessToken); } let options={ headers: headers, url:API_BASE_URL + url, method: mehtod, }; if(request){ //GET options.body=JSON.stringify(request); } return fetch(options.url, options) .then(async (res)=&gt;{ console.log(res.status, res); if(res.status===200){ return res.json(); }else if(res.status===400 ||res.status===403 || res.status===403){ const resData=await getData(res.json()); console.log(&quot;resData &quot;, resData); alert(resData.message); if(resData.errorCode===&quot;TOKEN_EXPIRED&quot;){ //접근 토큰 유효기간 만료 const result=await reissue(); if(result){ //call 함수를 재호출한다. return await call(url, mehtod, request); } }else if(resData.errorCode===&quot;ACCESS_DENIED&quot;){ } //오류 등 유효하지 않으면 로그인화면으로 window.location.href=&quot;/login&quot;; }else{ Promise.reject(res); throw Error(res); } }).catch((error) =&gt; { console.log(&quot;에러 &quot;, error); }); } //https://squirmm.tistory.com/entry/ReactJavaScript-Promise-값-가져오기 //promiseresult 데이터값 반환 async function getData(resData){ return await resData.then((promiseResult)=&gt;{ return promiseResult; }); } //접근 토큰 및 갱신토큰 재발급 처리 async function reissue(){ let headers=new Headers({&#39;Content-Type&#39;: &#39;application/json&#39;}); //로컬 스토리지에서 갱신토큰 가져오기 const refreshToken =localStorage.getItem(&#39;REFRESH_TOKEN&#39;); if(refreshToken &amp;&amp; refreshToken!=null){ headers.append(&#39;RefreshToken&#39;,refreshToken); } let options={ headers: headers, method: &quot;POST&quot;, }; return await fetch(API_BASE_URL+&quot;/api/auth/reissue&quot;,options) .then((res)=&gt;res.json()) .then(res=&gt;{ console.log(&quot;reissue (접근 토큰 및 갱신토큰 재발급 처리) :&quot;, res ); if(res.token){ //로컬스토리지에 저장; console.log(&quot;로컬스토리지에 저장 : &quot;, res.token.accessToken, res.token.refreshToken); localStorage.setItem(&quot;ACCESS_TOKEN&quot;, res.token.accessToken); localStorage.setItem(&quot;REFRESH_TOKEN&quot;, res.token.refreshToken); alert(&quot;갱신 처리 완료&quot;); return true; }else if(res.errorCode===&quot;INVALID_REFRESH_TOKEN&quot;) { alert(&quot;갱신 토큰이 유요하지 않습니다.&quot;); return false; }else { alert(&quot;기타 오류&quot;); return false; } }).catch(err=&gt;{ console.log(&quot;접근 토큰 및 갱신토큰 재발급 처리 오류 :&quot;, err); }) } //로그인 처리 export function signin(userDTO){ return call(&quot;/api/auth/signin&quot;, &quot;POST&quot;, userDTO) .then((res)=&gt;{ if(res){ if(res===&quot;invalid&quot;){ alert(&quot;아이디 또는 비밀번호가 일치하지 않습니다.&quot;); return; } //로컬 스토리지에 토큰 저장 localStorage.setItem(&quot;ACCESS_TOKEN&quot;, res.token.accessToken); localStorage.setItem(&quot;REFRESH_TOKEN&quot;, res.token.refreshToken); window.location.href=&quot;/&quot;; } }) } //로그 아웃 처리 export function signout(){ let headers=new Headers({&#39;Content-Type&#39;: &#39;application/json&#39;}); const refreshToken =localStorage.getItem(&#39;REFRESH_TOKEN&#39;); if(refreshToken &amp;&amp; refreshToken!=null){ headers.append(&#39;RefreshToken&#39;,refreshToken); } let options={ headers: headers, method: &quot;POST&quot;, }; fetch(API_BASE_URL+&quot;/api/auth/logout&quot;,options) .then(res=&gt;res.json()) .then(res=&gt;{ if(res.code===1){ localStorage.removeItem(&quot;ACCESS_TOKEN&quot;); localStorage.removeItem(&quot;REFRESH_TOKEN&quot;); }else if(res.code===-1){ console.log(&quot;로그아웃 오류 : &quot; ,res.error); alert(res.message); } window.location.href=&quot;/login&quot;; }).catch(err=&gt;{ console.log(&quot;에러 : &quot;,err); }); } //계정 생성 처리 export function signup(userDTO){ return call(&quot;/api/auth/signup&quot;, &quot;POST&quot;, userDTO) }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>3)App.js</strong></span></p><pre class="brush:as3;">import { AppBar, Button, Container, Grid, List, Paper, Toolbar, Typography } from &quot;@mui/material&quot;; import &quot;./App.css&quot;; import AddTodo from &quot;./components/AddTodo&quot;; import Todo from &quot;./components/Todo&quot;; import { useEffect, useState } from &quot;react&quot;; import { call, signout } from &quot;./config/ApiServie&quot;; function App() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() =&gt; { //1.데이터 목록 불러오기 //&quot;/api/admin&quot; ===&gt; 관리자 페이지 테스트 console.log(&quot;로그인시 최초 호출&quot;); call(&quot;/api/todo&quot;, &quot;GET&quot;, null).then((res) =&gt; { console.log(&quot;1.데이터 목록 불러오기 &quot; ,res); if(res===&quot;invalid&quot;){ window.location.href=&quot;/login&quot;; }else{ if(res&amp;&amp; res.data){ setItems(res.data) setLoading(false); } } }); }, []); //2.추가 const addItem = (item) =&gt; { call(&quot;/api/todo&quot;, &quot;POST&quot;, item).then((res) =&gt; setItems(res.data)); }; //3.삭제 const deleteItem = (item) =&gt; { call(&quot;/api/todo&quot;, &quot;DELETE&quot;, item).then((res) =&gt; setItems(res.data)); }; //수정 const editItem = (item) =&gt; { call(&quot;/api/todo&quot;, &quot;PUT&quot;, item).then((res) =&gt; setItems(res.data)); }; let todoItems = items &amp;&amp; items.length &gt; 0 &amp;&amp; ( &lt;Paper style={{ margin: 16 }}&gt; &lt;List&gt; {items.map((item) =&gt; { return ( &lt;Todo key={item.todoId} inputItem={item} editItem={editItem} deleteItem={deleteItem} /&gt; ); })} &lt;/List&gt; &lt;/Paper&gt; ); //navigationBar 추가 let navigationBar =( &lt;AppBar postion=&quot;static&quot; style={{ marginBottom: 300 }}&gt; &lt;Toolbar&gt; &lt;Grid justifyContent=&quot;space-between&quot; container&gt; &lt;Grid item&gt; &lt;Typography variant=&quot;h6&quot;&gt;오늘의 할일&lt;/Typography&gt; &lt;/Grid&gt; &lt;Grid item&gt; &lt;Button color=&quot;inherit&quot; onClick={signout}&gt; 로그아웃 &lt;/Button&gt; &lt;/Grid&gt; &lt;/Grid&gt; &lt;/Toolbar&gt; &lt;/AppBar&gt; ); // 로딩중이 아닐 때 랜더링할 부분 let todoListPage=( &lt;&gt; {navigationBar} &lt;Container maxWidth=&quot;md&quot; style={{top:70, position:&quot;relative&quot;}}&gt; &lt;AddTodo addItem={addItem} /&gt; &lt;div className=&quot;TodoList&quot;&gt;{todoItems}&lt;/div&gt; &lt;/Container&gt; &lt;/&gt; ) //로딩중리 때 래더링할 부분 let loadingPage=&lt;h1&gt;로딩중....&lt;/h1&gt; let content=loadingPage; if(!loading){ // 로딩중이 아니라면 todoListPage 를 선택 content=todoListPage; } return ( &lt;div className=&quot;App&quot;&gt; {content} &lt;/div&gt; ); } export default App; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-03-10 16:40:02 리액트 ref 사용 모달 개발 및 Portals 적용 http://macaronics.net/index.php/m04/react/view/2182 2182 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>TimerChallenge.jsx</strong></span></p><pre class="brush:as3;">import React, { useEffect, useRef, useState } from &#39;react&#39; import ResultModal from &#39;./ResultModal&#39;; const TimerChallenge = ({title, targetTime}) =&gt; { const timer =useRef(); const dialog=useRef(); const [timeRemaining, setTimeRemaining] = useState(targetTime*1000); const timerIsActive =timeRemaining &gt; 0 &amp;&amp; timeRemaining &lt; targetTime*1000; useEffect(()=&gt;{ if(timeRemaining &lt;= 0 ) { clearInterval(timer.current); dialog.current.open(); } }, [timeRemaining]); function handleReset(){ setTimeRemaining(targetTime*1000); } function handleStart(){ timer.current=setInterval(() =&gt;{ setTimeRemaining(prevTimeRemaining =&gt; prevTimeRemaining-10); }, 10); } function handleStop(){ dialog.current.open(); clearInterval(timer.current); } return ( &lt;&gt; &lt;ResultModal ref={dialog} targetTime={targetTime} timeRemaining={timeRemaining} result =&quot;YOU LOST&quot; onReset={handleReset} /&gt; &lt;section className=&#39;challenge&#39;&gt; &lt;h2&gt;{title}&lt;/h2&gt; {/* {timerExpired &amp;&amp; &lt;p&gt;You lost! &lt;/p&gt;} */} &lt;p className=&#39;challenge-time&#39;&gt; {targetTime} 초 {targetTime &gt;1 ? &#39;&#39; :&#39;&#39;} &lt;/p&gt; &lt;p&gt; &lt;button onClick={timerIsActive ?handleStop :handleStart}&gt; {timerIsActive ? &#39;멈추기&#39; : &#39;시작하기&#39;} &lt;/button&gt; &lt;/p&gt; &lt;p className={timerIsActive ? &#39;active&#39; : undefined}&gt; {timerIsActive ? &#39;타이머 작동중...&#39; : &#39;타이머 비활성&#39; } &lt;/p&gt; &lt;/section&gt; &lt;/&gt; ) } export default TimerChallenge</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>ResultModal.jsx</strong></span></p><pre class="brush:as3;">import {forwardRef, useImperativeHandle, useRef} from &#39;react&#39; import {createPortal} from &#39;react-dom&#39;; const ResultModal = forwardRef (({result, targetTime, timeRemaining , onReset} , ref) =&gt; { const dialog=useRef(); const userLost=timeRemaining &lt;=0; const formmaterdRemainingTime=(timeRemaining /1000).toFixed(2); const score = Math.round(( 1- timeRemaining/ (targetTime *1000) ) * 100); useImperativeHandle(ref, ()=&gt;{ return{ open(){ dialog.current.showModal(); } } }) return createPortal( &lt;dialog ref={dialog} className=&#39;result-modal&#39; onClose={onReset}&gt; {userLost &amp;&amp; &lt;h2&gt; {result}&lt;/h2&gt;} {!userLost &amp;&amp; &lt;h2&gt;{score}점 입니다.&lt;/h2&gt;} &lt;p&gt;목표 시간은 &lt;strong&gt;{targetTime} 초 였습니다.&lt;/strong&gt;&lt;/p&gt; {timeRemaining &gt;1 &amp;&amp; &lt;p&gt; &lt;strong&gt;{formmaterdRemainingTime} 초 남았는데 타이머를 멈췄습니다. &lt;/strong&gt;&lt;/p&gt; } &lt;form method=&#39;dialog&#39; onSubmit={onReset}&gt; &lt;button&gt;닫기&lt;/button&gt; &lt;/form&gt; &lt;/dialog&gt;, document.getElementById(&quot;modal&quot;) ); }) export default ResultModal</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:22px">index.html</span></strong></p><pre class="brush:as3;">~ &lt;body&gt; &lt;div id=&quot;modal&quot;&gt;&lt;/div&gt; &lt;div id=&quot;content&quot;&gt;&lt;/div&gt; &lt;script type=&quot;module&quot; src=&quot;/src/main.jsx&quot;&gt;&lt;/script&gt; ~</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/react-final-count">https://github.com/braverokmc79/react-final-count</a></p><p><a target="_blank" href="https://react-final-coun.netlify.app/">https://react-final-coun.netlify.app/</a></p><p><img alt="" width="300" height="171" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk0JCAvUo2vgaUUZ0wSK6byhVY2EoK4qV_dLXt_nECOR8T1vNUc-eq5WDQsMEToLJIxoYPzz5SzKHoQ6zcuPnPkOx7i2JU8nqXJkTucxe3zalHIs8hsloZCag_JEyjFAqhh7PxVGupjlGOjWBuwpA0jElb1XgT7wwDo8rAKDO_DsXWx5-Dwip_dY5pT5ls/s2383/2024-03-02%2018%2010%2029.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>6.리액트 -&nbsp;&nbsp;프로젝트 관리 앱&nbsp;프로젝트 관리 앱</p><p><a target="_blank" href="https://macaronics-project-management-app.netlify.app/">https://macaronics-project-management-app.netlify.app/</a></p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpaqcEc3QZjij8TVtB5sjdbeE7huS-_6VssbxVUDrTdY7d4Pddw4LB_Fwz7VA1QaoKyU6i-bCKAizS98qYvku1dX_mXrUczSkNxgMBp-8dlL6OJdGBBrS1owUeAQxE7Y0PwJR0ugnfqkV5ciDyMrxbcbIoIaZK9aQ2-Palli1lpfU4htZ7pRT_AkQg1Zvw/w320-h195/2024-03-03%2022%2051%2041.png" /></p><p>&nbsp;</p><p><a target="_blank" href="https://github.dev/braverokmc79/react-project-management-app">https://github.dev/braverokmc79/react-project-management-app</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-03-02 18:28:28 심플 프로젝트 http://macaronics.net/index.php/m05/computer/view/2181 2181 <p>&nbsp;</p><p>1.리액트 -틱택토</p><p><a target="_blank" href="https://tictactoe732.netlify.app/">https://tictactoe732.netlify.app/</a></p><p><img alt="" width="300" height="336" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-o0RTzBEiOckFcKGZiJvMNZ-QdukO1rQ_UtAeybL5sNS1yw2BC5DQZp7uSOhuR3iIDKfyvuVa1L7QEGp-BDi1U8AXY16MRtqVlAyopiKvgJbbD3hHWRYWwi5wudmIj7IXJO8HqY4fptsW1TFrDh1q_pI4cxmW7qrRnqPRLb5q3ZsEbIxMFmeB-vLtEzQk/s1367/2024-02-28%2022%2053%2055.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>2.퍼블-스타벅스</p><p><a target="_blank" href="https://dapper-kangaroo-bf6ba2.netlify.app/">https://dapper-kangaroo-bf6ba2.netlify.app/</a></p><p><img alt="" width="300" height="246" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdgpZj8YjZzBzYQ7PrUrVT3DPPb5-Cx0b3vbJFKdP3ce0oMivhZ9pfOPgw1vQtAtU6JCsYb8jX-x69NOJ0xfBTKRC37vHFh70CbNOjeMf_DGXcCncw8NMYW_Nfea5i9YsbJ6W9BypfUM98b4ShX5Ti67M4yH_DtawPoWVKUqnIh13XdIFF0w68zXR8Zstf/s1707/2024-02-28%2022%2054%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>3.리액트 -투자 계싼기</p><p><a target="_blank" href="https://investment-calc732.netlify.app/">https://investment-calc732.netlify.app/</a></p><p>&nbsp;</p><p><img alt="" width="300" height="258" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEUAWpMkueIrK8_4QPnifoYxTFoOJxs7siDVbRc8p_STLMdy27qF-CW-o9AM4Y8yOntOSIwAd6G3he0aHho-tFxftprlmcSPa_QSPnVPM6aWCOgmTzcMz52B230L5dVRPwdxoo-LTAY6VSo9gSXpnB03e3HcTP3RN7rzuxagNGKXtwGpART5SLa6PQTjqv/s1007/%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7%202024-02-29%20154510.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>4.리액트 -디지니 플래서 앱</p><p><a target="_blank" href="https://react-disney-plus-app.netlify.app/">https://react-disney-plus-app.netlify.app/</a></p><p><a target="_blank" href="https://react-disney-plus-app.netlify.app/https://react-disney-plus-app.netlify.app/">/</a><img alt="" width="300" height="170" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3Tgk6VJu3omzJGuF6sNQEoNYFUe8tVQwY8l9R0XkB0gvw8rhL21MEvYXwtP5UIDsIWOlm8Sx7Gw8sAcbuICA73ZfpA-5rzMMkpbYp5xz5HfIts2l0NqlIAS8iH3MLA8UVXu2W6wK-RFVrn4TO45fpBRcmMhqCyhxjGK2KRx7jdAZW3hODQOlY0j35sGeF/s2477/2024-02-29%2017%2058%2004.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>5.리액트 -&nbsp;&nbsp;<a rel="bookmark" itemprop="url" data-item-type="post" data-id="3418373342555018661" href="https://react-final-coun.netlify.app/">더 파이널 카운터 다운</a></p><p><a target="_blank" href="https://react-final-coun.netlify.app/">https://react-final-coun.netlify.app/</a></p><p><img alt="" width="300" height="171" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgk0JCAvUo2vgaUUZ0wSK6byhVY2EoK4qV_dLXt_nECOR8T1vNUc-eq5WDQsMEToLJIxoYPzz5SzKHoQ6zcuPnPkOx7i2JU8nqXJkTucxe3zalHIs8hsloZCag_JEyjFAqhh7PxVGupjlGOjWBuwpA0jElb1XgT7wwDo8rAKDO_DsXWx5-Dwip_dY5pT5ls/s2383/2024-03-02%2018%2010%2029.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>6.리액트 -&nbsp;&nbsp;프로젝트 관리 앱&nbsp;프로젝트 관리 앱</p><p><a target="_blank" href="https://macaronics-project-management-app.netlify.app/">https://macaronics-project-management-app.netlify.app/</a></p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpaqcEc3QZjij8TVtB5sjdbeE7huS-_6VssbxVUDrTdY7d4Pddw4LB_Fwz7VA1QaoKyU6i-bCKAizS98qYvku1dX_mXrUczSkNxgMBp-8dlL6OJdGBBrS1owUeAQxE7Y0PwJR0ugnfqkV5ciDyMrxbcbIoIaZK9aQ2-Palli1lpfU4htZ7pRT_AkQg1Zvw/w320-h195/2024-03-03%2022%2051%2041.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>7.리액트-장바구니</p><p>엘라강스 모드</p><p><a target="_blank" href="https://react-elegance-mode.netlify.app/">https://react-elegance-mode.netlify.app/</a></p><p><img alt="" width="300" height="196" src="https://blogger.googleusercontent.com/img/a/AVvXsEh3MKFdxAp9lNfO2sB9BVfcw3DGCLitMgaL7C_9BediM64fYtdCZpc9Qt54YzlbM4cTqVphQTU2Zn66OLDcmYHIWySbxKRNMtumpRMsYhb63IxB75806sBW8DnGpkpa4PQizDSORZehPCrrD7o1UbYrVlrqWJtaW-yujructJgekSCxwvO217_jiw9KPXNV" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>8. 리액트 -장소 선택(place picker)</p><p><a target="_blank" href="https://braverokmc79.github.io/react-place-picker/">https://braverokmc79.github.io/react-place-picker/</a></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBO3CaK1fyZl2ir6Pzud7zTNcfzSfvqXHnaQjZLbx1jCr1PJ6bojofzQgqic5YnFPiUS8Ar1EdqTfoVjV_5tk5i6iQaa4cIOKfPoR70BcRubj6IkTl7lT4Ce9uWbAmd4CLsH2W_Pk8yYHO4wJLTxpabuTtvEJr785jJ6sG7rCzdM27ucF7fSNvn1KupMNZ/s320/2024-03-17%2021%2007%2024.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-02-28 22:58:56 ★★★ spring boot AWS의 elastic beanstalk으로 배포 && nginx 스프링부트  리액트  연동 && CI/CD github action aws 배포 및 우분트 배포 && Bitbucket pipelines 우분트 배포 http://macaronics.net/index.php/m01/spring/view/2180 2180 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">1)</span></strong><strong>Cors &nbsp;설정은 리액트에서 프록시 설정으로&nbsp; &nbsp;여기 프로젝트에서는&nbsp; 스프링 부트와&nbsp; nginx 설정으로 해결 했다.</strong></p><p>&nbsp;</p><p>리액트(React) CORS처리</p><p><a target="_blank" href="https://woochan-dev.tistory.com/94">https://woochan-dev.tistory.com/94</a></p><p><strong><span style="color:#c0392b">주의 Nginx 연동&nbsp;</span></strong><span style="color:#2980b9"><span style="font-size:22px"><strong>Cors 설정</strong></span></span></p><p><strong><span style="color:#c0392b">모든 경로에 대해 cors 허용 으로 오류가 난다. </span></strong></p><p><strong><span style="color:#c0392b">또한&nbsp; restApi 페이지가 아닌경우 오류가 발생한다.</span></strong></p><p>ex) registry.addMapping(&quot;/**&quot;) * registry.addMapping(&quot;/cart/**&quot;), * registry.addMapping(&quot;/members/**&quot;)</p><p>&nbsp;</p><p>nginx 연동 다음을 참조</p><p><a target="_blank" href="https://macaronics.net/m02/linux/view/1819">https://macaronics.net/m02/linux/view/1819</a></p><p>&nbsp;</p><pre class="brush:as3;">package com.shop.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.util.StringUtils; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; import java.util.ArrayList; import java.util.List; @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Value(&quot;${uploadPath}&quot;) String uploadPath; private final long MAX_AGE_SECS=3600; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(&quot;/images/**&quot;) .setCachePeriod(60*10*6) // 1시간 .setCachePeriod(60*10*6) .addResourceLocations(uploadPath); registry .addResourceHandler(&quot;/upload/**&quot;) //jsp 페이지에서 /upload/** 이런주소 패턴이 나오면 발동 .addResourceLocations(uploadPath+&quot;/upload/&quot;) .setCachePeriod(60*10*6) // 1시간 .resourceChain(true) .addResolver(new PathResourceResolver()); } @Override public void addCorsMappings(CorsRegistry registry) { /** ★★★★★ nginx 연동의 경우 든 경로에 대해 cors 허용 하면 안된다. * 또한, * 현재 프로젝트가 todo 와 api 인데 * restapi 가 아닌 경로까지 적용되면 restapi 아닌 페이지는 오규가 발생한다. /모든 경로에 대해 cors 허용 ex) registry.addMapping(&quot;/**&quot;) * registry.addMapping(&quot;/cart/**&quot;), * registry.addMapping(&quot;/members/**&quot;) */ //★★★★★ nginx 연동의 경우 루트 /** 으로 설정이 안된다 다음과 같이 개별 설정 ★★★★★ registry.addMapping(&quot;/todo/**&quot;) .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;) .allowedHeaders(&quot;*&quot;) .allowCredentials(true) // &#39;Access-Control-Allow-Credentials&#39; header 는 요청 시 자격 증명이 필요함 .maxAge(MAX_AGE_SECS) .allowedOrigins( &quot;http://localhost:3000/&quot; ,&quot;https://ma7front.p-e.kr/&quot; ).exposedHeaders(&quot;authorization&quot;); //authorization 헤더를 넘기 위해 exposedHeaders 조건을 추가 registry.addMapping(&quot;/api/**&quot;) .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PUT&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;, &quot;OPTIONS&quot;) .allowedHeaders(&quot;*&quot;) .allowCredentials(true) // &#39;Access-Control-Allow-Credentials&#39; header 는 요청 시 자격 증명이 필요함 .maxAge(MAX_AGE_SECS) .allowedOrigins( &quot;http://localhost:3000/&quot; ,&quot;https://ma7front.p-e.kr/&quot; ).exposedHeaders(&quot;authorization&quot;); //authorization 헤더를 넘기 위해 exposedHeaders 조건을 추가 } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">2)주의</span></strong></p><p><strong><span style="color:#c0392b">백엔드 프론트엔드 로&nbsp; 분리된</span></strong></p><p><strong><span style="color:#c0392b">restapi 가 아닌&nbsp; 일반 웹개발 인경우&nbsp;&nbsp;템플릿&nbsp; 페이지 뷰 반환시&nbsp;&nbsp;개발환경에서&nbsp;&nbsp;return &quot;/member/memberLoginForm&quot;;</span></strong></p><p><strong><span style="color:#c0392b">return &quot;/~&quot;&nbsp; &nbsp;return&nbsp; 다음 경로에&nbsp;슬러시를 붙여도 정상 작동되나 배포시에는 오류가 난다.</span></strong></p><p><strong><span style="color:#c0392b">이 오류 때문에 한참 헤메였음</span></strong></p><p>&nbsp;</p><pre>@GetMapping(value = &quot;/login/error&quot;) public String loginError(Model model){ model.addAttribute(&quot;loginErrorMsg&quot;, &quot;아이디 또는 비밀번호를 확인해주세요&quot;); return &quot;member/memberLoginForm&quot;; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:16px"><strong>3)웹브라우저 확장 설치 프로그램에 대한 오류가 있을수 있으니 확인</strong></span></span></p><p>&nbsp;</p><p><br />&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-shop-maven-and-gradle-front">https://github.com/braverokmc79/jpa-shop-maven-and-gradle-front</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>0. 준비</p><p>실행환경..?</p><p>- Windows 10</p><p>- spring boot (Gradle, jar, java11)</p><p>미리 확인</p><p>- python 설치 확인 (cmd에서 python --version)</p><p>- aws cli v2 설치 (구글링 후 .msi 파일 다운, 설치)</p><p>&nbsp;</p><p>1. aws cli 설정</p><p>AWS 콘솔에서 IAM-액세스 관리-사용자</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT2lMP%2Fbtro7gk853I%2FjsuOR9nFZHKrdUkDDQmxVK%2Fimg.png" data-origin-width="1628" data-origin-height="829" alt="" src="https://blog.kakaocdn.net/dn/bT2lMP/btro7gk853I/jsuOR9nFZHKrdUkDDQmxVK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>[사용자 추가] 눌러서,</p><p>사용자이름: cli-user,</p><p>액세스유형: 프로그래밍 방식 액세스... 로 설정하고 [다음: 권한] 누름.</p><p>권한설정: 기존 정책 직접 연결-AdministratorAccess 선택 후 [다음:태그], [다음: 검토], [사용자 만들기]</p><p>&nbsp;</p><p>이 다음에 표시되는 화면에서&nbsp;<u>액세스키ID</u>,&nbsp;<u>비밀 액세스 키</u>를 복사.</p><p>&nbsp;</p><p>&nbsp;</p><p>+</p><p>cmd에서</p><pre>$ aws configure AWS Access Key ID [None]: &lt;액세스키 ID&gt; AWS Secret Access Key [None]: &lt;비밀 액세스키&gt; Default region name [None]: us-west-2 Default output format [None]: json</pre><p>&nbsp;</p><p>region name은 다른것도 가능..</p><p>한국 사용자가 많을 경우에는 ap-northeast-2를 쓰면 됨</p><p>+</p><p>이후에 pip로 ebcli 설치</p><pre>pip install awsebcli --upgrade -user</pre><p>&nbsp;</p><p>+</p><p>환경변수(시스템 변수)</p><p>Path에 %USERPROFILE%/AppData/Roaming/Python/Python39/Scripts 추가.</p><p>+</p><p>마지막으로 cmd 재시작, 설치 확인</p><pre>$ eb --version EB CLI 3.19.2 (Python 3.9.0)</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>2. 환경 초기화</p><p>&nbsp;</p><p>cmd에서&nbsp;<u>백엔드 프로젝트 폴더로 이동</u>한다.</p><p>이후 eb init 입력</p><pre class="brush:as3;">$eb init TodoApplication-backend Select a default region 1) us-east-1 : US East (N. Virginia) 2) us-west-1 : US West (N. California) 3) us-west-2 : US West (Oregon) 4) eu-west-1 : EU (Ireland) 5) eu-central-1 : EU (Frankfurt) 6) ap-south-1 : Asia Pacific (Mumbai) 7) ap-southeast-1 : Asia Pacific (Singapore) 8) ap-southeast-2 : Asia Pacific (Sydney) 9) ap-northeast-1 : Asia Pacific (Tokyo) 10) ap-northeast-2 : Asia Pacific (Seoul) 11) sa-east-1 : South America (Sao Paulo) 12) cn-north-1 : China (Beijing) 13) cn-northwest-1 : China (Ningxia) 14) us-east-2 : US East (Ohio) 15) ca-central-1 : Canada (Central) 16) eu-west-2 : EU (London) 17) eu-west-3 : EU (Paris) 18) eu-north-1 : EU (Stockholm) 19) eu-south-1 : EU (Milano) 20) ap-east-1 : Asia Pacific (Hong Kong) 21) me-south-1 : Middle East (Bahrain) 22) af-south-1 : Africa (Cape Town) (default is 3): 10 Application TodoApplication-backend has been created. Select a platform. 1) .NET Core on Linux 2) .NET on Windows Server 3) Docker 4) Go 5) Java 6) Node.js 7) PHP 8) Packer 9) Python 10) Ruby 11) Tomcat (make a selection): 5 Select a platform branch. 1) Corretto 21 running on 64bit Amazon Linux 2023 2) Corretto 17 running on 64bit Amazon Linux 2023 3) Corretto 11 running on 64bit Amazon Linux 2023 4) Corretto 8 running on 64bit Amazon Linux 2023 5) Corretto 17 running on 64bit Amazon Linux 2 6) Corretto 11 running on 64bit Amazon Linux 2 7) Corretto 8 running on 64bit Amazon Linux 2 (default is 1): 1 Alert: You chose a deprecated platform branch. It might not be supported in the future. Cannot setup CodeCommit because there is no Source Control setup, continuing with initialization Do you want to set up SSH for your instances? (Y/n): n</pre><p>이제 .elasticbeanstalk 이라는 폴더가 생겼다.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPuRGU%2FbtrpdpOPuCU%2FPkD2jQWj4F0t64UPgBZq1K%2Fimg.png" data-origin-width="244" data-origin-height="211" alt="" src="https://blog.kakaocdn.net/dn/PuRGU/btrpdpOPuCU/PkD2jQWj4F0t64UPgBZq1K/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>3. 백엔드 어플리케이션 설정</p><p>&nbsp;</p><p>application.properties 파일 수정</p><pre class="brush:as3;"># application.properties 파일 내용 spring.profiles.active=prod #애플리케이션 포트 설정 #server.port = 5000</pre><p>&nbsp;</p><p>&nbsp;</p><p>application.properties</p><p>설정 주의 : 잘못 설정하면 당연히 배포 오류가 난다.&nbsp;</p><p>&nbsp;책에서는&nbsp;&nbsp;application.properties 삭제하고 환경변수를 설정하라고 나오지만 다음과 같이 설정해도 된다.</p><p><strong>application.properties</strong></p><pre class="brush:as3;">spring.profiles.active=prod </pre><p>&nbsp;</p><p><strong>application-prod.properties</strong></p><pre class="brush:as3;">#애플리케이션 포트 설정 server.port = 5000 #spring.output.ansi.enabled=always #Mysql server spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://${rds.hostname}:${rds.port}/${rds.db.name} spring.datasource.username= ${rds.username} spring.datasource.password= ${rds.password} spring.jpa.open-in-view=false #create , update spring.jpa.hibernate.ddl-auto=update #파일 한 개당 최대 사이즈 spring.servlet.multipart.maxFileSize=20MB #요청당 최대 파일 크기 spring.servlet.multipart.maxRequestSize=100MB #상품 이미지 업로드 경로 itemImgLocation=/uploads/shop/item #리소스 업로드 경로 uploadPath=file:/uploads/shop/ #기본 batch size 설정 spring.jpa.properties.hibernate.default_batch_fetch_size=1000</pre><p>&nbsp;</p><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>책이랑 다르게 엘라스틱 빈스톡의 플랫폼을 [<u>1) Corretto 21 running on 64bit Amazon Linux 2023</u>] 로 선택했다.</p><p>(Java 8 running on 64bit amazon linux 옵션은 deprecataed 경고가 뜨기 때문...)</p><p>그래서 포트가 어떻게 되나... 여러번 시도 했었다.</p><p>근데 다른거 없었다. 포트 그냥 5000 쓰면 된다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>+</p><p>gadle.build의 dependencies에도 하나 추가한다.</p><pre># build.gradle 의 dependencies에 추가 runtimeOnly &#39;mysql:mysql-connector-java&#39;</pre><p>runtimeOnly &#39;com.h2database:h2&#39;가 있어도 그 아랫줄에 새로 끼워 넣으면 됨.</p><p>&nbsp;</p><p>+</p><p>그리고 새 클래스 작성</p><pre>package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HealthCheck { @GetMapping(&quot;/&quot;) public String healthCheck() { return &quot;The service is up and running...&quot;; } }</pre><p>AWS 로드밸런서는 기본 경로인 &quot;/&quot;에 HTTP 요청을 보내서 어플리케이션이 동작하는지 확인한다.</p><p>일라스틱 빈스톡은 이를 기반으로 어플리케이션이 실행중인 상태인지, 주의가 필요한 상태인지 확인해준다.</p><p>또 이 상태를 AWS 콘솔 화면에 표시해준다.</p><p>이를 위해서 &quot;/&quot;에 간단한 API를 만들어주면 좋다.</p><p>&nbsp;</p><p>+</p><p>이제 spring boot 프로젝트를 build한다.</p><pre># cmd로 프로젝트 폴더까지 이동한 후 $ gradle clean build Welcome to Gradle 7.3! Here are the highlights of this release: - Easily declare new test suites in Java projects - Support for Java 17 - Support for Scala 3 For more details see https://docs.gradle.org/7.3/release-notes.html Starting a Gradle Daemon (subsequent builds will be faster) &gt; Task :test 2021-12-28 11:59:50.262 INFO 4408 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit &#39;default&#39; 2021-12-28 11:59:50.272 INFO 4408 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down&#39; 2021-12-28 11:59:50.289 INFO 4408 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2021-12-28 11:59:50.314 INFO 4408 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. BUILD SUCCESSFUL in 1m 11s 7 actionable tasks: 7 executed</pre><p>그러면 build/libs 경로에 jar 파일이 생긴다.</p><p>버전 이름은 gradle.build 파일에서 설정할 수 있다.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs7MDT%2Fbtro7gesokx%2FkQieKLh0UCxq1OvPOnaE70%2Fimg.png" data-origin-width="597" data-origin-height="215" alt="" src="https://blog.kakaocdn.net/dn/s7MDT/btro7gesokx/kQieKLh0UCxq1OvPOnaE70/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>&nbsp;</p><p>+</p><p>.elasticbeanstalk/config.yml 에 deploy 부분 추가</p><pre># .elasticbeanstalk/config.yml 파일 내용 branch-defaults: default: environment: PROD-TODO-BACKEND # 추가 시작 deploy: artifact: build/libs/demo-0.0.3.jar # 추가 끝 global: application_name: TodoApplication-backend branch: null default_ec2_keyname: null default_platform: Corretto 11 running on 64bit Amazon Linux 2 default_region: us-west-2 include_git_submodules: true instance_profile: null platform_name: null platform_version: null profile: null repository: null sc: null workspace_type: Application</pre><p>&nbsp;</p><p>+</p><p>eb create 로 환경 생성&nbsp;</p><pre># cmd에서 프로젝트 폴더로 이동한 후 $ eb create --database --elb-type application --instance-type t2.micro Enter Environment Name (default is TodoApplication-backend-dev): PROD-TODO-BACKEND Enter DNS CNAME prefix (default is PROD-TODO-BACKEND22): Would you like to enable Spot Fleet requests for this environment? (y/N): N Enter an RDS DB username (default is &quot;ebroot&quot;): Enter an RDS DB master password: &lt;비밀번호 입력&gt; Retype password to confirm: &lt;비밀번호 입력&gt; Uploading: [##################################################] 100% Done... Environment details for: PROD-TODO-BACKEND Application name: TodoApplication-backend Region: us-west-2 Deployed Version: app-211228_143259 Environment ID: e-m32zspxdmx Platform: arn:aws:elasticbeanstalk:us-west-2::platform/Corretto 11 running on 64bit Amazon Linux 2/3.2.9 Tier: WebServer-Standard-1.0 CNAME: PROD-TODO-BACKEND22.us-west-2.elasticbeanstalk.com Updated: 2021-12-28 05:33:32.287000+00:00 Printing Status: 2021-12-28 05:33:30 INFO createEnvironment is starting. 2021-12-28 05:33:32 INFO Using elasticbeanstalk-us-west-2-490191556309 as Amazon S3 storage bucket for environment data. 2021-12-28 05:33:53 INFO Created target group named: arn:aws:elasticloadbalancing:us-west-2:490191556309:targetgroup/awseb-AWSEB-DZM8O8EKV3XS/e5c4b46318194e1d 2021-12-28 05:33:53 INFO Created security group named: sg-0ce0bec80d01e332c 2021-12-28 05:34:09 INFO Created security group named: awseb-e-m32zspxdmx-stack-AWSEBSecurityGroup-STVZ3AC9TCPA 2021-12-28 05:34:09 INFO Created Auto Scaling launch configuration named: awseb-e-m32zspxdmx-stack-AWSEBAutoScalingLaunchConfiguration-1N9SNDTFZLLM5 2021-12-28 05:34:09 INFO Created RDS database security group named: awseb-e-m32zspxdmx-stack-awsebrdsdbsecuritygroup-1vo76ck4ah7r0 2021-12-28 05:34:24 INFO Creating RDS database named: aa17sko6ijnmpkt. This may take a few minutes. 2021-12-28 05:36:29 INFO Created load balancer named: arn:aws:elasticloadbalancing:us-west-2:490191556309:loadbalancer/app/awseb-AWSEB-18IATENQI5FEY/208b51a89b8a681f 2021-12-28 05:36:44 INFO Created Load Balancer listener named: arn:aws:elasticloadbalancing:us-west-2:490191556309:listener/app/awseb-AWSEB-18IATENQI5FEY/208b51a89b8a681f/9e890169d65e24ce 2021-12-28 05:42:51 INFO Created RDS database named: aa17sko6ijnmpkt 2021-12-28 05:43:54 INFO Created Auto Scaling group named: awseb-e-m32zspxdmx-stack-AWSEBAutoScalingGroup-1J2JGBKYPBVNS 2021-12-28 05:43:54 INFO Waiting for EC2 instances to launch. This may take a few minutes. 2021-12-28 05:43:54 INFO Created Auto Scaling group policy named: arn:aws:autoscaling:us-west-2:490191556309:scalingPolicy:c3bc20fd-e70e-45ea-bc76-b5c8ec638995:autoScalingGroupName/awseb-e-m32zspxdmx-stack-AWSEBAutoScalingGroup-1J2JGBKYPBVNS:policyName/awseb-e-m32zspxdmx-stack-AWSEBAutoScalingScaleUpPolicy-GI0BN5M0F3WR 2021-12-28 05:43:54 INFO Created Auto Scaling group policy named: arn:aws:autoscaling:us-west-2:490191556309:scalingPolicy:8937a9a1-0781-4876-94f5-e21743b04e39:autoScalingGroupName/awseb-e-m32zspxdmx-stack-AWSEBAutoScalingGroup-1J2JGBKYPBVNS:policyName/awseb-e-m32zspxdmx-stack-AWSEBAutoScalingScaleDownPolicy-15LV9A4LA8XNU 2021-12-28 05:44:09 INFO Created CloudWatch alarm named: awseb-e-m32zspxdmx-stack-AWSEBCloudwatchAlarmLow-1RTKL2U1BUJJU 2021-12-28 05:44:09 INFO Created CloudWatch alarm named: awseb-e-m32zspxdmx-stack-AWSEBCloudwatchAlarmHigh-5BZ9YSG55UY3 2021-12-28 05:44:15 INFO Instance deployment successfully detected a JAR file in your source bundle. 2021-12-28 05:44:15 INFO Instance deployment successfully generated a &#39;Procfile&#39;. 2021-12-28 05:44:18 INFO Instance deployment completed successfully. 2021-12-28 05:44:51 INFO Application available at PROD-TODO-BACKEND22.us-west-2.elasticbeanstalk.com. 2021-12-28 05:44:51 INFO Successfully launched environment: PROD-TODO-BACKEND</pre><p>배포 완료..!</p><p>&nbsp;</p><p>&nbsp;</p><p>5. 배포 상태 확인</p><p>AWS 콘솔 - Elastic Beanstalk - 환경에서 배포 상태 확인.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnpodg%2Fbtro07I67FM%2FaJwGa7GQHapNFodjIicl31%2Fimg.png" data-origin-width="1477" data-origin-height="522" alt="" src="https://blog.kakaocdn.net/dn/npodg/btro07I67FM/aJwGa7GQHapNFodjIicl31/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>+</p><p>URL로 접속해서 위에서 작성한 HealthCheck 클래스 내용이 잘 나오는지 확인.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3pq3w%2FbtrpaIO4wcK%2FOska7FkVbOci6nbK1QEevk%2Fimg.png" data-origin-width="743" data-origin-height="140" alt="" src="https://blog.kakaocdn.net/dn/3pq3w/btrpaIO4wcK/Oska7FkVbOci6nbK1QEevk/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>5. RDS DB 연결 설정</p><p>&nbsp;</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbP3CsV%2FbtrpdoieDjx%2FyWiWeJZMCKW6Ktml5hdIEK%2Fimg.png" data-origin-width="1815" data-origin-height="873" alt="" src="https://blog.kakaocdn.net/dn/bP3CsV/btrpdoieDjx/yWiWeJZMCKW6Ktml5hdIEK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>elastic beanstalk-구성에서 연결된 데이터베이스의 엔드포인트 클릭.</p><p>&nbsp;</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpFbI0%2FbtrpeKegROq%2F5rGwl2kwEVu449BoDHge10%2Fimg.png" data-filename="백배포1-2.PNG" data-origin-width="1504" data-origin-height="842" alt="" src="https://blog.kakaocdn.net/dn/pFbI0/btrpeKegROq/5rGwl2kwEVu449BoDHge10/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>해당 DB 상세에서 VPC 보안그룹 눌러서 이동</p><p>&nbsp;</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGblcK%2Fbtro7gFxO3J%2FEZqoKCvIg4Qh57kQhKtLg1%2Fimg.png" data-filename="백배포1-3.PNG" data-origin-width="1535" data-origin-height="775" alt="" src="https://blog.kakaocdn.net/dn/GblcK/btro7gFxO3J/EZqoKCvIg4Qh57kQhKtLg1/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>보안그룹 - 인바운드 규칙 - 인바운드 규칙 편집</p><p>&nbsp;</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCX1kC%2Fbtro31PFLHu%2FSjwwgcS9GjHQqk12TJ61hK%2Fimg.png" data-filename="백배포1-4.PNG" data-origin-width="1785" data-origin-height="818" alt="" src="https://blog.kakaocdn.net/dn/bCX1kC/btro31PFLHu/SjwwgcS9GjHQqk12TJ61hK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>규칙 추가 - 유형:MYSQL/Aurora, 소스: Anywhere - 저장</p><p>&nbsp;</p><p>&nbsp;</p><p>+</p><p>이후에 백엔드 어플리케이션으로 돌아와서 설정 추가</p><pre># application.properties 파일 내용 server.port: 5000 spring.jpa.database=mysql spring.jpa.show-sql=true spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://&lt;엔드포인트 링크&gt;:3306/&lt;db이름&gt; spring.datasource.username=&lt;rds 사용자&gt; spring.datasource.password=&lt;rds 사용자 비번&gt;</pre><p>- rds 사용자/비번은 eb create에서 썼던 정보를 갖다 써도 된다..</p><p>- db이름은 보통 &#39;ebdb&#39;로 생성되는 듯.. 아니면 aws 콘솔 - rds - 구성에서 확인 가능</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqL3Ep%2Fbtro37BZ39l%2FHvfklF9Z33xkSMIuMKUxlK%2Fimg.png" data-origin-width="1416" data-origin-height="693" alt="" src="https://blog.kakaocdn.net/dn/cqL3Ep/btro37BZ39l/HvfklF9Z33xkSMIuMKUxlK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>&nbsp;</p><p>+</p><p>cmd에서 다시 build하고 재배포하기</p><pre>$ gradlew clean build &gt; Task :test 2021-12-28 20:33:12.849 INFO 14328 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit &#39;default&#39; 2021-12-28 20:33:12.854 INFO 14328 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2021-12-28 20:33:14.082 INFO 14328 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. BUILD SUCCESSFUL in 39s 8 actionable tasks: 8 executed</pre><pre>$ eb deploy Uploading: [##################################################] 100% Done... 2021-12-28 11:33:53 INFO Environment update is starting. 2021-12-28 11:33:58 INFO Deploying new version to instance(s). 2021-12-28 11:34:03 INFO Instance deployment successfully detected a JAR file in your source bundle. 2021-12-28 11:34:04 INFO Instance deployment successfully generated a &#39;Procfile&#39;. 2021-12-28 11:34:07 INFO Instance deployment completed successfully. 2021-12-28 11:34:12 INFO New application version was deployed to running EC2 instances. 2021-12-28 11:34:12 INFO Environment update completed successfully.</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>6. 작동 확인</p><p>&nbsp;</p><p>url로 접속</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI9kpb%2Fbtro14eNx27%2FBrsxjigdkc5OCRsV7zOyUK%2Fimg.png" data-origin-width="748" data-origin-height="154" alt="" src="https://blog.kakaocdn.net/dn/I9kpb/btro14eNx27/Brsxjigdkc5OCRsV7zOyUK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>+</p><p>POSTMAN으로 API 응답 확인</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbClLKs%2FbtrpeIHv9vu%2FZA7RGuqfbjXN5SQkqIMDV0%2Fimg.png" data-origin-width="1069" data-origin-height="740" alt="" src="https://blog.kakaocdn.net/dn/bClLKs/btrpeIHv9vu/ZA7RGuqfbjXN5SQkqIMDV0/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>+</p><p>DB에 적용 잘 되었는지 확인해봤다.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4CoJL%2FbtrpaIVLYGx%2FXwBky15Jhi3jDGyftpOWiK%2Fimg.png" data-origin-width="1086" data-origin-height="392" alt="" src="https://blog.kakaocdn.net/dn/c4CoJL/btrpaIVLYGx/XwBky15Jhi3jDGyftpOWiK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">1.AWS 엘라스틱 빈스토어 배포</span></strong></span></p><p><span style="font-size:18px"><strong><span style="color:#c0392b">$ eb create --database --elb-type application --instance-type t2.micro</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">2.환경설정</span></strong></span></p><p><span style="font-size:18px"><span style="color:#c0392b"><strong>$ eb setenv SPRING_PROFILES_ACTIVE=prod</strong></span></span><br />&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">3.프로젝트 클린및 빌들처리</span></strong></span></p><p><strong><span style="color:#c0392b"><span style="font-size:18px">$ gradlew clean build</span></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">4.AWS 엘라스틱 빈스토어 </span></strong>&nbsp;<span style="color:#2980b9"><strong>재배포</strong></span></span></p><p><span style="font-size:18px"><span style="color:#c0392b"><strong>$ eb deploy</strong></span></span></p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">5. RDS 확인</span></strong></span></p><p><span style="color:#c0392b"><span style="font-size:18px"><strong>$ aws rds describe-db-instances &nbsp;--region ap-northeast-2</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">{ &quot;DBInstances&quot;: [ { &quot;DBInstanceIdentifier&quot;: &quot;awseb-e-fw8zsguxww-stack-awsebrdsdatabase-p27b8jditfzi&quot;, &quot;DBInstanceClass&quot;: &quot;db.t2.micro&quot;, &quot;Engine&quot;: &quot;mysql&quot;, &quot;DBInstanceStatus&quot;: &quot;available&quot;, &quot;MasterUsername&quot;: &quot;tododb&quot;, &quot;DBName&quot;: &quot;ebdb&quot;, &quot;Endpoint&quot;: { &quot;Address&quot;: &quot;awseb-e-fw8zsguxww-stack-awsebrdsdatabase-p27b8jditfzi.c780mcakmy87.ap-northeast-2.rds.amazonaws.com&quot;, &quot;Port&quot;: 3306, &quot;HostedZoneId&quot;: &quot;ZLA2NUCOLGUUR&quot; }, &quot;AllocatedStorage&quot;: 5, &quot;InstanceCreateTime&quot;: &quot;2024-02-13T01:47:26.571000+00:00&quot;, &quot;PreferredBackupWindow&quot;: &quot;15:38-16:08&quot;, &quot;BackupRetentionPeriod&quot;: 1, &quot;DBSecurityGroups&quot;: [], &quot;VpcSecurityGroups&quot;: [ { &quot;VpcSecurityGroupId&quot;: &quot;sg-08145e4fc1f2fdcb6&quot;, &quot;Status&quot;: &quot;active&quot; } ], &quot;DBParameterGroups&quot;: [ { &quot;DBParameterGroupName&quot;: &quot;default.mysql8.0&quot;, &quot;ParameterApplyStatus&quot;: &quot;in-sync&quot; } ], &quot;AvailabilityZone&quot;: &quot;ap-northeast-2c&quot;, &quot;DBSubnetGroup&quot;: { &quot;DBSubnetGroupName&quot;: &quot;default&quot;, &quot;DBSubnetGroupDescription&quot;: &quot;default&quot;, &quot;VpcId&quot;: &quot;vpc-01e5c5e74ca650e53&quot;, &quot;SubnetGroupStatus&quot;: &quot;Complete&quot;, &quot;Subnets&quot;: [ { &quot;SubnetIdentifier&quot;: &quot;subnet-0172dfd13a50c2eef&quot;, &quot;SubnetAvailabilityZone&quot;: { &quot;Name&quot;: &quot;ap-northeast-2c&quot; }, &quot;SubnetOutpost&quot;: {}, &quot;SubnetStatus&quot;: &quot;Active&quot; }, </pre><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">6. 오토 스케일링 그룹 확인</span></strong></span></p><p><span style="color:#c0392b"><span style="font-size:18px"><strong>$ aws autoscaling describe-auto-scaling-groups --region ap-northeast-2</strong></span></span></p><pre class="brush:as3;">{ &quot;AutoScalingGroups&quot;: [ { &quot;AutoScalingGroupName&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;AutoScalingGroupARN&quot;: &quot;arn:aws:autoscaling:ap-northeast-2:975049930287:autoScalingGroup:d9eebc7e-71c4-48ff-a94d-87f3d4548ba2:autoScalingGroupName/awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;LaunchConfigurationName&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingLaunchConfiguration-idrd96br0tsC&quot;, &quot;MinSize&quot;: 1, &quot;MaxSize&quot;: 4, &quot;DesiredCapacity&quot;: 1, &quot;DefaultCooldown&quot;: 360, &quot;AvailabilityZones&quot;: [ &quot;ap-northeast-2a&quot; ], &quot;LoadBalancerNames&quot;: [], &quot;TargetGroupARNs&quot;: [ &quot;arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:targetgroup/awseb-AWSEB-KJQFRLQJ3D1Y/0f7f94aba89a2c98&quot; ], &quot;HealthCheckType&quot;: &quot;EC2&quot;, &quot;HealthCheckGracePeriod&quot;: 0, &quot;Instances&quot;: [ { &quot;InstanceId&quot;: &quot;i-02ac4113c0cd0d4c8&quot;, &quot;InstanceType&quot;: &quot;t2.micro&quot;, &quot;AvailabilityZone&quot;: &quot;ap-northeast-2a&quot;, &quot;LifecycleState&quot;: &quot;InService&quot;, &quot;HealthStatus&quot;: &quot;Healthy&quot;, &quot;LaunchConfigurationName&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingLaunchConfiguration-idrd96br0tsC&quot;, &quot;ProtectedFromScaleIn&quot;: false } ], &quot;CreatedTime&quot;: &quot;2024-02-13T01:50:22.964000+00:00&quot;, &quot;SuspendedProcesses&quot;: [], &quot;VPCZoneIdentifier&quot;: &quot;&quot;, &quot;EnabledMetrics&quot;: [], &quot;Tags&quot;: [ { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;Name&quot;, &quot;Value&quot;: &quot;MACARONICS-TODO-API-SERVICE&quot;, &quot;PropagateAtLaunch&quot;: true }, { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;aws:cloudformation:logical-id&quot;, &quot;Value&quot;: &quot;AWSEBAutoScalingGroup&quot;, &quot;PropagateAtLaunch&quot;: true }, { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;aws:cloudformation:stack-id&quot;, &quot;Value&quot;: &quot;arn:aws:cloudformation:ap-northeast-2:975049930287:stack/awseb-e-fw8zsguxww-stack/46413f40-ca11-11ee-a86e-02c6da73bbd4&quot;, &quot;PropagateAtLaunch&quot;: true }, { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;aws:cloudformation:stack-name&quot;, &quot;Value&quot;: &quot;awseb-e-fw8zsguxww-stack&quot;, &quot;PropagateAtLaunch&quot;: true }, { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;elasticbeanstalk:environment-id&quot;, &quot;Value&quot;: &quot;e-fw8zsguxww&quot;, &quot;PropagateAtLaunch&quot;: true }, { &quot;ResourceId&quot;: &quot;awseb-e-fw8zsguxww-stack-AWSEBAutoScalingGroup-sZx4USt0K34f&quot;, &quot;ResourceType&quot;: &quot;auto-scaling-group&quot;, &quot;Key&quot;: &quot;elasticbeanstalk:environment-name&quot;, &quot;Value&quot;: &quot;MACARONICS-TODO-API-SERVICE&quot;, &quot;PropagateAtLaunch&quot;: true } ], &quot;TerminationPolicies&quot;: [ &quot;Default&quot; ], &quot;NewInstancesProtectedFromScaleIn&quot;: false, &quot;ServiceLinkedRoleARN&quot;: &quot;arn:aws:iam::975049930287:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling&quot;, &quot;CapacityRebalance&quot;: false, &quot;TrafficSources&quot;: [ { &quot;Identifier&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:targetgroup/awseb-AWSEB-KJQFRLQJ3D1Y/0f7f94aba89a2c98&quot;, &quot;Type&quot;: &quot;elbv2&quot; } ] } ] } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">7. 로드 밸런서 확인</span></strong></span></p><p><span style="color:#c0392b"><span style="font-size:18px"><strong>$ aws autoscaling describe-auto-scaling-groups --region ap-northeast-2</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">{ &quot;LoadBalancers&quot;: [ { &quot;LoadBalancerArn&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:loadbalancer/app/awseb--AWSEB-Regi9wc1D6fr/6ad76b5ff715f5ea&quot;, &quot;DNSName&quot;: &quot;awseb--AWSEB-Regi9wc1D6fr-1594608003.ap-northeast-2.elb.amazonaws.com&quot;, &quot;CanonicalHostedZoneId&quot;: &quot;ZWKZPGTI48KDX&quot;, &quot;CreatedTime&quot;: &quot;2024-02-13T01:43:35.630000+00:00&quot;, &quot;LoadBalancerName&quot;: &quot;awseb--AWSEB-Regi9wc1D6fr&quot;, &quot;Scheme&quot;: &quot;internet-facing&quot;, &quot;VpcId&quot;: &quot;vpc-01e5c5e74ca650e53&quot;, &quot;State&quot;: { &quot;Code&quot;: &quot;active&quot; }, &quot;Type&quot;: &quot;application&quot;, &quot;AvailabilityZones&quot;: [ { &quot;ZoneName&quot;: &quot;ap-northeast-2c&quot;, &quot;SubnetId&quot;: &quot;subnet-0172dfd13a50c2eef&quot;, &quot;LoadBalancerAddresses&quot;: [] }, { &quot;ZoneName&quot;: &quot;ap-northeast-2b&quot;, &quot;SubnetId&quot;: &quot;subnet-0352d5b3e03eb46ba&quot;, &quot;LoadBalancerAddresses&quot;: [] }, { &quot;ZoneName&quot;: &quot;ap-northeast-2d&quot;, &quot;SubnetId&quot;: &quot;subnet-0376bc55c27aea134&quot;, &quot;LoadBalancerAddresses&quot;: [] }, { &quot;ZoneName&quot;: &quot;ap-northeast-2a&quot;, &quot;SubnetId&quot;: &quot;subnet-0b1d168e6106cafde&quot;, &quot;LoadBalancerAddresses&quot;: [] } ], &quot;SecurityGroups&quot;: [ &quot;sg-071d8686d5b0f60f0&quot; ], &quot;IpAddressType&quot;: &quot;ipv4&quot; } ] } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">8. 로드 밸런서와 ASG 를 연결하는 타깃 그룹 확인</span></strong></span></p><p><span style="color:#c0392b"><span style="font-size:18px"><strong>$ aws ec2 describe-instances --region ap-northeast-2</strong></span></span></p><pre class="brush:as3;">{ &quot;TargetGroups&quot;: [ { &quot;TargetGroupArn&quot;: &quot;arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:targetgroup/awseb-AWSEB-KJQFRLQJ3D1Y/0f7f94aba89a2c98&quot;, &quot;TargetGroupName&quot;: &quot;awseb-AWSEB-KJQFRLQJ3D1Y&quot;, &quot;Protocol&quot;: &quot;HTTP&quot;, &quot;Port&quot;: 80, &quot;VpcId&quot;: &quot;vpc-01e5c5e74ca650e53&quot;, &quot;HealthCheckProtocol&quot;: &quot;HTTP&quot;, &quot;HealthCheckPort&quot;: &quot;traffic-port&quot;, &quot;HealthCheckEnabled&quot;: true, &quot;HealthCheckIntervalSeconds&quot;: 15, &quot;HealthCheckTimeoutSeconds&quot;: 5, &quot;HealthyThresholdCount&quot;: 3, &quot;UnhealthyThresholdCount&quot;: 5, &quot;HealthCheckPath&quot;: &quot;/&quot;, &quot;Matcher&quot;: { &quot;HttpCode&quot;: &quot;200&quot; }, &quot;LoadBalancerArns&quot;: [ &quot;arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:loadbalancer/app/awseb--AWSEB-Regi9wc1D6fr/6ad76b5ff715f5ea&quot; ], &quot;TargetType&quot;: &quot;instance&quot;, &quot;ProtocolVersion&quot;: &quot;HTTP1&quot;, &quot;IpAddressType&quot;: &quot;ipv4&quot; } ] } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">9.&nbsp; CloudWatch&nbsp; &nbsp; &nbsp;ec2 인스턴스확인</span></strong></span></p><p><span style="color:#c0392b"><span style="font-size:18px"><strong>$ aws elbv2 describe-target-groups --region ap-northeast-2</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">{ &quot;Reservations&quot;: [ { &quot;Groups&quot;: [], &quot;Instances&quot;: [ { &quot;AmiLaunchIndex&quot;: 0, &quot;ImageId&quot;: &quot;ami-027f129eeb456ff15&quot;, &quot;InstanceId&quot;: &quot;i-02ac4113c0cd0d4c8&quot;, &quot;InstanceType&quot;: &quot;t2.micro&quot;, &quot;LaunchTime&quot;: &quot;2024-02-13T01:50:26+00:00&quot;, &quot;Monitoring&quot;: { &quot;State&quot;: &quot;disabled&quot; }, &quot;Placement&quot;: { &quot;AvailabilityZone&quot;: &quot;ap-northeast-2a&quot;, &quot;GroupName&quot;: &quot;&quot;, &quot;Tenancy&quot;: &quot;default&quot; }, &quot;PrivateDnsName&quot;: &quot;ip-172-31-7-241.ap-northeast-2.compute.internal&quot;, &quot;PrivateIpAddress&quot;: &quot;172.31.7.241&quot;, &quot;ProductCodes&quot;: [], &quot;PublicDnsName&quot;: &quot;ec2-52-79-249-222.ap-northeast-2.compute.amazonaws.com&quot;, &quot;PublicIpAddress&quot;: &quot;52.79.249.222&quot;, &quot;State&quot;: { &quot;Code&quot;: 16, &quot;Name&quot;: &quot;running&quot; }, &quot;StateTransitionReason&quot;: &quot;&quot;, &quot;SubnetId&quot;: &quot;subnet-0b1d168e6106cafde&quot;, &quot;VpcId&quot;: &quot;vpc-01e5c5e74ca650e53&quot;, &quot;Architecture&quot;: &quot;x86_64&quot;, &quot;BlockDeviceMappings&quot;: [ { </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>작동.. 잘된다.</p><p>&nbsp;</p><p>&nbsp;</p><p>..</p><p>끝!</p><p>하루 종일 헤맸다...</p><p>해결 과정을 정리해보자면..</p><p>&nbsp;</p><p>1.</p><p>처음엔 책에서 하라는데로 Java 8 running on linux로 배포를 했는데..</p><p>플랫폼 deprecated가 떠서 종료하고 다시 init 했다.</p><p>디폴트로 되어있던 Corretto 11 running on Amazon linux 2를 골랐다.</p><p>&nbsp;</p><p>2.</p><p>그 후엔 계속에서 502 Bad Gateway가 떴다. 로그를 확인했더니 아래와 같았다.</p><pre>connect() failed (111: Connection refused) while connecting to upstream</pre><p>6시간은 헤맸다. 계속 수정-빌드-배포를 반복했다. 열번 넘게 재배포 했는데 해결 못했다.....</p><p>연결 timeout 시간도 수정해보고.. application-prod.yaml에서 포트도 바꿔보고.. setenv PORT=5000 이런것도 써보고..</p><p>근데..? 아무튼 안됨.. ㅎㅎ..</p><p>...</p><p>..</p><p>밥먹고 천천히 다시 해보기로 결정 ㅠㅠ</p><p>&nbsp;</p><p>3.</p><p>application-prod.yaml, application-dev.yaml으로 나눠서 작성했던걸 다 삭제하고,</p><p>setenv했던 SPRING_PROFILES_ACTIVE=prod변수도 삭제했다.&nbsp;</p><p>&nbsp;</p><p>4.</p><p>application.properties를 다시 만들고, 단순하게 &#39;server.port=5000&#39;만 넣어서 재배포했더니</p><p>웬걸? 너무 잘되는거다.. 일단 포트가 문제가 아니었다는걸 알았다.</p><p>&nbsp;</p><p>5.</p><p>이 다음은 rds db의 connection을 확인하려했다.</p><p>책에서는 ${rds.username} 이런 식으로 변수를 썼는데..</p><p>application.properties로 바꾸고 나서는 이 부분에서 자꾸 에러가 났기 때문이다.</p><pre>Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution</pre><p>여차저차 알아보다가 [rds-인바운드규칙]을 설정하고 났더니 db 접속도 잘 되고 gradlew build도 잘 되었다.</p><p>aws에 재배포하고 접속했더니 성공이었다..! ㅠㅠ&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>출처 :<a target="_blank" href="http:// https://senna.tistory.com/7">&nbsp;https://senna.tistory.com/7</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#ecf0f1"><strong><span style="font-size:28px"><span style="background-color:#c0392b">※ AWS 일래스틱 빈스톡을 이용한 프론트엔드 배포</span></span></strong></span><br />&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:16px">1. eb 생성</span></strong></span></p><pre class="brush:as3;">$ eb init TodoApplication-frontend Select a default region 1) us-east-1 : US East (N. Virginia) 2) us-west-1 : US West (N. California) 3) us-west-2 : US West (Oregon) 4) eu-west-1 : EU (Ireland) 5) eu-central-1 : EU (Frankfurt) 6) ap-south-1 : Asia Pacific (Mumbai) 7) ap-southeast-1 : Asia Pacific (Singapore) 8) ap-southeast-2 : Asia Pacific (Sydney) 9) ap-northeast-1 : Asia Pacific (Tokyo) 10) ap-northeast-2 : Asia Pacific (Seoul) 11) sa-east-1 : South America (Sao Paulo) 12) cn-north-1 : China (Beijing) 13) cn-northwest-1 : China (Ningxia) 14) us-east-2 : US East (Ohio) 15) ca-central-1 : Canada (Central) 16) eu-west-2 : EU (London) 17) eu-west-3 : EU (Paris) 18) eu-north-1 : EU (Stockholm) 19) eu-south-1 : EU (Milano) 20) ap-east-1 : Asia Pacific (Hong Kong) 21) me-south-1 : Middle East (Bahrain) 22) il-central-1 : Middle East (Israel) 23) af-south-1 : Africa (Cape Town) 24) ap-southeast-3 : Asia Pacific (Jakarta) 25) ap-northeast-3 : Asia Pacific (Osaka) (default is 3): 10 Application TodoApplication-frontend has been created. It appears you are using Node.js. Is this correct? (Y/n): Y Select a platform branch. 1) Node.js 20 running on 64bit Amazon Linux 2023 2) Node.js 18 running on 64bit Amazon Linux 2023 3) Node.js 18 running on 64bit Amazon Linux 2 4) Node.js 16 running on 64bit Amazon Linux 2 (Deprecated) 5) Node.js 14 running on 64bit Amazon Linux 2 (Deprecated) (default is 1): 1 Cannot setup CodeCommit because there is no Source Control setup, continuing with initialization Do you want to set up SSH for your instances? (Y/n): n</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:16px"><strong>2. api-config.js&nbsp; &nbsp; 설정</strong></span></span></p><pre class="brush:as3;">let backendHost; const hostname =window &amp;&amp; window.location &amp;&amp; window.location.hostname; if(hostname===&quot;localhost&quot;){ backendHost=&quot;http://localhost:8080&quot;; // backendHost=&quot;http://mcaronics-todo-api-service.ap-northeast-2.elasticbeanstalk.com&quot;; }else{ backendHost=&quot;http://mcaronics-todo-api-service.ap-northeast-2.elasticbeanstalk.com&quot;; } export const API_BASE_URL = backendHost;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:16px"><strong>3. 리액트 프로젝트에서 다음과 같이 디렉토리 생성</strong></span></span></p><p><span style="font-size:16px"><strong>윈도우에서는 Git bash&nbsp; 창을 띄어 실행</strong></span></p><p>&nbsp;</p><p><strong>$ mkdir -p .platform/hooks/prebuild</strong><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:16px">4.&nbsp;01_configure_swap_space.sh 파일 생성</span></strong></span></p><p>&nbsp;</p><p><strong><span style="font-size:16px">vi&nbsp; &nbsp;</span>.platform/hooks/prebuild/<span style="font-size:16px">01_configure_swap_space.sh</span></strong></p><p>&nbsp;</p><pre class="brush:as3;">#!/bin/bash SWAPFILE=/var/swapfile if [ -f $SWAPFILE ]; then echo &quot;$SWAPFILE found, skip&quot; exit; fi /bin/dd if=/dev/zero of=$SWAPFILE bs=2M count=2048 /bin/chmod 600 $SWAPFILE /sbin/mkswap $SWAPFILE /sbin/swapon $SWAPFILE </pre><p>&nbsp;</p><p>&nbsp;</p><p>이와 같은 작업을 해 주는 이유는 우리가 t2/t3 계열의 메모리가 작은 인스턴스를 사용하기 때문이다.&nbsp;&nbsp;</p><p>메모리가 부족해 Out of&nbsp; Memory 에러가 나는 것을 방지하기 위해 Swap 파일을 만들어 준다.</p><p>Swap 이란 메모리가 부족할 때,&nbsp; 사용하지 않는 메모리의 일부를 디스크, 즉 파일로 옮겨 메모리를 확보하는 기술이다.</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:16px">5. 파일권한 설정</span></strong></span></p><p>&nbsp;Linux -&nbsp;<em>Chmod</em>&nbsp;+<em>x</em>&nbsp;[파일명] &middot;&nbsp;<em>chmod</em>&nbsp;+<em>x</em>&nbsp;backup 명령어로 backup 파일에 실행할수 있는 권한을 준다.&nbsp;</p><p>&nbsp;</p><p>$ chmod +x &nbsp;.platform/hooks/prebuild/01_configure_swap_space.sh</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">6.node 버전 확인후&nbsp;&nbsp;package.json 에 engine 버전 추가</span></strong></span></p><p>&nbsp;</p><p>nvm 으로 프로젝트에 맞는 node 버전 변경하기</p><pre>nvm 설치 및 node 설치 - 사용법(mac&amp;windows)</pre><p>출처: <a target="_blank" href="https://jang8584.tistory.com/295">https://jang8584.tistory.com/295</a> [개발자의 길:티스토리]</p><p><a target="_blank" href="https://jang8584.tistory.com/295">https://jang8584.tistory.com/295</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>$ node --version<br />v21.6.1</p><p>&nbsp;</p><p>package.json</p><pre class="brush:as3;">{ &quot;name&quot;: &quot;front-end&quot;, &quot;version&quot;: &quot;0.1.0&quot;, &quot;private&quot;: true, &quot;engines&quot;: {&quot;node&quot;: &quot;21.6.1&quot;},</pre><p>&nbsp;</p><p><strong><span style="color:#8e44ad">노드 버전에 따라 설치가 안되는 경우가 있으니</span></strong></p><p><span style="color:#2980b9"><strong>reifyNode:node_modules/core-js-pure 에 걸릴 경우</strong></span>&nbsp;<strong><span style="color:#8e44ad">&nbsp;node 버전 문제 따라서,</span></strong></p><p><strong><span style="color:#8e44ad">배포전 반드시&nbsp; &nbsp;node_modules 디렉토리를 삭제후&nbsp; 다시 npm install 로 정상적으로 설치 되는지 확인할것</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">7.npm 을 이용해 소스코드를 빌드한다.</span></strong></span></p><p><strong>$ npm run build</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">8. 커밋</span></strong></span></p><p>nodejs. 를 사용하는 일래스틱 빈스톡의 경우 커밋되지 않은&nbsp; 변경 사항들은 배포되지 않기 때문에&nbsp;</p><p>커밋을 해야 한다.</p><p>&nbsp;</p><p>$ git add . &nbsp;&amp;&amp; git commit -m &quot;commit for front deployment&quot;</p><p>&nbsp;</p><p>만약&nbsp;</p><p>다음과 같은 오류가 있을 경우</p><p>[ERROR] An error occurred during execution of command [app-deploy] - [RunAppDeployPreBuildHooks]. Stop running the command. Error: Command .platform/hooks/prebuild/01_configure_swap_space.sh failed with error fork/exec .platform/hooks/prebuild/01_configure_swap_space.sh: no such file or directory</p><p>&nbsp;</p><p>다음을 참조&nbsp;</p><p>&nbsp;git 에서 CRLF 개행 문자 차이로 인한 문제 해결하기&nbsp;</p><p><a target="_blank" href="https://www.lesstif.com/gitbook/git-crlf-20776404.html">https://www.lesstif.com/gitbook/git-crlf-20776404.html</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">9. eb create</span></strong></span></p><p>$ eb create --elb-type application --instance-type t3.micro</p><p>명령후 실행후 다음과 같은 설정후&nbsp; &nbsp;<strong><span style="color:#8e44ad">10분정도</span></strong> 기다리면</p><p>&nbsp;</p><pre class="brush:as3;">$ eb create --elb-type application --instance-type t3.micro Enter Environment Name (default is TodoApplication-frontend-dev): MACARONICS-TODO-UI-SERVICE Enter DNS CNAME prefix (default is MACARONICS-TODO-UI-SERVICE): macaronics-todo-ui-service Would you like to enable Spot Fleet requests for this environment? (y/N): N Creating application version archive &quot;app-240213_154243338938&quot;. </pre><p>&nbsp;</p><p>====&gt;</p><pre class="brush:as3;">$ eb create --elb-type application --instance-type t3.micro Enter Environment Name (default is TodoApplication-frontend-dev): MCARONICS-TODO-UI-SERVICE Enter DNS CNAME prefix (default is MCARONICS-TODO-UI-SERVICE): macaronics-todo-ui-service Would you like to enable Spot Fleet requests for this environment? (y/N): N Creating application version archive &quot;app-240213_154619174060&quot;. Uploading: [##################################################] 100% Done... Environment details for: MCARONICS-TODO-UI-SERVICE Application name: TodoApplication-frontend Region: ap-northeast-2 Deployed Version: app-240213_154619174060 Environment ID: e-fpw56kwp7j Platform: arn:aws:elasticbeanstalk:ap-northeast-2::platform/Node.js 20 running on 64bit Amazon Linux 2023/6.1.0 Tier: WebServer-Standard-1.0 CNAME: macaronics-todo-ui-service.ap-northeast-2.elasticbeanstalk.com Updated: 2024-02-13 06:56:12.592000+00:00 Printing Status: 2024-02-13 06:56:10 INFO createEnvironment is starting. 2024-02-13 06:56:12 INFO Using elasticbeanstalk-ap-northeast-2-975049930287 as Amazon S3 storage bucket for environment data. 2024-02-13 06:56:38 INFO Created security group named: sg-01c2c8144a2795cb2 2024-02-13 06:56:38 INFO Created security group named: awseb-e-fpw56kwp7j-stack-AWSEBSecurityGroup-RQI3TUBU4KNB 2024-02-13 06:56:54 INFO Created Auto Scaling launch configuration named: awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingLaunchConfiguration-Lkbufl6H2qnu 2024-02-13 06:56:54 INFO Created target group named: arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:targetgroup/awseb-AWSEB-WPQGZQSGGG53/86fa30d63221329f 2024-02-13 06:57:09 INFO Created Auto Scaling group named: awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingGroup-XHGIL9funIiH 2024-02-13 06:57:09 INFO Waiting for EC2 instances to launch. This may take a few minutes. 2024-02-13 06:57:25 INFO Created Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-2:975049930287:scalingPolicy:79ae3320-1141-4829-810e-54700db04bf9:autoScalingGroupName/awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingGroup-XHGIL9funIiH:policyName/awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingScaleUpPolicy-GrnpVTfu2HF0 2024-02-13 06:57:25 INFO Created Auto Scaling group policy named: arn:aws:autoscaling:ap-northeast-2:975049930287:scalingPolicy:410c88e2-f370-47de-8e99-59b174af1c9b:autoScalingGroupName/awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingGroup-XHGIL9funIiH:policyName/awseb-e-fpw56kwp7j-stack-AWSEBAutoScalingScaleDownPolicy-wwW9YwnOQK52 2024-02-13 06:57:25 INFO Created CloudWatch alarm named: awseb-e-fpw56kwp7j-stack-AWSEBCloudwatchAlarmHigh-cJDKsK1FtUCp 2024-02-13 06:57:25 INFO Created CloudWatch alarm named: awseb-e-fpw56kwp7j-stack-AWSEBCloudwatchAlarmLow-6ULjKFHlC0P4 2024-02-13 06:59:15 INFO Created load balancer named: arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:loadbalancer/app/awseb--AWSEB-jWgZhzivBSAA/8fa6f04d047f8f8a 2024-02-13 06:59:15 INFO Created Load Balancer listener named: arn:aws:elasticloadbalancing:ap-northeast-2:975049930287:listener/app/awseb--AWSEB-jWgZhzivBSAA/8fa6f04d047f8f8a/abb50fdb25e2b8ba 2024-02-13 07:00:07 WARN Instance deployment: The deployment used the default Node.js version for your platform version instead of the Node.js version included in your &#39;package.json&#39;. 2024-02-13 07:00:12 INFO Instance deployment completed successfully. 2024-02-13 07:01:17 INFO Successfully launched environment: MCARONICS-TODO-UI-SERVICE</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>팁:</p><p><strong>t2.micro 또는 t3.micro 인스턴스를 사용하는 경우 일래스틱 빈스톡 환경 설정이 중간에 멈추는 경우가 있다.</strong></p><p><strong>만약 일래스틱 빈스톡 상태가 10분 넘게 확인 상태로 변경되지 않는다면 EC2 화면으로 가 인스턴스 상태 &gt; 인스턴스 종료 메뉴를 통해 실행 중인 EC2&nbsp;</strong></p><p><strong>인스턴스를 종료시키도록 하자.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">10. eb 재 배포 방법</span></strong></span></p><p><span style="color:#8e44ad"><span style="font-size:16px"><strong>$ eb deploy</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>배포시 에러 로그 확인 방법</strong></p><p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikMUi0rBh_2FkEM1D5y2Tw-09o4Zm33wmw9NTO6B0G69m-fYTRawEUcKacNRIeoxemCLmkELe7LuSXAXqJHJkVTuPxwcQSi_JH0a0zoihcqR-BWxYCF8hFo3Y__WTU2G4bLnKFa0_FkTQrmUxUzzZcWPw-6EzIk3k7m1uJoN4XcgitoF0idKchyphenhyphenfh3rDKj/s1912/2024-02-13%2016%2036%2031.png">https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikMUi0rBh_2FkEM1D5y2Tw-09o4Zm33wmw9NTO6B0G69m-fYTRawEUcKacNRIeoxemCLmkELe7LuSXAXqJHJkVTuPxwcQSi_JH0a0zoihcqR-BWxYCF8hFo3Y__WTU2G4bLnKFa0_FkTQrmUxUzzZcWPw-6EzIk3k7m1uJoN4XcgitoF0idKchyphenhyphenfh3rDKj/s1912/2024-02-13%2016%2036%2031.png</a></p><p><img alt="" width="900" height="468" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEikMUi0rBh_2FkEM1D5y2Tw-09o4Zm33wmw9NTO6B0G69m-fYTRawEUcKacNRIeoxemCLmkELe7LuSXAXqJHJkVTuPxwcQSi_JH0a0zoihcqR-BWxYCF8hFo3Y__WTU2G4bLnKFa0_FkTQrmUxUzzZcWPw-6EzIk3k7m1uJoN4XcgitoF0idKchyphenhyphenfh3rDKj/s1912/2024-02-13%2016%2036%2031.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>EB 인스턴스 삭제 방법</strong></p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMGs4g1WF5WKSdO48HmNlFrTjCv7kO71NtaYxPy_M2aUAcRvqr3dD8WV993Z3sa3kK2Nqc2JmWpDG20dSSpqhHh1P_pchcpicAgUlMRpt0bMAA_qNCq2bFj03EkDptThWKeRKn30bmmjZXmEg9cFaoJQNzFpXF4VhWwTj87CSVT1oidrT2v4CVnCFyhfrB/s1867/2024-02-13%2016%2038%2040.png">https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMGs4g1WF5WKSdO48HmNlFrTjCv7kO71NtaYxPy_M2aUAcRvqr3dD8WV993Z3sa3kK2Nqc2JmWpDG20dSSpqhHh1P_pchcpicAgUlMRpt0bMAA_qNCq2bFj03EkDptThWKeRKn30bmmjZXmEg9cFaoJQNzFpXF4VhWwTj87CSVT1oidrT2v4CVnCFyhfrB/s1867/2024-02-13%2016%2038%2040.png</a></p><p><img alt="" width="900" height="466" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMGs4g1WF5WKSdO48HmNlFrTjCv7kO71NtaYxPy_M2aUAcRvqr3dD8WV993Z3sa3kK2Nqc2JmWpDG20dSSpqhHh1P_pchcpicAgUlMRpt0bMAA_qNCq2bFj03EkDptThWKeRKn30bmmjZXmEg9cFaoJQNzFpXF4VhWwTj87CSVT1oidrT2v4CVnCFyhfrB/s1867/2024-02-13%2016%2038%2040.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">※프론트 리액트의 경우&nbsp; AWS 대신에&nbsp; &nbsp;React 앱을 무료로 배포하는 10가지 방법</span></strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://devsnote.com/writings/2139">https://devsnote.com/writings/2139</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">※ nginx 스프링부트&nbsp; 리액트&nbsp; 연동&nbsp;</span></strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1) 리액트를 빌드 한다.&nbsp;</strong></p><p><strong>$npm run build 한다.</strong></p><p>여기서는&nbsp;home/react/jpaApp/build 경로로 빌드 파일이 생성 되었다.</p><p>&nbsp;</p><p><strong>2)&nbsp; nginx&nbsp; &nbsp;기본 루트 경로는&nbsp; /usr/share/nginx/html; 인데 이 경로를&nbsp; 리액트 build&nbsp; 경로로 변경</strong></p><p>다음과 같이 root 경로를 리액트 빌드 파일 경로로 변경 처리 한다&nbsp; .&nbsp;root /home/react/jpaApp/build;</p><pre>server { server_name ma7front.p-e.kr www.ma7front.p-e.kr; listen 80; listen [::]:80; location / { root /home/react/jpaApp/build; # nginx 기본 루트 경로 HTML파일이 위치할 경로 index index.html index.htm; try_files $uri /index.html; } }</pre><p>&nbsp;</p><p>ngix 를 재구동 한다.</p><p>$systemctl restart nginx&nbsp;</p><p>&nbsp;</p><p><strong>3)리액트 디렉토리&nbsp;</strong><strong>nginx 유저 권한으로 변경 시키기</strong></p><p><strong>만약에 500 에러가 나거나 권한 설정 에러 403에러가 나오면</strong></p><p>nginx.conf&nbsp; 파일을 열고&nbsp;</p><p>$ vim /etc/nginx/nginx.conf</p><p>맨 상단의 user 권한을 확인한다</p><p>nginx.conf</p><pre>user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; ~</pre><p>현재 유저 권한이 www-data 로 나와 있는데</p><p><strong>react 디렉토리를 다음과 같이 nginx 유저 권한으로 변경 시킨다.</strong></p><p>&nbsp;</p><p><strong>$chown -R www-data:www-data&nbsp; </strong>/home/<strong>react/&nbsp;</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">또는 react&nbsp; 서버 배포&nbsp;&nbsp;&nbsp;유저로 명으로 변경후 nginx&nbsp; 재실행한다.</span></strong></p><pre class="brush:as3;">user nginx; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; ~</pre><p><strong>$ systemctl restart nginx</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre>upstream tomcat { ip_hash; server 127.0.0.1:5000; } server { server_name ma7server.p-e.kr www.ma7server.p-e.kr; ## 다음 woff2 파일들이 차단될 경우 설정 location ~* \.(eot|ttf|woff|woff2)$ { add_header Access-Control-Allow-Origin *; } ######################## 일반 웹 설정 ################### location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-NginX-Proxy true; proxy_pass http://tomcat; proxy_redirect off; charset utf-8; } ######################## /todo 시작되는 경로와 /api 로 시작되는 경로는 restapi jwt 설정 ################### location /todo { proxy_hide_header Access-Control-Allow-Origin; add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; #모든 도메인 허용할경우 add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, PATCH, PUT, DELETE, OPTIONS&#39; always; add_header &#39;Access-Control-Allow-Credentials&#39; &#39;true&#39;; add_header &#39;Access-Control-Allow-Headers&#39; &#39;Content-Type, Authorization, Cookie&#39; always; add_header &#39;Access-Control-Max-Age&#39; 1728000; proxy_pass http://tomcat ; proxy_redirect off; charset utf-8; } location /api { proxy_hide_header Access-Control-Allow-Origin; add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; #모든 도메인 허용할경우 add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, PATCH, PUT, DELETE, OPTIONS&#39; always; add_header &#39;Access-Control-Allow-Credentials&#39; &#39;true&#39;; add_header &#39;Access-Control-Allow-Headers&#39; &#39;Content-Type, Authorization, Cookie&#39; always; add_header &#39;Access-Control-Max-Age&#39; 1728000; proxy_pass http://tomcat ; proxy_redirect off; charset utf-8; } } ######################## front 설정 ################### server { server_name ma7front.p-e.kr www.ma7front.p-e.kr; listen 80; listen [::]:80; location / { root /home/react/jpaApp/build; # nginx 기본 루트 경로 HTML파일이 위치할 경로 index index.html index.htm; try_files $uri /index.html; } } </pre><p><br />&nbsp;</p><p><span style="color:#2980b9"><strong>SSL&nbsp;certbot&nbsp;&nbsp;</strong></span></p><p>다음 사이트에서 운영체제 서버환경등 플랫폼에 맞게 메뉴얼에 맞게 설치하면 된다.</p><p><a target="_blank" href="https://certbot.eff.org/">https://certbot.eff.org/</a></p><p>&nbsp;</p><p><strong>=&gt; ssl certbot&nbsp; 적용후 코드</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre>upstream tomcat { ip_hash; server 127.0.0.1:5000; } server { server_name ma7server.p-e.kr www.ma7server.p-e.kr; location ~* \.(eot|ttf|woff|woff2)$ { add_header Access-Control-Allow-Origin *; } location / { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-NginX-Proxy true; proxy_pass http://tomcat; proxy_redirect off; charset utf-8; } location /todo { proxy_hide_header Access-Control-Allow-Origin; add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; #모든 도메인 허용할경우 add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, PATCH, PUT, DELETE, OPTIONS&#39; always; add_header &#39;Access-Control-Allow-Credentials&#39; &#39;true&#39;; add_header &#39;Access-Control-Allow-Headers&#39; &#39;Content-Type, Authorization, Cookie&#39; always; add_header &#39;Access-Control-Max-Age&#39; 1728000; proxy_pass http://tomcat ; proxy_redirect off; charset utf-8; } location /api { proxy_hide_header Access-Control-Allow-Origin; add_header &#39;Access-Control-Allow-Origin&#39; &#39;*&#39; always; #모든 도메인 허용할경우 add_header &#39;Access-Control-Allow-Methods&#39; &#39;GET, POST, PATCH, PUT, DELETE, OPTIONS&#39; always; add_header &#39;Access-Control-Allow-Credentials&#39; &#39;true&#39;; add_header &#39;Access-Control-Allow-Headers&#39; &#39;Content-Type, Authorization, Cookie&#39; always; add_header &#39;Access-Control-Max-Age&#39; 1728000; proxy_pass http://tomcat ; proxy_redirect off; charset utf-8; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/ma7server.p-e.kr/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/ma7server.p-e.kr/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = ma7server.p-e.kr) { return 301 https://$host$request_uri; } # managed by Certbot server_name ma7server.p-e.kr www.ma7server.p-e.kr; listen 80; return 404; # managed by Certbot } ######################## front 설정 ################### server { server_name ma7front.p-e.kr www.ma7front.p-e.kr; listen 80; listen [::]:80; location / { root /home/react/jpaApp/build; # nginx 기본 루트 경로 HTML파일이 위치할 경로 index index.html index.htm; try_files $uri /index.html; } listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/ma7front.p-e.kr/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/ma7front.p-e.kr/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = ma7front.p-e.kr) { return 301 https://$host$request_uri; } # managed by Certbot server_name ma7front.p-e.kr www.ma7front.p-e.kr; listen 80; return 404; # managed by Certbot } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">※&nbsp; CI/CD&nbsp; - Github&nbsp;&nbsp;</span></strong></span></span></p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#2980b9">1.</span><span style="color:#8e44ad"> AWS&nbsp; 스프링부트 서버</span><span style="color:#2980b9">&nbsp; GitHub Actions으로 배포 자동화해 보기</span></span></strong></p><p>&nbsp;</p><p>참조 :</p><p><a target="_blank" href="https://goldenrabbit.co.kr/2023/07/05/github-actions%EC%9C%BC%EB%A1%9C-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94%ED%95%B4-%EB%B3%B4%EA%B8%B0a-k-a-ci-cd-2%ED%8E%B8/">https://goldenrabbit.co.kr/2023/07/05/github-actions으로-배포-자동화해-보기a-k-a-ci-cd-2편/</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">name: CI/CD # 1 깃허브 액션 이름 변경 on: push: branches: [ todoProject ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: distribution: &#39;corretto&#39; java-version: &#39;17&#39; - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle run: ./gradlew clean build #2 현재시간가져오기 - name: Get current time uses: josStorer/get-current-time@v2.0.2 id: current-time with: format: YYYY-MM-DDTHH-mm-ss utcOffset: &quot;+09:00&quot; # 3 배포용 패키지 경로 저장 - name: Set artifact run: echo &quot;artifact=$(ls ./build/libs)&quot; &gt;&gt; $GITHUB_ENV # 4 빈스토크 배포 - name: Beanstalk Deploy uses: einaregilsson/beanstalk-deploy@v20 with: aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} application_name: TodoApplication-backend environment_name: MACARONICS-TODO-API-SERVICE version_label: github-action-${{steps.current-time.outputs.formattedTime}} region: ap-northeast-2 deployment_package: ./build/libs/${{env.artifact}}</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#2980b9">2.</span><span style="color:#8e44ad">&nbsp; 리액트</span><span style="color:#2980b9">&nbsp; GitHub Actions 으로 우분트 배포 자동화 하기</span></span></strong></p><p>&nbsp;</p><p>참조 : 다음 참조 사이트를 보고 설정 방법을 익힌다.</p><p>1)&nbsp; <a target="_blank" href="https://velog.io/@osh8242/github-actions%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9E%90%EB%8F%99%EB%B0%B0%ED%8F%AC-CICD">github actions를 활용한 자동배포 CI/CD</a></p><p>2)&nbsp;&nbsp;<a target="_blank" href="https://www.youtube.com/watch?v=1i-0CdCANHQ"><strong>Deploy code to an Ubuntu server with GitHub actions</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">리눅스에서 다음과 같이 설정</span></strong></p><p><br /><strong>1)디렉토리 GIT 소유권 관련 오류 해결</strong><br />git config --global --add safe.directory &nbsp;/home/springboot/jpa-shop-maven-and-gradle</p><p><br /><strong>2)Git 인증 정보 등록해놓고 사용하기</strong><br />git config --global credential.helper &lt;옵션&gt;</p><p>git config --global credential.helper &nbsp;store</p><p><br /><strong>3)+x 옵션 또는 755 권한으로 실행 권한을 추가해주자.</strong></p><p><a target="_blank" href="http://https://velog.io/@jinny-l/gradlew-permission-denied-issue"><strong>https://velog.io/@jinny-l/gradlew-permission-denied-issue</strong></a><br />chmod +x ./gradlew</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>아래 워크&nbsp;workflow&nbsp; 참고로 실행하고</p><p><strong><span style="color:#c0392b">sudo 시 비밀번호 입력에 대한 문제 때문에 오류가 발생할 수 있으니&nbsp;&nbsp;다음과&nbsp; 같이&nbsp; ssh 접속 유저명에 권한을&nbsp; 부여 한다.</span></strong></p><pre class="brush:as3;">react 계정에 sudo 권한 추가 echo &#39;react ALL=NOPASSWD: ALL&#39; &gt;&gt; /etc/sudoers</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>ubuntu-react-deploy.yml</strong></p><pre class="brush:as3;"># 우분트22 workflow name: Deploy to Ubuntu-22 on Push Event # 작업이 실행되는 시기를 제어합니다. UI를 사용하여 수동으로 트리거되면 워크플로가 실행됩니다. # or API. on: push: branches : [&quot;main&quot;] # 워크플로 실행은 순차적으로 또는 병렬로 실행될 수 있는 하나 이상의 작업으로 구성됩니다. jobs: # 배포작업 &quot;deploy&quot; deploy: # 작업이 실행될 실행기 유형 runs-on: ubuntu-latest # 작업환경 : Ubuntu 최신버전 18 , 22 등 버전입력 하면 작동 안될수 있음 # 단계는 작업의 일부로 실행될 일련의 작업을 나타냅니다. steps: # Build 전 pre-code-check (코드 검사) - name: Checkout code uses: actions/checkout@v2 # 배포 셸을 사용하여 단일 명령을 실행합니다. - name: Deploy uses: appleboy/ssh-action@v0.1.10 with: host: ${{ secrets.REACT_SERVER_HOST }} username: ${{ secrets.REACT_SERVER_USER }} password: ${{ secrets.REACT_SERVER_PASSWORD }} port: ${{ secrets.REACT_SERVER_PORT }} script: &#39;cd /home/react/jpa-shop-maven-and-gradle-front &amp;&amp; ls -ltr &amp;&amp; sudo git stash &amp;&amp; sudo git pull &amp;&amp; npm install &amp;&amp; npm run build &amp;&amp; sudo systemctl restart nginx&#39;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#2980b9">3.</span><span style="color:#8e44ad">&nbsp; 스프링부트 그래들</span><span style="color:#2980b9">&nbsp; GitHub Actions 으로 우분트 배포 자동화 하기</span></span></strong></p><p>&nbsp;</p><p>위 2번 참조후&nbsp;</p><p>&nbsp;</p><p><strong>1. build.gradle 샘플 참조</strong></p><pre class="brush:as3;">plugins { id &#39;java&#39; id &#39;org.springframework.boot&#39; version &#39;3.2.2&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.1.4&#39; id &#39;org.jetbrains.kotlin.jvm&#39; } group = &#39;com.shop&#39; version = &#39;0.0.1-SNAPSHOT&#39; java { } repositories { mavenCentral() } //Gradle에서 빌드하여 생성하는 jar 파일명 변경하기 bootJar{ archivesBaseName = &#39;ROOT&#39; archiveFileName = &#39;ROOT.jar&#39; archiveVersion = &quot;0.0.0&quot; } 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-validation&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39; implementation group: &#39;com.github.gavlyukovskiy&#39;, name: &#39;p6spy-spring-boot-starter&#39;, version: &#39;1.9.1&#39; implementation group: &#39;nz.net.ultraq.thymeleaf&#39;, name: &#39;thymeleaf-layout-dialect&#39;, version: &#39;3.3.0&#39; implementation group: &#39;org.modelmapper&#39;, name: &#39;modelmapper&#39;, version: &#39;3.2.0&#39; implementation &#39;org.springframework.boot:spring-boot-starter-oauth2-client&#39; //jwt version: &#39;0.12.4&#39; =&gt; implementation group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-api&#39;, version: &#39;0.11.5&#39; runtimeOnly group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-impl&#39;, version: &#39;0.11.5&#39; implementation group: &#39;io.jsonwebtoken&#39;, name: &#39;jjwt-jackson&#39;, version: &#39;0.11.5&#39; compileOnly &#39;org.projectlombok:lombok&#39; developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39; runtimeOnly &#39;com.h2database:h2&#39; runtimeOnly &#39;org.mariadb.jdbc:mariadb-java-client&#39; runtimeOnly &#39;com.mysql:mysql-connector-j&#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; // ⭐ Spring boot 3.x이상에서 QueryDsl 패키지를 정의하는 방법 implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39; annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot; annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot; annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot; // com.google.guava implementation group: &#39;com.google.guava&#39;, name: &#39;guava&#39;, version: &#39;33.0.0-jre&#39; implementation &quot;org.jetbrains.kotlin:kotlin-stdlib-jdk8&quot; } //-plain.jar 를 생성 금지 jar { enabled = false } tasks.named(&#39;test&#39;) { useJUnitPlatform() } def querydslSrcDir = &#39;src/main/generated&#39; clean { delete file(querydslSrcDir) } tasks.withType(JavaCompile) { options.generatedSourceOutputDirectory = file(querydslSrcDir) options.compilerArgs.add(&quot;-parameters&quot;) } kotlin { jvmToolchain(17) } // 테스트 코드를 제외한 빌드 수행 또는 gradle build -x test //https://velog.io/@may_yun/빌드시-테스트-코드-제외하기 tasks.withType(Test) { enabled = false } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>2.workflows&nbsp; &nbsp; (ma7server.yml)</strong></p><pre class="brush:as3;">name: ma7server/CI-CD # 1 우분트 배로 on: push: branches: [ todoProject ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: distribution: &#39;corretto&#39; java-version: &#39;17&#39; #2 현재시간가져오기 - name: Get current time uses: josStorer/get-current-time@v2.0.2 id: current-time with: format: YYYY-MM-DDTHH-mm-ss utcOffset: &quot;+09:00&quot; # 3.배포 셸을 사용하여 단일 명령을 실행 또는 직접 스크립트 입력 합니다. - name: Deploy uses: appleboy/ssh-action@v0.1.10 with: host: ${{ secrets.SB_SERVER_HOST }} username: ${{ secrets.SB_SERVER_USER }} password: ${{ secrets.SB_SERVER_PASSWORD }} port: ${{ secrets.SB_SERVER_PORT }} script: &#39;cd /home/springboot/jpa-shop-maven-and-gradle &amp;&amp; git pull &amp;&amp; ./gradlew clean build &amp;&amp; sh /home/springboot/jpa-shop-maven-and-gradle/ci-cd-script.sh&#39;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3. 셀 스크립트 작성&nbsp;sh /home/springboot/jpa-shop-maven-and-gradle/ci-cd-script.sh</strong></p><p><strong><span style="color:#c0392b">nohup java -jar&nbsp; 실행 명령어를&nbsp;workflows&nbsp; 에서&nbsp; &nbsp;작성후&nbsp; push 하면&nbsp; github action 에서&nbsp;</span></strong><strong><span style="color:#c0392b">&nbsp;리눅스에서 실행 종료 값이 반환 되지 않아 무한으로 실행 중으로 표시&nbsp;된다.&nbsp;</span></strong></p><p><strong><span style="color:#c0392b">따라서 리눅스에서 스크립트로 다음과 같이 작성해서 처리한다.</span></strong></p><p>&nbsp;</p><pre class="brush:as3;">#!/bin/bash killall -9 java nohup java -jar /home/springboot/jpa-shop-maven-and-gradle/build/libs/ROOT.jar &gt; Output.log 2&gt;&amp;1 &amp; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">※&nbsp; CI/CD&nbsp; - Github&nbsp; Bitbucket pipelines 우분트 배포&nbsp;&nbsp;</span></strong></span></span></p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#2980b9"><strong>1.&nbsp;bitbucket 에서 프로젝트 생성 후&nbsp; 생성 된 프로젝트에서 좌측메뉴의 파이프라인을 선택한다</strong></span></span></p><p>여러가지 선택 템플릿이 존재하는데, 여기서는 커스텀으로 할것라 임의로 아무거나 선택하면 된다.</p><p><img alt="" width="900" height="528" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkmXgi94pUg3MnariqulZ-buW4MWCjrXfAgN0IAB0Zat_Btj1tVbSiu1seLJgJ8AMEXd7kXKDU7CGJtqqyn2afidJz2wWitDcN2HZahEYuTpA-IeSGfGbeY79p5E0zL2XRFgW1X-_chL1VUuF5SVvlNVkrrkg1c2sHdKv7dFDBM82kINzWdIFsMBiJtFXf/s2507/2024-02-17%2017%2016%2014.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:18px"><strong>2. bitbucket-pipelines.yml 임의 이름으로 파일을 만든후 다음과 같이 작성한다.</strong></span></span></p><p>&nbsp;</p><p><img alt="" width="900" height="492" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivfsWxGkNCPIH6U59pLDkqoc210jRVfCq56fv7EGbCxInucond_ySlKMxdQMehOIpfcC2I0hjXH2WmGo0scevQew3VjksipVP_EHSLsgN21hJ7lzDiQ2fMtHr2wEztCL7DFz986oS5NZW7av52XapkWqxYafxJvKWVQURQ1gjyvmpjEb4K441J4DbGKkzb/s2538/2024-02-17%2017%2025%2041.png" /></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"># 이는 JavaScript용 샘플 빌드 구성입니다. # 더 많은 예시를 보려면 https://confluence.atlassian.com/x/14UWN에서 가이드를 확인하세요. # .yml 구성을 들여쓰려면 공백만 사용하세요. # ----- # Docker Hub의 사용자 지정 Docker 이미지를 빌드 환경으로 지정할 수 있습니다. image: atlassian/default-image:latest pipelines: default: - step: name: &quot;SSH Deploy to production web&quot; deployment: staging caches: - composer script: - echo &quot;Deploying to production environment&quot; - pipe: atlassian/ssh-run:0.2.2 variables: SSH_USER: $REACT_SSH_USER SERVER: $REACT_SSH_SERVER COMMAND: &#39;sh /home/react/bitbucket-react-deployment-script.sh&#39; PORT : $REACT_SSH_PORT</pre><p>&nbsp;</p><p><span style="color:#ffffff"><strong><span style="background-color:#c0392b">매우 주의 :&nbsp; yml &nbsp;코드 끝에 탭키나 줄바꿈 처리 등이 있으면 에러가 발생한다.</span></strong></span></p><p>&nbsp;</p><p>파이프라인 옵션에 관한 설명은 다음을 참조</p><p><a target="_blank" href="https://velog.io/@banjjoknim/Bitbucket-Pipelines-살펴보기">https://velog.io/@banjjoknim/Bitbucket-Pipelines-살펴보기</a></p><p>&nbsp;</p><p>&nbsp;</p><p>$REACT_SSH_USER ,&nbsp;&nbsp; $REACT_SSH_SERVER&nbsp; ,&nbsp; $REACT_SSH_PORT 의 환경 변수가 있는데,</p><p>github actions 처럼 bitbucket 도&nbsp;다음과 설정하면 된다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#2980b9"><strong>3. 우분트서버&nbsp; 접속&nbsp;변수 설정</strong></span></span></p><p>&nbsp;</p><p><strong>맨 위의 우측 상단의 톱니 바퀴 모양에서 setting 를&nbsp; 클릭 -&gt; Workspace setting 를 클릭한다.</strong></p><p>&nbsp;</p><p><strong>-&gt;좌측 메뉴 맨 아래의 파이프라인의&nbsp;&nbsp;Workspace variables 버튼을 클릭한다.</strong></p><p>&nbsp;</p><p><img alt="" width="900" height="532" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihEa6KuGBaj59kfW4_TnoGQg2bjLYYWLvXmaxbM2iCgT65kROdbn1v5_ROMX5KGVcXZia5cKXyOI2-JSr0ZzgHmuJaXqqoTW3q0Du_o40xFh8MNRmC3GimlQmIbZdvwT1_vimL7SWzI6XGg29xt5N2IsLInVSEw_KhLx_DsQKGJQiufCl3o8RZqqymklPg/s2526/2024-02-17%2017%2032%2022.png" /></p><p><br />&nbsp;</p><p>임의 변수명을&nbsp;설정하고 값을 넣는다.</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="558" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2H3DoeIN3RUG9z6OXCjvDLWXtGZkXG9-Vd4C8olhyphenhyphengrBvbTdd6szSDSx6YsHCRmrwwNoi147StPxIkMBbya_ryckusn74EyxhNOJs321TrOSiEToPO0SN7hZtr0Fs1Dkv6_acPf8lvhAlVKHT_u7O-1EPrjv53HACEGNozhqEtxqg1oxO8nfBrpHMe6zu/s2366/2024-02-17%2017%2034%2037.png" /></p><p>&nbsp;</p><p>변수값들이&nbsp;&nbsp; bitbucket-pipelines.yml 파일에서 다음과 같이 사용 되었다.</p><p>SSH_USER: $REACT_SSH_USER</p><p>SERVER: $REACT_SSH_SERVER</p><p>PORT : $REACT_SSH_PORT</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#2980b9"><strong>4&nbsp; Bitbutket 이 우분트서버에&nbsp; &nbsp;접속 할수 있게&nbsp;</strong></span></span><span style="font-size:20px"><strong>Fingerprint 를 설정해 준다.</strong></span></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>프로젝트 -&gt; 맨 하단 Repository setting -&gt;&nbsp; SSH Keys&nbsp;</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="533" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjbVVyOmL3ag8xVMVYLUhPYYvk7cAn11yPtD07sVPQOPx8nhH40qFEvmas2gQYXmrz4DYOU8HnyMjM5VYUMLCnUCfjhPF8IDUNzg_yyoOQ4xRmUs3gaSfVD80gJWi0y2pd_cn10dTKb0U1CznCOyUw-6G3bB6WAFf8EDipz5priaj8T4Bygb5bhuMBUbHqh/s2470/2024-02-17%2017%2018%2005.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1)SSH Keys&nbsp;</strong></span>&nbsp;에서 다음과 같이 키생성 버튼을 누르면&nbsp; 임의&nbsp;SSH key 생성된다.</p><p>2)해당 키값을 리눅스의&nbsp;&nbsp;<strong>~/.ssh/authorized_keys&nbsp; &nbsp;파일명으로 생성후 복사해서&nbsp; 넣는다.</strong></p><p>3) 도메인 주소 혹은 아이피 주소 입력후&nbsp; 포트번호가 987 이라면&nbsp; sample.com:987&nbsp; 값을 host address 에 입력후 Fetch 버튼을 누르면&nbsp;</p><p>fingerprint 가 생성된다.</p><p>4) add 버튼을 눌러&nbsp; 핑거프린트를 추가해 준다.</p><p>&nbsp;</p><p>이 값이 있어야&nbsp; 권한 오류 없이 bitbucket 이 우분트 서버에 접속해서 CI/CD 작업을 진행 할수 있다.</p><p>&nbsp;</p><p><img alt="" width="900" height="534" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjYbgv5EBd5s4WP6eHFh0q9H7PiyImUZo9qDwpuWlOAI5R7fF3a8KKsbgA2K0_ZVUnP9yw-MG189ShZiO01ScGyHA9ktORKsaio51BPWLP7EMkyG4rNjZ3KHz1M4fTxjJGgcm5lT2tJS-0TIIVX4QQ413QFHWkUniqht8kCkCG49ibKG_k4V8Wr1Ex2dCmw/s2505/2024-02-17%2017%2019%2032.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#2980b9"><strong>5.&nbsp; 우분트 서버에서&nbsp; &nbsp;:&nbsp;</strong></span></span><span style="color:#2980b9">&nbsp;<span style="font-size:18px"><strong> bitbucket 에 접속해서 소스를 다운 받을 수 있게&nbsp; 키값 등록 하기</strong></span></span></p><p>&nbsp;</p><p>다음을 참조해서&nbsp; 우분트 서버에서&nbsp; 키값을 생성후&nbsp;&nbsp; 상단 메뉴의 setting -&gt; Workspace Setting -&gt;&nbsp; SSH 키값을 등록 한다.</p><p>&nbsp;</p><p><a target="_blank" href="https://eminentstar.tistory.com/63">https://eminentstar.tistory.com/63</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:18px"><strong>6. 우분트 서버에서&nbsp; :&nbsp; &nbsp;Clone this repository&nbsp; &nbsp;HTTPS 로 다운 받을 때 </strong><strong>&nbsp;비밀번호 설정하기</strong></span></span></p><p>&nbsp;</p><p><strong>Clone this repository&nbsp;</strong>&nbsp;SSH 로 다운 받지 않고 HTTPS 로 다운받으면 비밀번호를 입력하라는 문구 나오는데,</p><p>다음을 참조해서 적용하도록 하자.</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://velog.io/@shj5508/Bitbucket-App-password-적용-방법">https://velog.io/@shj5508/Bitbucket-App-password-적용-방법</a></p><p>&nbsp;</p><p><span style="color:#000000">이때&nbsp; git clone 을 하기 전에&nbsp; 다음을 먼저 실행하고&nbsp; 비밀번호를 입력하면 다음 부터는&nbsp; 입력하라는 문구가 나오지 않는다.</span></p><p>&nbsp;</p><p>Credential 정보 저장</p><pre>git config credential.helper store </pre><p>credential.helper의 store 옵션을 주게되면 해당 git directory에선 반영구적으로 인증 절차가 생략됩니다.(저장된 credential 정보를 이용해 인증 처리)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:18px"><strong>7. 우분트 서버에서&nbsp; :&nbsp; &nbsp;셀 스크립트 작성하기</strong></span></span></p><p>&nbsp;</p><p>우분트 서버에서&nbsp; 다음과 같이 임의&nbsp; 셀스크립트 파일을 만든후 실행할 명령어를 입력한다.</p><p>여기서는 /home/react/bitbucket-react-deployment-script.sh&nbsp; 에서 파일을 생성 하였다.</p><p>&nbsp;</p><p><strong>bitbucket-react-deployment-script.sh</strong></p><pre class="brush:as3;">#!/bin/bash echo -e $PWD cd /home/react/bitbuket-front-end echo -e $PWD git pull npm install npm run build systemctl restart nginx </pre><p>&nbsp;</p><p>&nbsp;</p><p>bitbucket 의 파이프라인의&nbsp;bitbucket-pipelines.yml&nbsp; 파일에서 다음 코드가&nbsp; bitbucket-react-deployment-script.sh&nbsp; &nbsp;을 실행하는 명령어를 입력하면 된다.</p><pre class="brush:as3;">COMMAND: &#39;sh /home/react/bitbucket-react-deployment-script.sh&#39; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>윈도우의 작업 환경에서 리액트 파일을 수정후 push 만 하면&nbsp; 자동으로 bitbucket&nbsp; 의 파이프라인이&nbsp; 우분트 서버에 패포까지 진행해서 처리해준다.</p><p>혹은 수동으로 우측 상단 메뉴에서 Run pipeline인 버튼을 눌러도 진행이 된다.</p><p>실행된 pipeline 목록에서 해당 메뉴를 클릭해서 들어가</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="310" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjoI_Tf5x9W1l47Wfs_59OAJpHMHrhxvDW3LKhpRD0m9PQtISTlkR7F-YDniIeXp9xCNAEcYnYR5PAFiiMNJh-yYeGgalpGTwIJnVblseRRLPbMSDFZs1u40XCrgWfJ3CkU1ohVjd3VNmlPy649sisph5Bz3zNf0E8mMqhDNqzma8uWzI8wzreKzfFT_YKS/s2364/2024-02-17%2018%2054%2046.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>오류가 있을시 좌측 상단에서 메뉴가 빨간색으로 표시되고 오류 메시지를 표시해 준다.</p><p>또한 , 우즉에서 build&nbsp; 에서 상세 보기를 통해 실행된 명령어들과 오류 발생시 해당 오류를 알 자세히 알 수 있다.</p><p>톰니바퀴&nbsp; 버튼을 누르면&nbsp;&nbsp;bitbucket-pipelines.yml&nbsp; 파일을 수정할 수 있으며,</p><p>수정과 동시에 자동으로 실행 처리 된다.</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="446" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgusyoZjLNwILRIAjGUK7aK3TJCVOydK2yNZ5uAX7FLpjIxz_tjdon7qStlWMaeA4Mo95KxQysPP26MDaq65Akqvfywwe5P-yth6EJVU3ZiIqv3pc2_iOfWrGe5kTSsjHYaS5UbXUDSAiEWPl6aZwhfjB4T0mqfDjEN19ZSrKGxNGW0MwB6MLlY04jWTe_b/s2437/2024-02-17%2018%2056%2024.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-02-13 10:13:52 AWS 프로덕션 배포 http://macaronics.net/index.php/m05/computer/view/2179 2179 <p>&nbsp;</p><p>&nbsp;</p><p>eb create --database --elb-type application --instance-type t3.micro<br />&nbsp;</p><p>&nbsp;</p><p>deploy:<br />&nbsp; artifact: build/libs/shop-gradle-0.0.1-SNAPSHOT.jar</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">$ eb setenv SPRING_PROFILES_ACTIVE=prod 2024-02-08 06:56:10 INFO Environment update is starting. 2024-02-08 06:56:21 INFO Updating environment MC1-TODO-API-SERVICE&#39;s configuration settings. 2024-02-08 06:56:43 INFO Instance deployment successfully detected a JAR file in your source bundle. 2024-02-08 06:56:43 INFO Instance deployment successfully generated a &#39;Procfile&#39;. 2024-02-08 06:56:50 INFO Instance deployment completed successfully. 2024-02-08 06:57:12 INFO Successfully deployed new configuration to environment. </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>환경설정</strong></span></p><pre class="brush:as3;"> eb setenv SPRING_PROFILES_ACTIVE=prod </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>재배포</strong></span><br />&nbsp;</p><pre class="brush:as3;">eb deploy </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>https://ma7server.p-e.kr</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-02-08 15:49:43 무료 이미지 생성 인공지능 API http://macaronics.net/index.php/m05/computer/view/2178 2178 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;무료로 제공되는 API의 기능이 제한될 수 있으며, 일부 제공업체는 무료 이용에 대한 사용량 제한을 설정할 수 있습니다. 몇 가지 인공지능 API의 예는 다음과 같습니다:</p><p>&nbsp;</p><ol><li><p><strong>DeepAI</strong>: DeepAI는 다양한 이미지 생성 및 변형 기능을 제공합니다. 스타일 전이, 이미지 노이즈 제거, 이미지 채색 등의 작업을 수행할 수 있습니다. 무료 플랜과 함께 사용 가능합니다.</p></li></ol><p><a target="_blank" href="https://deepai.org/machine-learning-model/text2img">https://deepai.org/machine-learning-model/text2img</a></p><p>&nbsp;</p><ol><li><p><strong>DeepArt</strong>: DeepArt는 유명한 작품의 스타일을 사용하여 입력 이미지를 변형하는 스타일 전이 서비스를 제공합니다. 무료로 사용 가능하지만, 고해상도 이미지 생성 등 일부 고급 기능은 유료입니다.</p></li></ol><p><a target="_blank" href="https://creativitywith.ai/">https://creativitywith.ai/</a></p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>Runway ML</strong>: Runway ML은 다양한 딥러닝 모델과 작업을 지원하는 플랫폼입니다. 스타일 전이, 이미지 생성, 이미지 편집 등의 작업을 수행할 수 있습니다. 무료 플랜과 함께 사용 가능하지만, 일부 기능은 유료입니다.</p></li></ol><p>&nbsp;</p><p><strong><a target="_blank" href="https://runwayml.com/">https://runwayml.com/</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>Google Cloud Vision API</strong>: Google Cloud Vision API는 이미지 인식 및 분석을 위한 API입니다. 무료로 제공되는 일부 기능 중에는 이미지 특성 탐지, 텍스트 감지 등이 있습니다. 다양한 기능을 사용하려면 요금이 부과될 수 있습니다.</p></li></ol><p>&nbsp;</p><p><a target="_blank" href="https://cloud.google.com/vision">https://cloud.google.com/vision</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-02-08 14:51:01 ★스프링부트 체크박스 리스트 객체 를 스프링에서 받기 http://macaronics.net/index.php/m01/spring/view/2177 2177 <p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;tr th:each=&quot;cartItem : ${cartItems}&quot;&gt; &lt;td class=&quot;text-center align-middle&quot;&gt; &lt;input type=&quot;checkbox&quot; name=&quot;cartChkBox&quot; th:value=&quot;${cartItem.cartItemId}&quot;&gt; &lt;/td&gt; &lt;/tr&gt;</pre><p>&nbsp;</p><p><span style="font-size:28px"><strong>1. js</strong></span></p><pre class="brush:as3;"> function orders(){ const token=$(&quot;meta[name=&#39;_csrf&#39;]&quot;).attr(&quot;content&quot;); const header=$(&quot;meta[name=&#39;_csrf_header&#39;]&quot;).attr(&quot;content&quot;); const url=&quot;/cart/orders&quot;; const dataList=new Array(); const paramData=new Object(); $(&quot;input[name=cartChkBox]:checked&quot;).each(function(){ const cartItemId=$(this).val(); const data=new Object(); data[&quot;cartItemId&quot;]=cartItemId; dataList.push(data); }) paramData[&quot;cartOrderDtoList&quot;]=dataList; const param=JSON.stringify(paramData); console.log(&quot;dataList : &quot;,dataList); console.log(&quot;paramData : &quot;,dataList); $.ajax({ url:url, type:&quot;POST&quot;, beforeSend:function (xhr){ // 데이터를 전송하기 전에 헤더에 csrf값을 설정 xhr.setRequestHeader(header,token); }, dataType:&quot;json&quot;, cache:false, contentType:&quot;application/json&quot;, data:param, success:function(result,status){ alert(&quot;주문이 완료 되었습니다.&quot;); location.href=&quot;/orders&quot;; }, error:function(jqXHR, status, error){ console.log(&quot; 에러 : jqXHR : &quot;, jqXHR, &quot; =====status: &quot;, status, &quot; ===== error: &quot;, error); if(jqXHR.status ==&#39;401&#39;){ alert(&quot;로그인 후 이용해주세요.&quot;); location.rhref=&quot;/members/login&quot;; }else{ alert(jqXHR.responseJSON.message); } } }) }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:28px">DTO</span></strong></p><pre class="brush:as3;">import lombok.Getter; import lombok.Setter; import java.util.List; @Setter @Getter public class CartOrderDto { private Long cartItemId; /** 장바구니에서 여러 개의 상품을 주문하므로 CartOrderDto 클래스가 자기 자신을 가지고 있도록 cartOrderDtoList 변수 생성 */ private List&lt;CartOrderDto&gt; cartOrderDtoList; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>3. constroller</strong></span></p><pre class="brush:as3;"> /** * 장바구니에서 주문 */ @PostMapping(value = &quot;/cart/orders&quot;) @ResponseBody public ResponseEntity&lt;?&gt; orderCartItem(@RequestBody CartOrderDto cartOrderDto, @AuthenticationPrincipal PrincipalDetails principalDetails){ List&lt;CartOrderDto&gt; cartOrderDtoList = cartOrderDto.getCartOrderDtoList(); if(cartOrderDtoList==null || cartOrderDtoList.size()==0){ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(&quot;주문할 상품을 선택해 주세요.&quot;); } for(CartOrderDto cartOrder : cartOrderDtoList){ if(!cartService.validateCartItem(cartOrder.getCartItemId(), principalDetails.getEmail())){ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(&quot;주문 권한이 없습니다.&quot;); } } Long orderId=cartService.ordrerCartItem(cartOrderDtoList, principalDetails.getEmail()); return ResponseEntity.status(HttpStatus.OK).body(orderId); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :<a target="_blank" href="https://github.com/braverokmc79/jpa-shop-test2">https://github.com/braverokmc79/jpa-shop-test2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-28 18:53:34 React.js, 스프링 부트, AWS로 배우는 웹 개발 http://macaronics.net/index.php/m01/spring/view/2176 2176 <p>&nbsp;</p><p>&nbsp;</p><p>목차</p><p><strong>1장. 개발을 시작하기 전에</strong><br /><br />1.1 Todo 웹 애플리케이션<br />1.1.1 Todo 웹 애플리케이션 기능<br />1.1.2 Todo 웹 애플리케이션 아키텍처<br />1.1.3 기술과 구현 사이<br />1.1.4 정리<br />1.2 배경 지식<br />1.2.1 하이퍼텍스트 트랜스퍼 프로토콜<br />1.2.2 자바스크립트 오브젝트 노테이션<br />1.2.3 서버란?<br />1.2.4 정적 웹 서버<br />1.2.5 동적 웹 서버<br />1.2.6 자바 서블릿 컨테이너/엔진<br />1.2.7 정리<br /><br /><strong>2장. 백엔드 개발</strong><br /><br />2.1 백엔드 개발 환경 설정<br />2.1.1 자바 8 설치<br />2.1.2 이클립스 설치<br />2.1.3 스프링 프레임워크와 의존성 주입<br />2.1.4 스프링 프레임워크와 디스패처 서블릿(중제목)<br />2.1.5 스프링 부트 프로젝트 설정<br />2.1.6 메인 메서드와 @SpringBootApplication<br />2.1.7 빌드 자동화 툴: Gradle과 라이브러리<br />2.1.8 디펜던시 라이브러리 추가<br />2.1.9 롬복<br />2.1.10 포스트맨 API 테스트<br />2.1.11 정리<br />2.2 백엔드 서비스 아키텍처<br />2.2.1 레이어드 아키텍처<br />2.2.2 모델, 엔티티, DTO<br />2.2.3 REST API<br />2.2.4 컨트롤러 레이어 : 스프링 REST API 컨트롤러<br />2.2.5 서비스 레이어 : 비즈니스 로직<br />2.2.6 퍼시스턴스 레이어 : 스프링 데이터 JPA<br />2.2.7 정리<br />2.3 서비스 개발 및 실습<br />2.3.1 Create Todo 구현<br />2.3.2 Retrieve Todo 구현<br />2.3.3 Update Todo 구현<br />2.3.4 Delete Todo 구현<br />2.3.5 정리<br /><br /><strong>3장. 프론트엔드 개발</strong><br /><br />3.1 프론트엔드 개발 환경 설정<br />3.1.1 Node<br />3.1.2 비주얼 스튜디오 코드 설치<br />3.1.3 프론트엔드 애플리케이션 생성<br />3.1.4 material-ui 패키지 설치<br />3.1.5 브라우저의 작동 원리<br />3.1.6 React<br />3.1.7 정리<br />3.2 프론트엔드 서비스 개발<br />3.2.1 Todo 리스트<br />3.2.2 Todo 추가<br />3.2.3 Todo 삭제<br />3.2.4 Todo 수정<br />3.2.5 정리<br />3.3 서비스 통합<br />3.3.1 componentDidMount<br />3.3.2 CORS<br />3.3.3 fetch<br />3.3.4 정리<br /><br /><strong>4장. 인증 백엔드 통합</strong><br /><br />4.1 REST API 인증 기법<br />4.1.1 Basic 인증<br />4.1.2 토큰 기반 인증<br />4.1.3 JSON 웹 토큰<br />4.1.4 정리<br />4.2 User 레이어 구현<br />4.2.1 UserEntity<br />4.2.2 UserRepository<br />4.2.3 UserService<br />4.2.4 UserController<br />4.2.5 정리<br />4.3 스프링 시큐리티 통합<br />4.3.1 JWT 생성 및 반환 구현<br />4.3.2 스프링 시큐리티와 서블릿 필터<br />4.3.3 JWT를 이용한 인증 구현<br />4.3.4 스프링 시큐리티 설정<br />4.3.5 TodoController에서 인증된 유저 사용하기<br />4.3.6 패스워드 암호화<br />4.3.7 정리<br /><br /><strong>5장. 인증 프론트엔드 통합</strong><br /><br />5.1 라우팅<br />5.1.1 react-router-dom<br />5.1.2 react-router-dom 라이브러리가 필요한 이유<br />5.1.3 로그인 컴포넌트<br />5.1.4 접근 거부 시 로그인 페이지로 라우팅하기<br />5.1.5 정리<br />5.2 로그인 페이지<br />5.2.1 로그인을 위한 API 서비스 메서드 작성<br />5.2.2 로그인에 성공<br />5.2.3 정리<br />5.3 로컬 스토리지를 이용한 액세스 토큰 관리<br />5.3.1 로컬 스토리지<br />5.3.2 액세스 토큰 저장<br />5.3.3 정리<br />5.4 로그아웃과 글리치 해결<br />5.4.1 로그아웃 서비스<br />5.4.2 네비게이션 바와 로그아웃<br />5.4.3 UI 글리치 해결<br />5.4.4 정리<br />5.5 계정 생성 페이지<br />5.5.1 계정 생성 로직<br />5.5.2 정리<br /><br /><strong>6장. 프로덕션 배포</strong><br /><br />6.1 서비스 아키텍처<br />6.1.1 EC2<br />6.1.2 라우트 53 - DNS<br />6.1.3 애플리케이션 로드밸런서<br />6.1.4 오토 스케일링 그룹<br />6.1.5 VPC와 서브넷<br />6.1.6 일라스틱 빈스톡<br />6.1.7 정리<br />6.2 AWS CLI와 EB CLI 설치<br />6.2.1 AWS 계정 생성<br />6.2.2 파이썬 설치<br />6.2.3 AWS CLI 설치<br />6.2.4 AWS CLI 설정<br />6.2.5 pip을 이용해 EB CLI 설치<br />6.2.6 윈도우 사용자를 위한 환경 변수 설정<br />6.2.7 정리<br />6.3 AWS 일라스틱 빈스톡을 이용한 백엔드 배포<br />6.3.1 일라스틱 빈스톡이란?<br />6.3.2 eb init을 이용해 애플리케이션 생성<br />6.3.3 백엔드 애플리케이션 설정<br />6.3.4 eb create를 이용해 AWS에 환경 생성<br />6.3.5 애플리케이션 배포<br />6.3.6 환경 구성<br />6.3.7 엔드포인트 테스팅<br />6.3.8 프론트엔드 통합 테스팅<br />6.3.9 정리<br />6.4 AWS 일라스틱 빈스톡을 이용한 프론트엔드 배포<br />6.4.1 eb init을 이용해 애플리케이션 생성<br />6.4.2 eb create를 이용한 애플리케이션 배포<br />6.4.3 크로스-오리진 문제<br />6.4.4 정리<br />6.5 Route53 도메인 설정<br />6.5.1 도메인 구매<br />6.5.2 호스팅 영역 생성<br />6.5.3 백엔드 재배포<br />6.5.4 프론트엔드 재배포<br />6.5.5 정리<br />6.6 백엔드/프론트엔드 AWS Certificate Manager를 이용한 https 설정<br />6.6.1 인증서 요청<br />6.6.2 백엔드 애플리케이션 HTTPS설정<br />6.6.3 프론트엔드 애플리케이션 HTTPS 설정<br />6.6.4 정리</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong>소스코드</strong></p><p><a target="_blank" href="https://github.com/fsoftwareengineer/todo-application">https://github.com/fsoftwareengineer/todo-application</a></p><p><br /><strong>Amazon Corretto 11 영구 URL</strong></p><p><a target="_blank" href="https://docs.aws.amazon.com/ko_kr/corretto/latest/corretto-11-ug/downloads-list.html">https://docs.aws.amazon.com/ko_kr/corretto/latest/corretto-11-ug/downloads-list.html</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>구글 구아바 설치</strong></p><p><a target="_blank" href="https://mvnrepository.com/artifact/com.google.guava/guava/33.0.0-jre">https://mvnrepository.com/artifact/com.google.guava/guava/33.0.0-jre</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-26 22:17:42 ★JPA 페이지 처리 querydsl 페이징 처리 pagination , 스프링부트 페이징 처리 pageMaker 반환 http://macaronics.net/index.php/m01/spring/view/2175 2175 <p>&nbsp;</p><p><span style="font-size:18px"><strong>스프링부트 버전&nbsp; 3.2.1</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#e74c3c"><span style="font-size:22px"><strong>1. 컨트롤</strong></span></span></p><p><span style="font-size:22px"><strong>ItemController</strong></span></p><pre class="brush:as3;"> @GetMapping(value = {&quot;/admin/items&quot;, &quot;/admin/items/{page}&quot;}) public String itemManage(ItemSearchDto itemSearchDto, @PathVariable(&quot;page&quot;) Optional&lt;Integer&gt; page, PageMaker pageMaker, Model model){ int pageInt=page.orElse(0); Pageable pageable = PageRequest.of(pageInt, 10); Page&lt;Item&gt; items = itemService.getAdminItemPage(itemSearchDto, pageable); /// pagination html 처리 String pagination=pageMaker.pageObject(items, pageInt, 10, 5 , &quot;/admin/items/&quot; ,&quot;js&quot; ); model.addAttribute(&quot;items&quot;, items); model.addAttribute(&quot;itemSearchDto&quot;, itemSearchDto); model.addAttribute(&quot;maxPage&quot;, 5); model.addAttribute(&quot;pagination&quot;, pagination); return &quot;item/itemMng&quot;; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>2.PageMaker</strong></span></span></p><pre class="brush:as3;">import lombok.Data; import lombok.ToString; import org.springframework.data.domain.Page; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; //MySQL PageMaker @Data @ToString public class PageMaker { private int page; private int pageSize=10; private int pageStart; private int totalCount; //전체 개수 private int startPage; // 시작 페이지 private int endPage; // 끝페이지 private boolean prev; // 이전 여부 private boolean next; // 다음 여부 private boolean last; //마지막 페이지 여부 private int displayPageNum=10; //하단 페이징 &lt;&lt; 1 2 3 4 5 6 7 8 9 10 &gt;&gt; private int tempEndPage; private String searchQuery; Page&lt;?&gt; pageObject; private void calcData(){ endPage=(int)(Math.ceil(page / (double)displayPageNum)*displayPageNum); startPage=(endPage - displayPageNum) +1; if(endPage&gt;=tempEndPage)endPage=tempEndPage; prev =startPage ==1 ? false :true; next =endPage *pageSize &gt;=totalCount ? false :true; } /** * * * @param pageObject Page&lt;?&gt; 반환된 리스트값 * @param pageInt 현재 페이지 * @param pageSize 페이지사이즈 * @param displayPageNum 하단 페이징 기본 10설정 &lt;&lt; 1 2 3 4 5 6 7 8 9 10 &gt;&gt; * @param pageUrl url 주소 * @param type ajax, href = 자바스크립트 , 링크 * @return */ public String pageObject(Page&lt;?&gt; pageObject, Integer pageInt , Integer pageSize, Integer displayPageNum , String pageUrl, String type) { this.pageObject = pageObject; this.page=pageInt==0? 1:pageInt+1; if(pageSize!=null){ this.pageSize=pageSize; } this.tempEndPage=pageObject.getTotalPages(); if(displayPageNum!=null){ this.displayPageNum=displayPageNum; }else this.displayPageNum=10; this.totalCount=Math.toIntExact(pageObject.getTotalElements()); calcData(); if(StringUtils.hasText(pageUrl)){ if(type.equalsIgnoreCase(&quot;JS&quot;)){ return paginationJs(pageUrl); }else if(type.equalsIgnoreCase(&quot;HREF&quot;)){ return paginationHref(pageUrl); }else if(type.equalsIgnoreCase(&quot;PATHVARIABLE&quot;)){ return paginationPathVariable(pageUrl); } }return null; } /** * javascript page 버튼 클릭 반환 * @param url * @return */ public String paginationJs(String url) { StringBuffer sBuffer = new StringBuffer(); sBuffer.append(&quot;&lt;ul class=&#39;pagination justify-content-center&#39;&gt;&quot;); if (prev) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39; &gt;&lt;a class=&#39;page-link&#39; onclick=&#39;javascript:page(0)&#39; &gt;처음&lt;/a&gt;&lt;/li&gt;&quot;); } if (prev) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; onclick=&#39;javascript:page(&quot;+ (startPage - 2)+&quot;)&#39;; &gt;&amp;laquo;&lt;/a&gt;&lt;/li&gt;&quot;); } String active = &quot;&quot;; for (int i = startPage; i &lt;= endPage; i++) { if (page==i) { active = &quot;class=&#39;page-item active&#39;&quot;; } else { active = &quot;class=&#39;page-item&#39;&quot;; } sBuffer.append(&quot;&lt;li &quot; + active + &quot; &gt;&quot;); sBuffer.append(&quot;&lt;a class=&#39;page-link&#39; onclick=&#39;javascript:page(&quot;+ (i-1) +&quot;)&#39;; &gt;&quot; + i + &quot;&lt;/a&gt;&lt;/li&gt;&quot;); sBuffer.append(&quot;&lt;/li&gt;&quot;); } if (next &amp;&amp; endPage &gt; 0 &amp;&amp; endPage &lt;= tempEndPage) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; onclick=&#39;javascript:page(&quot;+ (endPage) +&quot;)&#39;; &gt;&amp;raquo;&lt;/a&gt;&lt;/li&gt;&quot;); } if (next &amp;&amp; endPage &gt; 0 &amp;&amp; !isLast()) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt; &lt;a class=&#39;page-link&#39; onclick=&#39;javascript:page(&quot;+ (tempEndPage-1) +&quot;)&#39;; &gt;마지막&lt;/a&gt;&lt;/li&gt;&quot;); } sBuffer.append(&quot;&lt;/ul&gt;&quot;); return sBuffer.toString(); } public String makeSearch(int page){ UriComponents uriComponents= UriComponentsBuilder.newInstance() .queryParam(&quot;searchQuery&quot;, searchQuery) .queryParam(&quot;page&quot;, page) .build(); return uriComponents.toUriString(); } /** * 링크 파리미터 반환 * @param url * @return */ public String paginationHref(String url){ StringBuffer sBuffer=new StringBuffer(); sBuffer.append(&quot;&lt;ul class=&#39;pagination justify-content-center&#39;&gt;&quot;); if(prev){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+makeSearch(1)+&quot;&#39;&gt;처음&lt;/a&gt;&lt;/li&gt;&quot;); } if(prev){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+makeSearch(startPage-2)+&quot;&#39;&gt;&amp;laquo;&lt;/a&gt;&lt;/li&gt;&quot;); } String active=&quot;&quot;; for(int i=startPage; i &lt;=endPage; i++){ if (page==i) { active = &quot;class=&#39;page-item active&#39;&quot;; sBuffer.append(&quot;&lt;li &quot; +active+&quot; &gt; &quot;); sBuffer.append(&quot;&lt;a class=&#39;page-link&#39; href=&#39;javascript:void(0)&#39;&gt;&quot;+i+&quot;&lt;/a&gt;&lt;/li&gt;&quot;); sBuffer.append(&quot;&lt;/li&gt;&quot;); } else { active = &quot;class=&#39;page-item&#39;&quot;; sBuffer.append(&quot;&lt;li &quot; +active+&quot; &gt; &quot;); sBuffer.append(&quot;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+makeSearch(i-1)+&quot;&#39;&gt;&quot;+i+&quot;&lt;/a&gt;&lt;/li&gt;&quot;); sBuffer.append(&quot;&lt;/li&gt;&quot;); } } if(next &amp;&amp; endPage&gt;0 &amp;&amp; endPage &lt;= tempEndPage){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+makeSearch(endPage)+&quot;&#39;&gt;&amp;raquo;&lt;/a&gt;&lt;/li&gt;&quot;); } if (next &amp;&amp; endPage &gt; 0 &amp;&amp; !isLast()) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+makeSearch(tempEndPage-1)+&quot;&#39;&gt;마지막&lt;/a&gt;&lt;/li&gt;&quot;); } sBuffer.append(&quot;&lt;/ul&gt;&quot;); return sBuffer.toString(); } public String paginationPathVariable(String url){ StringBuffer sBuffer=new StringBuffer(); sBuffer.append(&quot;&lt;ul class=&#39;pagination justify-content-center&#39;&gt;&quot;); if(prev){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+(1)+&quot;&#39;&gt;처음&lt;/a&gt;&lt;/li&gt;&quot;); } if(prev){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+(startPage-2)+&quot;&#39;&gt;&amp;laquo;&lt;/a&gt;&lt;/li&gt;&quot;); } String active=&quot;&quot;; for(int i=startPage; i &lt;=endPage; i++){ if (page==i) { active = &quot;class=&#39;page-item active&#39;&quot;; sBuffer.append(&quot;&lt;li &quot; +active+&quot; &gt; &quot;); sBuffer.append(&quot;&lt;a class=&#39;page-link&#39; href=&#39;javascript:void(0)&#39;&gt;&quot;+i+&quot;&lt;/a&gt;&lt;/li&gt;&quot;); sBuffer.append(&quot;&lt;/li&gt;&quot;); } else { active = &quot;class=&#39;page-item&#39;&quot;; sBuffer.append(&quot;&lt;li &quot; +active+&quot; &gt; &quot;); sBuffer.append(&quot;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+(i-1)+&quot;&#39;&gt;&quot;+i+&quot;&lt;/a&gt;&lt;/li&gt;&quot;); sBuffer.append(&quot;&lt;/li&gt;&quot;); } } if(next &amp;&amp; endPage&gt;0 &amp;&amp; endPage &lt;= tempEndPage){ sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+(endPage)+&quot;&#39;&gt;&amp;raquo;&lt;/a&gt;&lt;/li&gt;&quot;); } if (next &amp;&amp; endPage &gt; 0 &amp;&amp; !isLast()) { sBuffer.append(&quot;&lt;li class=&#39;page-item&#39;&gt;&lt;a class=&#39;page-link&#39; href=&#39;&quot;+url+(tempEndPage-1)+&quot;&#39;&gt;마지막&lt;/a&gt;&lt;/li&gt;&quot;); } sBuffer.append(&quot;&lt;/ul&gt;&quot;); return sBuffer.toString(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>3.서비스</strong></span></span></p><p><span style="font-size:22px"><strong>ItemService</strong></span></p><pre class="brush:as3;"> @Transactional(readOnly = true) public Page&lt;Item&gt; getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){ return itemRepository.getAdminItemPage(itemSearchDto, pageable); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>3.</strong></span></span><span style="color:#c0392b"><span style="font-size:22px"><strong>Repository</strong></span></span></p><p><span style="font-size:18px"><strong>1)ItemRepositoryCustom&nbsp;</strong></span></p><pre class="brush:as3;">import com.shop.dto.ItemSearchDto; import com.shop.entity.Item; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ItemRepositoryCustom { Page&lt;Item&gt; getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) ; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>2)ItemRepositoryCustomImpl</strong></span></p><pre class="brush:as3;">package com.shop.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.shop.constant.ItemSellStatus; import com.shop.dto.ItemSearchDto; import com.shop.dto.MainItemDto; import com.shop.dto.QMainItemDto; import com.shop.entity.Item; import com.shop.entity.QItem; import com.shop.entity.QItemImg; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import org.thymeleaf.util.StringUtils; import java.time.LocalDateTime; import java.util.List; import static com.shop.entity.QItem.item; @RequiredArgsConstructor @Log4j2 public class ItemRepositoryCustomImpl implements ItemRepositoryCustom { private final JPAQueryFactory queryFactory; /** * 상품 판매 상태 조건이 전체(null)을 리턴합니다. 결과값이 null 이면 * where 절에서 해당 조건은 무시됩니다. 상품 판매 상태 조건이 null 이 아니라 * 판매중 or 품절 상태라면 해당 조건의 상품만 조회합니다. * * @param searchSellStatus * @return */ private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) { return searchSellStatus == null ? null : item.itemSellStatus.eq(searchSellStatus); } /** * searchDateType 의 값에 따라서 dateTime 의 값을 이전 시간의 값으로 세팅 후 * 해당 시간 이후로 등록된 상품만 조회합니다. ddateTime 의 시간을 한달 전으로 * 세팅 후 최근 한달 동안 등록된 상품만 조회하도록 조건값을 반환합니다. * * @param searchDateType * @return */ private BooleanExpression regDtsAfter(String searchDateType) { LocalDateTime dateTime = LocalDateTime.now(); if (StringUtils.equals(&quot;all&quot;, searchDateType) || searchDateType == null) { return null; } else if (StringUtils.equals(&quot;1d&quot;, searchDateType)) { dateTime = dateTime.minusDays(1); } else if (StringUtils.equals(&quot;1w&quot;, searchDateType)) { dateTime = dateTime.minusWeeks(1); } else if (StringUtils.equals(&quot;1m&quot;, searchDateType)) { dateTime = dateTime.minusMonths(1); } else if (StringUtils.equals(&quot;6m&quot;, searchDateType)) { dateTime = dateTime.minusMonths(1); } return item.regTime.after(dateTime); } /** * searchBy 의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품 * 생성자의 아이디에 검색어를 포하하고 있는 상품을 조회하도록 조건값을 반환합니다. * * @param searchBy * @param searchQuery * @return */ private BooleanExpression searchByLike(String searchBy, String searchQuery) { if (StringUtils.equals(&quot;itemNm&quot;, searchBy)) { return item.itemNm.like(&quot;%&quot; + searchQuery + &quot;%&quot;); } else if (StringUtils.equals(&quot;createdBy&quot;, searchBy)) { return item.createdBy.like(&quot;%&quot; + searchQuery + &quot;%&quot;); } return null; } @Override public Page&lt;Item&gt; getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) { /** * where 조건절 : BooleanExpression 반환하는 조건문을 넣어 줍니다. * &quot;,&quot; 단위로 넣어줄 경우 and 조건으로 인식합니다. */ log.info(&quot;***** itemSearchDto : {}&quot;, itemSearchDto.toString()); List&lt;Item&gt; content = queryFactory.selectFrom(item) .where( regDtsAfter(itemSearchDto.getSearchDateType()), searchSellStatusEq(itemSearchDto.getSearchSellStatus()), searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()) ) .orderBy(item.id.desc()) .offset(pageable.getOffset()) //데이터를 가지고 올 시작 인텍스를 지정 .limit(pageable.getPageSize()) //한번에 가지고 올 최대 개수를 지정 .fetch(); //fetchResults() : 조회한 리스트 및 전체 개수를 포함하는 QueryResult 를 반환 //상품 데이터 리스트 조회 및 상품 데이터 전체 개수를 조회하는 2번의 쿼리문이 실행된다. JPAQuery&lt;Long&gt; countQuery = queryFactory. select(item.count()) .from(item) .where( regDtsAfter(itemSearchDto.getSearchDateType()), searchSellStatusEq(itemSearchDto.getSearchSellStatus()), searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()) ); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } /** * 검색어가 null 이 아니면 상품명에 해당 검색어가 포함되는 상품을 조회하는 조건을 반환합니다. * @param searchQuery * @return */ private BooleanExpression itemNmLike(String searchQuery){ return StringUtils.isEmpty(searchQuery) ? null : item.itemNm.like(&quot;%&quot; +searchQuery +&quot;%&quot;); } @Override public Page&lt;MainItemDto&gt; getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) { QItem item= QItem.item; QItemImg itemImg=QItemImg.itemImg; List&lt;MainItemDto&gt; content = queryFactory.select( new QMainItemDto( item.id, item.itemNm, item.itemDetail, itemImg.imgUrl, item.price ) ).from(itemImg) .join(itemImg.item, item) //itemImg 와 item 을 내부 조인 .where(itemImg.repimgYn.eq(&quot;Y&quot;)) //상품 이미지의 경우 대표 상품 이미지만 불러온다. .where(itemNmLike(itemSearchDto.getSearchQuery())) .orderBy(item.id.desc()) .offset(pageable.getOffset()) .limit((pageable.getPageSize())) .fetch(); JPAQuery&lt;Long&gt; countQuery = queryFactory.select( itemImg.count() ).from(itemImg) .join(itemImg.item, item) //itemImg 와 item 을 내부 조인 .where(itemImg.repimgYn.eq(&quot;Y&quot;)) //상품 이미지의 경우 대표 상품 이미지만 불러온다. .where(itemNmLike(itemSearchDto.getSearchQuery())); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>3)ItemRepository</strong></span></p><pre class="brush:as3;">import com.shop.entity.Item; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; import java.util.List; public interface ItemRepository extends JpaRepository&lt;Item,Long&gt;, QuerydslPredicateExecutor&lt;Item&gt;, ItemRepositoryCustom { }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>4. thymeleaf&nbsp;뷰 처리</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">&lt;!DOCTYPE html&gt; &lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot; xmlns:layout=&quot;http://www.ultraq.net.nz/thymeleaf/layout&quot; layout:decorate=&quot;~{layouts/layout1}&quot;&gt; &lt;!-- 사용자 스크립트 추가 --&gt; &lt;th:block layout:fragment=&quot;script&quot;&gt; &lt;script th:inline=&quot;javascript&quot;&gt; $(document).ready(function(){ $(&quot;#searchBtn&quot;).on(&quot;click&quot;,function(e) { e.preventDefault(); page(0); }); }); function page(page){ var searchDateType = $(&quot;#searchDateType&quot;).val(); var searchSellStatus = $(&quot;#searchSellStatus&quot;).val(); var searchBy = $(&quot;#searchBy&quot;).val(); var searchQuery = $(&quot;#searchQuery&quot;).val(); location.href=&quot;/admin/items/&quot; + page + &quot;?searchDateType=&quot; + searchDateType + &quot;&amp;searchSellStatus=&quot; + searchSellStatus + &quot;&amp;searchBy=&quot; + searchBy + &quot;&amp;searchQuery=&quot; + searchQuery; } &lt;/script&gt; &lt;/th:block&gt; &lt;!-- 사용자 CSS 추가 --&gt; &lt;th:block layout:fragment=&quot;css&quot;&gt; &lt;style&gt; select{ margin-right:10px; } &lt;/style&gt; &lt;/th:block&gt; &lt;div layout:fragment=&quot;content&quot;&gt; &lt;div class=&quot;form-inline justify-content-center mb-5&quot; th:object=&quot;${itemSearchDto}&quot;&gt; &lt;select th:field=&quot;*{searchDateType}&quot; class=&quot;form-control&quot; style=&quot;width:auto;&quot;&gt; &lt;option value=&quot;all&quot;&gt;전체기간&lt;/option&gt; &lt;option value=&quot;1d&quot;&gt;1일&lt;/option&gt; &lt;option value=&quot;1w&quot;&gt;1주&lt;/option&gt; &lt;option value=&quot;1m&quot;&gt;1개월&lt;/option&gt; &lt;option value=&quot;6m&quot;&gt;6개월&lt;/option&gt; &lt;/select&gt; &lt;select th:field=&quot;*{searchSellStatus}&quot; class=&quot;form-control&quot; style=&quot;width:auto;&quot;&gt; &lt;option value=&quot;&quot;&gt;판매상태(전체)&lt;/option&gt; &lt;option value=&quot;SELL&quot;&gt;판매&lt;/option&gt; &lt;option value=&quot;SOLD_OUT&quot;&gt;품절&lt;/option&gt; &lt;/select&gt; &lt;select th:field=&quot;*{searchBy}&quot; class=&quot;form-control&quot; style=&quot;width:auto;&quot;&gt; &lt;option value=&quot;itemNm&quot;&gt;상품명&lt;/option&gt; &lt;option value=&quot;createdBy&quot;&gt;등록자&lt;/option&gt; &lt;/select&gt; &lt;input th:field=&quot;*{searchQuery}&quot; type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;검색어를 입력해주세요&quot;&gt; &lt;button id=&quot;searchBtn&quot; type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&gt;검색&lt;/button&gt; &lt;/div&gt; &lt;div class=&quot;mt-3&quot;&gt; &lt;form th:action=&quot;@{&#39;/admin/items/&#39; + ${items.number}}&quot; role=&quot;form&quot; method=&quot;get&quot; th:object=&quot;${items}&quot;&gt; &lt;table class=&quot;table&quot;&gt; &lt;thead&gt; &lt;tr&gt; &lt;td&gt;상품아이디&lt;/td&gt; &lt;td&gt;상품명&lt;/td&gt; &lt;td&gt;상태&lt;/td&gt; &lt;td&gt;등록자&lt;/td&gt; &lt;td&gt;등록일 &lt;/td&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;tr th:each=&quot;item, status: ${items.getContent()}&quot;&gt; &lt;td th:text=&quot;${item.id}&quot;&gt;&lt;/td&gt; &lt;td&gt; &lt;a th:href=&quot;&#39;/admin/item/&#39;+${item.id}&quot; th:text=&quot;${item.itemNm}&quot;&gt;&lt;/a&gt; &lt;/td&gt; &lt;td th:text=&quot;${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL ? &#39;판매중&#39; : &#39;품절&#39;}&quot;&gt;&lt;/td&gt; &lt;td th:text=&quot;${item.createdBy}&quot;&gt;&lt;/td&gt; &lt;td th:text=&quot;${item.regTime}&quot;&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;div th:with=&quot;start=${(items.number/maxPage)*maxPage + 1}, end=(${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) &lt; items.totalPages ? start + (maxPage - 1) : items.totalPages)})&quot; &gt; &lt;ul class=&quot;pagination justify-content-center&quot;&gt; &lt;li class=&quot;page-item&quot; th:classappend=&quot;${items.first}?&#39;disabled&#39;&quot;&gt; &lt;a th:onclick=&quot;&#39;javascript:page(&#39; + ${items.number - 1} + &#39;)&#39;&quot; aria-label=&#39;Previous&#39; class=&quot;page-link&quot;&gt; &lt;span aria-hidden=&#39;true&#39;&gt;이전&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;li class=&quot;page-item&quot; th:each=&quot;page: ${#numbers.sequence(start, end)}&quot; th:classappend=&quot;${items.number eq page-1}?&#39;active&#39;:&#39;&#39;&quot;&gt; &lt;a th:onclick=&quot;&#39;javascript:page(&#39; + ${page - 1} + &#39;)&#39;&quot; th:inline=&quot;text&quot; class=&quot;page-link&quot;&gt;[[${page}]]&lt;/a&gt; &lt;/li&gt; &lt;li class=&quot;page-item&quot; th:classappend=&quot;${items.last}?&#39;disabled&#39;&quot;&gt; &lt;a th:onclick=&quot;&#39;javascript:page(&#39; + ${items.number + 1} + &#39;)&#39;&quot; aria-label=&#39;Next&#39; class=&quot;page-link&quot;&gt; &lt;span aria-hidden=&#39;true&#39;&gt;다음&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;div th:utext=&quot;${pagination}&quot;&gt;&lt;/div&gt; &lt;/form&gt; &lt;/div&gt; &lt;/div&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>Item</strong></span></p><pre class="brush:as3;">import com.shop.constant.ItemSellStatus; import com.shop.dto.ItemFormDto; import com.shop.entity.base.BaseEntity; import com.shop.exception.OutOfStockException; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Entity @Table(name = &quot;item&quot;) @Getter @Setter @ToString public class Item extends BaseEntity { @Id @Column(name = &quot;item_id&quot;) @GeneratedValue(strategy =GenerationType.IDENTITY ) private Long id; //상품코드 @Column(nullable = false, length = 50) private String itemNm; //상품명 @Column(name=&quot;price&quot;, nullable = false) private int price; //가격 @Column(nullable= false) private int stockNumber; //재고수량 @Lob @Column(nullable = false) private String itemDetail; //상품 상세 설명 @Enumerated(EnumType.STRING) private ItemSellStatus itemSellStatus; //상품 판매 상태 public void updateItem(ItemFormDto itemFormDto){ this.itemNm = itemFormDto.getItemNm(); this.price = itemFormDto.getPrice(); this.stockNumber = itemFormDto.getStockNumber(); this.itemDetail = itemFormDto.getItemDetail(); this.itemSellStatus = itemFormDto.getItemSellStatus(); } public void removeStock(int stockNumber){ int restStock = this.stockNumber - stockNumber; if(restStock &lt; 0){ throw new OutOfStockException(&quot;상품의 재고가 부족합니다. (현재 재고 수량 : &quot; + this.stockNumber + &quot;)&quot;); } this.stockNumber = restStock; } public void addStock(int stockNumber){ this.stockNumber += stockNumber; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>ItemSearchDto</strong></span></p><pre class="brush:as3;">import com.shop.constant.ItemSellStatus; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Getter @Setter @ToString public class ItemSearchDto { /** * 현재 시간과 상품 등록일을 비교해서 상품 데이터를 조회합니다. 조회시간 기준은 아래와 같습니다. * all: 상품 등록일 전체 * 1d: 최근 하루 동안 등록된 상품 * 1w: 최근 일주일 동안 등록된 상품 * 1m: 최근 한달 동안 등록된 상품 * 6m: 최근 6개월 동안 등록된 상품 * */ private String searchDateType; /** * 상품의 판매상태를 기준으로 상품 데이터를 조회합니다. */ private ItemSellStatus searchSellStatus;; /** * 상품을 조회할 때 어떤 유형으로 조회할지 선택합니다 * itemNm:상품명 * createdBy: 상품 등록자 아이디 */ private String searchBy; /** * 조회할 검색어 저장할 변수입니다. * searchBy 가 itemNm 일 경우 상품명을 기준으로 검색하고, * createdBy 일 경우 상품 등록자 아이디 기준으로 검색합니다. */ private String searchQuery=&quot;&quot;; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#2980b9"><strong>EX )</strong></span></span></p><p>컨트롤</p><pre class="brush:as3;"> @GetMapping(value = {&quot;/&quot;, &quot;&quot;}) public String main(ItemSearchDto itemSearchDto, @PathVariable(value =&quot;page&quot;) Optional&lt;Integer&gt; page, PageMaker pageMaker, Model model){ int pageInt=page.orElse(0); if(pageInt==0&amp;&amp;pageMaker.getPage()!=0)pageInt=pageMaker.getPage(); Pageable pageable = PageRequest.of(pageInt , 6); Page&lt;MainItemDto&gt; items = itemService.getMainItemPage(itemSearchDto, pageable); String pagination = pageMaker.pageObject(items, pageInt, 6, 5, &quot;/&quot; , &quot;href&quot;); log.info(&quot;************** pagination {}:&quot;, pageMaker.toString()); model.addAttribute(&quot;items&quot;, items); model.addAttribute(&quot;itemSearchDto&quot;, itemSearchDto); model.addAttribute(&quot;pagination&quot;, pagination); return &quot;main&quot;; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>뷰</p><pre class="brush:as3;"> &lt;input type=&quot;hidden&quot; name=&quot;searchQuery&quot; th:value=&quot;${itemSearchDto.searchQuery}&quot;&gt; &lt;div th:if=&quot;${not #strings.isEmpty(itemSearchDto.searchQuery)}&quot; class=&quot;center&quot;&gt; &lt;p class=&quot;h3 font-weight-bold&quot; th:text=&quot;${itemSearchDto.searchQuery}+ &#39;검색 결과&#39;&quot;&gt;&lt;/p&gt; &lt;/div&gt; &lt;div class=&quot;row mt-5&quot;&gt; &lt;th:block th:each=&quot;item, status:${items.getContent()}&quot;&gt; &lt;div class=&quot;col-md-4 margin&quot;&gt; &lt;div class=&quot;card&quot;&gt; &lt;a th:href=&quot;&#39;/item/&#39;+${item.id}&quot; class=&quot;text-dark&quot;&gt; &lt;img th:src=&quot;${item.imgUrl}&quot; class=&quot;card-img-top&quot; th:alt=&quot;${item.itemNm}&quot; height=&quot;400&quot;&gt; &lt;div class=&quot;card-body&quot;&gt; &lt;h4 class=&quot;card-title&quot;&gt;[[${item.itemNm}]]&lt;/h4&gt; &lt;p class=&quot;card-text&quot;&gt;[[${item.itemDetail}]]&lt;/p&gt; &lt;h3 class=&quot;card-title text-danger&quot;&gt;[[${item.price}]]원&lt;/h3&gt; &lt;/div&gt; &lt;/a&gt; &lt;/div&gt; &lt;/div&gt; &lt;/th:block&gt; &lt;/div&gt; &lt;div class=&quot;mt-5&quot;&gt; &lt;div th:utext=&quot;${pagination}&quot;&gt;&lt;/div&gt; &lt;/div&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.dev/braverokmc79/jpa-shop-test2">https://github.dev/braverokmc79/jpa-shop-test2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-25 22:50:40 ★스프링부트 Audiging 을 이용한 엔티티 공통 속성 공통 http://macaronics.net/index.php/m01/spring/view/2174 2174 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>1. AuditorAwareImpl</strong></span></p><pre class="brush:as3;">package com.shop.config; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import java.util.Optional; public class AuditorAwareImpl implements AuditorAware&lt;String&gt; { @Override public Optional&lt;String&gt; getCurrentAuditor() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String userId=&quot;&quot;; if (authentication!= null) { userId = authentication.getName(); } return Optional.of(userId); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>2.AuditConfig</strong></span></p><pre class="brush:as3;">package com.shop.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @Configuration /** JPA의 Audiging 기능을 활성화 합니다.*/ @EnableJpaAuditing public class AuditConfig { /** * 등록자와 수정자를 처리해주는 AuditorAware 을 빈으로 등록합니다. */ @Bean public AuditorAware&lt;String&gt; auditorProvider (){ return new AuditorAwareImpl(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>3.BaseTimeEntity</strong></span></p><pre class="brush:as3;">package com.shop.entity.base; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; /** * Auditing 를 적용하기 위해서 @EntityListeners 어노테이션 추가 */ @EntityListeners(value={AuditingEntityListener.class}) @MappedSuperclass @Getter @Setter public abstract class BaseTimeEntity { @CreatedDate @Column(updatable = false) private LocalDateTime regTime; @LastModifiedDate private LocalDateTime updateTime; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>4.BaseEntity</strong></span></p><pre class="brush:as3;">package com.shop.entity.base; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import lombok.Getter; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @EntityListeners(value={AuditingEntityListener.class}) @MappedSuperclass @Getter public abstract class BaseEntity extends BaseTimeEntity{ @CreatedBy @Column(updatable = false) private String createBy; @LastModifiedBy private String modifiedBy; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>5.적용</strong></span></p><pre class="brush:as3;">package com.shop.entity; import com.shop.constant.Role; import com.shop.dto.MemberFormDto; import com.shop.entity.base.BaseEntity; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.springframework.security.crypto.password.PasswordEncoder; @Entity @Table(name = &quot;member&quot;) @Getter @Setter @ToString public class Member extends BaseEntity { @Id @Column(name = &quot;member_id&quot;) @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Column(unique = true) private String email; private String password; private String address; @Enumerated(EnumType.STRING) private Role role; public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) { Member member = new Member(); member.setName(memberFormDto.getName()); member.setEmail(memberFormDto.getEmail()); member.setAddress(memberFormDto.getAddress()); String password=passwordEncoder.encode(memberFormDto.getPassword()); member.setPassword(password); member.setRole(Role.ROLE_USER); //member.setRole(Role.ROLE_ADMIN); return member; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>6.테스트</strong></span></p><pre class="brush:as3;">package com.shop.repository; import com.shop.entity.Member; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @Transactional @TestPropertySource(locations = &quot;classpath:application-test.properties&quot;) class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @PersistenceContext EntityManager em; @Test @DisplayName(&quot;Auditing 테스트&quot;) @WithMockUser(username = &quot;gildong&quot;, roles = &quot;USER&quot;) public void audiginTest(){ Member newMember =new Member(); memberRepository.save(newMember); em.flush(); em.clear(); Member member = memberRepository.findById(newMember.getId()).orElseThrow(EntityNotFoundException::new); System.out.println(&quot;register time : &quot;+member.getRegTime()); System.out.println(&quot;update time : &quot;+member.getUpdateTime()); System.out.println(&quot;create member : &quot;+member.getCreateBy()); System.out.println(&quot;modify member : &quot;+member.getModifiedBy()); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>===&gt;</p><p>register time : 2024-01-22T20:49:34.814453<br />update time : 2024-01-22T20:49:34.814453<br />create member : gildong<br />modify member : gildong</p><p>&nbsp;</p><p>&nbsp;</p><p>소스:</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-shop-test2">https://github.com/braverokmc79/jpa-shop-test2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-22 20:50:49 리액트 swiper 설정 http://macaronics.net/index.php/m04/react/view/2173 2173 <p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="429" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicRikuSKSVOVU1L72U2-_kwLEsYbXh5mYGbcXfvLVgwQlkw4xAwwOja-Jc0nLa9gSwwyRDmDYBgdR46YNhArteu4ZqqwUtcpRVXRpr8KdEJqhFWJZHrPV3mIDH8pf1fB-ErTSLmwag_NAQPRLndtKrQicjG7_-KayF9Az-ccPgPYylKFbT7lxdJNaNPp4s/s2423/2024-01-18%2018%2056%2020.png" /></p><p>&nbsp;</p><pre> npm i swiper</pre><p>&nbsp;</p><p><a target="_blank" href="https://swiperjs.com/react">https://swiperjs.com/react</a></p><pre class="brush:as3;">// import Swiper core and required modules import { Navigation, Pagination, Scrollbar, A11y } from &#39;swiper/modules&#39;; import { Swiper, SwiperSlide } from &#39;swiper/react&#39;; // Import Swiper styles import &#39;swiper/css&#39;; import &#39;swiper/css/navigation&#39;; import &#39;swiper/css/pagination&#39;; import &#39;swiper/css/scrollbar&#39;; export default () =&gt; { return ( &lt;Swiper // install Swiper modules modules={[Navigation, Pagination, Scrollbar, A11y]} spaceBetween={50} slidesPerView={3} navigation pagination={{ clickable: true }} scrollbar={{ draggable: true }} onSwiper={(swiper) =&gt; console.log(swiper)} onSlideChange={() =&gt; console.log(&#39;slide change&#39;)} &gt; &lt;SwiperSlide&gt;Slide 1&lt;/SwiperSlide&gt; &lt;SwiperSlide&gt;Slide 2&lt;/SwiperSlide&gt; &lt;SwiperSlide&gt;Slide 3&lt;/SwiperSlide&gt; &lt;SwiperSlide&gt;Slide 4&lt;/SwiperSlide&gt; ... &lt;/Swiper&gt; ); };</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">freeMode : false, // 슬라이드 넘길 때 위치 고정 여부 autoHeight : true,&nbsp; // 현재 활성 슬라이드높이 맞게 높이조정 a11y : false, // 접근성 매개변수(접근성 관련 대체 텍스트 설정이 가능)&nbsp; resistance : false, // 슬라이드 터치 저항 여부 slideToClickedSlide : true, // 해당 슬라이드 클릭시 슬라이드 위치로 이동 allowTouchMove : true, // (false-스와이핑안됨)버튼으로만 슬라이드 조작이 가능 watchOverflow : true // 슬라이드가 1개 일 때 pager, button 숨김 여부 설정 slidesOffsetBefore : number, // 슬라이드 시작 부분 여백 slidesOffsetAfter : number,&nbsp; &nbsp; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>EX) Row.js</strong></span></span></p><pre class="brush:as3;">import React, { useCallback, useEffect, useState } from &#39;react&#39; import { axiosInnstance } from &#39;../api/axios&#39;; import &#39;./Row.css&#39;; import MovieModal from &#39;./MovieModal&#39;; import { Autoplay, Navigation, Pagination, Scrollbar, A11y } from &#39;swiper/modules&#39;; import { Swiper, SwiperSlide } from &#39;swiper/react&#39;; import &#39;swiper/css&#39;; import &#39;swiper/css/navigation&#39;; import &#39;swiper/css/pagination&#39;; import &#39;swiper/css/scrollbar&#39;; import styled from &#39;styled-components&#39;; const Row = ({title, id, delay,fetchUrl}) =&gt; { const [movies, setMovies] = useState([]); const [modalOpen, setModalOpen] = useState(false); //클릭한 영화 정보 가져오기 const [movieSelected, setMovieSelection] = useState({}); const fetchMovieData= useCallback( async () =&gt; { const response= await axiosInnstance.get(fetchUrl, {}); setMovies(response.data.results); }, [fetchUrl] ); useEffect(() =&gt;{ fetchMovieData(); }, [fetchMovieData]); const handleClick=useCallback((movie)=&gt;{ console.log(&quot;movie &quot;,movie); setModalOpen(true); setMovieSelection(movie); }, [setModalOpen, setMovieSelection]); return ( &lt;Container&gt; &lt;h2&gt;{title}&lt;/h2&gt; &lt;Swiper modules={[Autoplay, Navigation, Pagination, Scrollbar, A11y]} spaceBetween={10} slidesPerView={5} navigation //arrow 버튼 사용유무 pagination={{ clickable: true }} // 페이지 버튼 사용여부 scrollbar={{ draggable: true }} // onSwiper={(swiper) =&gt; console.log(swiper)} // onSlideChange={() =&gt; console.log(&#39;slide change&#39;)} // loop={true} //loop 기능을 사용할지 여부 =&gt; 버전업 기본값 true 사용하면 waring speed={1500} //슬라이드 이동 속도 autoplay={{ delay: delay, disableOnInteraction: false, }} breakpoints={{ 1968: { slidesPerView: 6, slidesPerGroup: 6, }, 1568: { slidesPerView: 5, slidesPerGroup: 5, }, 1378: { slidesPerView: 4, slidesPerGroup: 4, }, 1060: { slidesPerView: 3, slidesPerGroup: 3, }, 756: { slidesPerView: 2, slidesPerGroup: 2, }, 456: { slidesPerView:1, slidesPerGroup: 1, }, }} className=&quot;mySwiper&quot; &gt; {movies.map((movie, index) =&gt; ( &lt;SwiperSlide className=&#39;row__poster&#39; key={index}&gt; &lt;img key={movie.id} src={`https://image.tmdb.org/t/p/w300${movie.backdrop_path}`} alt={movie.title} onClick={()=&gt;handleClick(movie)} /&gt; &lt;/SwiperSlide&gt; ))} &lt;/Swiper&gt; {modalOpen &amp;&amp; &lt;MovieModal {...movieSelected} setModalOpen={setModalOpen} /&gt; } &lt;/Container&gt; ) } export default Row const Container=styled.div` padding: 0, 0 30px; `; </pre><p>&nbsp;</p><p>&nbsp;</p><p>Row.css</p><pre class="brush:as3;">.slider { position: relative; scroll-behavior: smooth !important; } .slider__arrow-left { background-clip: content-box; padding: 20px 0; box-sizing: border-box; transition: 400ms all ease-in-out; cursor: pointer; width: 80px; z-index: 1000; position: absolute; left: 0; top: 0; height: 100%; display: flex; align-items: center; justify-content: center; visibility: hidden; } .slider__arrow-right { padding: 20px 0; background-clip: content-box; box-sizing: border-box; transition: 400ms all ease-in-out; cursor: pointer; width: 80px; z-index: 1000; position: absolute; right: 0; top: 0; height: 100%; display: flex; align-items: center; justify-content: center; visibility: hidden; } .arrow { transition: 400ms all ease-in-out; font-size: 38px; font-weight: bold; } .arrow:hover { transition: 400ms all ease-in-out; transform: scale(1.5); } .slider:hover .slider__arrow-left { transition: 400ms all ease-in-out; visibility: visible; } .slider:hover .slider__arrow-right { transition: 400ms all ease-in-out; visibility: visible; } .slider__arrow-left:hover { background: rgba(20, 20, 20, 0.5); transition: 400ms all ease-in-out; } .slider__arrow-right:hover { background: rgba(20, 20, 20, 0.5); transition: 400ms all ease-in-out; } .row__posters { display: flex; overflow-y: hidden; overflow-x: scroll; padding: 20px 0 20px 20px; scroll-behavior: smooth !important; cursor: pointer; } .row__posters::-webkit-scrollbar { display: none; } .row__poster { object-fit: contain; width: 100%; max-height: 144px; margin-right: 10px; transition: transform 450ms; border-radius: 4px; } .row__poster:hover { transform: scale(1.08); } .row__posterLarge { max-height: 320px; } .row__posterLarge:hover { transform: scale(1.1); opacity: 1; } .row__arrow-left { position: absolute; top: 0; left: 20px; height: 100%; width: 32px; background: rgba(0, 0, 0, 0.2); display: flex; align-items: center; } .row__arrow-right { position: absolute; top: 0; right: 0px; height: 100%; width: 32px; background: rgba(0, 0, 0, 0.2); display: flex; align-items: center; } @media screen and (min-width: 1200px) { .row__poster { max-height: 160px; } .row__posterLarge { max-height: 360px; } } @media screen and (max-width: 768px) { .row__poster { max-height: 100px; } .row__posterLarge { max-height: 280px; } } .swiper-pagination{ text-align: right !important; } .swiper-pagination-bullet{ background: gray !important; opacity: 1 !important; } .swiper-pagination-bullet-active{ background-color: #fff !important; } .swiper-horizontal { text-align: center !important; } .swiper .swiper-button-next, .swiper .swiper-button-prev{ color: #fff; } .row__poster{ cursor: pointer; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/react-disney-plus-app">https://github.com/braverokmc79/react-disney-plus-app</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-18 19:00:18 리액트, 모달 창 외부 클릭시 영화 상세 페이지 구현하기 http://macaronics.net/index.php/m04/react/view/2172 2172 <p>&nbsp;</p><p>&nbsp;</p><p>훅 만들기</p><p><span style="font-size:22px"><strong>src/hooks/useOnClickOutside.js</strong></span></p><pre class="brush:as3;">import { useEffect } from &#39;react&#39; const useOnClickOutside = (ref, handler) =&gt; { useEffect(() =&gt; { const listener=(event)=&gt;{ if(!ref.current || ref.current.contains(event.target)){ console.log(&#39;outside click&#39;); return; } handler(); }; document.addEventListener(&#39;mousedown&#39;, listener); document.addEventListener(&#39;touchstart&#39;, listener); return()=&gt;{ document.removeEventListener(&#39;mousedown&#39;, listener); document.removeEventListener(&#39;touchstart&#39;, listener); } }, [ref, handler]); } export default useOnClickOutside;</pre><p>&nbsp;</p><p>&nbsp;</p><p>modal.</p><p>index.js</p><pre class="brush:as3;">import React, { useEffect, useRef } from &#39;react&#39; import &#39;./MovieModal.css&#39;; import useOnClickOutside from &#39;../../hooks/useOnClickOutside&#39;; const MovieModal = ({ adult, backdrop_path, genre_ids, id, original_language, original_title, overview, popularity, poster_path, release_date, title, video, vote_average, setModalOpen }) =&gt; { const ref=useRef(); useOnClickOutside(ref, ()=&gt;{setModalOpen(false)}); return ( &lt;div className=&#39;presentation&#39; role=&quot;presentation&quot;&gt; &lt;div className=&#39;wrapper-modal&#39;&gt; &lt;div className=&#39;modal&#39; ref={ref} &gt; &lt;span onClick={()=&gt;setModalOpen(false)} className=&#39;modal-close&#39;&gt;x&lt;/span&gt; &lt;img className=&#39;modal__poster-img&#39; src={`https://image.tmdb.org/t/p/original/${backdrop_path}`} alt=&quot;modal-img&quot; /&gt; &lt;div className=&#39;modal__content&#39;&gt; &lt;p className=&#39;modal_details&#39;&gt; &lt;span className=&#39;modal__user_perc&#39;&gt;100% for you&lt;/span&gt;{&quot; &quot;} {release_date} &lt;/p&gt; &lt;h2 className=&#39;modal__title&#39;&gt;{title ? title : original_title }&lt;/h2&gt; &lt;p className=&#39;modal__overview&#39;&gt;평점 : {vote_average}&lt;/p&gt; &lt;p className=&#39;modal__overview&#39;&gt;{overview}&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; ) } export default MovieModal</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/react-disney-plus-app">https://github.com/braverokmc79/react-disney-plus-app</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-18 17:08:29 리액트 TMDB 가로 이미지 가져오 http://macaronics.net/index.php/m04/react/view/2171 2171 <p>&nbsp;</p><p>가로형 이미지 목록을 가져온 후</p><pre class="brush:as3;"> https://api.themoviedb.org/3/movie/1071215/images?api_key=0a08e3348b874d0aa2d426ffc04357069d&amp;language=en-US&amp;include_image_language=en </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> const {data: widthImageList}= await axiosWidthImageInnstance.get(`${movieId}/images`,{}); setBackgroundPositionPoster( widthImageList?.backdrops[0]?.file_path);</pre><p>&nbsp;</p><p>목록중 하나를 선택해서&nbsp;backdrops 데이터를 가져오면된다. 다음은 0번째 배열값 데이터를 가져오는 코드이다.</p><p>&nbsp;</p><pre class="brush:as3;"> //가로형 이미지 목록 가져오기 //https://api.themoviedb.org/3/movie/1071215/images/?api_key=08d90cc4e7968b1f8323e51588a0d42cf06&amp;language=en-US&amp;include_image_language=en //https://api.themoviedb.org/3/movie/1071215/images?api_key=0a08e38b874d0aa2d42326ffc04357069d&amp;language=en-US&amp;include_image_language=en const {data: widthImageList}= await axiosWidthImageInnstance.get(`${movieId}/images`,{}); setBackgroundPositionPoster( widthImageList?.backdrops[0]?.file_path); // console.log(&quot;widthImageList ==&gt; &quot;, widthImageList.backdrops); // console.log(&quot;한개 가져오기 widthImageList ==&gt; &quot;,movieId , widthImageList.backdrops[0].file_path);</pre><p>&nbsp;</p><p>&nbsp;</p><p>github =&gt;&nbsp;react-disney-plus-app 프로젝트</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>다음 전체 코드</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Banner.js</strong></p><pre class="brush:as3;">import React, { useEffect, useS tate } from &#39;react&#39; import {axiosInnstance, axiosWidthImageInnstance} from &#39;../api/axios&#39;; import requests from &#39;../api/requests&#39;; import &#39;./Banner.css&#39;; import styled from &#39;styled-components&#39;; // const fetch = require(&#39;node-fetch&#39;); // const url = &#39;https://api.themoviedb.org/3/movie/movie_id?language=en-US&#39;; // const options = {method: &#39;GET&#39;, headers: {accept: &#39;application/json&#39;}}; // fetch(url, options) // .then(res =&gt; res.json()) // .then(json =&gt; console.log(json)) // .catch(err =&gt; console.error(&#39;error:&#39; + err)); const Container =styled.div` display: flex; justify-content: center; align-items: center; flex-direction: column; width: 100%; height: 100vh; `; const HomeContainer =styled.div` width: 100%; height: 100%; `; const Iframe =styled.iframe` width: 100%; height: 100%; z-index: -1; opacity: 0.65; border:none; &amp;::after{ content:&quot;&quot;; position: absolute; top: 0; left:0; width: 100%; height: 100%; } ` const Banner = () =&gt; { const [movie, setMovie] = useState([]); const [isClicked, setIsClicked] = useState(false); const [backgroundPositionPoster, setBackgroundPositionPoster] =useState(&quot;&quot;); useEffect(() =&gt; { fetchData(); }, []); const fetchData = async () =&gt; { //현재 상영중인 영화 정보를 가져오기(여러 영화) //const response=await axiosInnstance.get(requests.fetchPopularMovies); //fetchNowPlaying const response=await axiosInnstance.get(requests.fetchNowPlaying); // console.log(&quot;response ==&gt; &quot; , response.data.results); //여러 영화 중 영화 하나의 ID를 가져오기 const movieId=response.data.results[ Math.floor(Math.random() * response.data.results.length) ].id; //console.log(&quot;movieId ==&gt; &quot; ,movieId); //특정 영화의 더 상세한 정보를 가져오기 //https://api.themoviedb.org/3/movies/848326 const {data:movieDetail}=await axiosInnstance.get(`/movie/${movieId}`,{ params:{append_to_response:&quot;videos&quot;} }); console.log(&quot;widthImageList&quot;, movieId); //가로형 이미지 목록 가져오기 //https://api.themoviedb.org/3/movie/1071215/images/?api_key=08d90cc4e7964348b1f8e51588a0d42cf06&amp;language=en-US&amp;include_image_language=en //https://api.themoviedb.org/3/movie/1071215/images?api_key=0a08e3348b874d0aa2d426ffc04357069d&amp;language=en-US&amp;include_image_language=en const {data: widthImageList}= await axiosWidthImageInnstance.get(`${movieId}/images`,{}); setBackgroundPositionPoster( widthImageList?.backdrops[0]?.file_path); // console.log(&quot;widthImageList ==&gt; &quot;, widthImageList.backdrops); // console.log(&quot;한개 가져오기 widthImageList ==&gt; &quot;,movieId , widthImageList.backdrops[0].file_path); setMovie(movieDetail); } //https://image.tmdb.org/t/p/original/iNgn9LP0iMuLSnWqolivcY3Y7F6.jpg //https://image.tmdb.org/t/p/w1280/qzgEPduJyQ6RkgMdn4nbjdKUYJM.jpg //https://api.themoviedb.org/3/movie//buvBq2zLP7CcJth8tjrI4znvfEO/images //`url(https://image.tmdb.org/t/p/original${movie.poster_path})` const truncate=(str, n) =&gt; { return str?.length &gt; n? str.substr(0, n) + &#39;...&#39; : str; }; if(isClicked){ return ( &lt;&gt; &lt;Container &gt; &lt;HomeContainer&gt; &lt;Iframe src={`https://youtube.com/embed/${movie?.videos?.results[0]?.key}?controls=0&amp;loop=1&amp;mute=1&amp;playlist=${movie?.videos?.results[0]?.key}`} width=&quot;640&quot; height=&quot;360&quot; allow=&#39;autoplay; fullscreen&#39; &gt; &lt;/Iframe&gt; &lt;/HomeContainer&gt; &lt;/Container&gt; &lt;button onClick={()=&gt;setIsClicked(false)} className=&#39;btn-close&#39;&gt;X&lt;/button&gt; &lt;/&gt; ) }else{ console.log(&quot;movie ==&gt; &quot;, movie); return ( &lt;header className=&#39;banner&#39; style={{ // backgroundImage: `url(https://image.tmdb.org/t/p/original${movie.poster_path})` , backgroundImage: `url(https://image.tmdb.org/t/p/original${backgroundPositionPoster})`, backgroundPosition:&quot;top center&quot;, backgroundSize:&quot;100% 100%&quot;, backgroundRepeat:&quot;no-repeat&quot;, }} &gt; &lt;div className=&#39;banner__contents&#39;&gt; &lt;h1 className=&#39;banner__title&#39;&gt; {movie.title || movie.name || movie.original_name} &lt;/h1&gt; &lt;div className=&#39;banner__buttons&#39;&gt; {movie?.videos?.results[0]?.key&amp;&amp; &lt;button className=&#39;banner__button play&#39; onClick={()=&gt;setIsClicked(true)} &gt;Play&lt;/button&gt; } &lt;/div&gt; &lt;p className=&#39;banner__description&#39;&gt; {truncate(movie?.overview, 100)} &lt;/p&gt; &lt;/div&gt; &lt;div className=&#39;bannner--fadeBottom&#39; /&gt; &lt;/header&gt; ) } } export default Banner </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>requests.js</strong></p><pre class="brush:as3;">const requests = { fetchNowPlaying: &quot;/movie/now_playing&quot;, fetchTrending: &quot;/trending/all/week&quot;, fetchTopRated: &quot;/movies/top_rated&quot;, fetchActionMovies: &quot;/discover/movie?with_genres=28&quot;, fetchComedyMovies: &quot;/discover/movie?with_genres=35&quot;, fetchHorrorMovies: &quot;/discover/movie?with_genres=27&quot;, fetchRomanceMovies: &quot;/discover/movie?with_genres=10749&quot;, fetchDocumentaryMovies: &quot;/discover/movie?with_genres=99&quot;, fetchThrillerMovies: &quot;/discover/movie?with_genres=12&quot;, fetchPopularMovies: &quot;/movie/popular&quot;, } export default requests ; </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>axios.js</strong></p><pre class="brush:as3;">import axios from &#39;axios&#39; export const axiosInnstance = axios.create({ baseURL: &#39;https://api.themoviedb.org/3&#39;, params: { api_key: &#39;08d90cc4e7968b1f8e515883213a0d42cf06&#39;, language: &#39;ko-KR&#39; } }); //가로 이미지 목록 가졀오기 //https://api.themoviedb.org/3/movie/1071215/images?api_key=0a08e22338b874d0aa2d426ffc04357069d&amp;language=en-US&amp;include_image_language=en export const axiosWidthImageInnstance = axios.create({ baseURL: &#39;https://api.themoviedb.org/3/movie/&#39;, params: { api_key: &#39;08d90cc4e7968b1f8e5158823123a0d42cf06&#39;, language: &#39;en-US&#39;, include_image_language:&#39;en&#39; } }); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-17 10:41:55 리액트 글자 생략 truncate 만들기 . 리액트 Iframe http://macaronics.net/index.php/m04/react/view/2170 2170 <p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> const truncate=(str, n) =&gt; { return str?.length &gt; n? str.substr(0, n) + &#39;...&#39; : str; }; </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> const Iframe =styled.iframe` width: 100%; height: 100%; z-index: -1; opacity: 0.65; border:none; &amp;::after{ content:&quot;&quot;; position: absolute; top: 0; left:0; width: 100%; height: 100%; } ` &lt;Iframe src={`https://youtube.com/embed/${movie?.videos?.results[0]?.key}?controls=0&amp;loop=1&amp;mute=1&amp;playlist=${movie?.videos?.results[0]?.key}`} width=&quot;640&quot; height=&quot;360&quot; allow=&#39;autoplay; fullscreen&#39; &gt; &lt;/Iframe&gt;</pre><p>&nbsp;</p> 2024-01-15 15:05:29 영화 api 주소 http://macaronics.net/index.php/m05/computer/view/2169 2169 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><a target="_blank" href="http://www.omdbapi.com/">http://www.omdbapi.com/</a></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><a target="_blank" href="https://www.themoviedb.org/">https://www.themoviedb.org/</a></strong></span></p><p>&nbsp;</p><p><strong><span style="font-size:20px"><a target="_blank" href="https://developer.themoviedb.org/reference/genre-movie-list">https://developer.themoviedb.org/reference/genre-movie-list</a></span></strong></p> 2024-01-14 17:11:57 git + github 사용법 , 버전관리 http://macaronics.net/index.php/m05/computer/view/2168 2168 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>//한칸뒤로 되돌리기</strong></span><br />git reset --hard HEAD~1&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">//다시 원래상태로 되돌리기 원상복구</span></strong><br />git reset --hard ORIG_HEAD</p><p><br />git log --oneline</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">//2칸뒤로 되돌리기</span></strong><br />git reset --hard HEAD~2</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>//다시 원래상태로 되돌리기 원상복구</strong><br />git reset --hard ORIG_HEAD</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>//브렌치 추가</strong></span><br />git branch purple</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>//브렌치 확인</strong></span><br />$ git branch</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>//purple 브렌치로 이동</strong></span><br />$ git checkout purple</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>***** git purple 브렌치만 &nbsp;가져오기 *****</strong></span><br />$ git clone https://github.com/braverokmc79/git-practice.git</p><p>$ git branch -r<br />&nbsp; origin/HEAD -&gt; origin/master<br />&nbsp; origin/master<br />&nbsp; origin/purple</p><p>$ git checkout -t orign/purple</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>***** git master 브렌츠로 이동 후 purple 브렌치 삭제 *****</strong></span><br />$ git checkout master<br />$ git branch -d purple</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>//브렌치 확인</strong></span><br />$ git branch</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>*****1) git yellow 브렌츠 생성후 이동 *****</strong></span><br />$ git branch yellow</p><p>$ git checkout yellow</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>*****2) git yellow 브렌츠 생성후 이동 *****</strong></span><br />git checkout -b yellow</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>=======연습-충돌(conflict) -로컬 병합(Merge)</strong></span></p><p><span style="font-size:20px"><strong>//git push 시도시 충돌 오류</strong></span><br />&nbsp;/e/study/React/git-practice (master)<br />$ git push origin master<br />To https://github.com/braverokmc79/git-practice.git<br />&nbsp;! [rejected] &nbsp; &nbsp; &nbsp; &nbsp;master -&gt; master (fetch first)<br />error: failed to push some refs to &#39;https://github.com/braverokmc79/git-practice.git&#39;<br />hint: Updates were rejected because the remote contains work that you do not<br />hint: have locally. This is usually caused by another repository pushing to<br />hint: the same ref. If you want to integrate the remote changes, use<br />hint: &#39;git pull&#39; before pushing again.<br />hint: See the &#39;Note about fast-forwards&#39; in &#39;git push --help&#39; for details.</p><p>/e/study/React/git-practice (master)</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>// 뒤로 한칸 되돌리기</strong></span></p><p><br />$ git reset --hard HEAD~1<br />HEAD is now at d01fa91 4</p><p><br /><span style="font-size:20px"><strong>========================================================&gt;<br />다음과 같이 pull 시도시 충돌 날경우</strong></span></p><p>$ git pull origin master<br />remote: Enumerating objects: 5, done.<br />remote: Counting objects: 100% (5/5), done.<br />remote: Compressing objects: 100% (1/1), done.<br />remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0<br />Unpacking objects: 100% (3/3), 266 bytes | 4.00 KiB/s, done.<br />From https://github.com/braverokmc79/git-practice<br />&nbsp;* branch &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;master &nbsp; &nbsp; -&gt; FETCH_HEAD<br />&nbsp; &nbsp;d01fa91..9166a1f &nbsp;master &nbsp; &nbsp; -&gt; origin/master<br />Updating c60c1a6..9166a1f<br />Fast-forward<br />&nbsp;index.html | 2 +-<br />&nbsp;1 file changed, 1 insertion(+), 1 deletion(-)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br />========================================================&gt;<br /><strong><span style="color:#c0392b">git pull 에러 해결 (Your local changes to the following files would be overwritten by merge ) -git&nbsp;stash</span></strong></p><p><br />출처: https://goddaehee.tistory.com/253 [갓대희의 작은공간:티스토리]</p><p><a target="_blank" href="https://goddaehee.tistory.com/253">https://goddaehee.tistory.com/253</a></p><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-12 16:31:16 JavaScript 쓰로틀(throttle)과 디바운스(debounce) http://macaronics.net/index.php/m04/javascript/view/2167 2167 <p>&nbsp;</p><p><span style="font-size:22px"><strong>쓰로틀 (Throttle)</strong></span></p><p>먼저, 쓰로틀은 이벤트를 일정 주기로 발생시키는것을 목적으로 합니다.<br />대표적으로 사용되는 예는 포털이나 쇼핑몰과 같이 검색이 필요한 사이트에서 유저가 검색을 위해 키보드 이벤트가 발생했을 때 추천검색어를 보여주는 경우입니다.</p><p>키보드 이벤트의 경우 짧은 시간에 많이 발생하는 이벤트입니다.<br />특히, 한글의 경우 자음과 모음을 조합하는 형태로 사용하기 때문에 예를 들어 &quot;언제&quot;라는 단어를 검색한다고 한다면&nbsp;ㅇ, 어, 언, 얹, 언제에서 모든 키 입력이 발생하고&nbsp;ㅇ, 어, 얹&nbsp;과 같은 단어는 굳이 호출할 필요가 없는 단어입니다.</p><p>&nbsp;</p><pre class="brush:as3;">&lt;script&gt; const inputBox = document.querySelector(&quot;#updateMonthlyBranchTxt&quot;); //Throttle let timer; function throttle(callbackFn, timeout) { if(!timer) { timer = setTimeout(() =&gt; { timer = null; callbackFn(); }, timeout); } } inputBox.addEventListener(&quot;keyup&quot;, (e) =&gt; { const content=e.target.value; throttle(() =&gt;{ $.ajax({ url: &quot;sub_017_ok.php&quot;, type:&quot;post&quot;, data: { content }, success:function(data){ console.log(&quot;data : &quot;,data); data = JSON.parse(data); console.log(&quot;data : &quot;,data); }, error:function(error){ console.log(&quot;error : &quot;,error); } }); },500); }); &lt;/script&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:22px">디바운스 (Debounce)</span></strong></p><p>디바운스는&nbsp;<strong>일정시간 내에 반복적으로 이벤트가 발생했을 때 이벤트 횟수와 관계없이 마지막 이벤트가 발생한 후 일정시간 뒤에 콜백함수를 한번만 호출</strong>하는 기법입니다.</p><p>대표적으로 resize 이벤트의 경우 조금만 사이즈를 변경해도 이벤트가 어마어마하게 발생합니다.<br />만약, 이 이벤트가 발생했을 때마다 무거운 연산이 필요한 로직이 콜백함수에 포함되어 있다면 이는 성능에 무리를 줄 수 있습니다.</p><p>&nbsp;</p><pre class="brush:as3;">et timer; function debounce(callbackFn, timeout) { if(timer) { clearTimeout(timer); } timer = setTimeout(() =&gt; { callbackFn(); }, timeout); } window.addEventListener(&quot;resize&quot;, () =&gt; { console.log(&quot;event&quot;); debounce(() =&gt; console.log(&quot;debounce!&quot;), 200); });</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>출처:</p><p><a target="_blank" href="https://velog.io/@sisofiy626/JavaScript-%EC%93%B0%EB%A1%9C%ED%8B%80throttle%EA%B3%BC-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4debounce">https://velog.io/@sisofiy626/JavaScript-%EC%93%B0%EB%A1%9C%ED%8B%80throttle%EA%B3%BC-%EB%94%94%EB%B0%94%EC%9A%B4%EC%8A%A4debounce</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>리액트에서&nbsp; &nbsp;useDebounce Cusotm Hooks 만들기</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>src/hooks/useDebounce.js</strong></p><pre class="brush:as3;"> import { useEffect, useState } from &#39;react&#39; export const useDebounce=(value, delay) =&gt; { const [debounceValue, setDebounceValue] = useState(value); useEffect(() =&gt; { const handler = setTimeout(() =&gt; { setDebounceValue(value); }, delay); return()=&gt;{ clearTimeout(handler); } },[value, delay]); return debounceValue; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">import _ from &#39;lodash&#39;; import React, { useEffect, useState } from &#39;react&#39; import { useLocation, useNavigate } from &#39;react-router-dom&#39;; import { axiosInnstance } from &#39;../../api/axios&#39;; import &#39;./SearchPage.css&#39;; import { useDebounce } from &#39;../../hooks/useDebounce&#39;; //Throttle // let timer; // function debounce(callbackFn, timeout) { // if(timer) { // clearTimeout(timer); // } // timer = setTimeout(() =&gt; { // callbackFn(); // }, timeout); // } const SearchPage = () =&gt; { const [searchResults, setSearchResults] = useState([]); const navigate=useNavigate(); const useQuery = () =&gt; { return new URLSearchParams(useLocation().search); }; let query=useQuery(); //const searchTerm=query.get(&#39;q&#39;); const debounceSearchTerm=useDebounce(query.get(&#39;q&#39;), 500); useEffect(() =&gt; { if(debounceSearchTerm){ fetchSearchMovie(debounceSearchTerm); } }, [debounceSearchTerm]); const fetchSearchMovie = async(debounceSearchTerm) =&gt; { try { const response=await axiosInnstance.get(`/search/multi?include_adult=false&amp;query=${debounceSearchTerm}`); setSearchResults(response.data.results); } catch (error) { console.error(&#39;error &#39;,error); } } if(searchResults.length &gt; 0){ return ( &lt;section className=&#39;search-container&#39;&gt; { searchResults.map((movie) =&gt; { if(movie.backdrop_path!=null &amp;&amp; movie.media_type!==&quot;person&quot;){ const moiveImageUrl=`https://image.tmdb.org/t/p/w500${movie.backdrop_path}`; return ( &lt;div className=&#39;movie&#39; key={movie.id}&gt; &lt;div className=&#39;movie__column-poster&#39; onClick={()=&gt;navigate(`/${movie.id}`)}&gt; &lt;img src={moiveImageUrl} alt={movie.title} className=&#39;movie__poster&#39; /&gt; &lt;/div&gt; &lt;/div&gt; ); }else{ return null; } }) } &lt;/section&gt; ) }else{ return ( &lt;section className=&#39;no-results&#39;&gt; &lt;div className=&#39;no-results__text&#39;&gt; &lt;p&gt; 찾고자하는 검색어 &quot;{debounceSearchTerm}&quot; 에 맞는 영화가 없습니다. &lt;/p&gt; &lt;/div&gt; &lt;/section&gt; ) } } export default SearchPage</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-11 15:18:32 자바스크립트 추천 라이브러 http://macaronics.net/index.php/m04/javascript/view/2166 2166 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:22px"><strong>1.lodash</strong></span></span></p><p>lodash는 JavaScript의 인기 있는 라이브러리 중 하나입니다.&nbsp;<br />보통의 경우 array, collection, date 등 데이터의 필수적인 구조를 쉽게 다루기 위해 사용합니다. JavaScript에서 배열 안의 객체들의 값을 handling(배열, 객체 및 문자열 반복 / 복합적인 함수 생성) 할 때 유용합니다.&nbsp;<br />이런 점으로 인해 JavaScript의 코드를 줄여주고, 빠른 작업에 도움이 됩니다. 특히 frontend 환경에서 많이 쓰입니다.</p><p><br />throttle&nbsp;&nbsp; 사용</p><p><br />공식문서<br /><a target="_blank" href="https://lodash.com/">https://lodash.com/</a></p><p>cdn<br /><a target="_blank" href="https://cdnjs.com/libraries/lodash.js">https://cdnjs.com/libraries/lodash.js</a></p><p>사용법<br /><a target="_blank" href="https://velog.io/@kysung95/짤막글-lodash-알고-쓰자">https://velog.io/@kysung95/짤막글-lodash-알고-쓰자</a></p><p>&nbsp;</p><p>예</p><pre class="brush:as3;">window.addEventListener(&quot;scroll&quot;, _.throttle(function(){ if(window.scrollY &gt; 500){ //버튼 보이기! gsap.to(&quot;#to-top&quot;,.2, { x:0, }); }else{ //버튼 숨기기! gsap.to(&quot;#to-top&quot;,.2, { x:100, }); } }, 300)); function toTop(event){ console.log(event); gsap.to(window,.7, { scrollTo:0, }); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:22px"><strong>2. lowdb &nbsp;&nbsp;</strong></span></span><br />JSON이라는 파일 형식을 이용하여 데이터를 저장하는 아주 간단한 데이터베이스입니다.&nbsp;<br />JSON이란 숫자, 문자열, 배열, 객체, Boolean으로만 이루어진 데이터 파일이기 때문에, 대부분의 컴퓨터 언어에서 손쉽게 만들고 사용할 수 있습니다.</p><p><br />공식문서<br /><a target="_blank" href="https://github.com/typicode/lowdb">https://github.com/typicode/lowdb</a></p><p>cdn<br /><a target="_blank" href="https://www.jsdelivr.com/package/npm/lowdb">https://www.jsdelivr.com/package/npm/lowdb</a></p><p>사용법<br /><a target="_blank" href="https://goodmemory.tistory.com/94">https://goodmemory.tistory.com/94</a></p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="color:#2980b9"><span style="font-size:22px"><strong>3.gsap &nbsp;: 애니메이션 라이브러리</strong></span></span></p><p><br />프론트엔드 개발자와 디자이너들이 쉽게 사용할 수 있는 타임라인 기반의 애니메이션 자바스크립트 라이브러리이다.</p><p>CSS와 바닐라 자바스크립트만으로도 동적인 화면을 만들 수 있지만 GSAP은 세밀한 움직임과 동작의 연속성을 훨씬 간편하게 설정할 수 있다.</p><p>&nbsp;</p><p>공식문서<br /><a target="_blank" href="https://gsap.com/docs/v3/">https://gsap.com/docs/v3/</a></p><p><br />cdn<br /><a target="_blank" href="https://cdnjs.com/libraries/gsap">https://cdnjs.com/libraries/gsap</a></p><p>사용법<br /><a target="_blank" href="https://velog.io/@kimheewon/GSAP-애니메이션-사용법기초">https://velog.io/@kimheewon/GSAP-애니메이션-사용법기초</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:22px"><span style="color:#2980b9">4.swiper</span></span></strong><br />Swiper.js란 모바일 및 데스크탑에 사용하기 적합한 오픈소스 JavaScript 라이브러리입니다.<br />간단한 HTML, CSS, JS 코드로 반응형 슬라이드쇼 및 스와이퍼 기능을 쉽게 구현할 수 있습니다.</p><p><br />공식문서<br /><a target="_blank" href="https://swiperjs.com/">https://swiperjs.com/</a></p><p><br />cdn<br /><a target="_blank" href="https://swiperjs.com/get-started">https://swiperjs.com/get-started</a></p><p><br />사용법<br /><a target="_blank" href="https://shadesign.tistory.com/entry/swiper-slide-총정리사용법-적용-옵션">https://shadesign.tistory.com/entry/swiper-slide-총정리사용법-적용-옵션</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:22px"><strong>5.ScrollMagic</strong></span></span></p><p>웹퍼블리싱 프로젝트를 하다 보면 고객사의 요청에 의해 스크롤에 따른 애니메이션(동적인 화면)을 구현해야 할 경우가 있습니다.<br />완성도 높고 화려한 메인 페이지를 만들기 위해서 스크롤 애니메이션을 요청하는 경우도 있습니다.<br />출처: https://www.biew.co.kr/entry/ScrollMagic-스크롤매직-라이브러리-설치-방법-및-동작원리-제-1편 [웹퍼블리싱 - 퍼블리싱 이야기 맑은커뮤니케이션:티스토리]</p><p><br />공식문서<br /><a target="_blank" href="https://scrollmagic.io/docs/">https://scrollmagic.io/docs/</a></p><p><br />cdn<br /><a target="_blank" href="https://scrollmagic.io/">https://scrollmagic.io/</a></p><p><br />사용법<br /><a target="_blank" href="https://nykim.work/30">https://nykim.work/30</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>스타벅스 소스</p><p><a target="_blank" href="https://braverokmc79.github.io/starbucks/">https://braverokmc79.github.io/starbucks/</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.dev/braverokmc79/starbucks">https://github.dev/braverokmc79/starbucks</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-10 20:06:10 css 16.9 비율 유지 - 유튜브 동영상 설정 http://macaronics.net/index.php/m05/css3/view/2165 2165 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;div class=&quot;container&quot;&gt; &lt;div class=&quot;item&quot;&gt;&lt;/div&gt; &lt;/div&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">.container .item{ width: 100%; height: 0; padding-top: 56.25%; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>=============================&nbsp; &nbsp;=============================</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>html</strong></span></p><pre class="brush:as3;"> &lt;!-- YOUTUBE VIDEO --&gt; &lt;section class=&quot;youtube&quot;&gt; &lt;div class=&quot;youtube__area&quot;&gt; &lt;div id=&quot;player&quot;&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;youtube__cover&quot;&gt;&lt;/div&gt; &lt;div class=&quot;inner&quot;&gt; &lt;img src=&quot;./images/floating1.png&quot; alt=&quot;Icon&quot; class=&quot;floating floating1&quot;&gt; &lt;img src=&quot;./images/floating2.png&quot; alt=&quot;Icon&quot; class=&quot;floating floating2&quot;&gt; &lt;img src=&quot;./images/floating3.png&quot; alt=&quot;Icon&quot; class=&quot;floating floating3&quot;&gt; &lt;/div&gt; &lt;/section&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>css</strong></span></p><pre class="brush:as3;">/* YOUTUBE VIDEO */ .youtube{ position: relative; height: 700px; background-color: #333; overflow: hidden; } .youtube .youtube__area{ width: 1920px; background-color: orange; position: absolute; left: 50%; margin-left: calc(1920px / -2); top: 50%; margin-top: calc(1920px * 9 / 16 / -2); } .youtube .youtube__area::before{ content: &quot;&quot;; display: block; width: 100%; height: 0; padding-top:56.25% } .youtube .youtube__cover{ background-image: url(&quot;../images/video_cover_pattern.png&quot;); background-color: rgba(0,0,0,.3); position: absolute; top:0; left: 0; width: 100%; height: 100%; } #player{ position: absolute; top: 0; left: 0; width: 100%; height: 100%; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px">js</span></p><p><a target="_blank" href="https://developers.google.com/youtube/iframe_api_reference?hl=ko">https://developers.google.com/youtube/iframe_api_reference?hl=ko</a></p><pre class="brush:as3;"> // 2. This code loads the IFrame Player API code asynchronously. var tag = document.createElement(&#39;script&#39;); tag.src = &quot;https://www.youtube.com/iframe_api&quot;; var firstScriptTag = document.getElementsByTagName(&#39;script&#39;)[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); // 3. This function creates an &lt;iframe&gt; (and YouTube player) // after the API code downloads. var player; function onYouTubeIframeAPIReady() { player = new YT.Player(&#39;player&#39;, { videoId: &#39;An6LvWQuj_8&#39;,//최초 재생할 유튜브 ID playerVars: { autoplay: true, //자동 재생 유무 loop: true,//반복 재생 유무 playlist:&#39;An6LvWQuj_8&#39; }, events:{ onReady: function(event) { event.target.mute(); //음소거 } } }); } onYouTubeIframeAPIReady(); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :<a target="_blank" href="https://github.com/braverokmc79/starbucks">https://github.com/braverokmc79/starbucks</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-09 10:36:42 리액트 redux 5 이상에서 createStore 미들웨어 추가시 오류 http://macaronics.net/index.php/m04/react/view/2164 2164 <p>&nbsp;</p><p>&nbsp;</p><p>리덕스 문서</p><p>&nbsp;</p><p><a target="_blank" href="https://redux.js.org/usage/server-rendering">https://redux.js.org/usage/server-rendering</a></p><p>&nbsp;</p><p>// Compile an initial state</p><p>&nbsp;</p><pre class="brush:as3;">let preloadedState = { counter: 0, };</pre><p>&nbsp;</p><p>preloadedState 추가해 준다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">import React from &#39;react&#39;; import ReactDOM from &#39;react-dom/client&#39;; import &#39;./index.css&#39;; import App from &#39;./App&#39;; import reportWebVitals from &#39;./reportWebVitals&#39;; import { applyMiddleware, legacy_createStore as createStore } from &#39;redux&#39;; import rootReducer from &#39;./reducers&#39;; import { Provider } from &#39;react-redux&#39;; import {thunk} from &#39;redux-thunk&#39;; const root = ReactDOM.createRoot( document.getElementById(&#39;root&#39;) as HTMLElement ); const loggerMiddleware = (store: any) =&gt; (next: any) =&gt; (action: any) =&gt; { console.log(&quot;store&quot;, store); console.log(&quot;action&quot;, action); next(action); } const middleware = applyMiddleware(thunk, loggerMiddleware); let preloadedState = { counter: 0, }; const store = createStore(rootReducer,preloadedState, middleware); const render = () =&gt; root.render( &lt;React.StrictMode&gt; &lt;Provider store={store} &gt; &lt;App value={store.getState()} onIncrement={() =&gt; store.dispatch({ type: &quot;INCREMENT&quot; })} onDecrement={() =&gt; store.dispatch({ type: &quot;DECREMENT&quot; })} /&gt; &lt;/Provider&gt; &lt;/React.StrictMode &gt; ); render(); store.subscribe(render); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2024-01-07 22:29:12 스프링부트로 시작하는 웹 프로젝트! http://macaronics.net/index.php/m01/spring/view/2163 2163 <p>&nbsp;</p><p>&nbsp;</p><p>스프링부트로 시작하는 웹 프로젝트!</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:22px">강의 목록</span></span></strong></p><p><a target="_blank" href="https://www.youtube.com/playlist?list=PLmAWMAo-opQwl2v5CA2-mcWqjQHqtYcZR"><strong>https://www.youtube.com/playlist?list=PLmAWMAo-opQwl2v5CA2-mcWqjQHqtYcZR</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p>강사 연락처</p><ul><li><p><a data-nodeid="1049" href="https://www.scode.gg/p/010-7242-6181">박상원 강사</a></p></li><li><p>qoro@naver.com</p></li></ul><p>관련링크</p><ul><li><p><a data-nodeid="1054" href="https://github.com/SangWon7242/sb_app_2022_10_13">강사 리포지터리</a></p></li><li><p><a data-nodeid="1057" href="https://start.spring.io/#!type=gradle-project&amp;language=java&amp;platformVersion=2.7.4&amp;packaging=jar&amp;jvmVersion=17&amp;groupId=com.sbs.exam&amp;artifactId=sb_app_2022_10_13&amp;name=sb_app_2022_10_13&amp;description=Demo%20project%20for%20Spring%20Boot&amp;packageName=com.sbs.exam.sb_app_2022_10_13&amp;dependencies=mariadb,lombok,devtools,web,thymeleaf,security,mybatis">Spring Initializr</a></p></li></ul><p>설치 프로그램</p><ul><li><p><a data-nodeid="1061" href="https://www.jetbrains.com/ko-kr/idea/download/#section=windows">인텔리제이</a></p></li><li><p><a data-nodeid="1064" href="https://git-scm.com/">GIT</a></p></li><li><p><a data-nodeid="1067" href="https://www.apachefriends.org/">XAMPP</a></p></li><li><p><a data-nodeid="1070" href="https://github.com/webyog/sqlyog-community/wiki/Downloads">SQLYOG</a></p></li></ul><p>D2coding 폰트사용</p><ul><li><p><a data-nodeid="1074" href="https://github.com/naver/d2codingfont/releases">D2coding 폰트</a></p><ul><li><p>D2Coding-Ver1.3.2-20180524.zip</p></li><li><p>압축해제</p></li><li><p>D2Coding/D2CodingBold-Ver1.3.2-20180524.ttf 만 설치</p></li></ul></li></ul><p>깃 명령어</p><pre>git init git remote add origin [원격리포지터리 주소] git config --globar user.name [이름] git config --globar user.email [이메일] git status git add . git commit -m &quot;세팅&quot; git push orgin master </pre><p>강의</p><p>1강</p><ul><li><p><a data-nodeid="1083" href="https://youtu.be/PePgcehNlYQ">영상 - 22 10 13, ken 10787, 1강, 개요</a></p></li></ul><p>2강</p><ul><li><p><a data-nodeid="1087" href="https://youtu.be/npDJ8LHbkEs">영상 - 22 10 13, ken 10787, 2강, 인텔리제이 설치 및 세팅</a></p></li></ul><p>3강</p><ul><li><p><a data-nodeid="1091" href="https://youtu.be/pzZZNnCPE8Q">영상 - 22 10 13, ken 10787, 3강, xampp, SqlYog 설치 및 세팅</a></p></li></ul><p>4강</p><ul><li><p><a data-nodeid="1095" href="https://youtu.be/sJIAk4hf7b4">영상 - 22 10 13, ken 10787, 4강, git 설치</a></p></li></ul><p>5강</p><ul><li><p><a data-nodeid="1099" href="https://youtu.be/rE4bj1J72xU">영상 - 22 10 13, ken 10787, 5강, 프로젝트 세팅</a></p></li><li><p>application.yml 에 적용</p></li></ul><pre>server: port: 8081 spring: profiles: active: dev datasource: driver-class-name: org.mariadb.jdbc.Driver # driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy url: jdbc:mariadb://127.0.0.1:3306/sb_app_2022_10_14?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;serverTimezone=Asia/Seoul username: root password: </pre><p>6강</p><ul><li><p><a data-nodeid="1104" href="https://youtu.be/q1G1z5YQCrY">영상 - 22 10 13, ken 10787, 6강, 프로젝트의 소스코드를 관리하기 위한 리포지터리 생성</a></p></li><li><p><a data-nodeid="1115" href="https://github.com/SangWon7242/sb_app_2022_10_13">메인 리포지터리, https://github.com/SangWon7242/sb_app_2022_10_13</a></p></li></ul><p>7강</p><ul><li><p><a data-nodeid="1119" href="https://youtu.be/fsJ7FAQ5uJE">영상 - 22 10 13, ken 10787, 7강, git init 으로 로컬 저장소(.git 폴더) 생성</a></p></li></ul><p>8강</p><ul><li><p><a data-nodeid="1123" href="https://youtu.be/JUmAzCLbewA">영상 - 22 10 13, ken 10787, 8강, git remote add origin 으로 로컬 저장소를 원격 저장소와 연결</a></p></li></ul><p>9강</p><ul><li><p><a data-nodeid="1127" href="https://youtu.be/MRFcJS78CKY">영상 - 22 10 13, ken 10787, 9강, git config user 로 로컬 저장소에게 작업자가 누군지 알려줌</a></p></li></ul><p>10강</p><ul><li><p><a data-nodeid="1131" href="https://youtu.be/MJUjX7yGkUw">영상 - 22 10 13, ken 10787, 10강, git으로 공유하고 싶지 않은 부분을 결정하는 .gitignore</a></p></li></ul><p>11강</p><ul><li><p><a data-nodeid="1135" href="https://youtu.be/BlCPj7RHelA">영상 - 22 10 13, ken 10787, 11강, git status, git add, git commit 으로 로컬저장소에 소스코드 저장</a></p></li></ul><p>12강</p><ul><li><p><a data-nodeid="1139" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/407816e416a9a347fde53805453a29cac6d9d039">GIT</a></p></li><li><p><a data-nodeid="1142" href="https://youtu.be/Mnaac7bRJB8">영상 - 22 10 13, ken 10787, 12강, git push origin master 로 로컬저장소(.git 폴더)의 파일들을 원격저장소에 업로드</a></p></li></ul><p>13강</p><ul><li><p><a data-nodeid="1146" href="https://youtu.be/p82oHcCK-m8">영상 - 22 10 13, ken 10787, 13강, 인텔리제이 프로젝트에서 github 원격저장소, git을 삭제 후 다시 만드는 방법, git관련 오류해결</a></p></li></ul><p>14강</p><ul><li><p><a data-nodeid="1150" href="https://youtu.be/Tz71IodnGSE">영상 - 22 10 13, ken 10787, 14강, git pull origin master 를 이용해서 새로운 환경에 기존에 작업된 소스코드 받아오는 방법</a></p></li></ul><p>15강</p><ul><li><p><a data-nodeid="1154" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/d643d0df53fbee7a1ca6cb41671352822241f65b">GIT</a></p></li><li><p><a data-nodeid="1157" href="https://youtu.be/5ie1Z2G8OSo">영상 - 22 10 13, ken 10787, 15강, 컨트롤러 테스트</a></p></li></ul><p>16강</p><ul><li><p><a data-nodeid="1161" href="https://youtu.be/Ylzkz6bhdVI">영상 - 22 10 13, ken 10787, 16강, MVC 개념과 스프링부트에서의 MVC</a></p></li></ul><p>17강</p><ul><li><p><a data-nodeid="1165" href="https://youtu.be/zR8gxlJRM6U">영상 - 22 10 13, ken 10787, 17강, 프레임워크 개요</a></p></li></ul><p>18강</p><ul><li><p><a data-nodeid="1169" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/f025a518df1cf9753bd07f8d7b61232e1496196a">GIT</a></p></li><li><p><a data-nodeid="1172" href="https://youtu.be/pc2mYBLkKew">영상 - 22 10 13, ken 10787, 18강, /usr/home/main2, /usr/home/main3 추가</a></p></li></ul><p>19강</p><ul><li><p><a data-nodeid="1176" href="https://youtu.be/g0nBlzcYZo0">영상 - 22 10 18, ken 10787, 19강, 새로운 환경에서 작업을 이어가는 방법</a></p></li></ul><p>20강</p><ul><li><p><a data-nodeid="1180" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/d29951fa1df6ef0bff35e156dadd9ffefee0da37">GIT</a></p></li><li><p><a data-nodeid="1183" href="https://youtu.be/KarJmK0TOck">영상 - 22 10 18, ken 10787, 20강, count변수의 값 초기화 액션 구현</a></p></li></ul><p>21강</p><ul><li><p><a data-nodeid="1187" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/bbdc84b5555c2a267943c8ed61527c9b08710463">GIT</a></p></li><li><p><a data-nodeid="1190" href="https://youtu.be/yE9OmCd0WMc">영상 - 22 10 18, ken 10787, 21강, count변수의 값 초기화 액션 구현</a></p></li></ul><p>22강</p><ul><li><p><a data-nodeid="1194" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/b171d94b4074b15d3a5e4c2a4d6eee5386f03160">GIT</a></p></li><li><p><a data-nodeid="1197" href="https://youtu.be/Fc2zTCA_eFU">영상 - 22 10 18, ken 10787, 22강, count 조절 액션 구현</a></p></li></ul><p>23강</p><ul><li><p><a data-nodeid="1201" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2bb8823ebcf765d68a8462b913db8b8870819046">GIT</a></p></li><li><p><a data-nodeid="1204" href="https://youtu.be/77DT8K0sxXQ">영상 - 22 10 18, ken 10787, 23강, 액션메서드에서 각종 리턴타입 실험</a></p></li></ul><p>24강</p><ul><li><p><a data-nodeid="1208" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/56e28a422f77284a400e80eb93209b05bc7521b8">GIT</a></p></li><li><p><a data-nodeid="1211" href="https://youtu.be/m7-ukJ_2AzY">영상 - 22 10 18, ken 10787, 24강, 게시물 컨트롤러에, 게시물 추가, 리스트 기능 구현</a></p></li><li><p>목표</p></li><li><p>프로젝트 세팅 후, 24강의 기능을 5분안에 만들 수 있어야 합니다.</p></li></ul><p>25강</p><ul><li><p><a data-nodeid="1217" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/9f6949a9ec02a23648eb50aea5b18b99da0de77a">GIT</a></p></li><li><p><a data-nodeid="1220" href="https://youtu.be/tFT0CuZqwDU">영상 - 22 10 20, ken 10787, 25강, 테스트용 게시물 10개 자동생성</a></p></li></ul><p>26강</p><ul><li><p><a data-nodeid="1224" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e42a9ac5e058e51244f98c5914c65cb87505c626">GIT</a></p></li><li><p><a data-nodeid="1227" href="https://youtu.be/MgtzttTNvxU">영상 - 22 10 20, ken 10787, 26강, writeArticle 메서드를 통해서 게시물 작성로직 중복제거</a></p></li></ul><p>27강</p><ul><li><p><a data-nodeid="1231" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/fa6625a78eb097668a92fe356dc8b26d71f38589">GIT</a></p></li><li><p><a data-nodeid="1234" href="https://youtu.be/0GY9e9qhDfI">영상 - 22 10 20, ken 10787, 27강, getArticle, deleteArticle 서비스 메서드로 게시물 삭제기능 구현</a></p></li></ul><p>28강</p><ul><li><p><a data-nodeid="1238" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/19d4723e91b1eb5c1f6981e404185b559649c707">GIT</a></p></li><li><p><a data-nodeid="1241" href="https://youtu.be/b4399p7cRhY">영상 - 22 10 20, ken 10787, 28강, 게시물 수정 액션</a></p></li></ul><p>29강</p><ul><li><p><a data-nodeid="1245" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e61351e87b663d08b7814f3389f2beb56988aa3e">GIT</a></p></li><li><p><a data-nodeid="1248" href="https://youtu.be/1KxYmvSnXwo">영상 - 22 10 20, ken 10787, 29강, 게시물 상세보기 액션</a></p></li></ul><p>30강</p><ul><li><p><a data-nodeid="1252" href="https://youtu.be/B0uk4IFm_Nc">영상 - 22 10 20, ken 10787, 30강, MVC 개념</a></p></li></ul><p>31강</p><ul><li><p><a data-nodeid="1256" href="https://youtu.be/o294sMUZ2BM">영상 - 22 10 20, ken 10787, 31강, 스프링부트에서의 MVC</a></p></li></ul><p>32강</p><ul><li><p><a data-nodeid="1260" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/83ec381ac700bb00062d5a254597f5fcce7d0967">GIT</a></p></li><li><p><a data-nodeid="1263" href="https://youtu.be/Ip6ASA5oZEA">영상 - 22 10 20, ken 10787, 32강, ArticleService 추가 후 UsrArticleController에 연결</a></p></li></ul><p>33강</p><ul><li><p><a data-nodeid="1267" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/02a2896bd9ffe4af7f9f36434e680b3d0e5fdd12">GIT</a></p></li><li><p><a data-nodeid="1270" href="https://youtu.be/qXnolsnoQf0">영상 - 22 10 20, ken 10787, 33강, UsrArticleController의 서비스 로직을 ArticleService 로 옮김</a></p></li></ul><p>34강</p><ul><li><p><a data-nodeid="1274" href="https://youtu.be/GIRPbxLH3Tg">영상 - 22 10 20, ken 10787, 34강, 기존내용 복습하는 방법</a></p></li></ul><p>35강</p><ul><li><p><a data-nodeid="1278" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/4be7df0bf8aa2ff560712dd9a3a18887b28a7d16">GIT</a></p></li><li><p><a data-nodeid="1281" href="https://youtu.be/TPO77PFqfk4">영상 - 22 10 21, ken 10787, 35강, ArticleService에 있던 단순 데이터 저장과 관련된 로직은 ArticleRepository 으로 옮김</a></p></li></ul><p>36강</p><ul><li><p><a data-nodeid="1285" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7c1724a91c0e3a51b1feb6e5c7715b0691a2b287">GIT</a></p></li><li><p><a data-nodeid="1288" href="https://youtu.be/3y4MASnqc98">영상 - 22 10 21, ken 10787, 36강, DB 스키마 생성, 게시물 테이블 생성, DB 접속정보 수정</a></p></li></ul><p>37강</p><ul><li><p><a data-nodeid="1292" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/ef2b576eaa78d0f4e83194f744ec170061c5152e">GIT</a></p></li><li><p><a data-nodeid="1295" href="https://youtu.be/w_3K32V7MIE">영상 - 22 10 21, ken 10787, 37강, 마이바티스, MySQL JDBC 드라이버 추가, 설정 파일에 DB접속정보 기입</a></p></li></ul><p>38강</p><ul><li><p><a data-nodeid="1299" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/eb794161bee0f2ded0dbb81f666414b7a159f38d">GIT</a></p></li><li><p><a data-nodeid="1302" href="https://youtu.be/7iP333Bq6eI">영상 - 22 10 21, ken 10787, 38강, DELETE, UPDATE, SELECT 관련 쿼리를 메서드와 연결</a></p></li></ul><p>39강</p><ul><li><p><a data-nodeid="1306" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3a5d6775613f6ce43718cfc43da236fdf0e724dd">GIT</a></p></li><li><p><a data-nodeid="1313" href="https://youtu.be/3okzVdEuKdw">영상 - 22 10 21, ken 10787, 39강, INSERT 쿼리를 메서드와 연결, last_insert_id 사용</a></p></li></ul><p>40강</p><ul><li><p><a data-nodeid="1317" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/bf80fe2f6109db4fbc13dd38a80a8f860f80025b">GIT</a></p></li><li><p><a data-nodeid="1320" href="https://youtu.be/1Q2-o0X055w">영상 - 22 10 25, ken 10787, 40강, 게시물 수정시, 입력된 정보만 수정</a></p></li></ul><p>41강</p><ul><li><p><a data-nodeid="1324" href="https://youtu.be/kj368ggcIoc">영상 - 22 10 25, ken 10787, 41강, HTML, CSS, JS 연습툴 코드펜, 세팅하는 방법</a></p></li><li><p><a data-nodeid="1327" href="https://codepen.io/">코드펜, https://codepen.io/</a></p></li></ul><p>42강</p><ul><li><p><a data-nodeid="1331" href="https://youtu.be/PRvjVwePC8A">영상 - 22 10 25, ken 10787, 42강, 테일윈드 소개 및 사용법</a></p></li><li><p><a data-nodeid="1334" href="https://codepen.io/pen">새 코드펜</a></p></li><li><p><a data-nodeid="1337" href="https://codepen.io/sangwon7242/pen/BaVazry">CODEPEN - 테일윈드 사용</a></p></li><li><p><a data-nodeid="1340" href="https://nerdcave.com/tailwind-cheat-sheet">테일윈드 치트시트, https://nerdcave.com/tailwind-cheat-sheet</a></p></li></ul><p>43강</p><ul><li><p><a data-nodeid="1344" href="https://youtu.be/pGxvjXhIY2w">영상 - 22 10 25, ken 10787, 43강, 무조건 봐야하는, CSS 튜토리얼 5종 소개</a></p></li><li><p><a data-nodeid="1347" href="https://flukeout.github.io/">CSS 선택자 튜토리얼, CSS DINER, https://flukeout.github.io/</a></p></li><li><p><a data-nodeid="1350" href="https://flexboxfroggy.com/">CSS FELX 튜토리얼, flex 개구리, https://flexboxfroggy.com/</a></p></li><li><p><a data-nodeid="1353" href="http://www.flexboxdefense.com/">CSS FELX 튜토리얼, flex 디펜스, http://www.flexboxdefense.com/</a></p></li><li><p><a data-nodeid="1356" href="https://codepen.io/enxaneta/full/adLPwv">CSS FELX 총정리, flex play ground, https://codepen.io/enxaneta/full/adLPwv</a></p></li><li><p><a data-nodeid="1359" href="https://cssgridgarden.com/">CSS GRID 튜토리얼, GRID GARDEN, https://cssgridgarden.com/</a></p></li><li><p>추가</p><ul><li><p><a data-nodeid="1363" href="https://wiken.io/ken/9430">웹 코딩 기초. ken/9430</a></p></li></ul></li></ul><p>44강</p><ul><li><p><a data-nodeid="1367" href="https://youtu.be/Y25cCX3Vgx8">영상 - 22 10 25, ken 10787, 44강, 자바스크립트 기초 튜토리얼 소개</a></p></li><li><p><a data-nodeid="1370" href="https://school.programmers.co.kr/learn/courses/3">프로그래머스 자바스크립트 입문 강의</a></p><ul><li><p><a data-nodeid="1373" href="https://codepen.io/jangka44/live/ExWwyKK">풀이, to2.kr/cAD</a></p><ul><li><p>풀이에 나온 정답을 기준으로 처음부터 끝까지, 힌트없이 문제를 혼자힘으로 푸는데, 30분이 넘으면 안됩니다.</p></li></ul></li></ul></li></ul><p>45강</p><ul><li><p><a data-nodeid="1378" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/35cd131de328cb9ef3e68e7eb89c8abc5882825a">GIT</a></p></li><li><p><a data-nodeid="1381" href="https://youtu.be/sJQsW9mjrtw">영상 - 22 10 25, ken 10787, 45강, 회원가입을 처리하기 위해, UsrMemberController, MemberService 추가</a></p></li></ul><p>46강</p><ul><li><p><a data-nodeid="1385" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/81dc73e128a21c677d85bbc626e8c63233c689d5">GIT</a></p></li><li><p><a data-nodeid="1388" href="https://youtu.be/MHfgyKknKDg">영상 - 22 10 25, ken 10787, 46강, 회원 테이블 추가</a></p></li></ul><p>47강</p><ul><li><p><a data-nodeid="1392" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/06a4821e4b0b31c6b0bdda9d2207ddc436de9da0">GIT</a></p></li><li><p><a data-nodeid="1395" href="https://youtu.be/nOlboUKfP-Q">영상 - 22 10 25, ken 10787, 47강, MemberRepository 추가, 회원가입 처리</a></p></li></ul><p>48강</p><ul><li><p><a data-nodeid="1399" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/4b3164514f6903c5ab59133fe155f2ba00824cc3">GIT</a></p></li><li><p><a data-nodeid="1402" href="https://youtu.be/CrPp2DkuYlc">영상 - 22 10 27, ken 10787, 48강, 회원가입 완료 후, 생성된 회원 보여줌</a></p></li></ul><p>49강</p><ul><li><p><a data-nodeid="1406" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/9759967883806127adc5b6168e1f1425716b41c6">GIT</a></p></li><li><p><a data-nodeid="1409" href="https://youtu.be/pGmdzafBj3Q">영상 - 22 10 27, ken 10787, 49강, 회원가입시, 로그인아이디 중복 체크</a></p></li></ul><p>50강</p><ul><li><p><a data-nodeid="1413" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/00a290ccab776f0547296ebfaf3df8a2bcf65a24">GIT</a></p></li><li><p><a data-nodeid="1416" href="https://youtu.be/Az9ehFvZEf8">영상 - 22 10 27, ken 10787, 50강, 회원가입 시, 입력데이터 유효성 체크</a></p></li></ul><p>51강</p><ul><li><p><a data-nodeid="1420" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a1b3137d210098415e7c0124e7f0057418d3b725">GIT</a></p></li><li><p><a data-nodeid="1423" href="https://www.scode.gg/p/10787">영상 - 22 10 27, ken 10787, 51강, 회원가입 시, 입력데이터 유효성 체크, 공백도 체크</a></p></li></ul><p>52강</p><ul><li><p><a data-nodeid="1427" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/70f884b0c853a8a90b8c5015048c928bf02461c7">GIT</a></p></li><li><p><a data-nodeid="1430" href="https://youtu.be/biRn7bJajbw">영상 - 22 10 27, ken 10787, 52강, Ut.empty 함수 도입</a></p></li></ul><p>53강</p><ul><li><p><a data-nodeid="1434" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7da3fe8bf80709a8a34f53d693acb2499d159bf0">GIT</a></p></li><li><p><a data-nodeid="1437" href="https://youtu.be/RsGPw2tZmzo">영상 - 22 10 27, ken 10787, 53강, 회원가입시, 이름+이메일 중복체크</a></p></li></ul><p>54강</p><ul><li><p><a data-nodeid="1441" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3653229fa1bfcaa8f7ee9b384b46a946ca824a98">GIT</a></p></li><li><p><a data-nodeid="1444" href="https://youtu.be/GvCCFiV8dYI">영상 - 22 10 27, ken 10787, 54강, Ut.f 함수 도입으로 복잡한 문장 구성을 편하게</a></p></li></ul><p>55강</p><ul><li><p><a data-nodeid="1448" href="https://youtu.be/FN_-oG0RjvA">영상 - 22 11 01, ken 10787, 55강, 현재까지의 구조에서의 문제점과 ResultData 클래스의 필요성</a></p></li></ul><p>56강</p><ul><li><p><a data-nodeid="1452" href="https://youtu.be/EqHO2IK3IQk">영상 - 22 11 01, ken 10787, 56강, ResultData 클래스의 필요성 다시 설명</a></p></li></ul><p>57강</p><ul><li><p><a data-nodeid="1456" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/162e9afd93edc0a44fb0bb44b4eaba2832d5a9ba">GIT</a></p></li><li><p><a data-nodeid="1459" href="https://youtu.be/BST549WHuG4">영상 - 22 11 01, ken 10787, 57강, 범용 보고서 클래스, ResultData 구현</a></p></li></ul><p>58강</p><ul><li><p><a data-nodeid="1463" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/541f1f1611ff6bf00c54fe872142e2ffd2da0e6c">GIT</a></p></li><li><p><a data-nodeid="1466" href="https://youtu.be/Hxs5RI1zCjo">영상 - 22 11 01, ken 10787, 58강, usr/article/getArticle 에 ResultData 적용</a></p></li></ul><p>59강</p><ul><li><p><a data-nodeid="1470" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/0f4550490a621bb7705b7641e358a5da614d0881">GIT</a></p></li><li><p><a data-nodeid="1473" href="https://youtu.be/2SjtNwhRpR0">영상 - 22 11 01, ken 10787, 59강, /usr/article/doAdd 에 ResultData 적용</a></p></li></ul><p>60강</p><ul><li><p><a data-nodeid="1477" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a9ec1fd569122c1201ecdb942feb681f2b03af73">GIT</a></p></li><li><p><a data-nodeid="1480" href="https://youtu.be/GRqsFMgFgFs">영상 - 22 11 01, ken 10787, 60강, /usr/article/getArticles 에 ResultData 적용</a></p></li></ul><p>61강</p><ul><li><p><a data-nodeid="1484" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/cfb32e9ea022ee2888f695ed26ad934fbc07a001">GIT</a></p></li><li><p><a data-nodeid="1487" href="https://youtu.be/PFqfffOZ5us">영상 - 22 11 01, ken 10787, 61강, /usr/member/doJoin 에 ResultData 적용</a></p></li></ul><p>62강</p><ul><li><p><a data-nodeid="1491" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a2ff9180bf778b9d07c8ed2e47b6581198941856">GIT</a></p></li><li><p><a data-nodeid="1494" href="https://youtu.be/3T53uKuvgzg">영상 - 22 11 01, ken 10787, 62강, ResultData 클래스에 제너릭 기능 추가</a></p></li></ul><p>63강</p><ul><li><p><a data-nodeid="1498" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/1667ea4ef29fb6897620b80c631eef3d064dd4cc">GIT</a></p></li><li><p><a data-nodeid="1501" href="https://youtu.be/r60tD7eLUzc">영상 - 22 11 01, ken 10787, 63강, /usr/article/doAdd 에 ResultData.newData 적용</a></p></li></ul><p>64강</p><ul><li><p><a data-nodeid="1505" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/16eb5339952c7f1b6057d4cb5c8813179c4815dd">GIT</a></p></li><li><p><a data-nodeid="1508" href="https://youtu.be/lxQht3bHW7Y">영상 - 22 11 01, ken 10787, 64강, /usr/article/doDelete, doModify에 ResultData 적용</a></p></li></ul><p>65강</p><ul><li><p><a data-nodeid="1512" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7715bb60f25f846e4381ada2eb67db4bcb2816a0">GIT</a></p></li><li><p><a data-nodeid="1515" href="https://youtu.be/68yEzNxjLoo">영상 - 22 11 03, ken 10787, 65강, UsrMemberController, MemberService 에 ResultData 제너릭 적용</a></p></li></ul><p>66강</p><ul><li><p><a data-nodeid="1521" href="https://youtu.be/wkzVy_4yrUc">영상 - 22 11 03, ken 10787, 66강, 62강 ~ 65강 내용 정리</a></p></li></ul><p>67강</p><ul><li><p><a data-nodeid="1525" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/d5b118c8c6f50a1e2e9fe07b0c7576a808f12909">GIT</a></p></li><li><p><a data-nodeid="1528" href="https://youtu.be/Gc1GUU_JTvk">영상 - 22 11 03, ken 10787, 67강, Article 클래스에 regDate, updateDate 필드 추가</a></p></li></ul><p>68강</p><ul><li><p><a data-nodeid="1532" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/60189660d85eb8ed21eb1285168dcc091eac1519">GIT</a></p></li><li><p><a data-nodeid="1535" href="https://youtu.be/dmaMPxTioMw">영상 - 22 11 03, ken 10787, 68강, 로그인 기능 구현</a></p></li></ul><p>69강</p><ul><li><p><a data-nodeid="1539" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/02810b92248244837b0151226d90af00493ac55f">GIT</a></p></li><li><p><a data-nodeid="1542" href="https://youtu.be/Ltr3cuoWMFY">영상 - 22 11 03, ken 10787, 69강, 로그아웃 기능 구현</a></p></li></ul><p>70강</p><ul><li><p><a data-nodeid="1546" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/0f37499e9a5b4fe07de8004c5f0878842fb45a6e">GIT</a></p></li><li><p><a data-nodeid="1549" href="https://youtu.be/QRyTteekmSs">영상 - 22 11 03, ken 10787, 70강, 게시물 테이블에 회원번호칼럼 추가</a></p></li></ul><p>71강</p><ul><li><p><a data-nodeid="1553" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/c37b650fbee310b1b377d83edded6d27c99d3cfc">GIT</a></p></li><li><p><a data-nodeid="1556" href="https://youtu.be/XglQRJQAeqA">영상 - 22 11 03, ken 10787, 71강, 게시물 작성시, 로그인 여부 체크, 작성자 정보 저장</a></p></li></ul><p>72강</p><ul><li><p><a data-nodeid="1560" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3336e87c3c531ebbb47367d75ceb0896ad7f6949">GIT</a></p></li><li><p><a data-nodeid="1563" href="https://youtu.be/20h5ZIXhYz0">영상 - 22 11 03, ken 10787, 72강, 게시물 삭제시 권한체크</a></p></li></ul><p>73강</p><ul><li><p><a data-nodeid="1567" href="https://youtu.be/XEgxTpV_ITw">영상 - 22 11 03, ken 10787, 73강, 복습하는 방법, 메서드 하나를 재구현하는 방법</a></p></li><li><p>모든 소스코드를 마지막 커밋시점 으로 되돌리는 명령어</p><ul><li><p>git checkout -f .</p></li></ul></li></ul><p>74강</p><ul><li><p><a data-nodeid="1573" href="https://youtu.be/fQXM02gf5pU">영상 - 22 11 03, ken 10787, 74강, 복습하는 방법, 과거로 돌아가서 특정 기능을 구현해보는 방법</a></p></li><li><p>특정 커밋 시점으로 가는 명령어(즉 소스코드 전체를 과거로 돌리는 명령어)</p><ul><li><p>git checkout -f 커밋번호</p><ul><li><p>EX : git checkout -f 16eb5339952c7f1b6057d4cb5c8813179c4815dd</p><ul><li><p>64강이 완료된 직후로 이동</p></li></ul></li><li><p>과거로 이동했다면, 꼭, db/schema.sql 파일을 다시 열어서(꼭 다시 열어야 함) 실행해야 과거의 상황을 완벽히 재현할 수 있다.</p></li></ul></li></ul></li><li><p>다시 현재로 돌아오는 명령어(아래 2개의 명령어를 순서대로 입력)</p><ul><li><p>git checkout -f .</p></li><li><p>git checkout -f master</p></li></ul></li></ul><p>75강</p><ul><li><p><a data-nodeid="1585" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/82e7e0336f29bae30536c8bf8449da08abd3b827">GIT</a></p></li><li><p><a data-nodeid="1588" href="https://youtu.be/2qwZY9_5qns">영상 - 22 11 03, ken 10787, 75강, 게시물 수정시 권한체크</a></p></li></ul><p>76강</p><ul><li><p><a data-nodeid="1592" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/81f1c6be4e38fcfd093cf09e60e0f9de717a75b7">GIT</a></p></li><li><p><a data-nodeid="1595" href="https://youtu.be/xrHLrTMN480">영상 - 22 11 03, ken 10787, 76강, ResultData 에 data1Name 추가</a></p></li></ul><p>77강</p><ul><li><p><a data-nodeid="1599" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2bd4ab4c7d50e3db8cefed98c56dc81c548d4672">GIT</a></p></li><li><p><a data-nodeid="1602" href="https://youtu.be/0Z1Cyc6-OpA">영상 - 22 11 04, ken 10787, 77강, JSP 적용</a></p></li></ul><p>78강</p><ul><li><p><a data-nodeid="1606" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2b16a58737f004120ff0bda66c4b8130a2e6d6e5">GIT</a></p></li><li><p><a data-nodeid="1609" href="https://youtu.be/io1RgaWUzeA">영상 - 22 11 04, ken 10787, 78강, 게시물 리스트, JSP로 구현</a></p></li></ul><p>79강</p><ul><li><p><a data-nodeid="1613" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/4c4bbced30a7bee582050e36f79f778a8320e7d7">GIT</a></p></li><li><p><a data-nodeid="1616" href="https://youtu.be/4CBVzJcwTlE">영상 - 22 11 04, ken 10787, 79강, 사이트 헤더 작업</a></p></li></ul><p>80강</p><ul><li><p><a data-nodeid="1620" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/943c6bb68251cad66f7dbfbcf3a2556306b24023">GIT</a></p></li><li><p><a data-nodeid="1623" href="https://youtu.be/L1a2Vb9XFZ8">영상 - 22 11 04, ken 10787, 80강, 사이트 공통 CSS, JS 로딩, 지마켓 산스 적용</a></p></li></ul><p>81강</p><ul><li><p><a data-nodeid="1627" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/76e8f6301d7b52936474cb8ce5d9c2523b0f96c9">GIT</a></p></li><li><p><a data-nodeid="1630" href="https://youtu.be/RKHw9WVIsDU">영상 - 22 11 04, ken 10787, 81강, 각 페이지별로 중복되는, 공통 상단내용 부분을 head.jspf 로 모으기</a></p></li></ul><p>82강</p><ul><li><p><a data-nodeid="1634" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/8e1e50128e1844386d89063d05915ee723a1a3a8">GIT</a></p></li><li><p><a data-nodeid="1637" href="https://youtu.be/s7ntMZl97FI">영상 - 22 11 04, ken 10787, 82강, 프론트엔드 라이브러리 불러오기</a></p></li></ul><p>83강</p><ul><li><p><a data-nodeid="1641" href="https://nerdcave.com/tailwind-cheat-sheet">테일윈드 치트시트</a></p></li><li><p><a data-nodeid="1644" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a654270651c52ff5c0152d241d1048097c93dea2">GIT</a></p></li><li><p><a data-nodeid="1647" href="https://youtu.be/geSoXMiwdE4">영상 - 22 11 04, ken 10787, 83강, 전체 페이지 레이아웃</a></p></li></ul><p>84강</p><ul><li><p><a data-nodeid="1651" href="https://youtu.be/8K2g1B224-c">영상 - 22 11 04, ken 10787, 84강, 복습하는 방법, 공부하는 방법</a></p></li></ul><p>85강</p><ul><li><p><a data-nodeid="1655" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/847febffc2d9cc807a9c65751bb27eee5cd59737">GIT</a></p></li><li><p><a data-nodeid="1658" href="https://youtu.be/gB2yTPVV2e8">영상 - 22 11 04, ken 10787, 85강, 게시물 상세페이지 구현</a></p></li></ul><p>86강</p><ul><li><p><a data-nodeid="1662" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/4414657996e6523f8a8845c2090b29ed9ecbf3c1">GIT</a></p></li><li><p><a data-nodeid="1665" href="https://youtu.be/vkSxXJM6MiI">영상 - 22 11 04, ken 10787, 86강, 게시물 리스트 칼럼 너비 조정</a></p></li></ul><p>87강</p><ul><li><p><a data-nodeid="1669" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e6766d76c3608b44b6768a374433e085bf61a786">GIT</a></p></li><li><p><a data-nodeid="1672" href="https://youtu.be/oSPhm7GgcpE">영상 - 22 11 04, ken 10787, 87강, 게시물 리스트에서 작성자명 표기</a></p></li></ul><p>88강</p><ul><li><p><a data-nodeid="1676" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3c2cc5960a07f43d7d2775842ba14044aaee862f">GIT</a></p></li><li><p><a data-nodeid="1679" href="https://youtu.be/ioSuZ615c_Y">영상 - 22 11 08, ken 10787, 88강, 게시물 상세페이지에서 작성자명 표시</a></p></li></ul><p>89강</p><ul><li><p><a data-nodeid="1683" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/bdaf7047fed3e3ca362694377b4fa19dd71c3203">GIT</a></p></li><li><p><a data-nodeid="1686" href="https://youtu.be/w2AbVSXjrEY">영상 - 22 11 08, ken 10787, 89강, 게시물 상세페이지에서 삭제, 수정 버튼 추가</a></p></li></ul><p>90강</p><ul><li><p><a data-nodeid="1690" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/8425750e76adafc1ff3ea7fc9e7d0836c50bf286">GIT</a></p></li><li><p><a data-nodeid="1693" href="https://youtu.be/i13F1YkR_tU">영상 - 22 11 08, ken 10787, 90강, 출력용 게시물 가져올 때, 액터가 해당 게시물을 삭제/수정 할 수 있는지도 파악</a></p></li></ul><p>91강</p><ul><li><p><a data-nodeid="1697" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/244bfee968e2fbcd33fbe442b117848c37031fbd">GIT</a></p></li><li><p><a data-nodeid="1700" href="https://youtu.be/tAOhik49evU">영상 - 22 11 08, ken 10787, 91강, 게시물 삭제 후 replace로 이동</a></p></li></ul><p>92강</p><ul><li><p><a data-nodeid="1706" href="https://youtu.be/0D07j0xlR0I">영상 - 22 11 08, ken 10787, 92강, 89 ~ 91강 내용 정리</a></p></li></ul><p>93강</p><ul><li><p><a data-nodeid="1710" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/cb879edfa7e6ba6d85fc3dab13a936cc1fa6d3ba">GIT</a></p></li><li><p><a data-nodeid="1713" href="https://youtu.be/2y6LjK6bW6g">영상 - 22 11 08, ken 10787, 93강, Rq 클래스 도입으로 로그인 여부와 관련된 중복된 로직 제거</a></p></li></ul><p>94강</p><ul><li><p><a data-nodeid="1717" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/bc8c78a9c3a17bbed996108f30cb40e7f028b162">GIT</a></p></li><li><p><a data-nodeid="1720" href="https://youtu.be/2Jkqbt2vlh4">영상 - 22 11 10, ken 10787, 94강, BeforeAction 인터셉터 구현</a></p></li></ul><p>95강</p><ul><li><p><a data-nodeid="1724" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/065e058c5a2e3b5c1a9dbdd99d1ecab2b477973f">GIT</a></p></li><li><p><a data-nodeid="1727" href="https://youtu.be/h_kW_jFDje8">영상 - 22 11 10, ken 10787, 95강, BeforeAction 인터셉터 등록</a></p></li></ul><p>96강</p><ul><li><p><a data-nodeid="1731" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a382bec2181dc7c55fd3980cddb52b8d37df4418">GIT</a></p></li><li><p><a data-nodeid="1734" href="https://youtu.be/LMa-HJhw64k">영상 - 22 11 10, ken 10787, 96강, Rq객체를 BeforeAction 인터셉터에서 대표로 한번만 만들기</a></p></li></ul><p>97강</p><ul><li><p><a data-nodeid="1738" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/6b9dead05f9646cc82373da1ef6af39aa8929405">GIT</a></p></li><li><p><a data-nodeid="1741" href="https://youtu.be/36EFhW40WkM">영상 - 22 11 10, ken 10787, 97강, NeedLogin인터셉터 구현 및 등록</a></p></li></ul><p>98강</p><ul><li><p><a data-nodeid="1745" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a36b2b223a3e8ecb496305a3b5718601e7a49b86">GIT</a></p></li><li><p><a data-nodeid="1748" href="https://youtu.be/MNllRGYYakE">영상 - 22 11 10, ken 10787, 98강, NeedLogin인터셉터가 여러 액션들을 대신해서 자격없는 요청들을 앞선에서 거름</a></p></li></ul><p>99강</p><ul><li><p><a data-nodeid="1752" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/f20d2b4520e95997181117d8a38e432f9041d821">GIT</a></p></li><li><p><a data-nodeid="1755" href="https://youtu.be/E4EQ4M8x-KM">영상 - 22 11 10, ken 10787, 99강, 로그인 폼</a></p></li></ul><p>100강</p><ul><li><p><a data-nodeid="1759" href="https://youtu.be/9sggyt6zlkg">영상 - 22 11 10, ken 10787, 100강, 로그인 폼과 로그인 처리의 과정 설명</a></p></li></ul><p>101강</p><ul><li><p><a data-nodeid="1763" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/0f9ece3966ea52c6a21961ad178946ab4b9ecc82">GIT</a></p></li><li><p><a data-nodeid="1766" href="https://youtu.be/U-EyPnfldbw">영상 - 22 11 10, ken 10787, 101강, 로그인 처리에 Rq 적용</a></p></li></ul><p>102강</p><ul><li><p><a data-nodeid="1770" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/43127f850659934208c1bc0b7eb3a4a8a125b952">GIT</a></p></li><li><p><a data-nodeid="1773" href="https://youtu.be/bNWdZhqNWsg">영상 - 22 11 15, ken 10787, 102강, 로그아웃 처리에 Rq 적용</a></p></li></ul><p>103강</p><ul><li><p><a data-nodeid="1777" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e9ec9dd0c5878b1b438014e9a6604abbcda8ed51">GIT</a></p></li><li><p><a data-nodeid="1780" href="https://youtu.be/e2Riq4rfOkE">영상 - 22 11 15, ken 10787, 103강, 로그아웃 후에, 자바스크립트로 후처리</a></p></li></ul><p>104강</p><ul><li><p><a data-nodeid="1784" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3d73db55f82e155465b2db7d3a2c3157638390fe">GIT</a></p></li><li><p><a data-nodeid="1787" href="https://youtu.be/g7kQbptSK_4">영상 - 22 11 15, ken 10787, 104강, 게시물 수정폼을 보여주기전 체크</a></p></li></ul><p>105강</p><ul><li><p><a data-nodeid="1791" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/4aa2922903f19a3143a8fad2a96ae9f8e88b84af">GIT</a></p></li><li><p><a data-nodeid="1794" href="https://youtu.be/NVI5cDR3br0">영상 - 22 11 15, ken 10787, 105강, 게시물 수정 폼</a></p></li></ul><p>106강</p><ul><li><p><a data-nodeid="1798" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/53b794937553e55a680f3c0e2099cb04a311da5c">GIT</a></p></li><li><p><a data-nodeid="1801" href="https://youtu.be/Y2kK18mSfX0">영상 - 22 11 15, ken 10787, 106강, 게시물 수정 후에, 자바스크립트로 후처리</a></p></li></ul><p>107강</p><ul><li><p><a data-nodeid="1805" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/f526bdcf4f1fb8f3a1116ff2c32478434a275203">GIT</a></p></li><li><p><a data-nodeid="1808" href="https://youtu.be/MFL45D-7HKU">영상 - 22 11 15, ken 10787, 107강, 데이지UI적용</a></p></li></ul><p>108강</p><ul><li><p><a data-nodeid="1812" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/24e36b37dc3506f5f8bf046efe8a614fa0dcf193">GIT</a></p></li><li><p><a data-nodeid="1815" href="https://youtu.be/U2fbpO8B02I">영상 - 22 11 15, ken 10787, 108강, 화면너비 800px 이하 부터는 반응형 끄기</a></p></li></ul><p>109강</p><ul><li><p><a data-nodeid="1819" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a7f24164e67d53d4a46a7cc2e39e84bb2b0e595a">GIT</a></p></li><li><p><a data-nodeid="1822" href="https://youtu.be/hgB7ruXVVto">영상 - 22 11 15, ken 10787, 109강, 게시물 작성 폼</a></p></li></ul><p>110강</p><ul><li><p><a data-nodeid="1826" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/cb301cd945da5fe56a8425f8248175d0c8ddb938">GIT</a></p></li><li><p><a data-nodeid="1829" href="https://youtu.be/lGp2RCfC0u0">영상 - 22 11 17, ken 10787, 110강, 게시물 작성의 후처리를 자바스크립트로 수행</a></p></li></ul><p>111강</p><ul><li><p><a data-nodeid="1833" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/38c9f13c92e001af6f4925586bde4f263b1f93f7">GIT</a></p></li><li><p><a data-nodeid="1836" href="https://youtu.be/o9uArhNwgxE">영상 - 22 11 17, ken 10787, 111강, 멀티게시판 개념 도입, board 테이블 생성</a></p></li></ul><p>112강</p><ul><li><p><a data-nodeid="1840" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/bc730f8b958554d469d1d4b29e3eb9e866a97ff5">GIT</a></p></li><li><p><a data-nodeid="1843" href="https://youtu.be/CzDEA_pE9sw">영상 - 22 11 17, ken 10787, 112강, 게시물리스트에서 선택된 게시판의 이름을 페이지 제목으로 노출</a></p></li></ul><p>113강</p><ul><li><p><a data-nodeid="1847" href="https://youtu.be/S7EBJ40HjCc">영상 - 22 11 17, ken 10787, 113강, HttpServletRequest, HttpServletResponse, HttpSession 개념 설명</a></p></li></ul><p>114강</p><ul><li><p><a data-nodeid="1855" href="https://youtu.be/oS9JgbHg56Y">영상 - 22 11 17, ken 10787, 114강, Article 클래스의 extra__actorCanModify, extra__actorCanDelete 필드의 역할과 초기화 방법</a></p></li></ul><p>115강</p><ul><li><p><a data-nodeid="1859" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/8a4eb2093cf9adcc668254a85cf38c05bf908e3f">GIT</a></p></li><li><p><a data-nodeid="1862" href="https://youtu.be/Uz75FurcWwU">영상 - 22 11 17, ken 10787, 115강, 게시물리스트에서 존재하지 않는 게시판 체크</a></p></li></ul><p>116강</p><ul><li><p><a data-nodeid="1866" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2e8e70ecad3fae9aa54747fd4b22dc42446d386a">GIT</a></p></li><li><p><a data-nodeid="1869" href="https://youtu.be/B7q8XxVlxis">영상 - 22 11 17, ken 10787, 116강, 게시물리스트에 선택된 게시판에 관련된 게시물만 표시</a></p></li></ul><p>117강</p><ul><li><p><a data-nodeid="1873" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/63f8d06c65f7f204366939214b35bf1d3bed0822">GIT</a></p></li><li><p><a data-nodeid="1876" href="https://youtu.be/yAjbHUD-oAI">영상 - 22 11 18, ken 10787, 117강, 게시물리스트에 게시물개수 표시</a></p></li></ul><p>118강</p><ul><li><p><a data-nodeid="1880" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/8add142e2ce883d981540d9638bfcbad631cf57d">GIT</a></p></li><li><p><a data-nodeid="1883" href="https://youtu.be/bvgYin609L4">영상 - 22 11 18, ken 10787, 118강, 게시물 페이징을 위한 게시물 대량생성</a></p></li></ul><p>119강</p><ul><li><p><a data-nodeid="1887" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a7da12500918bfd170bb4507d530cc3a146a717a">GIT</a></p></li><li><p><a data-nodeid="1890" href="https://youtu.be/SR2namtsTas">영상 - 22 11 18, ken 10787, 119강, Rq 객체의 생성을 스프링에 맡김</a></p></li></ul><p>120강</p><ul><li><p><a data-nodeid="1894" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2a0f10b8b5239a4fef9b566962336aba7b79658b">GIT</a></p></li><li><p><a data-nodeid="1897" href="https://youtu.be/jCNxyoZD05g">영상 - 22 11 18, ken 10787, 120강, 게시물 작성시 게시판 선택</a></p></li></ul><p>121강</p><ul><li><p><a data-nodeid="1901" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/75b8989d628dd23935ececb5d1f2d6677bba9337">GIT</a></p></li><li><p><a data-nodeid="1904" href="https://youtu.be/W2pb0YB5K70">영상 - 22 11 18, ken 10787, 121강, BeforeActionInterceptor 에서 Rq 객체를 좀 더 확실하게 생성되도록 유도</a></p></li></ul><p>122강</p><ul><li><p><a data-nodeid="1908" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/2e3077bcef32565dfe07d810d8eaaa0be00052c0">GIT</a></p></li><li><p><a data-nodeid="1911" href="https://youtu.be/xK1366Yu_WU">영상 - 22 11 18, ken 10787, 122강, 게시물 리스트 page 파라미터 처리</a></p></li></ul><p>123강</p><ul><li><p><a data-nodeid="1915" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/b366a1abf694213443c82e10fb7b85c08febf7bf">GIT</a></p></li><li><p><a data-nodeid="1918" href="https://youtu.be/6Y2NPqsINQE">영상 - 22 11 18, ken 10787, 123강, 페이지 메뉴 구현</a></p></li></ul><p>124강</p><ul><li><p><a data-nodeid="1922" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/804fc81f607956d616d5fd5361480bb6a73dbcdd">GIT</a></p></li><li><p><a data-nodeid="1925" href="https://youtu.be/YaUjcf4vtAA">영상 - 22 11 18, ken 10787, 124강, 페이지 메뉴의 크기를 동적으로 조정</a></p></li></ul><p>125강</p><ul><li><p><a data-nodeid="1929" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/108a40f4ec4c2bdf061302d0b9624489339757be">GIT</a></p></li><li><p><a data-nodeid="1932" href="https://youtu.be/iPoV_fIY9og">영상 - 22 11 18, ken 10787, 125강, 페이지 메뉴의 최대크기를 지정</a></p></li></ul><p>125강</p><ul><li><p><a data-nodeid="1936" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/77724814f4e1ad3977dea4a9ac7ef58f82dedb7d">GIT</a></p></li><li><p><a data-nodeid="1939" href="https://youtu.be/AlVeonywgSc">영상 - 22 11 22, ken 10787, 125강, 페이지 메뉴를 통해서 페이지 이동을 해도, 선택된 게시판 정보가 누락되지 않도록</a></p></li></ul><p>126강</p><ul><li><p><a data-nodeid="1943" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/1cfe3f7f471dd332bbb8b799dd625769a68ffccf">GIT</a></p></li><li><p><a data-nodeid="1946" href="https://youtu.be/xSkV7wAUopk">영상 - 22 11 22, ken 10787, 126강, 데이지UI에서 명시적으로 다크모드가 활성되지 않도록 지정</a></p></li></ul><p>127강</p><ul><li><p><a data-nodeid="1950" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7814041d33934d99e1c6600b9ffe8709689ceccc">GIT</a></p></li><li><p><a data-nodeid="1953" href="https://youtu.be/8E-ngCQfqnw">영상 - 22 11 22, ken 10787, 127강, 리스트에서 검색타입과 검색어에 따라, 총 게시물 수 계산 구현</a></p></li></ul><p>128강</p><ul><li><p><a data-nodeid="1957" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e460ce8f6b144d7eb5b9b298b6c1d316fe68f767">GIT</a></p></li><li><p><a data-nodeid="1960" href="https://youtu.be/JjvswA2S0UE">영상 - 22 11 22, ken 10787, 128강, 게시물 검색어에 따라 게시물 필터, 게시물 페이지메뉴에 검색어 유지</a></p></li></ul><p>129강</p><ul><li><p><a data-nodeid="1964" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/84115094c2095b95330cd28c24bf722452b1e58e">GIT</a></p></li><li><p><a data-nodeid="1967" href="https://youtu.be/p6LFJ6jiQKA">영상 - 22 11 22, ken 10787, 129강, 게시물 리스트의 테이블 꾸미기</a></p></li></ul><p>130강</p><ul><li><p><a data-nodeid="1971" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7757af01b3441a0c05490fbd031866eb10d9915f">GIT</a></p></li><li><p><a data-nodeid="1974" href="https://youtu.be/PUzhM_JWD7g">영상 - 22 11 22, ken 10787, 130강, 검색박스 구현</a></p></li></ul><p>131강</p><ul><li><p><a data-nodeid="1978" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/5bb4262b6ae5463044428664f351434cf798cba6">GIT</a></p></li><li><p><a data-nodeid="1981" href="https://youtu.be/bIHm9jvWGgA">영상 - 22 11 22, ken 10787, 131강, 게시물 상세페이지가 보여질 때 마다, 조회수 1 증가</a></p></li></ul><p>꼭 보고 올것</p><ul><li><p><a data-nodeid="1985" href="https://www.youtube.com/watch?v=nKD1atl6cAw">Ajax가 무엇인지 설명하는 영상 (+CORS, fetch 어쩌구)</a></p></li><li><p><a data-nodeid="1988" href="https://youtu.be/fiqApu1t1Vc">jQuery - ajax</a></p></li><li><p><a data-nodeid="1991" href="https://www.youtube.com/watch?v=avfIUwDG2d8">10분 테코톡, 티거의 Ajax</a></p></li></ul><p>132강</p><ul><li><p><a data-nodeid="1995" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/74d2d424bc4a79f815ee298254539fa243f2c853">GIT</a></p></li><li><p><a data-nodeid="1998" href="https://youtu.be/wEgW-r1g_9g">영상 - 22 11 24, ken 10787, 132강, 조회수 증가를 다른 액션에서 수행</a></p></li></ul><p>133강</p><ul><li><p><a data-nodeid="2002" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/9a905a5f34884e1fec1a3b0bfefa9b1dce9efaf6">GIT</a></p></li><li><p><a data-nodeid="2005" href="https://youtu.be/Ymik2Bi8y60">영상 - 22 11 24, ken 10787, 133강, 조회수 증가를 ajax로 실행</a></p></li></ul><p>134강</p><ul><li><p><a data-nodeid="2009" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/89aba9c24186963c53c0985139dfdbdfcbe19959">GIT</a></p></li><li><p><a data-nodeid="2012" href="https://youtu.be/rR5KmF4M_XE">영상 - 22 11 24, ken 10787, 134강, 조회수 증가 성공에 대한 답장에, data2(글 번호) 정보 추가</a></p></li></ul><p>135강</p><ul><li><p><a data-nodeid="2016" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/90806127434b4099bca9e52cb0ad3e5a4541da83">GIT</a></p></li><li><p><a data-nodeid="2019" href="https://youtu.be/ljPBKt7sDns">영상 - 22 11 24, ken 10787, 135강, 로컬스토리지를 이용해서 브라우저가 한번 읽은 게시물에 대해서는 조회수 증가 요청 안함</a></p></li></ul><p>136강</p><ul><li><p><a data-nodeid="2023" href="https://youtu.be/1XjbovQd1p8">영상 - 22 11 24, ken 10787, 136강, 크롬 개발자 도구, 네트워크 탭 사용법</a></p></li></ul><p>137강</p><ul><li><p><a data-nodeid="2027" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/a7b78a6ef141c380ac4dcb930f2a6d5191058182">GIT</a></p></li><li><p><a data-nodeid="2030" href="https://youtu.be/TGzlsITmuCY">영상 - 22 11 24, ken 10787, 137강, 리액션포인트 테이블 추가</a></p></li></ul><p>138강</p><ul><li><p><a data-nodeid="2034" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/89db925bbc6010cd9c85a58b8e1397123a6cb114">GIT</a></p></li><li><p><a data-nodeid="2037" href="https://youtu.be/ovRBc3sJUiU">영상 - 22 11 24, ken 10787, 138강, 시간출력관련 함수를 Article 클래스에 만들어서 JSP에서 사용</a></p></li></ul><p>139강</p><ul><li><p><a data-nodeid="2041" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/34443f327cdac1e8165016a51b8d169a77193f02">GIT</a></p></li><li><p><a data-nodeid="2044" href="https://youtu.be/D-mIe7D_tJg">영상 - 22 11 29, ken 10787, 139강, 게시물리스트에, 추천수 노출 구현</a></p></li></ul><p>140강</p><ul><li><p><a data-nodeid="2048" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/cc96be1f8a64cce95ab35943d5d13e0602c7e4f5">GIT</a></p></li><li><p><a data-nodeid="2051" href="https://youtu.be/ejX6h1SM2lc">영상 - 22 11 29, ken 10787, 140강, 상세페이지, 수정페이지에서도 추천수 노출</a></p></li></ul><p>141강</p><ul><li><p><a data-nodeid="2055" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/b90b11146392e264663b6ba7453ad0adce3018ae">GIT</a></p></li><li><p><a data-nodeid="2058" href="https://youtu.be/UWT_Ywkyo08">영상 - 22 11 29, ken 10787, 141강, 각 게시물에서 사용자가 좋아요와 싫어요를 할 수 있는지 체크</a></p></li></ul><p>142강</p><ul><li><p><a data-nodeid="2062" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/3f089bc5979cd531b2f7cbb0f29e252b05f5b29d">GIT</a></p></li><li><p><a data-nodeid="2065" href="https://youtu.be/vN9GC416aTY">영상 - 22 11 29, ken 10787, 142강, 쿼리속도 최적화를 위해서, article 테이블에 좋아요, 싫어요 칼럼 추가</a></p></li></ul><p>143강</p><ul><li><p><a data-nodeid="2069" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/200f39ae0e618f9f6f9f78dc045b2f3a87726072">GIT</a></p></li><li><p><a data-nodeid="2072" href="https://youtu.be/fU-cnkWdMnk">영상 - 22 11 29, ken 10787, 143강, 좋아요, 싫어요 수를 article 테이블에서 손쉽게 가져오는 방식으로 변경</a></p></li></ul><p>144강</p><ul><li><p><a data-nodeid="2076" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/c63946901602efdff0f41fb6f0452ea808f81e9e">GIT</a></p></li><li><p><a data-nodeid="2079" href="https://youtu.be/sOaiAutgzcA">영상 - 22 12 01, ken 10787, 144강, 리액션포인트서비스, 리액션포인트리포지터리 도입</a></p></li></ul><p>145강</p><ul><li><p><a data-nodeid="2083" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/b368de15da797ae3e7b952e0ca9c399d06c3925e">GIT</a></p></li><li><p><a data-nodeid="2086" href="https://youtu.be/GX7fTDI5RYs">영상 - 22 12 01, ken 10787, 145강, 좋아요, 싫어요 구현</a></p></li></ul><p>146강</p><ul><li><p><a data-nodeid="2090" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/5035678c9a20e6015424c08bfeea2f6340093350">GIT</a></p></li><li><p><a data-nodeid="2093" href="https://youtu.be/tIgvbMGMShU">영상 - 22 12 01, ken 10787, 146강, 좋아요 취소버튼, 싫어요 취소버튼을 조건에 따라 노출</a></p></li></ul><p>147강</p><ul><li><p><a data-nodeid="2097" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/6f75e85fedb61f72d0ea6b1a3054021ca35b7b22">GIT</a></p></li><li><p><a data-nodeid="2100" href="https://youtu.be/MSCdI-LSwSM">영상 - 22 12 01, ken 10787, 147강, 좋아요, 싫어요 취소처리</a></p></li></ul><p>148강</p><ul><li><p><a data-nodeid="2104" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/fa2e427bd6482c095ac8734dea9ee92807f10d9d">GIT</a></p></li><li><p><a data-nodeid="2107" href="https://youtu.be/D2ZOJ8XFuuY">영상 - 22 12 02, ken 10787, 148강, 댓글 테이블 추가</a></p></li></ul><p>149강</p><ul><li><p><a data-nodeid="2111" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/d92e1357a22c9858cbe6a276c4872b407a3b22ac">GIT</a></p></li><li><p><a data-nodeid="2114" href="https://youtu.be/olRqGFzHILk">영상 - 22 12 02, ken 10787, 149강, 댓글작성 폼</a></p></li></ul><p>150강</p><ul><li><p><a data-nodeid="2118" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/ef7709f263c8b703ab631a76f4265e84c3d4c541">GIT</a></p></li><li><p><a data-nodeid="2121" href="https://youtu.be/36PejEZBj0M">영상 - 22 12 02, ken 10787, 150강, 댓글 폼 체크</a></p></li></ul><p>151강</p><ul><li><p><a data-nodeid="2125" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/e6e9a11dc149ddf944e31c3af43332a2dc32c66e">GIT</a></p></li><li><p><a data-nodeid="2128" href="https://youtu.be/2CGn8hvNNOw">영상 - 22 12 02, ken 10787, 151강, 댓글 작성 처리</a></p></li></ul><p>151강</p><ul><li><p><a data-nodeid="2132" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/cd8475d8718e2ff80ce39676557c6750a04665ed">GIT</a></p></li><li><p><a data-nodeid="2135" href="https://youtu.be/r9dVgFHW2SU">영상 - 22 12 02, ken 10787, 151강, 댓글테이블에 인덱스 걸기</a></p></li></ul><p>152강</p><ul><li><p><a data-nodeid="2139" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/b15369c36334b4aed0014f87233e73e4d441ae6a">GIT</a></p></li><li><p><a data-nodeid="2142" href="https://youtu.be/uo75MuJB9zc">영상 - 22 12 02, ken 10787, 152강, 댓글 리스트</a></p></li></ul><p>153강</p><ul><li><p><a data-nodeid="2146" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/9064e4379b15d7c18e0dc34454ae6052a7ec7e79">GIT</a></p></li><li><p><a data-nodeid="2149" href="https://youtu.be/W7QjAkFlr0I">영상 - 22 12 02, ken 10787, 153강, 댓글 삭제, 댓글 수정 버튼 노출</a></p></li></ul><p>154강</p><ul><li><p><a data-nodeid="2153" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/19d08023c012fa0d7b39826874a59102ec9d0c96">GIT</a></p></li><li><p><a data-nodeid="2156" href="https://youtu.be/GN-g6y_Tm9A">영상 - 22 12 06, ken 10787, 154강, 댓글 삭제</a></p></li></ul><p>155강</p><ul><li><p><a data-nodeid="2160" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/c871f6e0f5b7d3ed3693ed6008ce24f3553b2422">GIT</a></p></li><li><p><a data-nodeid="2163" href="https://youtu.be/6g1JxHLjOYY">영상 - 22 12 06, ken 10787, 155강, 댓글 수정 폼</a></p></li></ul><p>156강</p><ul><li><p><a data-nodeid="2167" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/022c77254fe85bfc39f829b449fddeb6ee834d1a">GIT</a></p></li><li><p><a data-nodeid="2170" href="https://youtu.be/oHUzkN0iXso">영상 - 22 12 06, ken 10787, 156강, 댓글 수정 폼 처리</a></p></li></ul><p>157강</p><ul><li><p><a data-nodeid="2174" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/f6f85af0ef371402198cbe1739c97e34f898eab6">GIT</a></p></li><li><p><a data-nodeid="2177" href="https://youtu.be/_N4efTwm0Fs">영상 - 22 12 06, ken 10787, 157강, 마이페이지</a></p></li></ul><p>158강</p><ul><li><p><a data-nodeid="2181" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/924a938b3a744a10314aca32573548c4614160b1">GIT</a></p></li><li><p><a data-nodeid="2184" href="https://youtu.be/PNA8ZqYBunE">영상 - 22 12 06, ken 10787, 158강, 인터셉터 설정에서 로그인이 필요한 페이지들 추가</a></p></li></ul><p>159강</p><ul><li><p><a data-nodeid="2188" href="https://github.com/SangWon7242/sb_app_2022_10_13/commit/7198259ac33636793ecaecb37498fdf9f2b5fac5">GIT</a></p></li><li><p><a data-nodeid="2191" href="https://youtu.be/tiV2cdnCdVY">영상 - 22 12 06, ken 10787, 159강, 회원정보수정 폼 처리 중 버그 발견</a></p></li><li><p>myPage.jsp 에서 redirectUri 불러오지 못하는 버그 발생</p></li><li><p>비빌번호 입력시 비빌번호 입력해달라는 버그 발생</p></li><li><p>빠른 처리후 영상 올릴 예정</p></li><li><p><a data-nodeid="2197" href="https://youtu.be/DYtjc4aeLXs">영상 - 22 12 21, ken 10787, 159강, 회원정보수정 폼 처리 오류 문제 해결</a></p></li></ul><p>향후 계획</p><ul><li><p>회원가입</p></li><li><p>아이디 찾기</p></li><li><p>비밀번호 암호화</p></li><li><p>이메일 발송테스트</p></li><li><p>비밀번호 찾기(임시 패스워드 발송)</p></li><li><p>needLogin인터셉터에 의해서 막힌 경우</p></li><li><p>로그인 성공시, 원래 가려 했던 곳으로 이동</p></li></ul><p>꼭 봐야할 영상</p><ul><li><p><a data-nodeid="2209" href="https://www.youtube.com/watch?v=1d38YZKCM88&amp;list=PLuHgQVnccGMDF6rHsY9qMuJMd295Yk4sa">생활코딩 - 관계형 데이터베이스 모델</a></p></li><li><p><a data-nodeid="2212" href="https://www.youtube.com/watch?v=dqcOa-fVWWo&amp;list=PLuHgQVnccGMB5q5uJIDhLlcC2V6tyXhY6">생활코딩 - 오라클</a></p></li></ul><p>기술서 예시</p><ul><li><p><a data-nodeid="2216" href="https://wiken.io/ken/4789">기술서 예시, wiken.io/ken/4789</a></p></li></ul><p>&nbsp;</p><p>&nbsp;</p> 2024-01-02 03:58:17 스프링부트 페이스북 연동 http://macaronics.net/index.php/m01/spring/view/2162 2162 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>페이스북 개발자 앱 등록</p><p><a target="_blank" href="https://growth-coder.tistory.com/141">https://growth-coder.tistory.com/141</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1. 라이브러리 등록</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-oauth2-client&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2.application.yml&nbsp; 설정</strong></p><pre class="brush:as3;">spring: security: oauth2: client: registration: facebook: client-id: client-secret: scope: - public_profile - email</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3.&nbsp;SecurityConfig&nbsp; 설정</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @RequiredArgsConstructor @EnableWebSecurity @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { pivate final OAuth2DetailsService oAuth2DetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http. ~ .and() .oauth2Login() .userInfoEndpoint() .userService(oAuth2DetailsService); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>4.&nbsp;OAuth2DetailsService</strong></p><pre class="brush:as3;">import java.util.Map; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.user.User; import com.cos.photogramstart.domain.user.UserRepository; @Service public class OAuth2DetailsService extends DefaultOAuth2UserService{ @Autowired private UserRepository userRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { System.out.println(&quot; OAuth 2 페이스북 로그인 &quot; +userRequest.getAccessToken()); OAuth2User oauth2User=super.loadUser(userRequest); Map&lt;String,Object&gt; userInfo= oauth2User.getAttributes(); String username=&quot;facebook_&quot;+(String)userInfo.get(&quot;id&quot;); String password= new BCryptPasswordEncoder().encode(UUID.randomUUID().toString()); String name=(String)userInfo.get(&quot;name&quot;); String email=(String)userInfo.get(&quot;email&quot;); System.out.println(&quot;************ oAuth2User.getAttributes : &quot; + oauth2User.getAttributes()); System.out.println(&quot;************ username : &quot; + username); System.out.println(&quot;************ password : &quot; + password); System.out.println(&quot;************ name : &quot; + name); System.out.println(&quot;************ email : &quot; + email); User userEntity=userRepository.findByUsername(username); if(userEntity==null) { User user =User.builder() .username(username) .password(password) .email(email) .name(name) .role(&quot;ROLE_USER&quot;) .build(); return new PrincipalDetails(userRepository.save(user), oauth2User.getAttributes()); }else { //페이스북으로 이미 회원가입이 되어 있는경우 return new PrincipalDetails(userEntity, oauth2User.getAttributes()); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>5. login.jsp</strong></p><pre class="brush:as3;"> &lt;!-- Oauth 소셜로그인 --&gt; &lt;div class=&quot;login__facebook&quot;&gt; &lt;button onclick=&quot;javascript:location.href=&#39;/oauth2/authorization/facebook&#39;&quot;&gt; &lt;i class=&quot;fab fa-facebook-square&quot;&gt;&lt;/i&gt; &lt;span&gt;Facebook으로 로그인&lt;/span&gt; &lt;/button&gt; &lt;/div&gt; &lt;!-- Oauth 소셜로그인end --&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>PrincipalDetails</strong></p><pre class="brush:as3;">import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import com.cos.photogramstart.domain.user.User; public class PrincipalDetails implements UserDetails , OAuth2User{ private static final long serialVersionUID = 3351489579645764340L; private User user; private Map&lt;String, Object&gt; attributes; public PrincipalDetails(User user) { this.user=user; } public PrincipalDetails(User user, Map&lt;String, Object&gt; attributes) { this.attributes=attributes; this.user=user; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } //권한:한개가 아닐 수 있음.(3개 이상의 권한) @Override public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() { Collection&lt;GrantedAuthority&gt; collector=new ArrayList&lt;&gt;(); // collector.add(new GrantedAuthority() { // // @Override // public String getAuthority() { // return user.getRole(); // } // }); collector.add(()-&gt; user.getRole()); return collector; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public Map&lt;String, Object&gt; getAttributes() { return attributes; } @Override public String getName() { return (String)attributes.get(&quot;name&quot;); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="http:// https://github.com/braverokmc79/spring-boot-jpa-web-release">&nbsp;https://github.com/braverokmc79/spring-boot-jpa-web-release</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-29 13:30:35 스프링부트 유효성검사를 AOP 처리 http://macaronics.net/index.php/m01/spring/view/2161 2161 <p>&nbsp;</p><p>&nbsp;</p><p>스프링부트 AOP 처리&nbsp; 컨트롤에서 작동처리</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-aop&lt;/artifactId&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><pre class="brush:as3;">import java.util.HashMap; import java.util.Map; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import com.cos.photogramstart.handler.ex.CustomValidationApiException; @Component @Aspect public class ValidationAdvice { //Controller.*(..) 모든 메소드 @Around(&quot;execution(* com.cos.macaronics.web.api.*Controller.*(..))&quot;) public Object apiAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println(&quot;web api 컨트롤러 ====== &quot;); //proceedingJoinPoint =&gt; profile 함수의 모든 곳에 접근할 수 있는 변수 //profile 함수보다 먼저 실행 Object[] args=proceedingJoinPoint.getArgs(); for(Object arg:args) { if(arg instanceof BindingResult) { System.out.println(&quot;유효성을 검사하는 함수&quot;); BindingResult errors=(BindingResult)arg; if(errors.hasErrors()) { Map&lt;String,String&gt; errorMap=new HashMap&lt;&gt;(); for(FieldError error: errors.getFieldErrors()) { errorMap.put(error.getField(), error.getDefaultMessage()); } throw new CustomValidationApiException(&quot;유효성 검사 실패함&quot;, errorMap); } } } return proceedingJoinPoint.proceed(); // profile 함수가 실행됨. } @Around(&quot;execution(* com.cos.macaronics.web.*Controller.*(..))&quot;) public Object advice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println(&quot;web 컨트롤러 ====== &quot;); return proceedingJoinPoint.proceed(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>컨트롤 예</p><pre class="brush:as3;">/** * 댓글 삭제 */ @RequiredArgsConstructor @RestController @Log4j2 public class CommentApiController { private final CommentService commentService; /** * 댓글 등록 * @param commentDto * @param principalDetails * @return */ @PostMapping(&quot;/api/comment&quot;) public ResponseEntity&lt;?&gt; commentSave(@Valid @RequestBody CommentDto commentDto, BindingResult bindingResult, @AuthenticationPrincipal PrincipalDetails principalDetails){ commentDto.setUser(principalDetails.getUser()); CommentResDTO commentResDTO= commentService.댓글쓰기(commentDto); return ResponseEntity.ok().body(new CMRespDto&lt;&gt;(1,&quot;댓글 등록 성공&quot;, commentResDTO)); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start">https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start</a></p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-28 22:01:35 ★ 스크롤 페이징 로딩 구현하기 http://macaronics.net/index.php/m04/jquery/view/2160 2160 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> // (1) 스토리 로드하기 let page=0; function storyLoad() { $.ajax({ url:`/api/image?page=${page}`, dataType:&quot;json&quot; }) .done(res=&gt;{ //console.log(&quot;(1) 스토리 로드하기 : &quot; ,res); res.data.content.forEach((image)=&gt;{ let storyItem=getStoryItem(image); $(&quot;#storyList&quot;).append(storyItem); }); }) .fail(error=&gt;{ consoleerror(&quot;에러 : &quot;, error); }) }; storyLoad();</pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">// (2) 스토리 스크롤 페이징하기 $(window).scroll(() =&gt; { console.log(&quot;1.윈도우 scrollTop : &quot;, $(window).scrollTop()); console.log(&quot;2.문서의 높이 :&quot;, $(document).height()); console.log(&quot;3.윈도우 높이&quot;, $(window).height()); let checkNum=$(window).scrollTop()-($(document).height() - $(window).height()); console.log(checkNum); console.log(&quot;&quot;); console.log(&quot;&quot;); if(checkNum&lt;1 &amp;&amp; checkNum &gt;-1){ console.log(&quot;========= 로드 ==============&quot;); page++; storyLoad(); } }); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-24 22:48:45 ★스프링 부트 JPA 커스텀 페이징 처리 http://macaronics.net/index.php/m01/spring/view/2159 2159 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>페이징 처리에 앞서프로젝트 최적화를 위헤 open-in-view: false 는 기본적으로 false 놓아야 하며,&nbsp; 지연로딩 전략을 사용한다.</p><p>@ManyToOne(fetch = FetchType.LAZY)</p><p>지연 전략시 다음과 같은&nbsp;FetchType이 Lazy</p><p>[JPA] Lazy 로딩으로 인한 JSON 반환 오류 (No serializer found for class 가 나온다&nbsp;</p><p>&nbsp;org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer)</p><p>따라서.&nbsp;</p><p>application.yml&nbsp; 다음과 같이 설정</p><pre class="brush:as3;">spring jackson: serialization: fail-on-empty-beans: false </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1.&nbsp;PageableCustom&nbsp; 생성</strong></p><pre class="brush:as3;">import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import lombok.Getter; @Getter public class PageableCustom { private boolean first; private boolean last; private boolean hasNext; private int totalPages; private long totalElements; private int page; private int size; public PageableCustom() { } public PageableCustom(Page&lt;?&gt; page) { this.first = page.isFirst(); this.last = page.isLast(); this.hasNext = page.hasNext(); this.totalPages = page.getTotalPages(); this.totalElements = page.getTotalElements(); this.page = page.getNumber() + 1; this.size = page.getSize(); } public PageableCustom(Slice&lt;?&gt; slice) { this.first = slice.isFirst(); this.last = slice.isLast(); this.hasNext = slice.hasNext(); this.page = slice.getNumber() + 1; this.size = slice.getSize(); } }</pre><p>&nbsp;</p><p><strong>2.PageCustom&nbsp; 생성</strong></p><pre class="brush:as3;">import lombok.Getter; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; import java.io.Serializable; import java.util.List; @Getter public class PageCustom&lt;T&gt; implements Serializable { private static final long serialVersionUID = 1L; private List&lt;T&gt; content; private PageableCustom pageableCustom; public PageCustom(List&lt;T&gt; content, Pageable pageable, Long total) { this.content = content; this.pageableCustom = new PageableCustom(new PageImpl&lt;T&gt;(content, pageable, total)); } public PageCustom(List&lt;T&gt; content, Pageable pageable, boolean hasNext) { this.content = content; this.pageableCustom = new PageableCustom(new SliceImpl&lt;T&gt;(content, pageable, hasNext)); } }</pre><p>&nbsp;</p><p>=================================================&nbsp;&nbsp;=================================================</p><p>&nbsp;</p><p><strong>3. 컨트롤 처리 예</strong></p><pre class="brush:as3;">import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.image.ImageResDTO; import com.cos.photogramstart.service.ImageService; import com.cos.photogramstart.utils.PageCustom; import com.cos.photogramstart.web.dto.CMRespDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController public class ImageApiController { private final ImageService imageService; @GetMapping(&quot;/api/image&quot;) public ResponseEntity&lt;?&gt; imageStory(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(size=3) Pageable pageable){ PageCustom&lt;ImageResDTO&gt; images= imageService.이미지스토리(principalDetails.getUser().getId(), pageable); return ResponseEntity.status(HttpStatus.OK).body(new CMRespDto&lt;&gt;(1,&quot;성공&quot;, images)); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>4.서비스&nbsp;</strong></p><p><strong>Image 엔티티를&nbsp; 반환처리하면 안 좋으므로 다음과 같이&nbsp;ImageResDTO 커스텀 객체를 만든후 페이징 처리 한다.</strong></p><p><strong>Page&lt;Image&gt;&nbsp; 로 반환시킨후&nbsp; &nbsp;커스텀&nbsp;&nbsp;List&lt;ImageResDTO&gt; 에 데이터를 추가한후&nbsp;&nbsp;PageCustom 반환 처리하면 된다.</strong></p><pre class="brush:as3;">import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.image.Image; import com.cos.photogramstart.domain.image.ImageRepository; import com.cos.photogramstart.domain.image.ImageResDTO; import com.cos.photogramstart.utils.PageCustom; import com.cos.photogramstart.web.dto.image.ImageUploadDto; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor @Log4j2 public class ImageService { private final ImageRepository imageRepository; @Transactional(readOnly = true) //영속성 컨텍스트 변경 감지를 해서, 더티체킹, flush 반영 x public PageCustom&lt;ImageResDTO&gt; 이미지스토리(long principalId, Pageable pageable){ /** 접속자만 구독 목록 가져오기 */ List&lt;Long&gt; ids =imageRepository.mySubscribes(principalId); Page&lt;Image&gt; pageImages=imageRepository.mStroy(ids, pageable); //Eager 전략일때는 영속성에서 데이터를 가져오지만 의 지연 전략으로 (FetchType.LAZY ) 인하여 user 객체 가져와야 한다. List&lt;ImageResDTO&gt; imageResDTOs=new ArrayList&lt;ImageResDTO&gt;(); for(Image image: pageImages) { ImageResDTO imageResDTO=image.toImageResDTO(); imageResDTO.setUser(userRepository.findById(image.getUser().getId()).orElse(null)); imageResDTOs.add(imageResDTO); } return new PageCustom&lt;ImageResDTO&gt;(imageResDTOs, pageImages.getPageable(), pageImages.getTotalElements()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>5.&nbsp;ImageRepository</strong></p><pre class="brush:as3;">import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface ImageRepository extends JpaRepository&lt;Image, Long&gt; { @Query(value=&quot;select * from Image where userId = :userId&quot; ,nativeQuery = true ) List&lt;Image&gt; findAllByUserImages(@Param(value = &quot;userId&quot;) long userId); //@Query(value=&quot;SELECT * FROM image WHERE userid IN (SELECT toUserId FROM Subscribe WHERE fromUserId =:principalId) ORDER BY id DESC &quot;, nativeQuery = true) /** * JPQL In 절 사용하기 * @param ids * @param pageable * @return */ @Query(value=&quot;SELECT i FROM Image i JOIN i.user u WHERE u.id in (:ids) &quot;) Page&lt;Image&gt; mStroy(@Param(value = &quot;ids&quot;) List&lt;Long&gt; ids, Pageable pageable); /** * 접속자의 구독목록 아이디만 가져오기 * @param principalId * @return */ @Query(value=&quot;SELECT t.id FROM Subscribe s JOIN s.fromUser u JOIN s.toUser t WHERE u.id =:principalId &quot;) List&lt;Long&gt; mySubscribes(long principalId); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:22px"><strong>엔티티</strong></span></span></p><p><strong>Image&nbsp;&nbsp;</strong></p><pre class="brush:as3;">import java.time.LocalDateTime; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.PrePersist; import com.cos.photogramstart.domain.user.User; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Builder @AllArgsConstructor @NoArgsConstructor @Data @Entity @ToString(exclude = &quot;user&quot;) public class Image { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String caption; private String originalFilename; private String postImageUrl; @JoinColumn(name=&quot;userId&quot;) @ManyToOne(fetch = FetchType.LAZY) private User user; private LocalDateTime createDate; @PrePersist public void createDate() { this.createDate=LocalDateTime.now(); } public ImageResDTO toImageResDTO() { return ImageResDTO.builder() .id(id) .caption(caption) .originalFilename(originalFilename) .postImageUrl(postImageUrl) .user(user) .userId(user.getId()) .createDate(createDate) .build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>ImageResDTO</strong></p><p>@JsonIgnoreProperties({&quot;images&quot;}) 설정해 줘야 하는데 다음과 같은 영속성 오류가 발생하기 때문이다.</p><p>&nbsp;</p><p>&quot;trace&quot;: &quot;org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role: com.cos.photogramstart.domain.user.User.images, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize</p><p>&nbsp;</p><p>유저 객체를 불러올때 다시 image 객체를 불러오는 반복된처처로&nbsp;&nbsp; could not initialize proxy - no Session;&nbsp; 라는 것이다.</p><p>따라서, json 반환시에는&nbsp; user 에서&nbsp;images 를 무시하는&nbsp; 처리로 @JsonIgnoreProperties 처리를 해준다.</p><p>&nbsp;</p><pre class="brush:as3;">import java.time.LocalDateTime; import com.cos.photogramstart.domain.user.User; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Builder @AllArgsConstructor @NoArgsConstructor @Data @ToString(exclude = &quot;user&quot;) public class ImageResDTO { private long id; private String caption; private String originalFilename; private String postImageUrl; @JsonIgnoreProperties({&quot;images&quot;}) private User user; private long userId; private LocalDateTime createDate; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>User</strong></p><pre class="brush:as3;">import java.time.LocalDateTime; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.PrePersist; import com.cos.photogramstart.domain.image.Image; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @ToString(exclude = &quot;images&quot;) @Builder @AllArgsConstructor @NoArgsConstructor @Getter @Setter @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(nullable = false, length=20, unique = true) private String username; @Column(nullable = false) private String password; private String name; private String website; //웹 사이트 private String bio; //자기소개 @Column(unique = true, nullable = false) private String email; private String phone; private String gender; private String profileImageUrl; //사진 private String role; //권한 @OneToMany(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY) private List&lt;Image&gt; images; //양방향 매핑 private LocalDateTime createDate; @PrePersist // 디비에 Insert 되기 직전에 실행 public void createDate() { this.createDate=LocalDateTime.now(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>결과</strong></p><pre class="brush:as3;">{ &quot;code&quot;: 1, &quot;message&quot;: &quot;성공&quot;, &quot;data&quot;: { &quot;content&quot;: [ { &quot;id&quot;: 12, &quot;caption&quot;: &quot;홍길동 &quot;, &quot;originalFilename&quot;: &quot;2312211906whiuzletyv_0.jpg&quot;, &quot;postImageUrl&quot;: &quot;1cdb123e-db89-42f7-b96a-44e80d14da43_2312211906whiuzletyv_0.jpg&quot;, &quot;user&quot;: { &quot;id&quot;: 5, &quot;username&quot;: &quot;ssar&quot;, &quot;password&quot;: &quot;$2a$10$.NK35QvezqQoh3uvE5S0G.AmtILUd6upEcu4MUi0jm03z6fsW0Vry&quot;, &quot;name&quot;: &quot;홍길동&quot;, &quot;website&quot;: null, &quot;bio&quot;: null, &quot;email&quot;: &quot;ssar@gmail.com&quot;, &quot;phone&quot;: null, &quot;gender&quot;: null, &quot;profileImageUrl&quot;: null, &quot;role&quot;: &quot;ROLE_USER&quot;, &quot;createDate&quot;: &quot;2023-12-22T17:38:56.687979&quot; }, &quot;userId&quot;: 5, &quot;createDate&quot;: &quot;2023-12-23T14:15:03.274058&quot; }, { &quot;id&quot;: 11, &quot;caption&quot;: &quot;333&quot;, &quot;originalFilename&quot;: &quot;2312200136ptuoqyzasv_0.jpg&quot;, &quot;postImageUrl&quot;: &quot;87cc4ed0-de8c-485f-8b02-61b95bc0961a_2312200136ptuoqyzasv_0.jpg&quot;, &quot;user&quot;: { &quot;id&quot;: 3, &quot;username&quot;: &quot;test3&quot;, &quot;password&quot;: &quot;$2a$10$qWtJO89AcvZKNFCg97dJN.AqbLmuc6two.D4s/knafkI5GoLxqoAq&quot;, &quot;name&quot;: &quot;김민종&quot;, &quot;website&quot;: null, &quot;bio&quot;: null, &quot;email&quot;: &quot;test3@gmail.com&quot;, &quot;phone&quot;: null, &quot;gender&quot;: null, &quot;profileImageUrl&quot;: null, &quot;role&quot;: &quot;ROLE_USER&quot;, &quot;createDate&quot;: &quot;2023-12-20T19:58:47.300206&quot; }, &quot;userId&quot;: 3, &quot;createDate&quot;: &quot;2023-12-22T19:49:10.005323&quot; }, { &quot;id&quot;: 10, &quot;caption&quot;: &quot;333&quot;, &quot;originalFilename&quot;: &quot;2312200136pqlhjryrdw_0.jpg&quot;, &quot;postImageUrl&quot;: &quot;17e59775-9854-4d85-b525-0f18b7ad1701_2312200136pqlhjryrdw_0.jpg&quot;, &quot;user&quot;: { &quot;id&quot;: 3, &quot;username&quot;: &quot;test3&quot;, &quot;password&quot;: &quot;$2a$10$qWtJO89AcvZKNFCg97dJN.AqbLmuc6two.D4s/knafkI5GoLxqoAq&quot;, &quot;name&quot;: &quot;김민종&quot;, &quot;website&quot;: null, &quot;bio&quot;: null, &quot;email&quot;: &quot;test3@gmail.com&quot;, &quot;phone&quot;: null, &quot;gender&quot;: null, &quot;profileImageUrl&quot;: null, &quot;role&quot;: &quot;ROLE_USER&quot;, &quot;createDate&quot;: &quot;2023-12-20T19:58:47.300206&quot; }, &quot;userId&quot;: 3, &quot;createDate&quot;: &quot;2023-12-22T19:49:01.636644&quot; } ], &quot;pageableCustom&quot;: { &quot;first&quot;: true, &quot;last&quot;: false, &quot;hasNext&quot;: true, &quot;totalPages&quot;: 2, &quot;totalElements&quot;: 5, &quot;page&quot;: 1, &quot;size&quot;: 3 } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>페이지 번호는 0 부</p><p>http://localhost:8080/api/image?page=0</p><p>http://localhost:8080/api/image?page=1</p><p>http://localhost:8080/api/image?page=2</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>소스</strong></p><p><a target="_blank" href="https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start">https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start</a></p><p><a target="_blank" href="https://github.dev/braverokmc79/EaszUp-Springboot-Photogram-Start">https://github.dev/braverokmc79/EaszUp-Springboot-Photogram-Start</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-23 20:10:11 hikariConfig 설정 http://macaronics.net/index.php/m01/spring/view/2158 2158 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>hikariConfig&nbsp; &nbsp;설정</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;bean id=&quot;hikariConfig&quot; class=&quot;com.zaxxer.hikari.HikariConfig&quot;&gt; &lt;property name=&quot;poolName&quot; value=&quot;mercatus_connection_pool&quot; /&gt; &lt;property name=&quot;maximumPoolSize&quot; value=&quot;50&quot; /&gt; &lt;property name=&quot;maxLifetime&quot; value=&quot;60000&quot; /&gt; &lt;property name=&quot;idleTimeout&quot; value=&quot;30000&quot; /&gt; &lt;property name=&quot;driverClassName&quot; value=&quot;${jdbc.driverClassName}&quot;&gt;&lt;/property&gt; &lt;property name=&quot;jdbcUrl&quot; value=&quot;${jdbc.url}&quot;&gt;&lt;/property&gt; &lt;property name=&quot;username&quot; value=&quot;${jdbc.username}&quot;&gt;&lt;/property&gt; &lt;property name=&quot;password&quot; value=&quot;${jdbc.password}&quot;&gt;&lt;/property&gt; &lt;/bean&gt; &lt;!-- 2. HikariCP 를 통해 커넥션을 관리하는 DataSource 객체(HikariDataSource) 설정 --&gt; &lt;!-- 생성자 파라미터로 1번에서 생성한 HikariConfig 객체 전달 --&gt; &lt;bean id=&quot;dataSource&quot; class=&quot;com.zaxxer.hikari.HikariDataSource&quot;&gt; &lt;constructor-arg ref=&quot;hikariConfig&quot;&gt;&lt;/constructor-arg&gt; &lt;/bean&gt; </pre><p>&nbsp;</p> 2023-12-20 14:27:12 스프링부트 p6spy 설정 http://macaronics.net/index.php/m01/spring/view/2157 2157 <p>&nbsp;</p><p>&nbsp;</p><p>스프링부트 3.0&nbsp; 이하&nbsp;</p><p>&nbsp;</p><p><strong>1.p6spy</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;com.github.gavlyukovskiy&lt;/groupId&gt; &lt;artifactId&gt;p6spy-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.8.0&lt;/version&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>2.</strong></p><p><strong>application.yml</strong></p><p>&nbsp;</p><pre class="brush:as3;"> datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mariadb://localhost:3306/test username: test password: 1234 logging: level: p6spy: info </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>3, P6spyPrettySqlFormatter</strong></p><pre class="brush:as3;"> import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import org.hibernate.engine.jdbc.internal.FormatStyle; import com.p6spy.engine.logging.Category; import com.p6spy.engine.spy.appender.MessageFormattingStrategy; public class P6spyPrettySqlFormatter implements MessageFormattingStrategy { @Override public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) { sql = formatSql(category, sql); Date currentDate = new Date(); SimpleDateFormat format1 = new SimpleDateFormat(&quot;yy.MM.dd HH:mm:ss&quot;); // return now + &quot;|&quot; + elapsed + &quot;ms|&quot; + category + &quot;|connection &quot; + connectionId // + &quot;|&quot; + P6Util.singleLine(prepared) + sql; return format1.format(currentDate) + &quot; | &quot; + &quot;OperationTime : &quot; + elapsed + &quot;ms&quot; + sql; } private String formatSql(String category,String sql) { if(sql ==null || sql.trim().equals(&quot;&quot;)) return sql; // Only format Statement, distinguish DDL And DML if (Category.STATEMENT.getName().equals(category)) { String tmpsql = sql.trim().toLowerCase(Locale.ROOT); if(tmpsql.startsWith(&quot;create&quot;) || tmpsql.startsWith(&quot;alter&quot;) || tmpsql.startsWith(&quot;comment&quot;)) { sql = FormatStyle.DDL.getFormatter().format(sql); }else { sql = FormatStyle.BASIC.getFormatter().format(sql); } sql = &quot;|\nHeFormatSql(P6Spy sql,Hibernate format):&quot;+ sql; } return sql; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>4.P6spyConfig</strong></p><pre class="brush:as3;"> import javax.annotation.PostConstruct; import org.springframework.context.annotation.Configuration; import com.p6spy.engine.spy.P6SpyOptions; @Configuration public class P6spyConfig { @PostConstruct public void setLogMessageFormat() { P6SpyOptions.getActiveInstance().setLogMessageFormat(P6spyPrettySqlFormatter.class.getName()); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>참고:</p><p>&nbsp;</p><p><a target="_blank" href="https://p6spy.readthedocs.io/en/latest/integration.html">https://p6spy.readthedocs.io/en/latest/integration.html</a></p><p><a target="_blank" href="https://github.com/HomoEfficio/dev-tips/blob/master/p6spy-%EC%84%A4%EC%A0%95.md">https://github.com/HomoEfficio/dev-tips/blob/master/p6spy-%EC%84%A4%EC%A0%95.md</a></p><p>&nbsp;</p><p><a target="_blank" href="https://devkuka.tistory.com/303">https://devkuka.tistory.com/303</a></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2055">https://macaronics.net/index.php/m01/spring/view/2055</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-19 18:20:36 웹뷰로 앱만들기 및 푸시연 http://macaronics.net/index.php/m02/hybrid/view/2156 2156 <p>&nbsp;</p><p>&nbsp;</p><p>웹 개발자를 위한 안드로이드 웹뷰 만들기</p><p><a target="_blank" href="https://stir.tistory.com/329">https://stir.tistory.com/329</a></p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-19 13:29:41 스프링부트 @RestController 응답 처리 , @Valid 유효성 체크 응답처리, 예외처리 http://macaronics.net/index.php/m01/spring/view/2155 2155 <p>&nbsp;</p><p>&nbsp;</p><p><strong>1. 라이브러리</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt; &lt;version&gt;2.5.2&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>2.&nbsp;CMRespDto</strong></p><pre class="brush:as3;">import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor @Data public class CMRespDto&lt;T&gt; { private int code; //1(성공), -1(실패) private String message; private T data; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>3.&nbsp; CustomValidationException</strong></p><pre class="brush:as3;">import java.util.Map; public class CustomValidationException extends RuntimeException { private static final long serialVersionUID = -807520916259076805L; private String message; private Map&lt;String,String&gt; errorMap; public CustomValidationException(String message, Map&lt;String,String&gt; errorMap) { super(message); this.message=message; this.errorMap=errorMap; } public Map&lt;String,String&gt; getErrorMap(){ return errorMap; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>CustomValidationApiException</strong></p><pre class="brush:as3;">import java.util.Map; public class CustomValidationApiException extends RuntimeException { private static final long serialVersionUID = -807520916259076805L; private Map&lt;String,String&gt; errorMap; public CustomValidationApiException(String message) { super(message); } public CustomValidationApiException(String message, Map&lt;String,String&gt; errorMap) { super(message); this.errorMap=errorMap; } public Map&lt;String,String&gt; getErrorMap(){ return errorMap; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>4.ControllerExceptionHandler</strong></p><pre class="brush:as3;"> import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; import com.cos.photogramstart.handler.ex.CustomApiException; import com.cos.photogramstart.handler.ex.CustomValidationApiException; import com.cos.photogramstart.handler.ex.CustomValidationException; import com.cos.photogramstart.utils.Script; import com.cos.photogramstart.web.dto.CMRespDto; @RestController @ControllerAdvice public class ControllerExceptionHandler { @ExceptionHandler(CustomValidationException.class) public String validationException(CustomValidationException e) { //CMRespDto,Script 비교 //1.클라이언트에게 응답할 때는 Script 좋음. //2.Ajax통신 - CMRespDto //3.Android 통신 - CMRespDto if(e.getErrorMap()!=null) { return Script.back(e.getErrorMap().toString()); }else { return Script.back(e.getMessage()); } } @ExceptionHandler(CustomValidationApiException.class) public ResponseEntity&lt;?&gt; validationApiException(CustomValidationApiException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new CMRespDto&lt;Object&gt;(-1, e.getMessage(), e.getErrorMap())) ; } @ExceptionHandler(CustomApiException.class) public ResponseEntity&lt;?&gt; apiException(CustomApiException e){ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new CMRespDto(-1, e.getMessage(), null)) ; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>===================================================================================================================</strong></p><p><strong>추가 참조</strong></p><p>&nbsp;</p><p>SignupDto</p><pre class="brush:as3;">import javax.validation.constraints.Email; import javax.validation.constraints.Max; import javax.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Length; import com.cos.photogramstart.domain.user.User; import lombok.Data; @Data //Getter, Setter public class SignupDto { @NotBlank(message=&quot;유저명을 입력해주세요.&quot;) @Length(max = 20) private String username; @NotBlank private String password; @NotBlank(message=&quot;이메일을 입력해주세요.&quot;) @Email(message = &quot;올바른 이메일 주소를 입력해 주세요.&quot;) private String email; @NotBlank private String name; public User toEntity() { return User.builder() .username(username) .password(password) .email(email) .name(name) .build(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>AuthController</strong></p><pre class="brush:as3;"> /** * 회원가입버튼-&gt; /auth/signup -&gt; /auth/signin * 회원가입버튼 x * @return */ @PostMapping(&quot;/auth/signup&quot;) public String signup(@Valid SignupDto signupDto, Errors errors, Model model) { // key=value(x-www-form-unlencoded) log.info(&quot;** 회원가입 처리 {}&quot;, signupDto.toString()); if(errors.hasErrors()) { Map&lt;String,String&gt; erroMap=new HashMap&lt;&gt;(); for(FieldError error : errors.getFieldErrors() ) { System.out.println(&quot;======================================&quot;); erroMap.put(error.getField(), error.getDefaultMessage()); System.out.println(&quot;======================================&quot;); } throw new CustomValidationException(&quot;유효성검사 실패함 &quot; , erroMap); } if(errors.hasErrors()) { log.info(&quot;*************** errors : {}&quot;, errors.toString()); return &quot;auth/signup&quot;; } //이메일 중복 체크 if(authService.existsByEmail(signupDto.getEmail())){ log.info(&quot;***** 이메일 중복 : {}&quot;, signupDto); errors.rejectValue(&quot;email&quot;, &quot;email&quot; , &quot;이메일 중복&quot;); return &quot;auth/signup&quot;; } User user=signupDto.toEntity(); log.info(user.toString()); authService.insertUser(user); return &quot;redirect:/auth/signin&quot;; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>signup.jsp</strong></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;%@ taglib prefix=&quot;form&quot; uri=&quot;http://www.springframework.org/tags/form&quot; %&gt; &lt;!--회원가입 인풋--&gt; &lt;form:form class=&quot;login__input&quot; action=&quot;/auth/signup&quot; method=&quot;post&quot; modelAttribute=&quot;signupDto&quot;&gt; &lt;!-- required=&quot;required&quot; --&gt; &lt;input type=&quot;text&quot; name=&quot;username&quot; placeholder=&quot;유저네임&quot; value=&quot;${signupDto.username}&quot; /&gt; &lt;div class=&quot;error&quot;&gt;&lt;form:errors path=&quot;username&quot;/&gt;&lt;/div&gt; &lt;input type=&quot;password&quot; name=&quot;password&quot; placeholder=&quot;패스워드&quot; required=&quot;required&quot; /&gt; &lt;div class=&quot;error&quot;&gt;&lt;form:errors path=&quot;password&quot;/&gt;&lt;/div&gt; &lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;이메일&quot; required=&quot;required&quot; value=&quot;${signupDto.email}&quot; /&gt; &lt;div class=&quot;error&quot;&gt;&lt;form:errors path=&quot;email&quot;/&gt;&lt;/div&gt; &lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;이름&quot; value=&quot;${signupDto.name}&quot; /&gt; &lt;div class=&quot;error&quot;&gt;&lt;form:errors path=&quot;name&quot;/&gt;&lt;/div&gt; &lt;button&gt;가입&lt;/button&gt; &lt;/form:form&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>UserApiController</strong></p><pre class="brush:as3;">import java.util.HashMap; import java.util.Map; import javax.validation.Valid; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RestController; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.user.User; import com.cos.photogramstart.handler.ex.CustomValidationApiException; import com.cos.photogramstart.service.UserService; import com.cos.photogramstart.web.dto.CMRespDto; import com.cos.photogramstart.web.dto.user.UserUpdateDto; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @RestController @RequiredArgsConstructor @Log4j2 public class UserApiController { private final UserService userService; @PutMapping(&quot;/api/user/{id}&quot;) public CMRespDto&lt;?&gt; udpate(@PathVariable int id, @Valid UserUpdateDto userUpdateDto , Errors errors, //꼭 @Valid 가 적혀있는 다름 파라미터에 적어야 됨 @AuthenticationPrincipal PrincipalDetails principalDetails) { if(errors.hasErrors()) { Map&lt;String,String&gt; erroMap=new HashMap&lt;&gt;(); for(FieldError error : errors.getFieldErrors()) { erroMap.put(error.getField(), error.getDefaultMessage()); } throw new CustomValidationApiException(&quot;유효성검사 실패함 &quot; , erroMap); }else { User userEntity=userService.회원수정(id, userUpdateDto.toEntity()); principalDetails.setUser(userEntity); log.info(&quot;****업데이트 유저 정보 데이터 : {}&quot;, userEntity.toString()); return new CMRespDto&lt;&gt;(1, &quot;회원수정완료&quot;, userEntity); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>CustomApiException</strong></p><pre class="brush:as3;">public class CustomApiException extends RuntimeException { private static final long serialVersionUID = -807520916259076805L; public CustomApiException(String message) { super(message); } } </pre><p>&nbsp;</p><p><strong>CustomValidationException</strong></p><pre class="brush:as3;">import java.util.Map; public class CustomValidationException extends RuntimeException { private static final long serialVersionUID = -807520916259076805L; private Map&lt;String, String&gt; errorMap; public CustomValidationException(String message, Map&lt;String, String&gt; errorMap) { super(message); this.errorMap = errorMap; } public Map&lt;String, String&gt; getErrorMap(){ return errorMap; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start">https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-14 18:03:15 Jenkins를 이용한 CI/CD Pipeline 구축 http://macaronics.net/index.php/m05/computer/view/2154 2154 <p>&nbsp;</p><p><strong>도커 참조</strong></p><p><a target="_blank" href="https://macaronics.net/m02/linux/view/1819">https://macaronics.net/m02/linux/view/1819</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>도커 설치</strong></p><p>* 도커로 통한 젠키스 설치</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/jenkinsci/docker/blob/master/README.md">https://github.com/jenkinsci/docker/blob/master/README.md</a></p><p>&nbsp;</p><pre class="brush:as3;">---- docker run -p 8089:8080 -p 50000:50000 --name jenkins-server --restart=on-failure -v jenkins_home:/jenkins_home jenkins/jenkins:lts-jdk17 ---- </pre><p>&nbsp;</p><p>Jenkins initial setup is required. An admin user has been created and a password generated.<br />Please use the following password to proceed to installation:</p><p>6526103de4944819a828e78a4167da2a</p><p>This may also be found at: /var/jenkins_home/secrets/initialAdminPassword</p><p>*************************************************************<br />*************************************************************<br />*************************************************************</p><p>2023-12-04 12:01:05.552+0000 [id=51] &nbsp; &nbsp;INFO &nbsp; &nbsp;jenkins.InitReactorRunner$1#onAttained: Completed initialization<br />2023-12-04 12:01:05.577+0000 [id=26] &nbsp; &nbsp;INFO &nbsp; &nbsp;hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running<br />2023-12-04 12:01:06.584+0000 [id=69] &nbsp; &nbsp;INFO &nbsp; &nbsp;h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller<br />2023-12-04 12:01:06.585+0000 [id=69] &nbsp; &nbsp;INFO &nbsp; &nbsp;hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong># 해당 컨테이너 실행</strong></p><p># docker exec -it jenkins-server bash</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>#&nbsp; ssb 테스트 배포 도커 서버 실행</strong></p><p>https://hub.docker.com/r/edowon0623/docker</p><p>윈도우 11&nbsp; c드라이브설정</p><pre class="brush:as3;">- docker run -itd --name docker-server -p 10022:22 -e container=docker --tmpfs /run --tmpfs /tmp -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /var/run/docker.sock:/var/run/docker.sock edowon0623/docker:latest /usr/sbin/init - </pre><p>&nbsp;</p><p>&nbsp;</p><p>비밀번호</p><p><br /><strong>P@ssw0rd</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:18px"><strong>도커파일로 도커 실행</strong></span></span></p><p>&nbsp;cat ./Dockerfile</p><pre class="brush:as3;">FROM tomcat:9.0 LABEL org.opencontainers.image.authors=&quot;edowon0623@gmail.com&quot; COPY ./hello-world.war /usr/local/tomcat/webapps</pre><p>&nbsp;</p><p><strong><span style="color:#000000">[root@711f42f604f0 ~]</span><span style="color:#c0392b"># docker build -t docker-in-server &nbsp;-f Dockerfile .</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>실행</strong></p><p>&nbsp;</p><p>--detach, -d</p><p>컨테이너를&nbsp;생성하고&nbsp;백그라운드에서&nbsp;작동시킨다.&nbsp;</p><p>-d 옵션 필수</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">docker run -d --privileged &nbsp;-p 7071:8080 --name mytomcat docker-in-server:latest</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>확인</strong></p><p>http://192.168.6.3:7071/hello-world/</p><p>&nbsp;</p><p>&nbsp;</p><p>===============================================</p><ul><li>실행 명령어<ul><li>docker build --tag=cicd-project -f Dockerfile .</li><li>docker images&nbsp;</li><li>docker image inspect cicd-project:latest</li><li>docker run <strong>-d </strong>-p 8080:8080 --name mytomcat cicd-project:latest</li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>젠킨스&nbsp; <strong>&nbsp;빌드 후 조치</strong></p><p>Send build artifacts over SSH&nbsp;</p><p>Exec command</p><pre class="brush:as3;">docker build --tag=cicd-project -f Dockerfile . ; docker run -d --privileged &nbsp;-p 7071:8080 --name mytomcat cicd-project:latest ;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ul><li><strong><span style="font-size:18px"><span style="color:#c0392b">Windows, MacOS intel chip) Ansible 컨테이너 실행 명령어</span></span></strong></li></ul><pre class="brush:as3;">docker run --privileged -itd --name ansible-server -p 20022:22 -p 8081:8080 -e container=docker -v /sys/fs/cgroup:/sys/fs/cgroup edowon0623/ansible:latest /usr/sbin/init 컨테이너에서 실행) vi /etc/sysconfig/docker sed -i -e &#39;s/overlay2/vfs/g&#39; /etc/sysconfig/docker-storage systemctl start docker systemctl status docker Windows, MacOS intel chip) Ansible 컨테이너 실행 명령어 docker run --privileged -itd --name ansible-server -p 20022:22 -p 8081:8080 -e container=docker -v /sys/fs/cgroup:/sys/fs/cgroup edowon0623/ansible:latest /usr/sbin/init 컨테이너에서 실행) vi /etc/sysconfig/docker sed -i -e &#39;s/overlay2/vfs/g&#39; /etc/sysconfig/docker-storage systemctl start docker systemctl status docker</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><strong><span style="color:#c0392b">Ansible</span></strong></span><span style="font-size:26px"><strong><span style="color:#c0392b"> </span></strong></span></p><p>&nbsp;</p><ul><li>소스 코드<ul><li><a href="https://github.com/joneconsulting/jenkins_cicd_script">https://github.com/joneconsulting/jenkins_cicd_script</a></li></ul></li></ul><p>Playbook 작성</p><ul><li><ul><li>vi first-playbook.yml<ul><li>https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample0.yml</li></ul></li><li>vi playbook-sample1.yml<ul><li>https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample1.yml</li></ul></li><li>MacOS) vi playbook-sample2.yml<ul><li>https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2.yml</li></ul></li><li>Windows) vi playbook-sample2-windows.yml<ul><li>https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-windows.yml</li></ul></li><li>Linux) vi playbook-sample2-linux.yml<ul><li>https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-linux.yml</li></ul></li></ul></li></ul><p>&nbsp;</p><p>tomcat을 다운로드 받는 부분에서 checksum 처리 시 아래와 같은 오류가 발생한 경우, OS 환경에 맞는 샘플 파일로 변경해서 실행해 보시기 바랍니다.&nbsp;&nbsp;<img title="스크린샷 2022-09-07 오전 9.37.45.png" alt="" width="1200" height="69" src="https://cdn.inflearn.com/public/files/courses/329275/units/123666/88edef29-77bd-49b0-b328-8c5164bab47f/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202022-09-07%20%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB%209.37.45.png" /></p><ul><li><strong>Tomcat 9.0.x 버전이 9.0.75 버전으로 업데이트 되었습니다.</strong><ul><li><strong>Tomcat 9.0.x 최신 버전은 아래 링크에서 확인하시고, 반영해 보기시 바랍니다.</strong><ul><li><a href="https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-linux.yml">https://dlcdn.apache.org/tomcat/tomcat-9/</a></li></ul></li></ul></li><li><strong>Windows 환경에서의 Playbook 실행 시 아래 파일을 사용해 주세요.&nbsp;</strong><ul><li><a href="https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-linux.yml">https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-windows.yml</a></li><li>Ansible-server에서 다음 명령어 ca-certificates 모듈 업데이트 필요<ul><li>yum install -y ca-certificates</li></ul></li></ul></li></ul><ul><li><strong>Linux 환경에서의 Playbook 실행 시 아래 파일을 사용해 주세요.&nbsp;</strong><ul><li><a href="https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-linux.yml">https://github.com/joneconsulting/jenkins_cicd_script/blob/master/playbook_script/playbook-sample2-linux.yml</a></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-12-03 21:08:04 체크박스 아이디 저장 체크시 쿠키저장 http://macaronics.net/index.php/m01/spring/view/2153 2153 <p>&nbsp;</p><p>&nbsp;</p><p>컨트</p><pre class="brush:as3;"> @PostMapping(&quot;LoginPro&quot;) @ResponseBody public ResponseEntity&lt;?&gt; loginPro(String member_id, MemberVO member, @RequestParam(required = false) boolean rememberId, HttpSession session, HttpServletResponse response, Map&lt;String, Object&gt; resultMap) { ~ 로그인 처리 코드 Cookie cookie = new Cookie(&quot;cookie_member_id&quot;, member.getMember_id()); if (rememberId) { // 아이디 저장 체크됨 cookie.setMaxAge(60 * 60 * 24 * 30); } else { // 아이디 저장 미체크 cookie.setMaxAge(0); // 쿠키 즉시 삭제한다는 의미 } response.addCookie(cookie); resultMap.put(&quot;status&quot;, &quot;success&quot;); return ResponseEntity.status(HttpStatus.OK).body(resultMap); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>jsp</p><pre class="brush:as3;"> &lt;input class=&quot;user&quot; type=&quot;text&quot; name=&quot;member_id&quot; id=&quot;id&quot; value=&quot;${cookie.cookie_member_id.value }&quot; style=&quot;align-content: center&quot; placeholder=&quot;아이디&quot;&gt; &lt;input class=&quot;pass&quot; type=&quot;password&quot; name=&quot;member_passwd&quot; id=&quot;passwd&quot;style=&quot;align-content: center&quot; placeholder=&quot;비밀번호&quot;&gt; &lt;div class=&quot;privateCheck&quot;&gt; &lt;input type=&quot;checkbox&quot; name=&quot;rememberId&quot; id=&quot;rememberId&quot; ${empty cookie.cookie_member_id.value ? &quot;&quot;: &quot;checked&quot;} &gt; &lt;a&gt;아이디 저장&lt;/a&gt; &lt;/div&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-11-27 22:48:31 스프링부트 SNS프로젝트 http://macaronics.net/index.php/m01/spring/view/2152 2152 <p>.</p><p>&nbsp;</p><p><em>스프링부트 SNS프로젝트</em></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="과정 이미지" src="https://www.e-itwill.com/data/file/955c74da04954df3f9e37509475c61f7.png" /></p><p>K-디지털과정&nbsp;내일배움카드전용&nbsp;온라인과정</p><p>[K-디지털] 스프링부트 SNS 포토그램 프로젝트(포트폴리오 제작)</p><p>강사 :&nbsp;최주호</p><p>교육기간 : &nbsp;(59회차)&nbsp;&nbsp;&nbsp;&nbsp;2023년 11월 22일 - 2023년 12월 19일 [내일배움카드]<br />(60회차)&nbsp;&nbsp;&nbsp;&nbsp;2023년 11월 29일 - 2023년 12월 26일 [내일배움카드]</p><p>#실전바탕#커리큘럼#포트폴리오#스프링부트#클론코딩#국비교육#무료코딩#KDC</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/codingspecialist/EaszUP-Springboot-Photogram-Start">https://github.com/codingspecialist/EaszUP-Springboot-Photogram-Start</a></p><p>&nbsp;</p><p>&nbsp;</p><p>포토그램 - 인스타그램 클론 코딩</p><p>STS 툴 버그가 발견되면 다른 버전으로 다운 받는 법</p><ul><li><a href="https://github.com/spring-projects/sts4/wiki/Previous-Versions">https://github.com/spring-projects/sts4/wiki/Previous-Versions</a></li></ul><p>STS 툴에 세팅하기 - 플러그인 설정 (JSP, Javascript)</p><ul><li><a rel="nofollow" href="https://blog.naver.com/getinthere/222322821611">https://blog.naver.com/getinthere/222322821611</a></li></ul><p>의존성</p><ul><li>Sring Boot DevTools</li><li>Lombok</li><li>Spring Data JPA</li><li>MariaDB Driver</li><li>Spring Security</li><li>Spring Web</li><li>oauth2-client</li></ul><pre>&lt;!-- 시큐리티 태그 라이브러리 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security&lt;/groupId&gt; &lt;artifactId&gt;spring-security-taglibs&lt;/artifactId&gt; &lt;/dependency&gt; &lt;!-- JSP 템플릿 엔진 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.tomcat&lt;/groupId&gt; &lt;artifactId&gt;tomcat-jasper&lt;/artifactId&gt; &lt;version&gt;9.0.43&lt;/version&gt; &lt;/dependency&gt; &lt;!-- JSTL --&gt; &lt;dependency&gt; &lt;groupId&gt;javax.servlet&lt;/groupId&gt; &lt;artifactId&gt;jstl&lt;/artifactId&gt; &lt;/dependency&gt;</pre><p>데이터베이스</p><pre>create user &#39;cos&#39;@&#39;%&#39; identified by &#39;cos1234&#39;; GRANT ALL PRIVILEGES ON *.* TO &#39;cos&#39;@&#39;%&#39;; create database photogram;</pre><p>yml 설정</p><pre>server: port: 8080 servlet: context-path: / encoding: charset: utf-8 enabled: true spring: mvc: view: prefix: /WEB-INF/views/ suffix: .jsp datasource: driver-class-name: org.mariadb.jdbc.Driver url: jdbc:mariadb://localhost:3306/photogram?serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true&amp;useSSL=false username: cos password: cos1234 jpa: open-in-view: true hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true servlet: multipart: enabled: true max-file-size: 2MB security: user: name: test password: 1234 #file: # path: C:/src/springbootwork-sts/upload/</pre><p>태그라이브러리</p><pre>&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot;%&gt; &lt;%@ taglib prefix=&quot;sec&quot; uri=&quot;http://www.springframework.org/security/tags&quot;%&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-11-16 20:58:57 simple rest api 빠르게 적용하기 http://macaronics.net/index.php/m01/spring/view/2151 2151 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>소스 </strong>:&nbsp;<a target="_blank" href="https://github.com/braverokmc79/restapi-spring-boot-study">https://github.com/braverokmc79/restapi-spring-boot-study</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">1.라이브러리 추가</span></strong></p><pre class="brush:as3;">spring-boot-starter-data-jpa h2 lombok validation hateoas springdoc-openapi-starter-webmvc-ui </pre><p>&nbsp;</p><p><strong>예) pom.xml</strong></p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt; &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt; &lt;parent&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt; &lt;version&gt;3.1.2&lt;/version&gt; &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt; &lt;/parent&gt; &lt;groupId&gt;com.example&lt;/groupId&gt; &lt;artifactId&gt;restfull-web-service&lt;/artifactId&gt; &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt; &lt;name&gt;restfull-web-service&lt;/name&gt; &lt;description&gt;Demo project for Spring Boot&lt;/description&gt; &lt;properties&gt; &lt;java.version&gt;17&lt;/java.version&gt; &lt;/properties&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.h2database&lt;/groupId&gt; &lt;artifactId&gt;h2&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.fasterxml.jackson.dataformat&lt;/groupId&gt; &lt;artifactId&gt;jackson-dataformat-xml&lt;/artifactId&gt; &lt;version&gt;2.14.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-hateoas&lt;/artifactId&gt; &lt;/dependency&gt; &lt;!-- Swagger 적용 Swagger 3.0.0부턴 1개의 종속성만 추가해도 된다. --&gt; &lt;!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui --&gt; &lt;dependency&gt; &lt;groupId&gt;org.springdoc&lt;/groupId&gt; &lt;artifactId&gt;springdoc-openapi-starter-webmvc-ui&lt;/artifactId&gt; &lt;version&gt;2.2.0&lt;/version&gt; &lt;/dependency&gt; &lt;!--- actuator --&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.data&lt;/groupId&gt; &lt;artifactId&gt;spring-data-rest-hal-explorer&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security&lt;/groupId&gt; &lt;artifactId&gt;spring-security-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;build&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt; &lt;configuration&gt; &lt;excludes&gt; &lt;exclude&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/exclude&gt; &lt;/excludes&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;/build&gt; &lt;/project&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">2. application.yml&nbsp;설정 예</span>&nbsp;</strong></p><pre class="brush:as3;">server: port: 8088 spring: # H2 Setting Info (H2 Console에 접속하기 위한 설정정보 입력) #h2: # console: # enabled: false # H2 Console을 사용할지 여부 (H2 Console은 H2 Database를 UI로 제공해주는 기능) # path: /h2-console # H2 Console의 Path # Database Setting Info (Database를 H2로 사용하기 위해 H2연결 정보 입력) datasource: driver-class-name: org.h2.Driver # Database를 H2로 사용하겠다. url: jdbc:h2:tcp://localhost/~/test # H2 접속 정보 username: sa # H2 접속 시 입력할 username 정보 (원하는 것으로 입력) password: # H2 접속 시 입력할 password 정보 (원하는 것으로 입력) output: ansi: enabled: always devtools: livereload: enabled: true restart: enabled: true messages: basename : messages # JPA 설정 jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create # DB 초기화 전략 (none, create, create-drop, update, validate) properties: hibernate: dialect: org.hibernate.dialect.H2Dialect format_sql: true # 쿼리 로그 포맷 (정렬) show_sql: true # 쿼리 로그 출력 defer-datasource-initialization: true sql: init: mode: always logging: level: org.springframework: info springdoc: packages-to-scan: com.example.restfullwebservice default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: path: / disable-swagger-default-url: true display-request-duration: true operations-sorter: alpha # #management: # endpoints: # web: # exposure: # include: &quot;*&quot; </pre><p>&nbsp;</p><p>&nbsp;</p><p>스프링부트&nbsp; 3.0&nbsp; 변경사항</p><p><a target="_blank" href="https://katastrophe.tistory.com/160">https://katastrophe.tistory.com/160</a></p><p>&nbsp;</p><p><strong><span style="font-size:26px">3. 목록 출력</span> </strong></p><p><strong>controller</strong></p><pre class="brush:as3;">package com.example.restfullwebservice.user; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Optional; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; @RestController @RequestMapping(&quot;/jpa&quot;) public class UserJpaController { @Autowired private UserRepository userRepository; // http://localhost:8088/jpa/users or http://localhost:8088/users @GetMapping(&quot;/users&quot;) public List&lt;User&gt; retrieveAllUsers(){ return userRepository.findAll(); } /** * [Spring] RestAPI와 Hateoas로 링크 삽입하기* * * https://katastrophe.tistory.com/160 * @param id * @return * * * http://localhost:8088/jpa/users/1 * */ @GetMapping(&quot;/users/{id}&quot;) public EntityModel&lt;User&gt; retrieveUser(@PathVariable int id){ Optional&lt;User&gt; user =userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found&quot;, id)); } // Resource EntityModel&lt;User&gt; userEntityModel=EntityModel.of(user.get()); WebMvcLinkBuilder linkTo= linkTo(methodOn(this.getClass()).retrieveAllUsers()); userEntityModel.add(linkTo.withRel(&quot;all-users&quot;)); return userEntityModel; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>* 출력 예* 출력 예</strong></p><pre class="brush:as3;"> * * { * &quot;name&quot;: &quot;User1&quot;, * &quot;joinDate&quot;: &quot;2023-11-13T15:00:00.000+00:00&quot;, * &quot;ssn&quot;: &quot;701010-11111&quot;, * &quot;_links&quot;: { * &quot;all-users&quot;: { * &quot;href&quot;: &quot;http://localhost:8088/jpa/users&quot; * } * } * }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">4.삭제</span></strong></p><pre class="brush:as3;"> /** * 삭제 * @param id */ @DeleteMapping(&quot;/users/{id}&quot;) public void deleteUser(@PathVariable int id){ log.info(&quot;**** deleteUser&quot;); userRepository.deleteById(id); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>5.등록</strong></span></p><pre class="brush:as3;"> /** 등록 * insert into tbl_user (join_date,name,password,ssn,id) values (?,?,?,?,?) * @param user * @return */ @PostMapping(&quot;/users&quot;) public ResponseEntity&lt;?&gt; createUser(@Valid @RequestBody User user){ User savedUser=userRepository.save(user); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path(&quot;/{id}&quot;) .buildAndExpand(savedUser.getId()) .toUri(); ResponseEntity&lt;User&gt; build = ResponseEntity.created(location).build(); log.info(&quot;** 유저 등록 : {}&quot; , build); //return ResponseEntity.status(HttpStatus.CREATED).body(build.toString()); return build; } </pre><p>&nbsp;</p><pre class="brush:as3;">{ &quot;name&quot;: &quot;User5&quot;, &quot;joinDate&quot;: &quot;2023-11-14T15:00:00.000+00:00&quot;, &quot;password&quot; :&quot;1111&quot;, &quot;ssn&quot;: &quot;411010-11111&quot; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>6.게시를 작성자 정보 가져오기</strong></span></p><p>&nbsp;</p><pre class="brush:as3;"> //게시글 및 작성자 정보 가져오기 // // http://localhost:8088/jpa/users/1/posts /** * * http://localhost:8088/jpa/users/1/posts * @param id * @return */ @GetMapping(&quot;/users/{id}/posts&quot;) public List&lt;Post&gt; retrieveAllPostsByUser(@PathVariable int id ){ Optional&lt;User&gt; user= userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s} not found&quot;, id)); } return </pre><p>&nbsp;</p><pre class="brush:as3;">/** 출력 =&gt; * [ * { * &quot;id&quot;: 1, * &quot;description&quot;: &quot;My first post&quot; * }, * { * &quot;id&quot;: 2, * &quot;description&quot;: &quot;My second post&quot; * } * ] * * */</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>7.게시물작성</strong></span></p><pre class="brush:as3;"> /** *게시물 저장 * http://localhost:8088/jpa/users/{id}/posts * http://localhost:8088/jpa/users/1/posts */ @PostMapping(&quot;/users/{id}/posts&quot;) public ResponseEntity&lt;?&gt; createPost( @PathVariable int id, @Valid @RequestBody Post post){ Optional&lt;User&gt; user= userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s} not found&quot;, id)); } post.setUser(user.get()); Post savedPost = postRepository.save(post); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path(&quot;/{id}&quot;) .buildAndExpand(savedPost.getId()) .toUri(); return ResponseEntity.created(location).build(); }</pre><pre class="brush:as3;"> /** * 샘플 저장 * { * * &quot;description&quot;:&quot;Hello&quot; * * } */ </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Post</strong></p><pre class="brush:as3;">package com.example.restfullwebservice.user; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @NoArgsConstructor public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = &quot;POST_ID&quot;) private Integer id; private String description; // User : Post -&gt; 1 : (0 :N), Main : Sub -&gt; Parent -&gt; Child @ManyToOne(fetch = FetchType.LAZY) @JsonIgnore private User user; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>User</strong></p><pre class="brush:as3;">package com.example.restfullwebservice.user; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.context.annotation.Primary; import java.util.Date; import java.util.List; @Entity @Getter @Setter @JsonIgnoreProperties(value = {&quot;password&quot; }) //@JsonFilter(&quot;UserInfo&quot;) @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString @EqualsAndHashCode(of=&quot;id&quot;) @Schema(description = &quot;사용자 상세 정보를 위한 도메인 객체&quot;) @Table(name = &quot;TBL_USER&quot;) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Size(min=2, message = &quot;Name 은 2글자 이상 입력해 주세요.&quot;) @Schema(description = &quot;사용자 이름을 입력해 주세요.&quot;) private String name; //과거데이터만 올수 있는 제약 조건 @Past @Schema(description = &quot;사용자 등록일을 입력해 주세요.&quot;) private Date joinDate; // @JsonIgnore @Schema(description = &quot;사용자 패스워드를 입력해 주세요.&quot;) private String password; // @JsonIgnore @Schema(description = &quot;사용자 주민번호를 입력해 주세요.&quot;) private String ssn; @OneToMany(mappedBy = &quot;user&quot;) private List&lt;Post&gt; posts; @Builder public User(Integer id, String name, Date joinDate, String password, String ssn){ this.id=id; this.name=name; this.joinDate=joinDate; this.password=password; this.ssn=ssn; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>전체 controller 내용</strong></p><pre class="brush:as3;">package com.example.restfullwebservice.user; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; import java.util.List; import java.util.Optional; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; @RestController @RequestMapping(&quot;/jpa&quot;) @Slf4j public class UserJpaController { @Autowired private UserRepository userRepository; @Autowired private PostRepository postRepository; // http://localhost:8088/jpa/users or http://localhost:8088/users @GetMapping(&quot;/users&quot;) public List&lt;User&gt; retrieveAllUsers(){ return userRepository.findAll(); } /** * [Spring] RestAPI와 Hateoas로 링크 삽입하기* * * https://katastrophe.tistory.com/160 * @param id * @return * * * http://localhost:8088/jpa/users/1 * * 출력 예 * * { * &quot;name&quot;: &quot;User1&quot;, * &quot;joinDate&quot;: &quot;2023-11-13T15:00:00.000+00:00&quot;, * &quot;ssn&quot;: &quot;701010-11111&quot;, * &quot;_links&quot;: { * &quot;all-users&quot;: { * &quot;href&quot;: &quot;http://localhost:8088/jpa/users&quot; * } * } * } */ @GetMapping(&quot;/users/{id}&quot;) public EntityModel&lt;User&gt; retrieveUser(@PathVariable int id){ Optional&lt;User&gt; user =userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found&quot;, id)); } // Resource EntityModel&lt;User&gt; userEntityModel=EntityModel.of(user.get()); WebMvcLinkBuilder linkTo= linkTo(methodOn(this.getClass()).retrieveAllUsers()); userEntityModel.add(linkTo.withRel(&quot;all-users&quot;)); return userEntityModel; } /** * 삭제 * @param id */ @DeleteMapping(&quot;/users/{id}&quot;) public void deleteUser(@PathVariable int id){ log.info(&quot;**** deleteUser&quot;); userRepository.deleteById(id); } /** 등록 * insert into tbl_user (join_date,name,password,ssn,id) values (?,?,?,?,?) * @param user * @return */ @PostMapping(&quot;/users&quot;) public ResponseEntity&lt;?&gt; createUser(@Valid @RequestBody User user){ User savedUser=userRepository.save(user); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path(&quot;/{id}&quot;) .buildAndExpand(savedUser.getId()) .toUri(); ResponseEntity&lt;User&gt; build = ResponseEntity.created(location).build(); log.info(&quot;** 유저 등록 : {}&quot; , build); //return ResponseEntity.status(HttpStatus.CREATED).body(build.toString()); return build; } //게시글 및 작성자 정보 가져오기 // // http://localhost:8088/jpa/users/1/posts /** * * http://localhost:8088/jpa/users/1/posts * @param id * @return */ @GetMapping(&quot;/users/{id}/posts&quot;) public List&lt;Post&gt; retrieveAllPostsByUser(@PathVariable int id ){ Optional&lt;User&gt; user= userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s} not found&quot;, id)); } return user.get().getPosts(); } /** 출력 =&gt; * [ * { * &quot;id&quot;: 1, * &quot;description&quot;: &quot;My first post&quot; * }, * { * &quot;id&quot;: 2, * &quot;description&quot;: &quot;My second post&quot; * } * ] * * */ /** *게시물 저장 * http://localhost:8088/jpa/users/{id}/posts * http://localhost:8088/jpa/users/1/posts */ @PostMapping(&quot;/users/{id}/posts&quot;) public ResponseEntity&lt;?&gt; createPost( @PathVariable int id, @Valid @RequestBody Post post){ Optional&lt;User&gt; user= userRepository.findById(id); if(user.isEmpty()){ throw new UserNotFoundException(String.format(&quot;ID[%s} not found&quot;, id)); } post.setUser(user.get()); Post savedPost = postRepository.save(post); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path(&quot;/{id}&quot;) .buildAndExpand(savedPost.getId()) .toUri(); return ResponseEntity.created(location).build(); } /** * 샘플 저장 * { * * &quot;description&quot;:&quot;Hello&quot; * * } */ } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-11-15 12:51:42 스프링 log4jdbc, 로그 찍기 http://macaronics.net/index.php/m01/spring/view/2150 2150 <p>&nbsp;</p><p>&nbsp;</p><p><strong>1. <span style="color:#e74c3c">log4jdbc</span></strong>&nbsp;&nbsp;<strong>라이브러리 추가</strong></p><p><a target="_blank" href="https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1"><strong>https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1</strong></a></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.bgee.log4jdbc-log4j2&lt;/groupId&gt; &lt;artifactId&gt;log4jdbc-log4j2-jdbc4.1&lt;/artifactId&gt; &lt;version&gt;1.16&lt;/version&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p><strong>2.&nbsp; src/main/resources 위치에 다음과 같이 파일 생성</strong></p><p><strong>log4jdbc.log4j2.properties</strong></p><pre class="brush:as3;">log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator log4jdbc.dump.sql.maxlinelength=0</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>3.&nbsp;&nbsp;driverClassName&nbsp; :net.sf.log4jdbc.sql.jdbcapi.DriverSpy</strong></p><p><strong>jdbcUrl 에&nbsp;log4jdbc 추가</strong></p><pre class="brush:as3;"> &lt;bean id=&quot;hikariConfig&quot; class=&quot;com.zaxxer.hikari.HikariConfig&quot;&gt; &lt;property name=&quot;driverClassName&quot; value=&quot;net.sf.log4jdbc.sql.jdbcapi.DriverSpy&quot;&gt;&lt;/property&gt; &lt;property name=&quot;jdbcUrl&quot; value=&quot;jdbc:log4jdbc:mysql://localhost:3306/dbname&quot;&gt;&lt;/property&gt; &lt;property name=&quot;username&quot; value=&quot;username&quot;&gt;&lt;/property&gt; &lt;property name=&quot;password&quot; value=&quot;1234&quot;&gt;&lt;/property&gt; &lt;/bean&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>4.&nbsp;log4j.xml</strong></p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;!DOCTYPE log4j:configuration PUBLIC &quot;-//APACHE//DTD LOG4J 1.2//EN&quot; &quot;log4j.dtd&quot;&gt; &lt;log4j:configuration xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&gt; &lt;!-- Appenders --&gt; &lt;appender name=&quot;console&quot; class=&quot;org.apache.log4j.ConsoleAppender&quot;&gt; &lt;param name=&quot;Target&quot; value=&quot;System.out&quot; /&gt; &lt;layout class=&quot;org.apache.log4j.PatternLayout&quot;&gt; &lt;param name=&quot;ConversionPattern&quot; value=&quot;%-5p: %c - %m%n&quot; /&gt; &lt;/layout&gt; &lt;/appender&gt; &lt;!-- Application Loggers --&gt; &lt;logger name=&quot;com.itwillbs.c3t2&quot;&gt; &lt;level value=&quot;info&quot; /&gt; &lt;/logger&gt; &lt;!-- 3rdparty Loggers --&gt; &lt;logger name=&quot;org.springframework.core&quot;&gt; &lt;level value=&quot;info&quot; /&gt; &lt;/logger&gt; &lt;logger name=&quot;org.springframework.beans&quot;&gt; &lt;level value=&quot;info&quot; /&gt; &lt;/logger&gt; &lt;logger name=&quot;org.springframework.context&quot;&gt; &lt;level value=&quot;info&quot; /&gt; &lt;/logger&gt; &lt;logger name=&quot;org.springframework.web&quot;&gt; &lt;level value=&quot;info&quot; /&gt; &lt;/logger&gt; &lt;!-- Root Logger --&gt; &lt;root&gt; &lt;priority value=&quot;info&quot; /&gt; &lt;appender-ref ref=&quot;console&quot; /&gt; &lt;/root&gt; &lt;/log4j:configuration&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>또는&nbsp;log4j2.xml</strong></p><p>&nbsp;</p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;Configuration&gt; &lt;Appenders&gt; &lt;Console name=&quot;console&quot; target=&quot;SYSTEM_OUT&quot;&gt; &lt;PatternLayout pattern=&quot;%d %5p [%c] %m%n&quot; /&gt; &lt;/Console&gt; &lt;/Appenders&gt; &lt;Loggers&gt; &lt;Logger name=&quot;egovframework&quot; level=&quot;DEBUG&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;Logger name=&quot;org.egovframe&quot; level=&quot;DEBUG&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;Logger name=&quot;org.springframework&quot; level=&quot;INFO&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;!-- SQL 쿼리 문장 --&gt; &lt;Logger name=&quot;java.sql&quot; level=&quot;ERROR&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;Logger name=&quot;jdbc.sqlonly&quot; level=&quot;ERROR&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;!-- log SQL with timing information, post execution --&gt; &lt;Logger name=&quot;jdbc.sqltiming&quot; level=&quot;INFO&quot; additivity=&quot;false&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Logger&gt; &lt;Logger name=&quot;jdbc.audit&quot; level=&quot;WARN&quot; additivity=&quot;false&quot;&gt; &lt;appender-ref ref=&quot;console&quot;/&gt; &lt;/Logger&gt; &lt;Logger name=&quot;jdbc.resultset&quot; level=&quot;WARN&quot; additivity=&quot;false&quot;&gt; &lt;appender-ref ref=&quot;console&quot;/&gt; &lt;/Logger&gt; &lt;Logger name=&quot;jdbc.resultSettable&quot; level=&quot;INFO&quot; additivity=&quot;false&quot;&gt; &lt;appender-ref ref=&quot;console&quot;/&gt; &lt;/Logger&gt; &lt;Root level=&quot;INFO&quot;&gt; &lt;AppenderRef ref=&quot;console&quot; /&gt; &lt;/Root&gt; &lt;/Loggers&gt; &lt;/Configuration&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-10-29 11:45:13 redmine 레드마인 설치 http://macaronics.net/index.php/m05/computer/view/2149 2149 <p>&nbsp;</p><p>&nbsp;</p><p>http://www.redmine.or.kr/projects/community/wiki/Redmine_설치_및_사용법</p><p>&nbsp;</p><p><strong>레드마인(Redmine) 소개</strong></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EC%86%8C%EA%B0%9C">소개</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EA%B8%B0%EB%8A%A5%EC%86%8C%EA%B0%9C">기능소개</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EB%B2%84%EC%A0%84%EB%B3%84_%EA%B8%B0%EB%8A%A5">버전별 기능</a></p><p>&nbsp;</p><p><a name="레드마인Redmine-설치"></a></p><p><strong>레드마인(Redmine) 설치</strong></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EB%A0%88%EB%93%9C%EB%A7%88%EC%9D%B8_%EC%84%A4%EC%B9%98(bitnami)">레드마인 설치(bitnami)</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EB%A0%88%EB%93%9C%EB%A7%88%EC%9D%B8_%EC%84%A4%EC%B9%98(Windows)">레드마인 설치(Windows)</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EB%A0%88%EB%93%9C%EB%A7%88%EC%9D%B8_%EC%84%A4%EC%B9%98(CentOS)">레드마인 설치(CentOS)</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EB%A0%88%EB%93%9C%EB%A7%88%EC%9D%B8_%EC%84%A4%EC%B9%98(Docker)?parent=Redmine_%EC%84%A4%EC%B9%98_%EB%B0%8F_%EC%82%AC%EC%9A%A9%EB%B2%95">레드마인 설치(Docker)</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/Rmagick_%EC%84%A4%EC%B9%98(Windows7)">rmagick 설치(Windows7)</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C">업그레이드</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8_%EC%84%A4%EC%B9%98">플러그인 설치</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8_%EC%86%8C%EA%B0%9C">플러그인 소개</a></p><p>&nbsp;</p><p><a name="레드마인-사용방법"></a></p><p><strong>레드마인 사용방법</strong></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EC%9D%BC%EB%B0%98_%EC%82%AC%EC%9A%A9%EC%9E%90">일반 사용자</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8_%EA%B4%80%EB%A6%AC%EC%9E%90">프로젝트 관리자</a></p><p><a href="http://www.redmine.or.kr/projects/community/wiki/%EC%8B%9C%EC%8A%A4%ED%85%9C_%EA%B4%80%EB%A6%AC%EC%9E%90">시스템 관리자</a></p><p>&nbsp;</p><p><a name="레드마인-활용"></a></p><p><strong>레드마인 활용</strong></p><p>&nbsp;</p><p>파일 (0)</p><p>&nbsp;</p><p>Powered by&nbsp;<a href="https://www.redmine.org/">Redmine</a>&nbsp;&copy; 2006-2023 Jean-Philippe Lang</p> 2023-10-27 23:23:51 스프링 유튜브 리스트 최신화 http://macaronics.net/index.php/m01/spring/view/2148 2148 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>* 유튜브 리스트 최신화</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">/** * 유튜브 리스트 최신화 * @return * @throws Exception */ @RequestMapping(&quot;youtuebList.do&quot;, method=RequestMethod.GET) @ResponseBody private Map createYoutubeList() { Map resultMap = new HashMap(); String youtubeUrl = &quot;https://www.googleapis.com/youtube/v3/search?key=키값8o&amp;part=snippet&amp;channelId=채널아이디&amp;order=date&amp;type=video&amp;safeSearch=moderate&amp;maxResults=10&amp;videoEmbeddable=true&quot;; HttpGet get = new HttpGet(youtubeUrl); String result = &quot;&quot;; DefaultHttpClient http = new DefaultHttpClient(); try { result = http.execute(get, new BasicResponseHandler()); JSONParser jsonParser = new JSONParser(); JSONObject jsonObject = (JSONObject) jsonParser.parse(result); JSONArray items = (JSONArray)jsonObject.get(&quot;items&quot;); //유튜브 데이터 초기화 macaronicsService.deleteYoutube(); for(int i = 0; i &lt; items.size(); i++) { JSONObject item = (JSONObject)items.get(i); JSONObject sitem = (JSONObject)item.get(&quot;snippet&quot;); // 상세정보 Log.info(i + &quot; **** sitem &quot; , sitem.toJSONString()); if(!Util.nvl(sitem.get(&quot;liveBroadcastContent&quot;)).equals(&quot;upcoming&quot;)) { // 실시간 방송은 제한다. JSONObject iitem = (JSONObject)item.get(&quot;id&quot;); // 비디오 아이디 JSONObject titem = (JSONObject)((JSONObject)sitem.get(&quot;thumbnails&quot;)).get(&quot;medium&quot;); // 썸네일 SnsItemVo obj = new SnsItemVo(); obj.setVideoId(Util.nvl(iitem.get(&quot;videoId&quot;))); String title = Util.nvl(sitem.get(&quot;title&quot;)); //제목 이모티콘 제거 Pattern emoticons = Pattern.compile(&quot;[\\uD83C-\\uDBFF\\uDC00-\\uDFFF]+&quot;); Matcher emoticonsMatcher = emoticons.matcher(title); title = emoticonsMatcher.replaceAll(&quot; &quot;); obj.setTitle(title); obj.setDesc(Util.nvl(sitem.get(&quot;description&quot;))); obj.setPublishDate(Util.substring(Util.nvl(sitem.get(&quot;publishedAt&quot;)), 0, 10)); obj.setThumbnailPath(Util.nvl(titem.get(&quot;url&quot;))); obj.setLink(&quot;https://www.youtube.com/watch?v=&quot; + obj.getVideoId()); //유튜브 데이터 DB 등록 처리 macaronicsService.insertYoutube(obj); } } resultMap.put(&quot;processing&quot;, &quot;over&quot;); resultMap.put(&quot;success&quot;, true); }catch (Exception e) { } return resultMap; } </pre><p>&nbsp;</p><p>//유튜브 데이터 초기화</p><pre class="brush:as3;"> &lt;delete id=&quot;deleteYoutube&quot;&gt; DELETE FROM TBL_YOUTUBE &lt;/delete&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>//유튜브 데이터 넣기</p><pre class="brush:as3;"> &lt;insert id=&quot;insertYoutube&quot; parameterType=&quot;map&quot;&gt; with upsert as( update TBL_YOUTUBE set SY_UPD_DT = current_timestamp where SY_VIDEO_ID = #{videoId} RETURNING * ) insert into TBL_YOUTUBE ( SY_VIDEO_ID ,SY_TITLE ,SY_THUMB ,SY_DESC ,SY_LINK ,SY_PUBLISH_DATE ,SY_REG_DT ) select #{videoId}, #{title}, #{thumbnailPath}, #{desc}, #{link}, #{publishDate}, current_timestamp WHERE NOT EXISTS(SELECT * FROM UPSERT) &lt;/insert&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-10-18 16:58:34 [JQUERY] Datetimepicker 사용법 http://macaronics.net/index.php/m04/jquery/view/2147 2147 <p>&nbsp;</p><p>&nbsp;</p><p>1.라이브러리</p><pre class="brush:as3;">&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;screen&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css&quot;&gt; &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js&quot;&gt;&lt;/script&gt; </pre><p>&nbsp;</p><p>2.주말 색상 변경 및&nbsp; 연도 글자크기 작게</p><pre class="brush:as3;">&lt;style&gt; .xdsoft_datetimepicker .xdsoft_label&gt;.xdsoft_select&gt;div&gt;.xdsoft_option { font-size: 13px; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_weekend:first-child { color: #f4361e; } .xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_weekend:last-child { color: #3a67ea; } &lt;/style&gt;</pre><p>&nbsp;</p><p>3.js</p><pre class="brush:as3;"> &lt;script type=&quot;text/javascript&quot;&gt; $(function() { $(&quot;.datetimepicker&quot;).datetimepicker({ /* format: &quot;Y-m-d H:i&quot;, */ lang : &#39;ko&#39;, format: &quot;Y-m-d&quot;, timepicker:false }); jQuery.datetimepicker.setLocale(&#39;kr&#39;); }); function focusStartDatePicker(){ $(&quot;#start_dt&quot;).focus(); } function focusEndDatePicker(){ $(&quot;#end_dt&quot;).focus(); } &lt;/script&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>4.html</p><pre class="brush:as3;"> &lt;div class=&quot;date_box&quot;&gt; &lt;a href=&quot;#&quot; onclick=&quot;focusStartDatePicker(); return false;&quot;&gt; &lt;img src=&quot;/images/common/btn_calendar.png&quot; alt=&quot;검색 시작일 선택&quot;&gt; &lt;/a&gt; &lt;input class=&#39;datetimepicker&#39; type=&quot;text&quot; name=&quot;start_dt&quot; id=&quot;start_dt&quot; value=&quot;${param.start_dt}&quot; size=&quot;15&quot; title=&quot;검색 시작일 형식: YYYY-MM-DD&quot; placeholder=&quot;YYYY-MM-DD&quot; maxlength=&quot;10&quot; style=&quot;text-align:center&quot;&gt; ~ &lt;a href=&quot;#&quot; onclick=&quot;focusEndDatePicker(); return false;&quot;&gt; &lt;img src=&quot;/images/common/btn_calendar.png&quot; alt=&quot;검색 종료일 선택&quot;&gt; &lt;/a&gt; &lt;input class=&#39;datetimepicker&#39; type=&quot;text&quot; name=&quot;end_dt&quot; id=&quot;end_dt&quot; value=&quot;${param.end_dt}&quot; size=&quot;15&quot; title=&quot;검색 종료일 형식: YYYY-MM-DD&quot; placeholder=&quot;YYYY-MM-DD&quot; maxlength=&quot;10&quot; style=&quot;text-align:center&quot;&gt; &lt;/div&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-10-09 23:04:37 SSL 설정 톰캣서버용 SSL 파일 받았을때, 제우스 웹투비 pem 파일 전환 http://macaronics.net/index.php/m05/computer/view/2146 2146 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1. 톰캣서버용 SSL 파일 받았을때, 제우스 웹투비 전환</strong></span></p><p>업체 받은 ssl 파일은 apach 디렉토리의<br />1)ca-bundle.crt,<br />2)star.sac.or.kr.crt,<br />3)star.sac.or.kr.key</p><p>파일과 tomcat 디렉토리 에서 패스워드.txt 입니다.<br />1)star.sac.or.kr.jks<br />2)star.sac.or.kr.keystore</p><p>&nbsp;</p><p>그런데 운영중인 파일은 apache 디렉토리의 3개의 파일은 동일하나<br />1)ca-bundle.crt,<br />2)star.sac.or.kr.crt,<br />3)star.sac.or.kr.key</p><p>pem 파일 대신 jks 파일<br />1)cert_wildcard.sac.or.kr.pem<br />2)prv_wildcard.sac.or.kr.pem<br />3)rootca_wildcard.sac.or.kr.pem<br />4)subca1_wildcard.sac.or.kr.pem</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:22px">2. SSL 적용 방법</span></strong></p><p>1) http.m 수정<br />&nbsp;</p><p>http.m)<br />*SSL<br />ssl1 CertificateFile=&quot;/home/aaa/webtob/ssl/20230905/cert_wildcard.sac.or.kr.pem&quot;,<br />CertificateKeyFile=&quot;/home/aaa/webtob/ssl/20230905/prv_wildcard.sac.or.kr.pem&quot;,<br />CACertificateFile=&quot;/home/aaa/webtob/ssl/20230905/rootca_wildcard.sac.or.kr.pem&quot;,<br />CertificateChainFile=&quot;/home/aaa/webtob/ssl/20230905/subca1_wildcard.sac.or.kr.pem&quot;,</p><p>2) http.m 수정한 디렉토리에 pem 파일 업로드<br />&nbsp;</p><p>3) 컴파일<br />$ wscfl -i http.m</p><p>4) webtob 재기동<br />웹투비 종료<br />$ wsdown</p><p>웹투비 기동<br />$ wsboot</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>3. pem 파일이 없는 관계로 jks 파일을 - &gt; PKCS12변형 -&gt; pem 파일로 변환처리</strong></span></p><pre>Bag Attributes ~ 37 31 Key Attributes: -----BEGIN ENCRYPTED PRIVATE KEY----- aaaaaaaaaa -----END ENCRYPTED PRIVATE KEY----- ~ -----BEGIN CERTIFICATE----- bbbbbbbb -----END CERTIFICATE----- </pre><p><br />1) jks 파일을 - &gt; PKCS12변형<br />keytool -importkeystore -srckeystore star.sac.or.kr.jks -destkeystore star.sac.or.kr.p12 -deststoretype PKCS12</p><p>2) pem 으로 변형<br />openssl pkcs12 -in star.sac.or.kr.p12 -out star.sac.or.kr.pem -clcerts</p><p>3) star.sac.or.kr.pem 로 생성된 파일 내용은 다음과 같습니다.</p><p>4) 코드 비교사이트<br /><a href="https://www.diffchecker.com/text-compare/">https://www.diffchecker.com/text-compare/</a></p><p>&nbsp;</p><p>①cert_wildcard.sac.or.kr.pem 파일 보시면</p><p>BEGIN CERTIFICATE 값만 존재하면 됩니다. 따라서, jks 파일을 - &gt; PKCS12변형 -&gt; pem 파일로 생성된<br />파일에서 BEGIN CERTIFICATE 값만 분리해서 cert_wildcard.sac.or.kr.pem 파일을 생성합니다.</p><p>&nbsp;</p><p>②prv_wildcard.sac.or.kr.pem 은<br />업체에서 받은&nbsp; ssl 파일중 apache 디렉토리에서 star.sac.or.kr.key 파일을 메모장으로 연다음 해당내용을 그대로 복사해서<br />넣습니다.</p><p>③rootca_wildcard.sac.or.kr.pem 파일은 고정된 값이기 때문에 작년도 것을 그대로 사용합니다.</p><p>&nbsp;</p><p>④subca1_wildcard.sac.or.kr.pem 파일은<br />업체에서 받&nbsp; ssl 파일중 apache 디렉토리에서 ca-bundle.crt 파일을 메모장으로 연다음<br />해당내용을 그대로 subca1_wildcard.sac.or.kr.pem 붙여넣기 합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-09-05 10:23:25 ssl http://macaronics.net/index.php/m05/computer/view/2145 2145 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">config/http.m 파일 열어서 1.아래와 같은 부분 경로 맞춰준다. *SSL newSSL CertificateFile = &quot;ssl/xxxx/xxxx.crt.pem(crt들어간거 넣으면 됩니다.)&quot; CertificateKeyFile = &quot;ssl/xxxx/xxxx.key.pem (key들어간거 찾으면 됩니다.)&quot; CACertificateFile = &quot;ssl/xxxx/chain-bundle.pem(chain들어간거 찾으면 됩니다.)&quot; CertificateChainFile = &quot;ssl/xxxx/xxxxRootCA(RootCA 들어간거 찾으면됩니다.)&quot; 수정한 후 2.컴파일 : wscfl -i http.m 3. webtob 재기동 : wsdown -&gt; wsboot ssl 정상등록이 되었는지 확실하게 확인하고 싶으면 www.sslshopper.com/ssl-checker.html</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-09-01 19:22:59 팝업 빔 http://macaronics.net/index.php/m05/html5/view/2144 2144 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">&lt;!doctype html&gt; &lt;html lang=&quot;ko&quot;&gt; &lt;head&gt; &lt;style&gt; #web_header_wrap{ z-index: 9999; } .dim-layer { display: block; position: fixed; _position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 91; } .dim-layer .dimBg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000; opacity: .5; filter: alpha(opacity=50); } #sub_tab2{ display: block; width: 70%; background: #f8f8f8; z-index: 999; position: relative; padding: 30px ; } #sub_tab2_parent{ position: relative; top:calc(100% + 117px); } @media (max-width: 1024px){ #m_header_wrap { display: flex; z-index: 9999; background: #fff; border:none; } .container { height: calc(100% - 60px); } #gisMap{ position: relative; top: 5px; } .dim-layer{/* */top: 60px; } .h4.p_st08.tit2 { display: block; z-index: 999; } #sub_tab2{ width: 85%; } #sub_tab2_parent{ top: calc(100% + 50px); } } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;/div&gt; &lt;div id=&quot;gis&quot;&gt; &lt;div class=&quot;site_guide&quot; id=&quot;guidePop&quot; &gt; &lt;/div&gt; &lt;div id=&quot;skip_navigation&quot;&gt; &lt;ul&gt; &lt;li&gt;&lt;a href=&quot;#start_menu&quot;&gt;주메뉴 바로가기&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href=&quot;#start_contents&quot;&gt;본문 바로가기&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/div&gt; &lt;div id=&quot;web_header_wrap&quot;&gt; &lt;/div&gt; &lt;!--&lt;div class=&quot;container&quot;&gt;--&gt; &lt;div class=&quot;container&quot; id=&quot;start_contents&quot; style=&quot;height: 0px&quot;&gt; &lt;div id=&quot;sub_tab2_parent&quot;&gt; &lt;div class=&quot;sub_tab_cont&quot; id=&quot;sub_tab2&quot; &gt; &lt;div class=&quot;h4 p_st08 tit2&quot;&gt;정보조회&lt;/div&gt; &lt;div class=&quot;c_pop waterInfo scroll&quot;&gt; &lt;div class=&quot;c_pop_cont&quot;&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;dim-layer&quot;&gt; &lt;div class=&quot;dimBg&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;div id=&quot;gisMap&quot; class=&quot;map&quot; style=&quot;width: 100%;height: 100%; z-index: 9&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-08-23 20:20:16 textarea 에 데이터를 입력후 웹표준에서 < , 따옴표 등 xss 처리 문제시 http://macaronics.net/index.php/m05/html5/view/2143 2143 <p>&nbsp;</p><p>&nbsp;</p><p>1. input&nbsp; &nbsp;태그에 데이터를 넣는다.</p><p>jsp 에서는 escapeXml true 옵션을 주어&nbsp; 처리한다.</p><p>&nbsp;</p><p>2. 자바스크립트로,&nbsp; \n 는 br 로 변경해 주며,&nbsp; jquery 에서 html 로 뿌려주는 방식으로</p><p>처리해 주면된다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;c:forEach items=&quot;${paging.result}&quot; var=&quot;item&quot; varStatus=&quot;vs&quot;&gt; &lt;span class=&quot;a_count&quot;&gt;&lt;/span&gt; &lt;div&gt; ~ &lt;div id=&quot;a_${vs.index}&quot;&gt;&lt;/div&gt; &lt;input type=&quot;hidden&quot; id=&quot;a_${vs.index}&quot; value=&quot;&lt;c:out value=&quot;${item.a}&quot; escapeXml=&quot;true&quot; /&gt;&quot;&gt; &lt;div id=&quot;b_${vs.index}&quot;&gt;&lt;/div&gt; &lt;input type=&quot;hidden&quot; id=&quot;b_${vs.index}&quot; value=&quot;&lt;c:out value=&quot;${item.b}&quot; escapeXml=&quot;true&quot; /&gt;&quot;&gt; &lt;div id=&quot; c_${vs.index}&quot;&gt;&lt;/div&gt; &lt;input type=&quot;hidden&quot; id=&quot;c_${vs.index}&quot; value=&quot;&lt;c:out value=&quot;${item.c}&quot; escapeXml=&quot;true&quot; /&gt;&quot;&gt; &lt;/div&gt; &lt;/c:forEach&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;script&gt; $(function(){ &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;const aCnt =$(&quot;.a_count&quot;).length; &nbsp;&nbsp; &nbsp;console.log(aCnt ) &nbsp;&nbsp; &nbsp;for(let i=0; i&lt;=aCnt ; i++){ &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;$(&quot;#a&quot;+i).html(replaceBrTag($(&quot;#a&quot;+i).val())); &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;$(&quot;#b&quot;+i).html(replaceBrTag($(&quot;#b&quot;+i).val())); &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; &nbsp;$(&quot;#c&quot;+i).html(replaceBrTag($(&quot;#c&quot;+i).val())); &nbsp;&nbsp; &nbsp;} &nbsp;&nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; }); function replaceBrTag(str){ &nbsp; &nbsp; if (str == undefined || str == null){ &nbsp; &nbsp; &nbsp; &nbsp; return &quot;&quot;; &nbsp; &nbsp; } &nbsp; &nbsp; str = str.replace(/\r\n/ig, &#39;&lt;br&gt;&#39;); &nbsp; &nbsp; str = str.replace(/\\n/ig, &#39;&lt;br&gt;&#39;); &nbsp; &nbsp; str = str.replace(/\n/ig, &#39;&lt;br&gt;&#39;); &nbsp; &nbsp; return str; } &lt;/script&gt;&nbsp;&nbsp; &nbsp;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-08-21 11:30:24 ★ Spring Boot 3.0 을 이용한 RESTful Web Services 개발 ( HATEOAS 적용,   springdoc-openapi  사용 , Springdo Documentation 구현 방법, Spring boot Actuator를 이용하여 모니터링 하기 , HAL Explorer 사용 ) http://macaronics.net/index.php/m01/spring/view/2142 2142 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>1.&nbsp;Level3 단계의 REST API 구현을 위한 HATEOAS 적용</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/questions/203512/entitymodel-deprecated-어떻게-바꾸면-될까요">https://www.inflearn.com/questions/203512/entitymodel-deprecated-어떻게-바꾸면-될까요</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.dev/braverokmc79/rest-api-width-spring">https://github.dev/braverokmc79/rest-api-width-spring</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/restapi-spring-boot-study">https://github.com/braverokmc79/restapi-spring-boot-study</a></p><p>&nbsp;</p><p><strong>hateoas&nbsp; 라이브러리 추가</strong></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.fasterxml.jackson.dataformat&lt;/groupId&gt; &lt;artifactId&gt;jackson-dataformat-xml&lt;/artifactId&gt; &lt;version&gt;2.14.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-hateoas&lt;/artifactId&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.example.restfullwebservice.user; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j; import lombok.extern.slf4j.Slf4j; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; import java.nio.file.attribute.UserPrincipalNotFoundException; import java.util.List; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @RestController @RequiredArgsConstructor @Slf4j public class UserController { private final UserDaoService service; // public UserController(UserDaoService service){ // this.service=service; // } // @GetMapping(&quot;/users&quot;) public List retrieveAllUsers(){ return service.findAll(); } //https://www.inflearn.com/questions/203512/entitymodel-deprecated-어떻게-바꾸면-될까요 //GET /users/1 or /users/10 -&gt; String /** * 다음을 참조 * https://github.dev/braverokmc79/rest-api-width-spring * @param id * @return */ @GetMapping(&quot;/users/{id}&quot;) public ResponseEntity<!--?--> retrieveUser(@PathVariable int id){ User user =service.findOne(id); if(user==null){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found &quot;, id) ); } /** * 링크 생성하기 * EntityModel.of(newEvent); Resource 객체를 가져와서 사용 */ // WebMvcLinkBuilder selfLinkBuilder=linkTo(UserController.class).slash(id); // URI createdUri=selfLinkBuilder.toUri(); // log.info(&quot;* createdUri {} &quot; , createdUri); //1.HATEOAS EntityModel entityModel = EntityModel.of(user); //2. 링크 추가 모든 유저보기 링크추가 WebMvcLinkBuilder linkTo=linkTo(methodOn(this.getClass()).retrieveAllUsers()); entityModel.add(linkTo.withRel(&quot;all-users&quot;)); //간소화 한줄 처리 - 셀프 링크 추가 entityModel.add( linkTo(methodOn(this.getClass()).retrieveUser(id)).withSelfRel() ); //3.반환처리 //return ResponseEntity.status(HttpStatus.OK).body(entityModel); return ResponseEntity.ok(entityModel ); } @PostMapping(&quot;/users&quot;) public ResponseEntity createUser(@Valid @RequestBody User user){ User savedUser =service.save(user); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path(&quot;/{id}&quot;) .buildAndExpand(savedUser.getId()) .toUri(); // log.info(&quot; ***** uri {} &quot;, uri); //ServletUriComponentsBuilder.fromCurrentRequest().path(&quot;/{id}&quot;).buildAndExpand(savedUser.getId()).toUri(); return ResponseEntity.created(location).build(); } @DeleteMapping(&quot;/users/{id}&quot;) public void deleteUser(@PathVariable int id){ User user =service.deleteById(id); if(user==null){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found &quot;, id)); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#c0392b">2.&nbsp;REST API Documentation을 위한 springdoc-openapi&nbsp;&nbsp;사용</span></strong></span></p><p>&nbsp;</p><p>2018 Swagger&nbsp; 업데이트&nbsp; 중단&nbsp; sprinddoc 는 지속적인 업데이트</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>출처 :&nbsp;<a target="_blank" href="https://colabear754.tistory.com/99">https://colabear754.tistory.com/99</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">공식문서 :&nbsp;&nbsp;</span><a target="_blank" href="https://springdoc.org/"><span style="color:#8e44ad">https://springdoc.org/</span></a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Gradle</strong></p><pre>implementation(&quot;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2&quot;)</pre><p>&nbsp;</p><p><strong>Maven</strong></p><pre class="brush:as3;"> &lt;!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui --&gt; &lt;dependency&gt; &lt;groupId&gt;org.springdoc&lt;/groupId&gt; &lt;artifactId&gt;springdoc-openapi-starter-webmvc-ui&lt;/artifactId&gt; &lt;version&gt;2.2.0&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p><span style="color:#d35400"><strong>위&nbsp; 라이브러만 추가후 구동만 시켜도 실행이 된다.</strong></span></p><p><strong><a target="_blank" href="http://localhost:8080/swagger-ui/index.html"><span style="color:#2980b9">http://localhost:8080/swagger-ui/index.html</span></a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Swagger 사용을 위한 기본 설정</strong></span></p><p>Springdoc은 Swagger UI 설정을 하는 방법이 Springfox와 다소 차이가 있다.</p><p>Springfox는 별도의 config 클래스에서 대부분의 설정을 하였지만 Springdoc는 config 클래스에서 API 문서페이지의 기본 설명만 작성하고 나머지 설정은</p><p>모두 application.properties 혹은 application.yml에서 설정한다.</p><p>또한 config 클래스에&nbsp;@EnableWebMvc&nbsp;어노테이션을 붙이지도 않는다.</p><p>&nbsp;</p><p><strong>SwaggerConfig</strong></p><p>&nbsp;</p><p><strong>라이브러리 추가&nbsp;</strong></p><pre class="brush:as3;">// Kotlin import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class SwaggerConfig { @Bean fun openAPI(): OpenAPI = OpenAPI() .components(Components()) .info(apiInfo()) private fun apiInfo() = Info() .title(&quot;Springdoc 테스트&quot;) .description(&quot;Springdoc을 사용한 Swagger UI 테스트&quot;) .version(&quot;1.0.0&quot;) }</pre><p>&nbsp;</p><pre class="brush:as3;">// Java import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springfrapackage com.example.restfullwebservice.config; //** //https://velog.io/@kjgi73k/Springboot3에-Swagger3적용하기 import io.swagger.v3.core.model.ApiDescription; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Spring boot Swagger 3.0 적용하기 * * https://dev-youngjun.tistory.com/258 * */ //@OpenAPIDefinition( // info=@Info(title=&quot;API 명세서&quot;, // description = &quot;Spring boot API&quot;, // version = &quot;v1&quot; // ) //) @Configuration public class SwaggerConfig { @Bean public OpenAPI openAPI() { return new OpenAPI() .components(new Components()) .info(apiInfo()); } private Info apiInfo() { Contact contact=new Contact(); contact.setName(&quot;Hong Gil Dong&quot;); contact.setUrl(&quot;https://macaronics.net&quot;); contact.setUrl(&quot;honggil@gmail.com&quot;); return new Info() .title(&quot;Springdoc 테스트&quot;) .description(&quot;Springdoc을 사용한 Swagger UI 테스트&quot;) .contact(contact) .version(&quot;1.0.0&quot;); } }mework.context.annotation.Configuration; @Configuration public class SwaggerConfig { @Bean public OpenAPI openAPI() { return new OpenAPI() .components(new Components()) .info(apiInfo()); } private Info apiInfo() { return new Info() .title(&quot;Springdoc 테스트&quot;) .description(&quot;Springdoc을 사용한 Swagger UI 테스트&quot;) .version(&quot;1.0.0&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>application.yml</strong></p><pre class="brush:as3;">springdoc: packages-to-scan: com.colabear754.springdoc_example.controllers default-consumes-media-type: application/json;charset=UTF-8 default-produces-media-type: application/json;charset=UTF-8 swagger-ui: path: / disable-swagger-default-url: true display-request-duration: true operations-sorter: alpha</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>Swagger 문서에 API 등록</strong></span></span></p><p>&nbsp;</p><p>Springdoc도 Springfox와 마찬가지로 application 속성의&nbsp;springdoc.packages-to-scan에 설정해놓은 패키지 내부의 클래스는 모두 자동으로 Swagger 문서에 등록된다.</p><p>만약 등록하고 싶지 않은 컨트롤러가 있다면&nbsp;@Hidden&nbsp;어노테이션을 이용하여 숨길 수 있다.</p><p>&nbsp;</p><p>Springdoc에서는 Swagger UI를 위한 어노테이션이 변경되었다. 어노테이션 외에는 변경할 점이 없기 때문에 컨트롤러에서 어노테이션만 변경해주면 된다</p><pre class="brush:as3;">// Kotlin import io.swagger.v3.oas.annotations.Hidden import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @Tag(name = &quot;예제 API&quot;, description = &quot;Swagger 테스트용 API&quot;) @RestController @RequestMapping(&quot;/&quot;) class ExampleController { @Operation(summary = &quot;문자열 반복&quot;, description = &quot;파라미터로 받은 문자열을 2번 반복합니다.&quot;) @Parameter(name = &quot;str&quot;, description = &quot;2번 반복할 문자열&quot;) @GetMapping(&quot;/returnStr&quot;) fun returnStr(@RequestParam str: String) = &quot;$str\n$str&quot; @GetMapping(&quot;/example&quot;) fun example() = &quot;예시 API&quot; @Hidden @GetMapping(&quot;/ignore&quot;) fun ignore() = &quot;무시되는 API&quot; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="858" height="378" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinCC3AFqmTRK6psLxx6V7HnsOMZdMGcDcsDQxjCm6S3zI4JY6s6iAHlDadBE5KnlSubJywH4crryEyNNVhzyCj0cw35ZdBnjIY16YptO33oLfs_EIQWbf8ntiu5KvNuFV5jgmUeekUnLeV_eXisr4EAaqqJinAbVQGJzP5UyqcK49mHangKXSEpOYzFR-V/s858/swagger.png" /></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">// Java import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = &quot;예제 API&quot;, description = &quot;Swagger 테스트용 API&quot;) @RestController @RequestMapping(&quot;/&quot;) public class ExampleController { @Operation(summary = &quot;문자열 반복&quot;, description = &quot;파라미터로 받은 문자열을 2번 반복합니다.&quot;) @Parameter(name = &quot;str&quot;, description = &quot;2번 반복할 문자열&quot;) @GetMapping(&quot;/returnStr&quot;) public String returnStr(@RequestParam String str) { return str + &quot;\n&quot; + str; } @GetMapping(&quot;/example&quot;) public String example() { return &quot;예시 API&quot;; } @Hidden @GetMapping(&quot;/ignore&quot;) public String ignore() { return &quot;무시되는 API&quot;; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Springdoc 공식 가이드에서 설명하는 어노테이션의 변화는 다음과 같다.</p><ul><li>@Api&nbsp;&rarr;&nbsp;@Tag</li><li>@ApiIgnore&nbsp;&rarr;&nbsp;@Parameter(hidden = true)&nbsp;or&nbsp;@Operation(hidden = true)&nbsp;or&nbsp;@Hidden</li><li>@ApiImplicitParam&nbsp;&rarr;&nbsp;@Parameter</li><li>@ApiImplicitParams&nbsp;&rarr;&nbsp;@Parameters</li><li>@ApiModel&nbsp;&rarr;&nbsp;@Schema</li><li>@ApiModelProperty(hidden = true)&nbsp;&rarr;&nbsp;@Schema(accessMode = READ_ONLY)</li><li>@ApiModelProperty&nbsp;&rarr;&nbsp;@Schema</li><li>@ApiOperation(value = &quot;foo&quot;, notes = &quot;bar&quot;)&nbsp;&rarr;&nbsp;@Operation(summary = &quot;foo&quot;, description = &quot;bar&quot;)</li><li>@ApiParam&nbsp;&rarr;&nbsp;@Parameter</li><li>@ApiResponse(code = 404, message = &quot;foo&quot;)&nbsp;&rarr;&nbsp;@ApiResponse(responseCode = &quot;404&quot;, description = &quot;foo&quot;)</li></ul><p>Spring Boot 프로젝트를 실행한 후 application 속성에서 설정한 포트와 경로로 이동하면 API가 등록되어 있는 Swagger UI 문서를 확인할 수 있다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#8e44ad">실행</span></strong></span></p><p>&nbsp;</p><p><a target="_blank" href="http://localhost:8080/swagger-ui/index.html">http://localhost:8080/swagger-ui/index.html</a></p><p><a target="_blank" href="http://localhost:8088/v3/api-docs">http://localhost:8080/v3/api-docs</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5jsoD%2FbtrWll7EDfR%2FLokyucuwKyNKC285shU7bK%2Fimg.png" data-origin-width="1019" data-origin-height="981" alt="" width="900" height="866" src="https://blog.kakaocdn.net/dn/b5jsoD/btrWll7EDfR/LokyucuwKyNKC285shU7bK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p><strong>Swagger에 등록된 API 테스트</strong></p><p>Springfox의 Swagger와 마찬가지로&nbsp;<em><strong>Try it out&nbsp;</strong></em>버튼을 클릭하면 API를 테스트할 수 있다.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcWFLx3%2FbtrWeft6zSF%2FC60eEUK4KsWzCyZqxIECk0%2Fimg.png" data-origin-width="886" data-origin-height="871" alt="" width="900" height="885" src="https://blog.kakaocdn.net/dn/cWFLx3/btrWeft6zSF/C60eEUK4KsWzCyZqxIECk0/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>API가 정상적으로 동작하는 것을 확인할 수 있다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#c0392b">3.&nbsp;Springdo Documentation 구현 방법</span></strong></span></p><p>&nbsp;</p><p><strong>@ApiModel&nbsp;&rarr;&nbsp;@Schema</strong></p><pre class="brush:as3;">package com.example.restfullwebservice.user; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Size; import lombok.*; import java.util.Date; @Getter @Setter @JsonIgnoreProperties(value = {&quot;password&quot; }) //@JsonFilter(&quot;UserInfo&quot;) @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString @EqualsAndHashCode(of=&quot;id&quot;) @Schema(description = &quot;사용자 상세 정보를 위한 도메인 객체&quot;) public class User { private Integer id; @Size(min=2, message = &quot;Name 은 2글자 이상 입력해 주세요.&quot;) @Schema(description = &quot;사용자 이름을 입력해 주세요.&quot;) private String name; //과거데이터만 올수 있는 제약 조건 @Past @Schema(description = &quot;사용자 등록일을 입력해 주세요.&quot;) private Date joinDate; // @JsonIgnore @Schema(description = &quot;사용자 패스워드를 입력해 주세요.&quot;) private String password; // @JsonIgnore @Schema(description = &quot;사용자 주민번호를 입력해 주세요.&quot;) private String ssn; @Builder public User(Integer id, String name, Date joinDate, String password, String ssn){ this.id=id; this.name=name; this.joinDate=joinDate; this.password=password; this.ssn=ssn; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#c0392b">4.&nbsp;Spring boot Actuator를 이용하여 스프링 애플리케이션 정보 모니터링 하기</span></strong></span></p><p>&nbsp;</p><p>Actuator를 사용하는 법은 아주 간단하다.</p><p>- maven dependency에 아래의 내용을 추가하기만 하면 된다.</p><p>&nbsp;</p><p><strong>maven</strong></p><pre class="brush:as3;"> &lt;!--- actuator --&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p><strong>gradle</strong></p><pre class="brush:as3;"> implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;</pre><p>&nbsp;</p><p>&nbsp;</p><p>actuator 라이브러리 추가후 구동후&nbsp;actuator 페이지에 접속하면</p><p><a target="_blank" href="http://localhost:8080/actuator">http://localhost:8080/actuator</a></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAoLDUGki8acmyYAuVOn3h-yoEJEGvZLFrUIjpcdEzvpEgUqtdRQwPOW2-1IqVIbLYKlTeYUAdr4ayDuWQSAhtwNGftVI8Tp66cp7fkL-EenvaQmJ3zsSGjyfyREWGGWpDoka7moofLw6tBA1llujmcJ5mURKuwTIRQWj8szRylJ1A1PSo-bODOkQ_CJ6Z/s16000/acture.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>더 많은 정보를 보기위해&nbsp; application.yml 에 다음과 같이 설정하면 된다.</p><pre class="brush:as3;">management: endpoints: web: exposure: include: &quot;*&quot; </pre><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi15c8Xnvs4fft3t-guOAR9dN4UYx5O5FFR_pyjQUtQ6Uzazb82iTlH6RqgxIu-l3GYR_rM1UwespxstRqf29m-Xy1_B0uhhpgyMOJLAV5dQPEuy7y_Ud-9DRYgGtIcCQnQ-G0WwHLR5Ls9GiusZGb1sk4J3CLtv7XBShyp6Y5TSzVXf59kpQcTI6x7UhHG/s16000/acture2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#c0392b">5.&nbsp;HAL Explorer이용한 HATEOAS 기능 구현</span></strong></span></p><p>&nbsp;</p><p><strong>HAL Browser&nbsp;</strong>Hypertext Application Language</p><blockquote data-ke-style="style1">REST API 설계 시 Response message의 포맷과는 상관없이<br />API를 쉽게 사용할 수 있는 메타정보를 하이퍼링크 형식으로 제공한다.<br /><br />&nbsp;</blockquote><ul><li>API 리소스 사이에서 일관적인 하이퍼링크를 제공</li><li>API 설계에서 HAL을 도입하게 되면 API 간 쉬운 검색이 가능해진다. &rarr; 더 나은 개발환경 제공</li></ul><p>dependency 추가</p><p>&nbsp;</p><p><strong>maven</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.data&lt;/groupId&gt; &lt;artifactId&gt;spring-data-rest-hal-explorer&lt;/artifactId&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p><strong>gradle</strong></p><pre class="brush:as3;"> implementation &#39;org.springframework.data:spring-data-rest-hal-explorer&#39; </pre><p>&nbsp;</p><p>explorer/html 접속후 url 입력 하면 된다.</p><p><a target="_blank" href="http://localhost:8088/explorer/index.html">http://localhost:8088/explorer/index.html</a></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="506" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigaDQJmZv3fs6cN1HyAjK7vy-9bd0hpahyxia_IFFxmDO2OcVUOe8brmnhPEhY51tYnihMX1M0d2Sl-DUN0vXdhQBAXiLqj7XZ9QjgGZ6NWBwqGLbTEm6Wm4A14CdSgfujs1O-Y9bPW6ykbRVXyaZOtXV5WbXke28ozNYnjFwflPaDRiQ44oz0J16Jobe7/s1597/hal-explorer.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong><span style="color:#c0392b">6.&nbsp;POST MAN&nbsp;Spring Security를 이용한 인증 처리</span></strong></span></p><p>&nbsp;</p><p>라이브러리 추가</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security&lt;/groupId&gt; &lt;artifactId&gt;spring-security-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="466" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi2wwh7erMEzyRJrT_KVb2UL29sWeNMReT2de2SVvKSogxtgutrkwaNuZzB5NWAOJAKnj_jIsEy7PYETL7X2kqpnVhEjOhAvkrc7vkdsDPybrZjKPejKeXwt-iR0g9FAgghlbSpsKn2b3NAaG9cboPcFdal_WuNKnJ3xNBDizuj06y_8JdC8WPBxF1o2OKU/s1732/Spring%20Security%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%20%EC%9D%B8%EC%A6%9D%20%EC%B2%98%EB%A6%AC.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><strong><span style="font-size:36px">또는</span></strong></span></p><p>&nbsp;</p><p><strong>application.yml</strong></p><pre class="brush:as3;">spring: security: user: name: username password: 1111</pre><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="555" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj91cplzK12RE6ovQVnxZi--rRTmWXZ1l1vM-o1m-Snpy-Msrf6VIgD3e57v05iSd2xIn3N580WIPEFhhQhACTbCZ7Tny4L6kNdPVeabqpWwGYO_EAgggnQE-0AHJ9kgJDNEp-TV65geiywR70W6woDCvv6GIpyb2sCJ5ytV6YWCkG66VZ2SlczwVm6-jb2/s16000/security%20.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><strong><span style="font-size:36px">또는</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>SecurityConfig&nbsp;&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.example.restfullwebservice.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.stereotype.Component; @EnableMethodSecurity(securedEnabled = true, prePostEnabled =true) @EnableWebSecurity @Component public class SecurityConfig { @Autowired public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser(&quot;test&quot;) .password(&quot;{noop}1111&quot;) .roles(&quot;USER&quot;); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="524" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgizZrzi883DskSiuDFNpBYFp1e_dkVvZ2Xyg0JOzKzHS-OdmcUoV5ELGCo9WiUxOjX_83Fh7DeL4QOSbIV02WjABIgNygxEQCfKLdxsRUA_E4Td4NBGcRrQcneWELccp15tGe6GtwclSfMLRKFoZsXuNgFn0mTgsi7vQayuWjguQ5V6p8j2NittZvJnn9E/s16000/securit3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-08-17 21:01:33 xss HTML 태그전환 http://macaronics.net/index.php/m01/java/view/2141 2141 <p>&nbsp;</p><p>&nbsp;</p><p>HTML 태그가 존재한다면 예제와 같이 replaceAll 메소드를 사용하여 치환된 문자열을 다시 원래의 태그로 치환함으로써 HTML 태그가 실행 가능하도록 설정하는 것을 권장한다.HTML 태그가 존재한다면 예제와 같이 replaceAll 메소드를 사용하여 치환된 문자열을 다시 원래의 태그로 치환함으로써 HTML 태그가 실행 가능하도록 설정하는 것을 권장한다.</p><p>&nbsp;</p><pre class="brush:as3;"> public static String cleanXSSHtmlConvertor(String value) { if(org.springframework.util.StringUtils.hasText(value)) { value = value.replaceAll(&quot;&lt;&quot;, &quot;&amp;lt;&quot;).replaceAll(&quot;&gt;&quot;, &quot;&amp;gt;&quot;); value = value.replaceAll(&quot;\\(&quot;, &quot;&amp;#40;&quot;).replaceAll(&quot;\\)&quot;, &quot;&amp;#41;&quot;); value = value.replaceAll(&quot;\&quot;&quot;, &quot;&amp;quot;&quot;); value = value.replaceAll(&quot;#&quot;, &quot;&amp;#35;&quot;); value = value.replaceAll(&quot;&amp;&quot;, &quot;&amp;#38;&quot;); value = value.replaceAll(&quot;&#39;&quot;, &quot;&amp;#x27;&quot;); return value; } return &quot;&quot;; } public static String unCleanXSSHtmlConvertor(String value) { if(org.springframework.util.StringUtils.hasText(value)) { value = value.replaceAll(&quot;&amp;lt;&quot;, &quot;&lt;&quot;).replaceAll(&quot;&amp;gt;&quot;, &quot;&gt;&quot;); value = value.replaceAll(&quot;&amp;#40;&quot;, &quot;\\(&quot;).replaceAll(&quot;&amp;#41;&quot;, &quot;\\)&quot;); value = value.replaceAll(&quot;&amp;quot;&quot;, &quot;\&quot;&quot;); value = value.replaceAll(&quot;&amp;#35;&quot;, &quot;#&quot;); value = value.replaceAll(&quot;&amp;#38;&quot;, &quot;&amp;&quot;); value = value.replaceAll(&quot;&amp;#x2F;&quot;, &quot;&#39;&quot;); return value; } return &quot;&quot;; }</pre><p>&nbsp;</p> 2023-08-17 14:35:06 Request Parameter와 Header를 이용한 API Version 관리 http://macaronics.net/index.php/m01/spring/view/2140 2140 <p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="555" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj8z4QTbI1Vno6TnvOsfv39cSty-XcEWk5kcRbwodK9f9c-_olYg_h50vTSYBsGRHCImf1fW3KG-NGUh5DURKaPFghuaMiAAM7nv3wbi52Zp0OsWLAxa0uO5qYL9NLXy8GpxY-lEQa29-eMDXX31FmDtd8tLhkdWyfk_Q2HWMlNsUySiT5J_Z8eFi0oEK73/s16000/Request%20Parameter%EC%99%80%20Header%EB%A5%BC%20%EC%9D%B4%EC%9A%A9%ED%95%9C%20API%20Version%20%EA%B4%80%EB%A6%AC.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> // GET /admin/users/1 -&gt; /admin/v1/users/1 // @GetMapping(&quot;/v1/users/{id}&quot;) // @GetMapping(value=&quot;/users/{id}&quot;, params=&quot;version=1&quot; ) //@GetMapping(value = &quot;/users/{id}&quot;, headers = &quot;X-API-TEST-VERSION=1&quot;) @GetMapping(value = &quot;/users/{id}&quot;, produces = &quot;application/vnd.myTest.appv1+json&quot;) public MappingJacksonValue retrieveUserV1(@PathVariable int id){ User user =service.findOne(id); if(user==null){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found&quot;, id)); } SimpleBeanPropertyFilter filter =SimpleBeanPropertyFilter.filterOutAllExcept(&quot;id&quot;,&quot;name&quot;, &quot;joinDate&quot;, &quot;ssn&quot;); FilterProvider filters =new SimpleFilterProvider().addFilter(&quot;UserInfo&quot;, filter); MappingJacksonValue mapping =new MappingJacksonValue(user); mapping.setFilters(filters); return mapping; } // GET /admin/users/1 -&gt; /admin/v2/users/1 // @GetMapping(&quot;/v2/users/{id}&quot;) // @GetMapping(value = &quot;/users/{id}&quot;, params = &quot;version=2&quot;) //@GetMapping(value = &quot;/users/{id}&quot;, headers = &quot;X-API-TEST-VERSION=2&quot;) @GetMapping(value = &quot;/users/{id}&quot; , produces = &quot;application/vnd.myTest.appv2+json&quot;) public MappingJacksonValue retrieveUserV2(@PathVariable int id){ System.out.println(&quot;MappingJacksonValue V2=========================&gt; &quot;); User user =service.findOne(id); if(user==null){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found&quot;, id)); } //User -&gt; User2 UserV2 userV2=new UserV2(); BeanUtils.copyProperties(user, userV2); //id, name, joinDate, password, ssn userV2.setGrade(&quot;VIP&quot;); SimpleBeanPropertyFilter filter =SimpleBeanPropertyFilter.filterOutAllExcept(&quot;id&quot;, &quot;name&quot;, &quot;joinDate&quot;, &quot;grade&quot;); FilterProvider filters =new SimpleFilterProvider().addFilter(&quot;UserInfo2&quot;, filter); MappingJacksonValue mapping =new MappingJacksonValue(userV2); mapping.setFilters(filters); return mapping; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-08-15 19:16:19 Spring Boot를 JSON Filtering http://macaronics.net/index.php/m01/spring/view/2139 2139 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>1. 개별 사용자 조회</strong></span></p><p>User&nbsp; 객쳉&nbsp; &nbsp;@JsonFilter(&quot;UserInfo&quot;)&nbsp; 어노테이션 추가</p><p>User</p><pre class="brush:as3;"> import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import java.util.Date; @Data @AllArgsConstructor //@JsonIgnoreProperties(value = {&quot;password&quot; }) @JsonFilter(&quot;UserInfo&quot;) public class User { private Integer id; @Size(min=2, message = &quot;Name 은 2글자 이상 입력해 주세요.&quot;) private String name; //과거데이터만 올수 있는 제약 조건 @Past private Date joinDate; // @JsonIgnore private String password; // @JsonIgnore private String ssn; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>AdminUserController</strong></p><pre class="brush:as3;"> @GetMapping(&quot;/users/{id}&quot;) public MappingJacksonValue retrieveUser(@PathVariable int id){ User user =service.findOne(id); if(user==null){ throw new UserNotFoundException(String.format(&quot;ID[%s] not found&quot;, id)); } SimpleBeanPropertyFilter filter =SimpleBeanPropertyFilter.filterOutAllExcept(&quot;id&quot;,&quot;name&quot;, &quot;joinDate&quot;, &quot;ssn&quot;); FilterProvider filters =new SimpleFilterProvider().addFilter(&quot;UserInfo&quot;, filter); MappingJacksonValue mapping =new MappingJacksonValue(user); mapping.setFilters(filters); return mapping; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2. 전제 사용자 조회</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/users&quot;) public MappingJacksonValue retrieveAllUsers() { List&lt;User&gt; users = service.findAll(); SimpleBeanPropertyFilter filter =SimpleBeanPropertyFilter.filterOutAllExcept(&quot;id&quot;,&quot;name&quot;, &quot;joinDate&quot;, &quot;password&quot;); FilterProvider filters =new SimpleFilterProvider().addFilter(&quot;UserInfo&quot;, filter); MappingJacksonValue mapping =new MappingJacksonValue(users); mapping.setFilters(filters); return mapping; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-08-14 20:47:34 Vue 3 강의 - 스프링부트 REST API를 이용하여 쇼핑몰 만들기 http://macaronics.net/index.php/m04/vue/view/2138 2138 <p>&nbsp;</p><p><img id="img" draggable="false" alt="" src="https://i.ytimg.com/vi/htYYSszfzv0/hqdefault.jpg?sqp=-oaymwEXCNACELwBSFryq4qpAwkIARUAAIhCGAE=&amp;rs=AOn4CLBv-qpRy05Y9t2KaglYJor9XP_yWA" /></p><p>모두 재생</p><p>Vue 3 강의</p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@africalibrary21">아프리카도서관</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=htYYSszfzv0&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=1&amp;pp=iAQB"><img alt="" src="https://i.ytimg.com/vi/htYYSszfzv0/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&amp;rs=AOn4CLB6JACbOXwKUrgyDivSYP_SiYJpOw" /></a></p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=htYYSszfzv0&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=1&amp;pp=iAQB">3:08:44지금 재생 중</a></p><p><a id="video-title" title="Vue 3 강의 - 스프링부트 REST API를 이용하여 쇼핑몰 만들기(JPA, MariaDB, JWT)" href="https://www.youtube.com/watch?v=htYYSszfzv0&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=1&amp;pp=iAQB">Vue 3 강의 - 스프링부트 REST API를 이용하여 쇼핑몰 만들기(JPA, MariaDB, JWT)</a></p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@africalibrary21">아프리카도서관</a></p><p>&bull;</p><p>조회수 1.6만회&nbsp;&bull;&nbsp;11개월 전</p><p>&nbsp;</p><p>2</p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=ADxbGlwhl_s&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=2&amp;pp=iAQB"><img alt="" src="https://i.ytimg.com/vi/ADxbGlwhl_s/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&amp;rs=AOn4CLCUrRwap8DkYnL3dUIbsD6P08uaQA" /></a></p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=ADxbGlwhl_s&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=2&amp;pp=iAQB">1:35:40지금 재생 중</a></p><p><a id="video-title" title="Vue 3 강의 - REST API를 이용하여 메모 애플리케이션 만들기(Node.js, Express, MariaDB)" href="https://www.youtube.com/watch?v=ADxbGlwhl_s&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=2&amp;pp=iAQB">Vue 3 강의 - REST API를 이용하여 메모 애플리케이션 만들기(Node.js, Express, MariaDB)</a></p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@africalibrary21">아프리카도서관</a></p><p>&bull;</p><p>조회수 1.4만회&nbsp;&bull;&nbsp;1년 전</p><p>&nbsp;</p><p>3</p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=JR0jSscNNd4&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=3&amp;pp=iAQB"><img alt="" src="https://i.ytimg.com/vi/JR0jSscNNd4/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&amp;rs=AOn4CLBVHZRJ6Qilp3nUZkyslV5ugvV6pA" /></a></p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=JR0jSscNNd4&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=3&amp;pp=iAQB">56:40지금 재생 중</a></p><p><a id="video-title" title="Vue 3 강의 - JWT를 이용하여 로그인 애플리케이션 만들기(Node.js, Express, JWT)" href="https://www.youtube.com/watch?v=JR0jSscNNd4&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=3&amp;pp=iAQB">Vue 3 강의 - JWT를 이용하여 로그인 애플리케이션 만들기(Node.js, Express, JWT)</a></p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@africalibrary21">아프리카도서관</a></p><p>&bull;</p><p>조회수 8.3천회&nbsp;&bull;&nbsp;1년 전</p><p>&nbsp;</p><p>4</p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=gmR_dHGrlx8&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=4&amp;pp=iAQB"><img alt="" src="https://i.ytimg.com/vi/gmR_dHGrlx8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&amp;rs=AOn4CLActVNZlYMeXSZ8SEc1luFfQEiB2g" /></a></p><p><a id="thumbnail" aria-hidden="true" tabindex="-1" rel="null" href="https://www.youtube.com/watch?v=gmR_dHGrlx8&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=4&amp;pp=iAQB">25:17지금 재생 중</a></p><p><a id="video-title" title="Vue 3 강의 - 메모 애플리케이션 배포하기(feat. 가비아 Node.js 호스팅)" href="https://www.youtube.com/watch?v=gmR_dHGrlx8&amp;list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-&amp;index=4&amp;pp=iAQB">Vue 3 강의 - 메모 애플리케이션 배포하기(feat. 가비아 Node.js 호스팅)</a></p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@africalibrary21">아프리카도서관</a></p><p>&bull;</p><p>조회수 2.5천회&nbsp;&bull;&nbsp;1년 전</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.youtube.com/playlist?list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-">https://www.youtube.com/playlist?list=PLtht1_et-35AQSnfVkqkjdfhBX_P-9U4-</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-07-09 20:34:44 페이징 화살표 http://macaronics.net/index.php/m05/html5/view/2137 2137 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.toptal.com/designers/htmlarrows/">https://www.toptal.com/designers/htmlarrows/</a></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;ul class=&quot;pagination&quot;&gt; &lt;c:if test=&quot;${map.pager.curBlock &gt; 1 }&quot;&gt; &lt;li&gt; &lt;a aria-label=&quot;Previous&quot; href=&quot;javascript:list(&#39;${pageAndSearch.searchQuery(1) }&#39;)&quot;&gt; &lt;span aria-hidden=&quot;true&quot;&gt;&amp;laquo;&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;/c:if&gt; &lt;c:if test=&quot;${map.pager.curBlock &gt; 1 }&quot;&gt; &lt;li&gt; &lt;a aria-label=&quot;Previous&quot; href=&quot;javascript:list(&#39;${pageAndSearch.searchQuery(map.pager.prevPage) }&#39;)&quot;&gt; &lt;span aria-hidden=&quot;true&quot;&gt;&amp;lsaquo;&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;/c:if&gt; &lt;c:forEach begin=&quot;${ map.pager.blockBegin }&quot; end=&quot;${ map.pager.blockEnd }&quot; var=&quot;page&quot;&gt; &lt;c:choose&gt; &lt;c:when test=&quot;${ page ==map.pager.curPage }&quot;&gt; &lt;li class=&quot;active&quot;&gt; &lt;a href=&quot;javascript:list(&#39;${pageAndSearch.searchQuery(page) }&#39;)&quot; &gt;${page}&lt;/a&gt; &lt;/li&gt; &lt;/c:when&gt; &lt;c:otherwise&gt; &lt;li &gt; &lt;a href=&quot;javascript:list(&#39;${pageAndSearch.searchQuery(page) }&#39;)&quot; &gt;${page}&lt;/a&gt; &lt;/li&gt; &lt;/c:otherwise&gt; &lt;/c:choose&gt; &lt;/c:forEach&gt; &lt;c:if test=&quot;${ map.pager.curBlock &lt; map.pager.totBlock }&quot;&gt; &lt;li&gt; &lt;a class=&quot;arrow next&quot; aria-label=&quot;Next&quot; href=&quot;javascript:list(&#39;${ pageAndSearch.searchQuery(map.pager.nextPage) }&#39;)&quot;&gt; &lt;span aria-hidden=&quot;true&quot;&gt;&amp;rsaquo;&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;/c:if&gt; &lt;c:if test=&quot;${map.pager.curPage &lt; map.pager.totPage }&quot;&gt; &lt;li&gt; &lt;a class=&quot;arrow nnext&quot; aria-label=&quot;Next&quot; href=&quot;javascript:list(&#39;${ pageAndSearch.searchQuery(map.pager.totPage) }&#39;)&quot;&gt; &lt;span aria-hidden=&quot;true&quot;&gt;&amp;raquo;&lt;/span&gt; &lt;/a&gt; &lt;/li&gt; &lt;/c:if&gt; &lt;/ul&gt; </pre><p>&nbsp;</p> 2023-06-28 09:51:25 반응형 갤러리 카드 card css http://macaronics.net/index.php/m05/css3/view/2136 2136 <p>&nbsp;</p><pre class="brush:as3;">&lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;style&gt; #macard-main{ width: 1280px; background: #fff; margin: 0 auto; height: 600px; } div.mcard-gallery { border: 1px solid #ccc; width:240px; background: #fff; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2) } div.mcard-gallery:hover { transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); border: 1px solid #777; box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.4); } div.mcard-gallery img { width: 100%; height: auto; } div.desc { padding: 5px 10px; text-align: left; } .mcard-responsive { padding: 0 6px; float: left; width: 20%; box-sizing: border-box; } @media only screen and (max-width: 700px) { .mcard-responsive { width: 49.99999%; margin: 6px 0; } } @media only screen and (max-width: 500px) { .mcard-responsive { width: 100%; } } .macard-clearfix:after { content: &quot;&quot;; display: table; clear: both; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;h2&gt;card-responsive Image gallery&lt;/h2&gt; &lt;h4&gt;Resize the browser window to see the effect.&lt;/h4&gt; &lt;div id=&quot;macard-main&quot;&gt; &lt;div class=&quot;mcard-responsive&quot;&gt; &lt;div class=&quot;mcard-gallery&quot;&gt; &lt;a target=&quot;_blank&quot; href=&quot;img_5terre.jpg&quot;&gt; &lt;img src=&quot;https://www.w3schools.com/css/img_5terre.jpg&quot; alt=&quot;Cinque Terre&quot; &gt; &lt;/a&gt; &lt;div class=&quot;desc&quot;&gt; &lt;p&gt;Add a description of the image here&lt;/p&gt; &lt;p&gt;Add a description of the image here&lt;/p&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;mcard-responsive&quot;&gt; &lt;div class=&quot;mcard-gallery&quot;&gt; &lt;a target=&quot;_blank&quot; href=&quot;img_forest.jpg&quot;&gt; &lt;img src=&quot;https://www.w3schools.com/css/img_5terre.jpg&quot; alt=&quot;Forest&quot; &gt; &lt;/a&gt; &lt;div class=&quot;desc&quot;&gt;Add a description of the image here&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;mcard-responsive&quot;&gt; &lt;div class=&quot;mcard-gallery&quot;&gt; &lt;a target=&quot;_blank&quot; href=&quot;img_lights.jpg&quot;&gt; &lt;img src=&quot;https://www.w3schools.com/css/img_5terre.jpg&quot; alt=&quot;Northern Lights&quot; &gt; &lt;/a&gt; &lt;div class=&quot;desc&quot;&gt;Add a description of the image here&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;mcard-responsive&quot;&gt; &lt;div class=&quot;mcard-gallery&quot;&gt; &lt;a target=&quot;_blank&quot; href=&quot;img_mountains.jpg&quot;&gt; &lt;img src=&quot;https://www.w3schools.com/css/img_5terre.jpg&quot; alt=&quot;Mountains&quot; &gt; &lt;/a&gt; &lt;div class=&quot;desc&quot;&gt;Add a description of the image here&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;mcard-responsive&quot;&gt; &lt;div class=&quot;mcard-gallery&quot;&gt; &lt;a target=&quot;_blank&quot; href=&quot;img_mountains.jpg&quot;&gt; &lt;img src=&quot;https://www.w3schools.com/css/img_5terre.jpg&quot; alt=&quot;Mountains&quot; &gt; &lt;/a&gt; &lt;div class=&quot;desc&quot;&gt;Add a description of the image here&lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;div class=&quot;macard-clearfix&quot;&gt;&lt;/div&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </pre><p>&nbsp;</p><p>cdn</p><pre class="brush:as3;">&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/gh/braverokmc79/css-galleray-card@v1.0.0/mcard-style.css&quot; &gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-06-07 11:18:32 스프링 기반 REST API 개발 - (백기선) - 5. REST API 보안 적용 http://macaronics.net/index.php/m01/spring/view/2135 2135 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드] 강의입니다.</strong></p><p>다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해</p><p>다양한 스프링 기술을 활용하여 REST API 개발</p><p>스프링 HATEOAS와 스프링 REST Docs 프로젝트 활용</p><p>테스트 주도 개발(TDD)</p><p>스프링으로 REST를 따르는 API를 만들어보자!<br />백기선의 스프링 기반 REST API 개발</p><p><img title="239527-img1-white.png" alt="" width="965" height="298" src="https://cdn.inflearn.com/public/files/courses/239527/a59979fe-29a4-481a-bdbc-a2150bccfdb6/239527-img1-white.png" /></p><p><strong>스프링 기반 REST API 개발</strong></p><p>이 강의에서는 다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발합니다.</p><p>그런&nbsp;<strong>REST API</strong>로 괜찮은가</p><p>2017년 네이버가 주관한 개발자 컨퍼런스 Deview에서&nbsp;<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a>라는 이응준님의 발표가 있었습니다. 현재 REST API로 불리는 대부분의 API가 실제로는 로이 필딩이 정의한 REST를 따르고 있지 않으며, 그 중에서도 특히 Self-Descriptive Message와 HATEOAS가 지켜지지 않음을 지적했고, 그에 대한 대안을 제시되었습니다.</p><p><strong>이번 강의는 해당 발표에 영감을 얻어 만들어졌습니다.</strong>&nbsp;2018년 11월에 KSUG에서 동일한 이름으로 세미나를 진행한 경험이 있습니다. 4시간이라는 짧지 않은 발표였지만, 빠르게 진행하느라 충분히 설명하지 못하고 넘어갔던 부분이 있었습니다. 내용을 더 보충하고, 또 해결하려는 문제에 대한 여러 선택지를 제공하는 것이 좋을 것 같아 이 강의를 만들게 되었습니다.<br />또한 이 강의에서는 제가 주로 사용하는&nbsp;<strong>IntelliJ 단축키</strong>도 함께 설명하고 있습니다.</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>인프런 :</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/spring_rest-api#">https://www.inflearn.com/course/spring_rest-api#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 자료 :&nbsp;&nbsp;<a target="_blank" href="https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit">https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 소스 :</p><p>&nbsp;</p><p><a target="_blank" href="https://gitlab.com/whiteship/natural">https://gitlab.com/whiteship/natural</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/keesun/study">https://github.com/keesun/study</a></p><p>ksug201811restapi</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌에서는 다음의 다양한 스프링 기술을 사용하여 REST API를 개발합니다.</strong></p><ul><li><p><strong>스프링 프레임워크</strong></p></li><li><p><strong>스프링 부트</strong></p></li><li><p><strong>스프링 데이터 JPA</strong></p></li><li><p><strong>스프링 HATEOAS</strong></p></li><li><p><strong>스프링 REST Docs</strong></p></li><li><p><strong>스프링 시큐리티 OAuth2</strong></p></li></ul><p>&nbsp;</p><p><strong>또한 개발은 테스트 주도 개발(TDD)로 진행하기 때문에 평소 테스트 또는 TDD에 관심있던 개발자에게도 이번 강좌가 도움이 될 것으로 기대합니다.</strong></p><p>&nbsp;</p><p><strong>사전 학습</strong></p><ul><li><p><strong>스프링 프레임워크 핵심 기술 (필수)</strong></p></li><li><p><strong>스프링 부트 개념과 활용 (필수)</strong></p></li><li><p><strong>스프링 데이터 JPA (선택)</strong></p></li></ul><p>&nbsp;</p><p><strong>학습 목표</strong></p><ul><li><p><strong>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해합니다.</strong></p></li><li><p><strong>다양한 스프링 기술을 활용하여 REST API를 개발할 수 있습니다.</strong></p></li><li><p><strong>스프링 HATEOAS와 스프링 REST Docs 프로젝트를 활용할 수 있습니다.</strong></p></li><li><p><strong>테스트 주도 개발(TDD)에 익숙해 집니다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>REST API 는 다음 두가지를 만족해야 한다.</strong></span></p><p><strong><span style="color:#2980b9">1) Self-Describtive Message</span></strong></p><p><strong><span style="color:#2980b9">2) HATEOAS</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[5]&nbsp;&nbsp;5. REST API 보안 적용</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>32. Account 도메인 추가</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16440&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16440&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>OAuth2로 인증을 하려면 일단 Account 부터</strong></p><ul><li><p><strong>id</strong></p></li><li><p><strong>email</strong></p></li><li><p><strong>password</strong></p></li><li><p><strong>roels</strong></p></li></ul><p>&nbsp;</p><p><strong>AccountRoles</strong></p><ul><li><p><strong>ADMIN, USER</strong></p></li></ul><p>&nbsp;</p><p><strong>JPA 맵핑</strong></p><ul><li><p><strong>@Table(&ldquo;Users&rdquo;)</strong></p></li></ul><p>&nbsp;</p><p><strong>JPA enumeration collection mapping</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) private Set roles; </pre><p>&nbsp;</p><p><strong>Event에 owner 추가</strong></p><pre class="brush:as3;"> @ManyToOne Account manager; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>1)Account</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import jakarta.persistence.*; import lombok.*; import java.util.Set; @Entity @Getter @Setter @EqualsAndHashCode(of=&quot;id&quot;) @AllArgsConstructor @NoArgsConstructor @Builder public class Account { @Id @GeneratedValue private Integer id; private String email; private String password; //권한 정보 //ADMIN, USER 권한 모두 있는 경우 //ADMIN 권한만 있는 경우 //USER 권한만 있는 경우 //컬렉션 타입으로 설정 @ElementCollection @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) private Set roles; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>2)&nbsp;AccountRole</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; public enum AccountRole { ADMIN, USER } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>33. 스프링 시큐리티 적용</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16441&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16441&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 시큐리티</strong></p><ul><li><p><strong>웹 시큐리티 (Filter 기반 시큐리티)</strong></p></li><li><p><strong>메소드 시큐리티&nbsp;</strong></p></li><li><p><strong>이 둘 다 Security Interceptor를 사용합니다.</strong></p><ul><li><p><strong>리소스에 접근을 허용할 것이냐 말것이냐를 결정하는 로직이 들어있음.</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><img width="543" height="288" alt="" src="data:image/png;base64,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" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure --> org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.6.8 </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>테스트 다 깨짐 (401 Unauthorized)</strong></p><ul><li><p><strong>깨지는 이유는 스프링 부트가 제공하는 스프링 시큐리티 기본 설정 때문</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>1)&nbsp; AccountService&nbsp;</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.io.Serial; import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; @Service public class AccountService implements UserDetailsService { @Autowired AccountRepository accountRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account= accountRepository.findByEmail(username).orElseThrow(() -&gt; new UsernameNotFoundException(username)); return new User(account.getEmail(), account.getPassword(), authorities(account.getRoles())); } private Collection<!--? extends GrantedAuthority--> authorities(Set roles){ return roles.stream().map(r-&gt;new SimpleGrantedAuthority(&quot;ROLE_&quot;+r.name())) .collect(Collectors.toSet()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>2) AccountRepository</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface AccountRepository extends JpaRepository { Optional findByEmail(String username); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>3) 테스트</strong></span></p><p><span style="font-size:22px"><strong>AccountServiceTest</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.Set; @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles(&quot;test&quot;) class AccountServiceTest { @Autowired AccountService accountService; @Autowired AccountRepository accountRepository; @Test public void findByUsername(){ //Given String password=&quot;1111&quot;; String username=&quot;junho@gmail.com&quot;; Account account=Account.builder() .email(username) .password(password) .roles(Set.of(AccountRole.ADMIN, AccountRole.USER)) .build(); accountRepository.save(account); //WHEN UserDetailsService userDetailsService=(UserDetailsService) accountService; UserDetails userDetails = userDetailsService.loadUserByUsername(username); //Then Assertions.assertThat(userDetails.getPassword()).isEqualTo(password); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>34. 예외테스트</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16442&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16442&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1. @Test(expected)</strong></p><p>&nbsp;</p><p><strong>예외 타입만 확인 가능</strong></p><p>&nbsp;</p><p><strong>2. try-catch</strong></p><p>&nbsp;</p><p><strong>예외 타입과 메시지 확인 가능.</strong></p><p><strong>하지만 코드가 다소 복잡.</strong></p><p>&nbsp;</p><p><strong>3. @Rule ExpectedException</strong></p><p>&nbsp;</p><p><strong>코드는 간결하면서 예외 타입과 메시지 모두 확인 가능</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>AccountServiceTest</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Rule; import org.junit.jupiter.api.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.Set; @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles(&quot;test&quot;) class AccountServiceTest { @Rule public ExpectedException expectedException=ExpectedException.none(); @Autowired AccountService accountService; @Autowired AccountRepository accountRepository; @Test public void findByUsername(){ //Given String password=&quot;1111&quot;; String username=&quot;junho@gmail.com&quot;; Account account=Account.builder() .email(username) .password(password) .roles(Set.of(AccountRole.ADMIN, AccountRole.USER)) .build(); accountRepository.save(account); //WHEN UserDetailsService userDetailsService=(UserDetailsService) accountService; UserDetails userDetails = userDetailsService.loadUserByUsername(username); //Then Assertions.assertThat(userDetails.getPassword()).isEqualTo(password); } @Test public void findByUsernameFail(){ String username = &quot;test@gmail.com&quot;; try{ accountService.loadUserByUsername(username); Assert.fail(&quot;supposed to be failed&quot;); }catch (UsernameNotFoundException e){ Assertions.assertThat(e.getMessage()).containsSequence(username); } } @Test public void findByUsernameFail2(){ //에러 발생 가능성을 먼저 코딩한다 String username=&quot;test@gmail.com&quot;; expectedException.expect(UsernameNotFoundException.class); expectedException.expectMessage(Matchers.containsString(username)); //When accountService.loadUserByUsername(username); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>35. 스프링 시큐리티 기본 설정</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16443&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16443&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>라이브러리</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-oauth2-client&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>RestapiApplication</strong></span></p><pre class="brush:as3;"> @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>1) AccountService</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; @Getter public class AccountService implements UserDetails { private Account account; public AccountService(Account account) { this.account = account; } @Override public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() { Collection&lt;GrantedAuthority&gt; collect = new ArrayList&lt;&gt;(); collect.add(new GrantedAuthority() { private static final long serialVersionUID = 1L; @Override public String getAuthority() { return account.getRoles().toString(); } }); return collect; } /** * 사용자를 인증하는 데 사용된 암호를 반환합니다. */ @Override public String getPassword() { return account.getPassword(); } /** * 사용자를 인증하는 데 사용된 사용자 이름을 반환합니다. null을 반환할 수 없습니다. */ @Override public String getUsername() { return account.getEmail(); } /** * 사용자의 계정이 만료되었는지 여부를 나타냅니다. 만료된 계정은 인증할 수 없습니다. */ @Override public boolean isAccountNonExpired() { return true; } /** * 사용자가 잠겨 있는지 또는 잠금 해제되어 있는지 나타냅니다. 잠긴 사용자는 인증할 수 없습니다. */ @Override public boolean isAccountNonLocked() { return true; } /** * 사용자의 자격 증명(암호)이 만료되었는지 여부를 나타냅니다. 만료된 자격 증명은 인증을 방지합니다. */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 사용자가 활성화되었는지 비활성화되었는지 여부를 나타냅니다. 비활성화된 사용자는 인증할 수 없습니다. */ @Override public boolean isEnabled() { // 우리 사이트 1년동안 회원이 로그인을 안하면!! 휴먼 계정으로 하기로 함. // 현재시간-로긴시간=&gt;1년을 초과하면 return false; return true; } public boolean isWriteEnabled() { if (account.getRoles().equals(AccountRole.USER)) return false; else return true; } public boolean isWriteAdminAndManagerEnabled() { if (account.getRoles().equals(AccountRole.ADMIN) || account.getRoles().equals(AccountRole.USER)) return true; else return false; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>2)PrincipalDetailsService</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class PrincipalDetailsService implements UserDetailsService{ private final AccountRepository accountRepository; private final BCryptPasswordEncoder passwordEncoder; public Account saveAccount(Account account){ account.setPassword(this.passwordEncoder.encode(account.getPassword())); return this.accountRepository.save(account); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account= accountRepository.findByEmail(username).orElseThrow(() -&gt; new UsernameNotFoundException(username)); return new User(account.getEmail(), account.getPassword(), authorities(account.getRoles())); } private Collection&lt;? extends GrantedAuthority&gt; authorities(Set&lt;AccountRole&gt; roles){ return roles.stream().map(r-&gt;new SimpleGrantedAuthority(&quot;ROLE_&quot;+r.name())) .collect(Collectors.toSet()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>3)SecurityConfig</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.configs; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.DispatcherType; import lombok.RequiredArgsConstructor; import net.macaronics.restapi.accounts.CustomAuthFailureHandler; import net.macaronics.restapi.accounts.PrincipalDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.stereotype.Component; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 1.securedEnabled * * @Secured 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다. 기본값은 false 2.prePostEnabled * * @PreAuthorize, @PostAuthorize 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다. * 기본값은 false 3.jsr250Enabled * * @RolesAllowed 애노테이션을 사용하여 인가 처리를 하고 싶을때 사용하는 옵션이다. 기본값은 false * * @Secured, @RolesAllowed 특정 메서드 호출 이전에 권한을 확인한다. SpEL 지원하지 않는다. * @Secured 는 스프링에서 지원하는 애노테이션이며, @RolesAllowed는 자바 표준 * * @Secured(&quot;ROLE_ADMIN&quot;) @RolesAllowed(&quot;ROLE_ADMIN&quot;) * */ //구글로그인이 완료된 뒤의 후처리가 필요함. 1.코드 받기(인증) , 2.엑세스토큰(권한), 3.사용자프로필 정보를 가져오기 //4.그 정보를 토대로 회원가입을 자동으로 진행시키기도 함 //4-2 (이메일,전화번호,이름,아이디) 쇼핑몰 -&gt; (집주소), @EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) // secured 어노테이션 활성화 @EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다. @RequiredArgsConstructor @Component public class SecurityConfig { private final CustomAuthFailureHandler customFailureHandler; private final ObjectMapper objectMapper; @Autowired PrincipalDetailsService principalDetailsService; private final BCryptPasswordEncoder passwordEncoder; /** * Spring Secureity 에서 인증은 AuthenticationManager 를 통해 이루어지며 * AuthenticationManagerBuilder 가 AuthenticationManager 를 생성합니다. * userDetailsService 를 구현하고 있는 객체로 principalDetailsServicee를 지정해 주며, 비밀번호 암호화를 * 위해 passwordEncoder 를 지정해줍니다. */ protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(principalDetailsService).passwordEncoder(passwordEncoder); } @Bean public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); } @Bean public AuthenticationProvider authenticationProvider() { var provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(principalDetailsService); provider.setPasswordEncoder(passwordEncoder); return provider; } @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(&quot;/**&quot;).allowedMethods(&quot;*&quot;); } // @Override // public void addResourceHandlers(ResourceHandlerRegistry registry) { // // /images/** 은 /resources/images/ 으로 시작하는 uri호출은 /resources/images/ 경로 하위에 있는 리소스 파일이다 라는 의미입니다. // registry.addResourceHandler(&quot;/resources/upload/**&quot;).addResourceLocations(&quot;file:///C:/upload/&quot;); // } }; } /** * 정적인 영역 무시 * @return */ @Bean public WebSecurityCustomizer configure() { return (web) -&gt; web.ignoring().requestMatchers( &quot;/h2-console/**&quot; ); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable().cors().disable() .authorizeHttpRequests(request -&gt; request .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .requestMatchers(&quot;/h2-console&quot;,&quot;/h2-console/**&quot;, &quot;/docs/index.html&quot;).permitAll() .requestMatchers(HttpMethod.GET,&quot;/api/**&quot;).authenticated() .anyRequest().authenticated() ) //.anonymous().and() .headers().frameOptions().sameOrigin() // 여기! .and() .formLogin( login -&gt; login.usernameParameter(&quot;email&quot;) .passwordParameter(&quot;password&quot;) .failureHandler(customFailureHandler).permitAll() ) // .formLogin(login -&gt; login // .loginPage(&quot;/loginForm&quot;) // .loginProcessingUrl(&quot;/login&quot;) // .usernameParameter(&quot;userId&quot;) // .passwordParameter(&quot;password&quot;) // .defaultSuccessUrl(&quot;/&quot;, true) // .failureHandler(customFailureHandler) // 로그인 오류 실패 체크 핸들러 // .permitAll() // ) .logout(); return http.build(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>4)CustomAuthFailureHandler</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import java.io.IOException; import java.net.URLEncoder; @Configuration @Log4j2 public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String errorMessage; log.info(&quot;***** CustomAuthFailureHandler ==&gt; {} &quot;, exception.getMessage()); if (exception instanceof BadCredentialsException) { errorMessage = &quot;아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.&quot;; } else if (exception instanceof InternalAuthenticationServiceException) { errorMessage = &quot;내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의하세요.&quot;; } else if (exception instanceof UsernameNotFoundException) { errorMessage = &quot;계정이 존재하지 않습니다. 회원가입 진행 후 로그인 해주세요.&quot;; } else if (exception instanceof AuthenticationCredentialsNotFoundException) { errorMessage = &quot;인증 요청이 거부되었습니다. 관리자에게 문의하세요.&quot;; } else { errorMessage = exception.getMessage(); // errorMessage = &quot;알 수 없는 이유로 로그인에 실패하였습니다 관리자에게 문의하세요.&quot;; } errorMessage = URLEncoder.encode(errorMessage, &quot;UTF-8&quot;); setDefaultFailureUrl(&quot;/loginForm?error=true&amp;exception=&quot; + errorMessage); super.onAuthenticationFailure(request, response, exception); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>36. 스프링 시큐리티 폼 인증설정</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16444&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16444&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>1) 테스트</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.accounts; import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Rule; import org.junit.jupiter.api.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import java.util.Set; @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles(&quot;test&quot;) class AccountServiceTest { @Rule public ExpectedException expectedException=ExpectedException.none(); @Autowired PrincipalDetailsService principalDetailsService; @Autowired AccountRepository accountRepository; @Autowired BCryptPasswordEncoder passwordEncoder; @Test public void findByUsername(){ //Given String password=&quot;1111&quot;; String username=&quot;junho@gmail.com&quot;; Account account=Account.builder() .email(username) .password(password) .roles(Set.of(AccountRole.ADMIN, AccountRole.USER)) .build(); Account save = principalDetailsService.saveAccount(account); //WHEN UserDetailsService userDetailsService=(UserDetailsService) principalDetailsService; UserDetails userDetails = userDetailsService.loadUserByUsername(username); //Then Assertions.assertThat(this.passwordEncoder.matches(password, userDetails.getPassword())).isTrue(); } @Test public void findByUsernameFail(){ String username = &quot;test@gmail.com&quot;; try{ principalDetailsService.loadUserByUsername(username); Assert.fail(&quot;supposed to be failed&quot;); }catch (UsernameNotFoundException e){ Assertions.assertThat(e.getMessage()).containsSequence(username); } } @Test public void findByUsernameFail2(){ //에러 발생 가능성을 먼저 코딩한다 String username=&quot;test@gmail.com&quot;; expectedException.expect(UsernameNotFoundException.class); expectedException.expectMessage(Matchers.containsString(username)); //When principalDetailsService.loadUserByUsername(username); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">2)시작시 테스트 계정 등록 처리</span></strong></p><pre class="brush:as3;">package net.macaronics.restapi; import net.macaronics.restapi.accounts.Account; import net.macaronics.restapi.accounts.AccountRole; import net.macaronics.restapi.accounts.PrincipalDetailsService; import org.modelmapper.ModelMapper; import org.modelmapper.config.Configuration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.Set; @SpringBootApplication public class RestapiApplication { public static void main(String[] args) { SpringApplication.run(RestapiApplication.class, args); } @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public ApplicationRunner applicationRunner(){ return new ApplicationRunner() { @Autowired PrincipalDetailsService principalDetailsService; @Override public void run(ApplicationArguments args) throws Exception { Account newAccount = Account.builder() .email(&quot;test@gmail.com&quot;) .password(&quot;1111&quot;) .roles(Set.of(AccountRole.ADMIN, AccountRole.USER)) .build(); Account saveAccount = principalDetailsService.saveAccount(newAccount); System.out.println( &quot;저장된 비밀번호 &quot; +saveAccount.getPassword()); } }; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://miro.medium.com/v2/resize:fit:720/format:webp/1*Vm4d5U8ZCx8VnxdPyELY1Q.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>AuthorizationServerConfigurerAdapter 스프링 부트&nbsp;AuthorizationServer&nbsp; dprecate 되어 스프링부트 3.0&nbsp; 서는 사용할 수 없다.</p><p>&nbsp;</p><p>참조 :</p><p>&nbsp;</p><p>1.&nbsp;<a target="_blank" href="https://m.blog.naver.com/varkiry05/221865197562">[springboot, oauth] Authorization Server(인증서버) 구축하기</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>2.&nbsp;<a target="_blank" href="https://buddhiprabhath.medium.com/spring-boot-oauth-2-0-separating-authorization-service-and-resource-service-1641ebced1f0">https://buddhiprabhath.medium.com/spring-boot-oauth-2-0-separating-authorization-service-and-resource-service-1641ebced1f0</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:26px">oaut2&nbsp; &nbsp;+ jwt 방식으로 사용할 것</span></strong></span></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>따라서,&nbsp; 강의 내용에서 인증 방식은 참조만 할것</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>@ConfigurationProperties 사용법&nbsp;</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>ConfigurationProperties</strong></p><p>*.properties , *.yml 파일에 있는 property를 자바 클래스에 값을 가져와서(바인딩) 사용할 수 있게 해주는 어노테이션</p><p>&nbsp;</p><p>&nbsp;</p><p>Spring boot 에서는 운영에 필요한 설정(DB 정보, LOG설정 등등 )들을&nbsp;*.properties , *.yml 에 써두고 관리한다.</p><p>이 설정은 KEY - VALUE 의 형태로 저장되어 관리하고 있으며 @Value 을 사용하여 바인딩을 할 수 있다.</p><p>&nbsp;</p><p>아래와 같은 properties 파일이 있다고 가정할 때&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">site-url.naver=https://www.naver.com site-url.google=https:/google.com</pre><p>&nbsp;</p><p>&nbsp;</p><p>@Value 를 사용하여 바인딩을 하면 다음과 같은 자바 코드가 나온다.</p><pre class="brush:as3;">@Value(&quot;${site-url.naver}&quot;) private String naver; @Value(&quot;${site-url.google}&quot;) private String google</pre><p>&nbsp;</p><p>@Value를 사용하여 바인딩을 하는 방법은 문자열을 사용하기에 오타가 날 수도 있다.</p><p>그래서 클래스 파일로 관리하는 방법을 찾아봤다</p><p>&nbsp;</p><p><strong>1. properties에서 오토컴플릿을 지원하도록 하기 위한 dependency를 추가</strong></p><pre class="brush:as3;">&lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-configuration-processor&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>2. 클래스 파일 생성</strong></p><p>@ConfigurationProperties 이 좋은 이유 여러 표기법에 대해서 오토로 바인딩해 준다. ( 아래 참고 )</p><p>&nbsp;</p><p>acme.my-project.person.first-nameproperties&nbsp;와&nbsp;.yml에&nbsp;<strong>권장</strong>되는 표기 방법&nbsp;</p><p>acme.myProject.person.firstName표준 카멜 케이스 문법.</p><p>acme.my_project.person.first_name.properties와&nbsp;.yml&nbsp;에서 사용가능한 방법 ( - 표기법이 더 표준 )</p><p>ACME_MYPROJECT_PERSON_FIRSTNAME시스템 환경 변수를 사용할 때 권장</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>@Component로 bean을 등록해야 한다.</p><p>@ConfigurationProperties에 prifix를 설정한다.</p><p>properties 파일에 있는 site-url.* 에 대하여 바인딩한다.</p><p>&nbsp;</p><pre class="brush:as3;">import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import lombok.Data; @Component @ConfigurationProperties(prefix = &quot;site-url&quot;) @Data public class siteUrlProperties { private String naver; private String google; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3. 확인</strong></p><pre class="brush:as3;">@Controller @RequestMapping(&quot;/&quot;) @Slf4j public class MainController { @Autowired siteUrlProperties siteUrlProperties; @GetMapping(&quot;&quot;) @ResponseBody public String test(Model model) { return siteUrlProperties.getNaver(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>@ConfiguConfigurationProperties 어노테이션을 사용하여 property 값을 사용하면 매핑을 유연하게 할 수 있다는 장점이 있지만 SpEL를 사용할 수 없다.</p><p>SpEL를 사용할 때에는 @Value를 사용해야 한다.</p><p>&nbsp;</p><p>그 외에는 @ConfiguConfigurationProperties를 사용하는게 코드가 깔끔해진다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>39.스프링 시큐리티 현재 사용자</strong></span></p><p>&nbsp;</p><p><strong>SecurityContext</strong></p><ul><li><p><strong>자바 ThreadLocal 기반 구현으로 인증 정보를 담고 있다.</strong></p></li><li><p><strong>인증 정보 꺼내는 방법:&nbsp;</strong></p></li></ul><p><strong>Authentication authentication = SecurityContextHolder.getContext().getAuthentication();</strong></p><p>&nbsp;</p><p><strong>@AuthenticationPrincipal spring.security.User user</strong></p><ul><li><p><strong>인증 안한 경우에 null</strong></p></li><li><p><strong>인증 한 경우에는 username과 authorities 참조 가능</strong></p></li></ul><p>&nbsp;</p><p><strong>spring.security.User를 상속받는 클래스를 구현하면</strong></p><ul><li><p><strong>도메인 User를 받을 수 있다.</strong></p></li><li><p><strong>@AuthenticationPrincipa me.whiteship.user.UserAdapter&nbsp;</strong></p></li><li><p><strong>Adatepr.getUser().getId()</strong></p></li></ul><p>&nbsp;</p><p><strong>SpEL을 사용하면</strong></p><ul><li><p><strong>@AuthenticationPrincipa(expression=&rdquo;account&rdquo;) me.whiteship.user.Account&nbsp;</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;">@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @AuthenticationPrincipal(expression = &quot;account&quot;) public @interface CurrentUser { } </pre><p>&nbsp;</p><p><strong>커스텀 애노테이션을 만들면</strong></p><ul><li><p><strong>@CurrentUser Account account</strong></p></li><li><p><strong>엇? 근데 인증 안하고 접근하면..?</strong></p></li></ul><p>&nbsp;</p><p><strong>expression = &quot;#this == &#39;anonymousUser&#39; ? null : account&quot;</strong></p><ul><li><p><strong>현재 인증 정보가 anonymousUse 인 경우에는 null을 보내고 아니면 &ldquo;account&rdquo;를 꺼내준다.</strong></p></li></ul><p>&nbsp;</p><p><strong>조회 API 개선</strong></p><ul><li><p><strong>현재 조회하는 사용자가 owner인 경우에 update 링크 추가 (HATEOAS)</strong></p></li></ul><p>&nbsp;</p><p><strong>수정 API 개선</strong></p><p><strong>현재 사용자가 이벤트 owner가 아닌 경우에 403 에러 발생</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>Events API 개선: 출력값 제한하기</strong></span></p><p>&nbsp;</p><p><strong>생성 API 개선</strong></p><ul><li><p><strong>Event owner 설정</strong></p></li><li><p><strong>응답에서 owner의 id만 보내 줄 것.</strong></p></li></ul><pre class="brush:as3;">{ &quot;id&quot; : 4, &quot;name&quot; : &quot;test 3PISM1Ju&quot;, &quot;description&quot; : &quot;test event&quot;, ... &quot;free&quot; : false, &quot;eventStatus&quot; : &quot;DRAFT&quot;, &quot;owner&quot; : { &quot;id&quot; : 3, &quot;email&quot; : &quot;keesun@email.com&quot;, &quot;password&quot; : &quot;{bcrypt}$2a$10$3z/rHmeYsKpoOQR3aUq38OmZjZNsrGfRZxSnmpLfL3lpLxjD5/JZ6&quot;, &quot;roles&quot; : [ &quot;USER&quot;, &quot;ADMIN&quot; ] }, </pre><p>&nbsp;</p><ul><li><p><strong>JsonSerializer&lt;User&gt; 구현</strong></p></li><li><p><strong>@JsonSerialize(using) 설정</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-28 18:15:47 스프링 기반 REST API 개발 - (백기선) - 4. 이벤트 조회 및 수정 REST API 개발 http://macaronics.net/index.php/m01/spring/view/2134 2134 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드] 강의입니다.</strong></p><p>다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해</p><p>다양한 스프링 기술을 활용하여 REST API 개발</p><p>스프링 HATEOAS와 스프링 REST Docs 프로젝트 활용</p><p>테스트 주도 개발(TDD)</p><p>스프링으로 REST를 따르는 API를 만들어보자!<br />백기선의 스프링 기반 REST API 개발</p><p><img title="239527-img1-white.png" alt="" width="965" height="298" src="https://cdn.inflearn.com/public/files/courses/239527/a59979fe-29a4-481a-bdbc-a2150bccfdb6/239527-img1-white.png" /></p><p><strong>스프링 기반 REST API 개발</strong></p><p>이 강의에서는 다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발합니다.</p><p>그런&nbsp;<strong>REST API</strong>로 괜찮은가</p><p>2017년 네이버가 주관한 개발자 컨퍼런스 Deview에서&nbsp;<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a>라는 이응준님의 발표가 있었습니다. 현재 REST API로 불리는 대부분의 API가 실제로는 로이 필딩이 정의한 REST를 따르고 있지 않으며, 그 중에서도 특히 Self-Descriptive Message와 HATEOAS가 지켜지지 않음을 지적했고, 그에 대한 대안을 제시되었습니다.</p><p><strong>이번 강의는 해당 발표에 영감을 얻어 만들어졌습니다.</strong>&nbsp;2018년 11월에 KSUG에서 동일한 이름으로 세미나를 진행한 경험이 있습니다. 4시간이라는 짧지 않은 발표였지만, 빠르게 진행하느라 충분히 설명하지 못하고 넘어갔던 부분이 있었습니다. 내용을 더 보충하고, 또 해결하려는 문제에 대한 여러 선택지를 제공하는 것이 좋을 것 같아 이 강의를 만들게 되었습니다.<br />또한 이 강의에서는 제가 주로 사용하는&nbsp;<strong>IntelliJ 단축키</strong>도 함께 설명하고 있습니다.</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>인프런 :</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/spring_rest-api#">https://www.inflearn.com/course/spring_rest-api#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 자료 :&nbsp;&nbsp;<a target="_blank" href="https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit">https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 소스 :</p><p>&nbsp;</p><p><a target="_blank" href="https://gitlab.com/whiteship/natural">https://gitlab.com/whiteship/natural</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/keesun/study">https://github.com/keesun/study</a></p><p>ksug201811restapi</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌에서는 다음의 다양한 스프링 기술을 사용하여 REST API를 개발합니다.</strong></p><ul><li><p><strong>스프링 프레임워크</strong></p></li><li><p><strong>스프링 부트</strong></p></li><li><p><strong>스프링 데이터 JPA</strong></p></li><li><p><strong>스프링 HATEOAS</strong></p></li><li><p><strong>스프링 REST Docs</strong></p></li><li><p><strong>스프링 시큐리티 OAuth2</strong></p></li></ul><p>&nbsp;</p><p><strong>또한 개발은 테스트 주도 개발(TDD)로 진행하기 때문에 평소 테스트 또는 TDD에 관심있던 개발자에게도 이번 강좌가 도움이 될 것으로 기대합니다.</strong></p><p>&nbsp;</p><p><strong>사전 학습</strong></p><ul><li><p><strong>스프링 프레임워크 핵심 기술 (필수)</strong></p></li><li><p><strong>스프링 부트 개념과 활용 (필수)</strong></p></li><li><p><strong>스프링 데이터 JPA (선택)</strong></p></li></ul><p>&nbsp;</p><p><strong>학습 목표</strong></p><ul><li><p><strong>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해합니다.</strong></p></li><li><p><strong>다양한 스프링 기술을 활용하여 REST API를 개발할 수 있습니다.</strong></p></li><li><p><strong>스프링 HATEOAS와 스프링 REST Docs 프로젝트를 활용할 수 있습니다.</strong></p></li><li><p><strong>테스트 주도 개발(TDD)에 익숙해 집니다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>REST API 는 다음 두가지를 만족해야 한다.</strong></span></p><p><strong><span style="color:#2980b9">1) Self-Describtive Message</span></strong></p><p><strong><span style="color:#2980b9">2) HATEOAS</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[4]&nbsp;&nbsp;이벤트 조회 및 수정 REST API 개발</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>28. 이벤트 목록조회 API 구현</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16435&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16435&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>페이징, 정렬 어떻게 하지?</strong></p><ul><li><p><strong>스프링 데이터 JPA가 제공하는 Pageable</strong></p></li></ul><p>&nbsp;</p><p><strong>Page&lt;Event&gt;에 안에 들어있는 Event 들은 리소스로 어떻게 변경할까?</strong></p><ul><li><p><strong>하나씩 순회하면서 직접 EventResource로 맵핑을 시킬까..</strong></p></li><li><p><strong>PagedResourceAssembler&lt;T&gt; 사용하기</strong></p></li></ul><p>&nbsp;</p><p><strong><a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web">테스트 할 때 Pageable 파라미터 제공하는 방법</a></strong></p><ul><li><p><strong>page: 0부터 시작</strong></p></li><li><p><strong>size: 기본값 20</strong></p></li><li><p><strong>sort: property,property(,ASC|DESC)</strong></p></li></ul><p>&nbsp;</p><p>테스트할것</p><pre class="brush:as3;">Event 목록 Page 정보와 함께 받기 content[0].id 확인 pageable 경로 확인 Sort과 Paging 확인 30개를 만들고, 10개 사이즈로 두번째 페이지 조회하면 이전, 다음 페이지로 가는 링크가 있어야 한다. 이벤트 이름순으로 정렬하기 page 관련 링크 Event를 EventResource로 변환해서 받기 각 이벤트 마다 self 링크 확인 self profile (create) 문서화 </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>1) EventController</strong></span></span></p><p><span style="color:#8e44ad"><strong><span style="font-size:20px">★&nbsp;</span>ResponseEntity&lt;PagedModel&lt;EntityModel&lt;Event&gt;&gt;&gt;&nbsp; &nbsp;EntityModel&lt;Event&gt;&nbsp; 를PagedModel&lt;Entity&gt; 로 감싸주고 이것을 다시</strong></span></p><p><span style="color:#8e44ad"><strong>ResponseEntity 로 감싸주었다.</strong></span></p><pre class="brush:as3;">@Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor @Log4j2 public class EventController { /**페이징 처리 */ @GetMapping public ResponseEntity&lt;PagedModel&lt;EntityModel&lt;Event&gt;&gt;&gt; queryEvent(Pageable pageable , PagedResourcesAssembler&lt;Event&gt; assembler){ Page&lt;Event&gt; page = this.eventRepository.findAll(pageable); PagedModel&lt;EntityModel&lt;Event&gt;&gt; entityModel=assembler.toModel(page,e-&gt; EventResource.of(e)); //링크 추가 entityModel.add(Link.of(&quot;/docs/index.html#resource-events-list&quot;).withRel(&quot;profile&quot;)); return ResponseEntity.ok(entityModel); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:20px">2) ★&nbsp;</span></strong>다음과 같은 형식으로&nbsp; Resource 만들어 처리해 주면된다.</span></p><p><span style="font-size:26px"><strong><span style="color:#8e44ad">EventResource</span></strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.events; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import java.net.URI; import java.util.ArrayList; import java.util.List; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; /** * ★★ 기본 소스 */ public class EventResource extends EntityModel&lt;Event&gt; { private static WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class); private EventResource(){ } public static EntityModel&lt;Event&gt; of(Event event, String profile){ List&lt;Link&gt; links = getSelfLink(event); links.add(Link.of(profile, &quot;profile&quot;)); return EntityModel.of(event, links); } public static EntityModel&lt;Event&gt; of(Event event){ List&lt;Link&gt; links = getSelfLink(event); return EntityModel.of(event, links); } private static List&lt;Link&gt; getSelfLink(Event event) { selfLinkBuilder.slash(event.getId()); List&lt;Link&gt; links = new ArrayList&lt;&gt;(); links.add(selfLinkBuilder.withSelfRel()); return links; } public static URI getCreatedUri(Event event) { return selfLinkBuilder.slash(event.getId()).toUri(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:24px"><strong>3) EventControllerTests</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">~ @Test @TestDescription(&quot;30개의 이벤트를 10개씩 두번째 페이지 조회하기&quot;) public void queryEvents() throws Exception{ //Given IntStream.range(0, 30).forEach(this::generateEvent); //when this.mockMvc.perform(get(&quot;/api/events&quot;) .param(&quot;page&quot;, &quot;1&quot;) .param(&quot;size&quot;, &quot;10&quot;) .param(&quot;sort&quot;, &quot;name,DESC&quot;) ) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath(&quot;page&quot;).exists()) .andExpect(jsonPath(&quot;_embedded.eventList[0]._links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.profile&quot;).exists()) .andDo(document(&quot;query_events&quot;)); } private void generateEvent(int index) { Event event=Event.builder() .name(&quot;event &quot;+index) .description(&quot;test event&quot;) .build(); this.eventRepository.save(event); } </pre><p>&nbsp;</p><p>출력=&gt;</p><p>&nbsp;</p><pre class="brush:as3;">{ &quot;_embedded&quot;: { &quot;eventList&quot;: [{ &quot;id&quot;: 27, &quot;name&quot;: &quot;event 26&quot;, &quot;description&quot;: &quot;test event&quot;, &quot;beginEnrollmentDateTime&quot;: null, &quot;closeEnrollmentDateTime&quot;: null, &quot;beginEventDateTime&quot;: null, &quot;endEventDateTime&quot;: null, &quot;location&quot;: null, &quot;basePrice&quot;: 0, &quot;maxPrice&quot;: 0, &quot;limitOfEnrollment&quot;: 0, &quot;offline&quot;: false, &quot;free&quot;: false, &quot;eventStatus&quot;: null, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events&quot; } } }, `~ ` &quot;_links&quot;: { &quot;first&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events?page=0&amp;size=10&amp;sort=name,desc&quot; }, &quot;prev&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events?page=0&amp;size=10&amp;sort=name,desc&quot; }, &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events?page=1&amp;size=10&amp;sort=name,desc&quot; }, &quot;next&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events?page=2&amp;size=10&amp;sort=name,desc&quot; }, &quot;last&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/events?page=2&amp;size=10&amp;sort=name,desc&quot; }, &quot;profile&quot;: { &quot;href&quot;: &quot;/docs/index.html#resource-events-create&quot; } }, &quot;page&quot;: { &quot;size&quot;: 10, &quot;totalElements&quot;: 30, &quot;totalPages&quot;: 3, &quot;number&quot;: 1 } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>29. 이벤트 한건 조회 API 구현</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16436&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16436&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>테스트할것</p><pre class="brush:as3;">조회하는 이벤트가 있는 경우 이벤트 리소스 확인 링크 self profile (update) 이벤트 데이터 조회하는 이벤트가 없는 경우 404 응답 확인 </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>1)EventController</strong></span></p><pre class="brush:as3;"> @GetMapping(value = &quot;/{id}&quot;) public ResponseEntity&lt;?&gt; getEvent(@PathVariable(&quot;id&quot;) Integer id) { Optional&lt;Event&gt; optionalEvent = this.eventRepository.findById(id); if(optionalEvent.isEmpty()){ return ResponseEntity.notFound().build(); } Event event =optionalEvent.get(); EntityModel&lt;Event&gt; entityModel = EventResource.of(event); entityModel.add(Link.of(&quot;/docs/index.html#resource-events-get&quot;).withRel(&quot;profile&quot;)); return ResponseEntity.ok(entityModel); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>2)EventControllerTests</strong></p><pre class="brush:as3;">import com.fasterxml.jackson.databind.ObjectMapper; import net.macaronics.restapi.common.RestDocsConfiguration; import net.macaronics.restapi.common.TestDescription; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.time.LocalDateTime; import java.util.stream.IntStream; import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @Import(RestDocsConfiguration.class) @ActiveProfiles(&quot;test&quot;) public class EventControllerTests { @Test @DisplayName(&quot;기존의 이벤트를 하나 조회하기&quot;) public void getEvent() throws Exception{ //Given Event event= this.generateEvent(100); //When &amp; Then this.mockMvc.perform( get(&quot;/api/events/{id}&quot;,event.getId())) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath(&quot;name&quot;).exists()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.profile&quot;).exists()) .andDo(document(&quot;get-an-event&quot;)); } @Test @DisplayName(&quot;없는 이벤트를 조회 했을때 404 응답받기&quot;) public void getEvent404() throws Exception{ //When &amp; Then this.mockMvc.perform(get(&quot;/api/events/11883&quot;)) .andExpect(status().isNotFound()); } private Event generateEvent(int index) { Event event=Event.builder() .name(&quot;event &quot;+index) .description(&quot;test event&quot;) .build(); return this.eventRepository.save(event); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>30. 이벤트 수정 API 구현</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16437&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16437&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><pre class="brush:as3;">수정하려는 이벤트가 없는 경우 404 NOT_FOUND 입력 데이터 (데이터 바인딩)가 이상한 경우에 400 BAD_REQUEST 도메인 로직으로 데이터 검증 실패하면 400 BAD_REQUEST (권한이 충분하지 않은 경우에 403 FORBIDDEN) 정상적으로 수정한 경우에 이벤트 리소스 응답 200 OK 링크 수정한 이벤트 데이터 </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1)EventContoller</strong></span></p><pre class="brush:as3;"> /** 수정하기 */ @PatchMapping(&quot;/{id}&quot;) public ResponseEntity&lt;?&gt; updateEvent(@PathVariable(&quot;id&quot;) Integer id, @RequestBody @Valid EventDto eventDto, Errors errors){ Optional&lt;Event&gt; optionalEvent = this.eventRepository.findById(id); if(optionalEvent.isEmpty()) return ResponseEntity.notFound().build(); if(errors.hasErrors())return badRequest(errors); eventValidator.validate(eventDto, errors); if(errors.hasErrors())return badRequest(errors); //커스텀 에러 로직상 에러잡기 Event existEvent=optionalEvent.get(); existEvent.updateEvent(eventDto); //서비스 객체를 만들지 않아서 더티체킹이 일어나지 않는다. 따라서, repository 실질적으로 저장처리 eventRepository.save(existEvent); EntityModel&lt;Event&gt; entityModel = EventResource.of(existEvent); entityModel.add(Link.of(&quot;/docs/index.html#resource-events-update&quot;).withRel(&quot;profile&quot;)); return ResponseEntity.ok(entityModel); } private ResponseEntity&lt;EntityModel&gt; badRequest(Errors errors) { return ResponseEntity.badRequest().body(errorsResource.addLink(errors)); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>2)&nbsp;EventControllerTests</strong></span></p><pre class="brush:as3;"> @Test @DisplayName(&quot;1)수정하기 - 이벤트 정상적으로 수정하기&quot;) public void updateEvent() throws Exception{ //Given Event event=this.generateEvent(200); String eventName=&quot;Updated event&quot;; EventDto eventDto = event.toEventDto(); eventDto.setName(eventName); this.mockMvc.perform(patch(&quot;/api/events/{id}&quot;, event.getId()) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(eventDto)) ) .andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath(&quot;name&quot;).value(eventName)) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.profile&quot;).exists()) .andDo(document(&quot;update-event&quot;, links( linkWithRel(&quot;self&quot;).description(&quot;link to self&quot;), linkWithRel(&quot;profile&quot;).description(&quot;link to update an existing event&quot;) // linkWithRel(&quot;query-events&quot;).description(&quot;link to query event&quot;), // linkWithRel(&quot;query-events&quot;).description(&quot;link to query event&quot;), // linkWithRel(&quot;query-events&quot;).description(&quot;link to query event&quot;) ///이하 생략 ) )); } @Test @DisplayName(&quot;2)수정하기 - 입력값이 비어있는 경우에 이벤트 수정 실패&quot;) public void updateEvent400_Empty() throws Exception{ //Given Event event=this.generateEvent(200); EventDto eventDto =new EventDto(); this.mockMvc.perform(patch(&quot;/api/events/{id}&quot;, event.getId()) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(eventDto)) ) .andExpect(status().isBadRequest()) .andDo(print()); } @Test @DisplayName(&quot;3)수정하기 -입력값이 잘못된 경우에 이벤트 수정 실패&quot;) public void updateEvent400_Wrong() throws Exception{ //Given Event event=this.generateEvent(200); EventDto eventDto =event.toEventDto(); eventDto.setBasePrice(2000000000); eventDto.setMaxPrice(200000); this.mockMvc.perform(patch(&quot;/api/events/{id}&quot;, event.getId()) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(eventDto)) ) .andExpect(status().isBadRequest()) .andDo(print()); } @Test @DisplayName(&quot;4)수정하기 - 존재하지 않는 이벤트 수정 실패&quot;) public void updateEvent404_notFound() throws Exception{ //Given Event event=this.generateEvent(200); EventDto eventDto =event.toEventDto(); this.mockMvc.perform(patch(&quot;/api/events/188888888&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(eventDto)) ) .andExpect(status().isNotFound()) .andDo(print()); } private Event generateEvent(int index) { Event event=Event.builder() .name(&quot;event &quot;+index) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(false) .offline(true) .eventStatus(EventStatus.DRAFT) .build(); return this.eventRepository.save(event); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>31. 테스트코드 리팩토링</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16438&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16438&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>여러 컨트롤러 간의 중복 코드 제거하기</strong></p><ul><li><p><strong>클래스 상속을 사용하는 방법</strong></p></li><li><p><strong>@Ignore 애노테이션으로 테스트로 간주되지 않도록 설정</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>BaseControllerTest</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.common; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Ignore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @Import(RestDocsConfiguration.class) @ActiveProfiles(&quot;test&quot;) @Ignore //테스트 클래스로 사용하지 않는다 public class BaseControllerTest { @Autowired public MockMvc mockMvc; @Autowired public ObjectMapper objectMapper; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-28 09:39:17 스프링부트 REST API 응답 한글깨짐 해결방법 http://macaronics.net/index.php/m01/spring/view/2133 2133 <p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventController </strong></span></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor @Log4j2 public class EventController { @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){ if(errors.hasErrors()){ return ResponseEntity.badRequest().body(errors); } //커스텀 validate 검사 eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ return ResponseEntity.badRequest().body(errors); } //modelMapper 오류 //Event event=modelMapper.map(eventDto, Event.class); Event event = eventDto.toEvent(); Integer eventId = event.getId(); //유료인지 무료인지 변경처리 event.update(); Event newEvent=this.eventRepository.save(event);//저장 /** * * ★ 링크 생성하기 * EntityModel.of(newEvent); Resource 객체를 가져와서 사용 * * **/ WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(eventId); URI createdUri = selfLinkBuilder.toUri(); EntityModel eventResource = EntityModel.of(newEvent); eventResource.add(linkTo(EventController.class).slash(eventId).withSelfRel()); eventResource.add(linkTo(EventController.class).withRel(&quot;query-events&quot;)); eventResource.add(selfLinkBuilder.withRel(&quot;update-event&quot;)); return ResponseEntity.created(createdUri).body(eventResource); }</pre><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventControllerTests</strong></span></p><pre class="brush:as3;"> @SpringBootTest @AutoConfigureMockMvc public class EventControllerTests { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Test @DisplayName(&quot;정상적으로 이벤트를 생성하는 테스트&quot;) public void createEvent() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(false) .offline(false) .eventStatus(EventStatus.DRAFT) .build(); mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) //.andExpect(header().string(HttpHeaders.CONTENT_TYPE, &quot;application/hal+json;charset=UTF-8&quot;)) .andExpect(jsonPath(&quot;free&quot;).value(false)) .andExpect(jsonPath(&quot;offline&quot;).value(true)) // .andExpect(jsonPath(&quot;eventStatus&quot;).value(EventStatus.DRAFT.toString())) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.query-events&quot;).exists()) .andExpect(jsonPath(&quot;_links.update-event&quot;).exists()); }</pre><p>&nbsp;</p><p>다음과 같이 한글 깨짐 출력이 나타나는데</p><pre class="brush:as3;">{ &quot;id&quot;:1, &quot;name&quot;:&quot;Spring&quot;, &quot;description&quot;:&quot;REST API Development with Spring&quot;, &quot;beginEnrollmentDateTime&quot;:&quot;2023-05-06T19:20:00&quot;, &quot;closeEnrollmentDateTime&quot;:&quot;2023-05-20T20:20:00&quot;, &quot;beginEventDateTime&quot;:&quot;2023-05-20T20:20:00&quot;, &quot;endEventDateTime&quot;:&quot;2023-11-26T20:20:00&quot;, &quot;location&quot;:&quot;&ecirc;&deg;•&euml;‚&uml;&igrave;—&shy; D2 &igrave;Š&curren;&iacute;ƒ€&iacute;…€ &iacute;Œ&copy;&iacute;† &euml;&brvbar;&not;&quot;, &quot;basePrice&quot;:100, &quot;maxPrice&quot;:200, &quot;limitOfEnrollment&quot;:100, &quot;offline&quot;:true, &quot;free&quot;:false, &quot;eventStatus&quot;:null, &quot;_links&quot;:{ &quot;self&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; }, &quot;query-events&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; }, &quot;update-event&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>다음을 참조해서 설정 변경</p><p><a target="_blank" href="http://honeymon.io/tech/2019/10/23/spring-deprecated-media-type.html">http://honeymon.io/tech/2019/10/23/spring-deprecated-media-type.html</a></p><p>&nbsp;</p><p>그러나, 이해가 안되면 다음과 같이&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>application.properties에 설정 추가</strong></span></p><p>&nbsp;</p><pre class="brush:as3;"># UTF-8 세팅 server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true</pre><p>&nbsp;</p><p>&nbsp;</p><p>테스트 설정</p><pre class="brush:as3;"> .andExpect(header().string(HttpHeaders.CONTENT_TYPE, &quot;application/hal+json;charset=UTF-8&quot;))</pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">{참 &quot;id&quot;:1, &quot;name&quot;:&quot;Spring&quot;, &quot;description&quot;:&quot;REST API Development with Spring&quot;, &quot;beginEnrollmentDateTime&quot;:&quot;2023-05-06T19:20:00&quot;, &quot;closeEnrollmentDateTime&quot;:&quot;2023-05-20T20:20:00&quot;, &quot;beginEventDateTime&quot;:&quot;2023-05-20T20:20:00&quot;, &quot;endEventDateTime&quot;:&quot;2023-11-26T20:20:00&quot;, &quot;location&quot;:&quot;강남역 D2 스타텀 팩토리&quot;, &quot;basePrice&quot;:100, &quot;maxPrice&quot;:200, &quot;limitOfEnrollment&quot;:100, &quot;offline&quot;:true, &quot;free&quot;:false, &quot;eventStatus&quot;:null, &quot;_links&quot;:{ &quot;self&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; }, &quot;query-events&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; }, &quot;update-event&quot;:{ &quot;href&quot;:&quot;http://localhost/api/events&quot; } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>참조 :</p><p><a target="_blank" href="https://smpark1020.tistory.com/169">https://smpark1020.tistory.com/169</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-27 09:12:26 인텔리제이 플러그인 추천 http://macaronics.net/index.php/m05/computer/view/2132 2132 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Atom Meterial Icons (필)<br />Code Screenshots (선택)<br />CodeGlance (필)<br />Codota AI Autocomplete for Java and Javascript (필)<br />GitToolBox (필)<br />Indent Rainbow (선택)<br />JWT(JSON Web Token) Analyzer (선택)<br />Key Promoter X (선택)<br />Korean Lanugage Pack / 한국어 언어 팩 (선택)<br />Nyan Progress Bar (필) / Pokemon Progress / Mario Progress ...<br />One Dark Theme (필)<br />Presentation Assistant (선택)<br />Prettier (프론트엔드-필수, 나머지 선택)<br />Rainbow Brackets (필)<br />SonaLint (필-코드냄새를 없애기 위해)<br />String Manipulation (선택)<br />WakaTime (선택)<br />.ignore 또는 Add to gitignore (필)<br />RestfulHelper (필)<br />Lombok (필)<br />Styled Components &amp;amp; Styled JSX (선택)<br />Grep Console (선택)<br />CamelCase (선택)<br />DTO Generator (선택)<br />POJO to JSON (필)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-23 21:42:46 스프링 기반 REST API 개발 - (백기선) - 3. HATEOAS와 Self-Describtive Message 적용 http://macaronics.net/index.php/m01/spring/view/2131 2131 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드] 강의입니다.</strong></p><p>다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해</p><p>다양한 스프링 기술을 활용하여 REST API 개발</p><p>스프링 HATEOAS와 스프링 REST Docs 프로젝트 활용</p><p>테스트 주도 개발(TDD)</p><p>스프링으로 REST를 따르는 API를 만들어보자!<br />백기선의 스프링 기반 REST API 개발</p><p><img title="239527-img1-white.png" alt="" width="965" height="298" src="https://cdn.inflearn.com/public/files/courses/239527/a59979fe-29a4-481a-bdbc-a2150bccfdb6/239527-img1-white.png" /></p><p><strong>스프링 기반 REST API 개발</strong></p><p>이 강의에서는 다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발합니다.</p><p>그런&nbsp;<strong>REST API</strong>로 괜찮은가</p><p>2017년 네이버가 주관한 개발자 컨퍼런스 Deview에서&nbsp;<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a>라는 이응준님의 발표가 있었습니다. 현재 REST API로 불리는 대부분의 API가 실제로는 로이 필딩이 정의한 REST를 따르고 있지 않으며, 그 중에서도 특히 Self-Descriptive Message와 HATEOAS가 지켜지지 않음을 지적했고, 그에 대한 대안을 제시되었습니다.</p><p><strong>이번 강의는 해당 발표에 영감을 얻어 만들어졌습니다.</strong>&nbsp;2018년 11월에 KSUG에서 동일한 이름으로 세미나를 진행한 경험이 있습니다. 4시간이라는 짧지 않은 발표였지만, 빠르게 진행하느라 충분히 설명하지 못하고 넘어갔던 부분이 있었습니다. 내용을 더 보충하고, 또 해결하려는 문제에 대한 여러 선택지를 제공하는 것이 좋을 것 같아 이 강의를 만들게 되었습니다.<br />또한 이 강의에서는 제가 주로 사용하는&nbsp;<strong>IntelliJ 단축키</strong>도 함께 설명하고 있습니다.</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>인프런 :</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/spring_rest-api#">https://www.inflearn.com/course/spring_rest-api#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 자료 :&nbsp;&nbsp;<a target="_blank" href="https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit">https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 소스 :</p><p>&nbsp;</p><p><a target="_blank" href="https://gitlab.com/whiteship/natural">https://gitlab.com/whiteship/natural</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/keesun/study">https://github.com/keesun/study</a></p><p>ksug201811restapi</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌에서는 다음의 다양한 스프링 기술을 사용하여 REST API를 개발합니다.</strong></p><ul><li><p><strong>스프링 프레임워크</strong></p></li><li><p><strong>스프링 부트</strong></p></li><li><p><strong>스프링 데이터 JPA</strong></p></li><li><p><strong>스프링 HATEOAS</strong></p></li><li><p><strong>스프링 REST Docs</strong></p></li><li><p><strong>스프링 시큐리티 OAuth2</strong></p></li></ul><p>&nbsp;</p><p><strong>또한 개발은 테스트 주도 개발(TDD)로 진행하기 때문에 평소 테스트 또는 TDD에 관심있던 개발자에게도 이번 강좌가 도움이 될 것으로 기대합니다.</strong></p><p>&nbsp;</p><p><strong>사전 학습</strong></p><ul><li><p><strong>스프링 프레임워크 핵심 기술 (필수)</strong></p></li><li><p><strong>스프링 부트 개념과 활용 (필수)</strong></p></li><li><p><strong>스프링 데이터 JPA (선택)</strong></p></li></ul><p>&nbsp;</p><p><strong>학습 목표</strong></p><ul><li><p><strong>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해합니다.</strong></p></li><li><p><strong>다양한 스프링 기술을 활용하여 REST API를 개발할 수 있습니다.</strong></p></li><li><p><strong>스프링 HATEOAS와 스프링 REST Docs 프로젝트를 활용할 수 있습니다.</strong></p></li><li><p><strong>테스트 주도 개발(TDD)에 익숙해 집니다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>REST API 는 다음 두가지를 만족해야 한다.</strong></span></p><p><strong><span style="color:#2980b9">1) Self-Describtive Message</span></strong></p><p><strong><span style="color:#2980b9">2) HATEOAS</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#8e44ad">참조 :</span></span></strong></p><p>1)&nbsp;스프링 기반 REST API 개발</p><p><a target="_blank" href="https://tram-devlog.tistory.com/entry/스프링-기반-REST-API-개발-KSUG-세미나">https://tram-devlog.tistory.com/entry/스프링-기반-REST-API-개발-KSUG-세미나</a></p><p>&nbsp;</p><p>2)에러 발상 처리<br /><a target="_blank" href="https://acet.pe.kr/924">https://acet.pe.kr/924</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[3]&nbsp;&nbsp;HATEOAS와 Self-Describtive Message 적용</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>19. 스프링 HATEOAS 소개</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16426&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16426&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 HATEOAS</strong></p><ul><li><p><strong><a href="https://docs.spring.io/spring-hateoas/docs/current/reference/html/">https://docs.spring.io/spring-hateoas/docs/current/reference/html/</a></strong></p></li><li><p><strong>링크 만드는 기능</strong></p><ul><li><p><strong>문자열 가지고 만들기</strong></p></li><li><p><strong>컨트롤러와 메소드로 만들기</strong></p></li></ul></li><li><p><strong>리소스 만드는 기능</strong></p><ul><li><p><strong>리소스: 데이터 + 링크</strong></p></li></ul></li><li><p><strong>링크 찾아주는 기능</strong></p><ul><li><p><strong>Traverson</strong></p></li><li><p><strong>LinkDiscoverers</strong></p></li></ul></li><li><p><strong>링크</strong></p><ul><li><p><strong>HREF</strong></p></li><li><p><strong>REL</strong></p><ul><li><p><strong>self</strong></p></li><li><p><strong>profile</strong></p></li><li><p><strong>update-event</strong></p></li><li><p><strong>query-events</strong></p></li></ul></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><strong><img width="351" height="199" alt="" src="data:image/png;base64,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3n2JSPMOe095DK+88TQeaHlbWWcq8i5A0ZqtaI2XWOWlOSituNnUoLVdfzjUv0lHm4+wrKccpRWNL47vAuteDjqUB6rKi5B3qaG88OJ3ndoXYqxESd5FVFQDqFLBolc/uLTz0nptetbmMVVlyM0vhtCc8LHo3hsu99x6Ur0mcbxFRa5TYYpWJ1z39cGqqnKUFFWgvKrhrLC0gb2jPWwapH5fwwFA0VK09/s5wP63QYCiZWqQgDQEKFqKVppMYi2KI0DRKm5I2SETEaBoKVoTpR6bNXcCFK25jxDjkwsBipailUuuMk4jE6BojQyczSmWAEVL0So2udkxcQQoWnH8WJoEGglQtBQtzwYS0EiAomVikIA0BChailaaTGItiiNA0SpuSNkhExGgaClaE6UemzV3AhStuY8Q45MLAYqWopVLrjJOIxOgaI0MnM0plgBFS9EqNrnZMXEEKFpx/FiaBBoJULQULc8GEtBIgKJlYpCANAQoWopWmkxiLYojQNEqbkjZIRMRoGgpWhOlHps1dwIUrbmPEOOTCwGKlqKVS64yTiMToGiNDJzNKZYARUvRKja52TFxBChacfxYmgQaCVC0FC3PBhLQSICiZWKQgDQEKFqKVppMYi2KI0DRKm5I2SETEaBoKVoTpR6bNXcCFK25jxDjkwsBipailUuuMk4jE6BojQyczSmWAEVL0So2udkxcQQoWnH8WJoEGglQtBQtzwYS0EiAomVikIA0BChailaaTGItiiNA0SpuSNkhExGgaClaE6UemzV3AhStuY8Q45MLAYqWopVLrjJOIxOgaI0MnM0plgBFS9EqNrnZMXEEKFpx/FiaBBoJULQULc8GEtBIgKJlYpCANAQoWopWmkxiLYojQNEqbkjZIRMRoGgpWhOlHps1dwIUrbmPEOOTCwGKlqKVS64yTiMToGiNDJzNKZYARUvRKja52TFxBChacfxYmgQaCVC0FC3PBhLQSICiZWKQgDQEKFqKVppMYi2KI0DRKm5I2SETEaBoKVoTpR6bNXcCFK25jxDjkwsBipailUuuMk4jE6BojQyczSmWAEVL0So2udkxcQQoWnH8WJoEGglQtBQtzwYS0EiAomVikIA0BChailaaTGItiiNA0SpuSNkhExGgaClaE6UemzV3AhStuY8Q45MLAYqWopVLrjJOIxOgaI0MnM0plgBF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" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>20. 스프링 HATEOAS 적용</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16427&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16427&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>버전 업</p><p>&nbsp;</p><p><strong><span style="font-size:20px">EventController</span></strong></p><pre class="brush:as3;">import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import java.net.URI; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor @Log4j2 public class EventController { private final EventRepository eventRepository; // private final ModelMapper modelMapper; private final EventValidator eventValidator; @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){ if(errors.hasErrors()){ return ResponseEntity.badRequest().body(errors); } //커스텀 validate 검사 eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ return ResponseEntity.badRequest().body(errors); } //modelMapper 오류 //Event event=modelMapper.map(eventDto, Event.class); Event event = eventDto.toEvent(); Integer eventId = event.getId(); //유료인지 무료인지 변경처리 event.update(); Event newEvent=this.eventRepository.save(event);//저장 /** * * ★ 링크 생성하기 * EntityModel.of(newEvent); Resource 객체를 가져와서 사용 * * **/ WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(eventId); URI createdUri = selfLinkBuilder.toUri(); log.info(&quot;* createdUri {} &quot; , createdUri); //출력 =&gt; * createdUri http://localhost/api/events EntityModel eventResource = EntityModel.of(newEvent); //셀프링크 추가 방법 eventResource.add(linkTo(EventController.class).slash(eventId).withSelfRel()); //1)링크추가방법 eventResource.add(linkTo(EventController.class).withRel(&quot;query-events&quot;)); //2)링크추가방법 eventResource.add(selfLinkBuilder.withRel(&quot;update-event&quot;)); return ResponseEntity.created(createdUri).body(eventResource); } ~</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.events; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc public class EventControllerTests { @Test @DisplayName(&quot;정상적으로 이벤트를 생성하는 테스트&quot;) public void createEvent() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(false) .offline(false) .eventStatus(EventStatus.DRAFT) .build(); mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) //.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, &quot;application/hal+json;charset=UTF-8&quot;)) .andExpect(jsonPath(&quot;free&quot;).value(false)) .andExpect(jsonPath(&quot;offline&quot;).value(true)) // .andExpect(jsonPath(&quot;eventStatus&quot;).value(EventStatus.DRAFT.toString())) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.query-events&quot;).exists()) .andExpect(jsonPath(&quot;_links.update-event&quot;).exists()); } ~ </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>한글 깨짐</strong></p><p><strong>application.properties</strong></p><pre class="brush:as3;"># REST API 에서 한글깨짐으로 인한 UTF-8 세팅 server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>EntityModel 사용방법 예</strong></p><p><a target="_blank" href="https://www.inflearn.com/questions/203512/entitymodel-deprecated-어떻게-바꾸면-될까요">https://www.inflearn.com/questions/203512/entitymodel-deprecated-어떻게-바꾸면-될까요</a></p><pre class="brush:as3;">// 전체 사용자 목록 @GetMapping(&quot;/users2&quot;) public ResponseEntity&lt;CollectionModel&lt;EntityModel&lt;User&gt;&gt;&gt; retrieveUserList2() { List&lt;EntityModel&lt;User&gt;&gt; result = new ArrayList&lt;&gt;(); List&lt;User&gt; users = service.findAll(); for (User user : users) { EntityModel entityModel = EntityModel.of(user); entityModel.add(linkTo(methodOn(this.getClass()).retrieveAllUsers()).withSelfRel()); result.add(entityModel); } return ResponseEntity.ok(CollectionModel.of(result, linkTo(methodOn(this.getClass()).retrieveAllUsers()).withSelfRel())); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>21. 스프링 &nbsp;REST Docs &nbsp;소개</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16428&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16428&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px"><span style="color:#c0392b">★&nbsp;</span>&nbsp;=&gt;&nbsp;<a target="_blank" href="https://backtony.github.io/spring/2021-10-15-spring-test-3/">Spring REST Docs 적용 및 최적화 하기</a></span></strong></p><p>&nbsp;</p><p><strong><a href="https://docs.spring.io/spring-restdocs/docs/2.0.2.RELEASE/reference/html5/">https://docs.spring.io/spring-restdocs/docs/2.0.2.RELEASE/reference/html5/</a></strong></p><p>&nbsp;</p><p><strong>REST Docs 코딩</strong></p><ul><li><p><strong>andDo(document(&ldquo;doc-name&rdquo;, snippets))</strong></p></li><li><p><strong>snippets</strong></p><ul><li><p><strong>links()</strong></p></li><li><p><strong>requestParameters() + parameterWithName()</strong></p></li><li><p><strong>pathParameters() + parametersWithName()</strong></p></li><li><p><strong>requestParts() + partWithname()</strong></p></li><li><p><strong>requestPartBody()</strong></p></li><li><p><strong>requestPartFields()</strong></p></li><li><p><strong>requestHeaders() + headerWithName()</strong></p></li><li><p><strong>requestFields() + fieldWithPath()</strong></p></li><li><p><strong>responseHeaders() + headerWithName()</strong></p></li><li><p><strong>responseFields() + fieldWithPath()</strong></p></li><li><p><strong>...</strong></p></li></ul></li><li><p><strong>Relaxed*</strong></p></li><li><p><strong>Processor</strong></p><ul><li><p><strong>preprocessRequest(prettyPrint())</strong></p></li><li><p><strong>preprocessResponse(prettyPrint())</strong></p></li><li><p><strong>...</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong>Constraint</strong></p><ul><li><p><strong><a href="https://github.com/spring-projects/spring-restdocs/blob/v2.0.2.RELEASE/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java">https://github.com/spring-projects/spring-restdocs/blob/v2.0.2.RELEASE/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java</a></strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>22. 스프링 &nbsp;REST Docs &nbsp;적용</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16429&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16429&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>REST Docs 자동 설정</strong></p><ul><li><p><strong>@AutoConfigureRestDocs</strong></p></li></ul><p>&nbsp;</p><p><strong>RestDocMockMvc 커스터마이징</strong></p><ul><li><p><strong>RestDocsMockMvcConfigurationCustomizer 구현한 빈 등록</strong></p></li><li><p><strong>@TestConfiguration</strong></p></li></ul><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><ul><li><p><strong>API 문서 만들기</strong></p><ul><li><p><strong>요청 본문 문서화</strong></p></li><li><p><strong>응답 본문 문서화</strong></p></li><li><p><strong>링크 문서화</strong></p><ul><li><p><strong>profile 링크 추가</strong></p></li></ul></li><li><p><strong>응답 헤더 문서화</strong></p></li></ul></li></ul><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>1)라이브러리 추가</strong></span></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; &lt;artifactId&gt;spring-restdocs-mockmvc&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>2)&nbsp;테스트 코드에&nbsp; @AutoConfigureRestDocs&nbsp; 어노테이션 추가 및&nbsp;&nbsp;마지막에&nbsp;&nbsp;.andDo(document(&quot;create-event&quot;)); 추가</strong></span></p><pre class="brush:as3;">@SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @Import(RestDocsConfiguration.class) public class EventControllerTests { @Test @DisplayName(&quot;정상적으로 이벤트를 생성하는 테스트&quot;) public void createEvent() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(false) .offline(false) .eventStatus(EventStatus.DRAFT) .build(); mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) //.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, &quot;application/hal+json;charset=UTF-8&quot;)) .andExpect(jsonPath(&quot;free&quot;).value(false)) .andExpect(jsonPath(&quot;offline&quot;).value(true)) // .andExpect(jsonPath(&quot;eventStatus&quot;).value(EventStatus.DRAFT.toString())) .andExpect(jsonPath(&quot;_links.self&quot;).exists()) .andExpect(jsonPath(&quot;_links.query-events&quot;).exists()) .andExpect(jsonPath(&quot;_links.update-event&quot;).exists()) //스프링 &nbsp;REST Docs &nbsp;적용 .andDo(document(&quot;create-event&quot;)); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">3) 마지막에&nbsp; 추가한다.</span></strong></p><pre class="brush:as3;"> //스프링 &nbsp;REST Docs &nbsp;적용 .andDo(document(&quot;create-event&quot;));</pre><p>&nbsp;</p><p><strong>테스트를 실행하면&nbsp; &nbsp;target 디렉토리의 generated-snippets 에 파일들이 다음과 같이 생성된다.</strong></p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyBsRPoM05NURDgqOzNoOzAb7Zf6egfRub70vwYTZof11MO6AD4Ult029cRB_mEd5zt2EP43qPc0ojF9XMEAO-hJKmy1olQ4P8cswVhe-l5azrBP6uc-b6GUGBBzaa3CfQNuIlS6txxTBuctsbHELVd9T78NVc9GEWFy7ECRSk5_p-VBB5CSh3rHaWAA/s16000/2023-05-27%2011%2044%2054.png" /></p><p>&nbsp;</p><p>&nbsp;정렬이 안되어 있어 보기 어려운데 다음과 같이&nbsp; &nbsp;RestDocsConfiguration&nbsp; 을 추가하면&nbsp; 자동으로 정렬 처리 된다.</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>4) RestDocsConfiguration</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.common; import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; /** * 스프링 REST Docs 적용후 formatting */ @TestConfiguration public class RestDocsConfiguration { @Bean public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer(){ return configurer-&gt;configurer.operationPreprocessors() .withRequestDefaults(prettyPrint()) .withResponseDefaults(prettyPrint()); } } </pre><p>&nbsp;</p><p><strong><span style="color:#2980b9">테스트 클래스에 다음과 같이 임포트 후 사용</span></strong></p><p>&nbsp;</p><pre class="brush:as3;">@SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @Import(RestDocsConfiguration.class) public class EventControllerTests { </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>23. 스프링 &nbsp;REST Docs &nbsp;각종 문서 조각 생성하기</strong></span></span></p><p>&nbsp;</p><p><strong>스프링 REST Docs: 링크, (Req, Res) 필드와 헤더 문서화</strong></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16430&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16430&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>요청 필드 문서화</strong></p><ul><li><p><strong>requestFields() + fieldWithPath()</strong></p></li><li><p><strong>responseFields() + fieldWithPath()</strong></p></li><li><p><strong>requestHeaders() + headerWithName()</strong></p></li><li><p><strong>responseHedaers() + headerWithName()</strong></p></li><li><p><strong>links() + linkWithRel()</strong></p></li></ul><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><p><img alt="" width="600" height="277" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizDMCuYfM9wiYg-UI3xg_7s9HOx7dfxkYsU9_LGrVzSPXTDeW9a9G2lpitsq6HU5QXwDO41bzV3UJ-pedlc1LuH7f_euu9_j0IvXoQYT5Mwcl_oxKNdAaoKAi7BEtnwEUML8MC6VjjFg8PqCI4rC39aUEpL4rdExpKGED4YAzvaQPkHo7zfF8t2VFdaw/s16000/2023-05-27%2011%2057%2052.png" /></p><p>&nbsp;</p><p><strong>Relaxed 접두어</strong></p><ul><li><p><strong>장점: 문서 일부분만 테스트 할 수 있다.</strong></p></li><li><p><strong>단점: 정확한 문서를 생성하지 못한다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>&nbsp;다음과 같이&nbsp; &nbsp;requestHeaders , requestFields , responseHeaders &nbsp;,responseFields 추가하면은 아래 이미지와 같이&nbsp; 파일들 추가 생성된다.</strong></p><p><strong><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhT86xZyxqLloJ8yCTISGF8ayQzBgB-k2vRFCM-n0Gr-gywYOs2XNeGvQBB7_ZvRsJhhEzZnCRRcNxBCLQB9wqt9PvQAGI-t80nkOzW2bdHEx77OkCW7nUoPQYvY70r0UtqUSugZehYaZDHIIBX81-h4UQKRPZZh--8nD02F4CYHO7FkT2nqOO-HSt38w/s16000/2023-05-27%2014%2039%2039.png" /></strong></p><p>&nbsp;</p><p>이때 주의 할점은 요청 파라미터 및 응답파리미터와 와 동일하게 빠짐없이 작성해 줘야 오류가 발행하지 않으며,</p><p>만약에 생략을 할 경우에는&nbsp;<strong>relaxedR </strong>를 붙여 줘서 적용 해야 한다.</p><p>&nbsp;</p><pre class="brush:as3;"> //스프링 &nbsp;REST Docs &nbsp;적용 .andDo(document(&quot;create-event&quot;, links(linkWithRel(&quot;self&quot;).description(&quot;link to self&quot;), linkWithRel(&quot;query-events&quot;).description(&quot;link to query events&quot;), linkWithRel(&quot;update-event&quot;).description(&quot;link to update an existing event&quot;) ), requestHeaders( headerWithName(HttpHeaders.ACCEPT).description(&quot;accept header&quot;), headerWithName(HttpHeaders.CONTENT_TYPE).description(&quot;content type header&quot;) ), //주의 : 요청 파라미터와 동일하게 빠짐없이 작성해 줘야 한다. requestFields( fieldWithPath(&quot;id&quot;).description(&quot;id of new Event&quot;), fieldWithPath(&quot;beginEnrollmentDateTime&quot;).description(&quot;beginEnrollmentDateTime of new event&quot;), fieldWithPath(&quot;name&quot;).description(&quot;name of new Enrollment&quot;), fieldWithPath(&quot;description&quot;).description(&quot;ddescription of new event&quot;), fieldWithPath(&quot;closeEnrollmentDateTime&quot;).description(&quot;closeEnrollmentDateTime of new event&quot;), fieldWithPath(&quot;beginEventDateTime&quot;).description(&quot;beginEventDateTime of new event&quot;), fieldWithPath(&quot;endEventDateTime&quot;).description(&quot;endEventDateTime of new event&quot;), fieldWithPath(&quot;location&quot;).description(&quot;location of new event&quot;), fieldWithPath(&quot;basePrice&quot;).description(&quot;basePrice of new event&quot;), fieldWithPath(&quot;maxPrice&quot;).description(&quot;maxPrice of new event&quot;), fieldWithPath(&quot;limitOfEnrollment&quot;).description(&quot;limitOfEnrollment of new event&quot;), fieldWithPath(&quot;offline&quot;).description(&quot;offline of new event&quot;), fieldWithPath(&quot;free&quot;).description(&quot;free of new event&quot;), fieldWithPath(&quot;eventStatus&quot;).description(&quot;eventStatus of new Enrollment&quot;) ), responseHeaders( headerWithName(HttpHeaders.LOCATION).description(&quot;Location header&quot;), headerWithName(HttpHeaders.CONTENT_TYPE).description(&quot;Content type&quot;) ), // relaxedResponseFields 응답의 일부분만 해당하는 자료 문서화 responseFields( fieldWithPath(&quot;id&quot;).description(&quot;id of new Event&quot;), fieldWithPath(&quot;beginEnrollmentDateTime&quot;).description(&quot;beginEnrollmentDateTime of new event&quot;), fieldWithPath(&quot;name&quot;).description(&quot;name of new Enrollment&quot;), fieldWithPath(&quot;description&quot;).description(&quot;ddescription of new event&quot;), fieldWithPath(&quot;closeEnrollmentDateTime&quot;).description(&quot;closeEnrollmentDateTime of new event&quot;), fieldWithPath(&quot;beginEventDateTime&quot;).description(&quot;beginEventDateTime of new event&quot;), fieldWithPath(&quot;endEventDateTime&quot;).description(&quot;endEventDateTime of new event&quot;), fieldWithPath(&quot;location&quot;).description(&quot;location of new event&quot;), fieldWithPath(&quot;basePrice&quot;).description(&quot;basePrice of new event&quot;), fieldWithPath(&quot;maxPrice&quot;).description(&quot;maxPrice of new event&quot;), fieldWithPath(&quot;limitOfEnrollment&quot;).description(&quot;limitOfEnrollment of new event&quot;), fieldWithPath(&quot;offline&quot;).description(&quot;offline of new event&quot;), fieldWithPath(&quot;free&quot;).description(&quot;free of new event&quot;), fieldWithPath(&quot;eventStatus&quot;).description(&quot;eventStatus of new Enrollment&quot;), fieldWithPath(&quot;_links.self.href&quot;).description(&quot;link to self&quot;), fieldWithPath(&quot;_links.query-events.href&quot;).description(&quot;link to query event list&quot;), fieldWithPath(&quot;_links.update-event.href&quot;).description(&quot;link to update existing event&quot;) ) )); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>25. 스프링 REST &nbsp;Docs &nbsp;문서빌드</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16431&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16431&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>현재 문서</strong></span></span></p><p>참조 :</p><p><a target="_blank" href="https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started">https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>1) 스프링부트 3.0&nbsp; 이상 -&nbsp;&nbsp; 다음plugin&nbsp; 코드를&nbsp; 추가한다.</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;!-- 추가--&gt; &lt;plugin&gt; &lt;groupId&gt;org.asciidoctor&lt;/groupId&gt; &lt;artifactId&gt;asciidoctor-maven-plugin&lt;/artifactId&gt; &lt;version&gt;2.2.1&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;generate-docs&lt;/id&gt; &lt;phase&gt;prepare-package&lt;/phase&gt; &lt;goals&gt; &lt;goal&gt;process-asciidoc&lt;/goal&gt; &lt;/goals&gt; &lt;configuration&gt; &lt;backend&gt;html&lt;/backend&gt; &lt;doctype&gt;book&lt;/doctype&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; &lt;artifactId&gt;spring-restdocs-asciidoctor&lt;/artifactId&gt; &lt;version&gt;3.0.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;/plugin&gt; &lt;plugin&gt; &lt;artifactId&gt;maven-resources-plugin&lt;/artifactId&gt; &lt;version&gt;2.7&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;copy-resources&lt;/id&gt; &lt;phase&gt;prepare-package&lt;/phase&gt; &lt;goals&gt; &lt;goal&gt;copy-resources&lt;/goal&gt; &lt;/goals&gt; &lt;configuration&gt; &lt;outputDirectory&gt; ${project.build.outputDirectory}/static/docs &lt;/outputDirectory&gt; &lt;resources&gt; &lt;resource&gt; &lt;directory&gt; ${project.build.directory}/generated-docs &lt;/directory&gt; &lt;/resource&gt; &lt;/resources&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;/plugin&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>2)&nbsp; 이미지와 같이 디렉토리 위치해(디렉토리 생성할것)&nbsp;&nbsp;index.adoc 파일을 생성해 준다.</strong></span></span></p><p><strong>src/docs/asciidoc/index.adoc</strong></p><p>&nbsp;</p><p><img alt="" width="397" height="618" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdXm74Cy7rUwItwRtjgAPH7fTqzfxVMbmISuGNJ-4RY6mhsD4OU4F1FUCx-HzY29fFUNopzyz26aIqui9V_BUCQz0s2IeMRxwXAb8zUSZ9oX_AmZ9Jz3GFsZH_o0WfRm8FyzfRC_egOF8zkW7fK50eXA5YpKJ8-eNxkmiqwKl90SKx_BdBgrq9bcR3OQ/s16000/2023-05-27%2015%2051%2007.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>3) index.adoc 파일 내용</strong></span></span></p><pre class="brush:as3;">= Natural REST API Guide 백기선; :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 4 :sectlinks: :operation-curl-request-title: Example request :operation-http-response-title: Example response [[overview]] = 개요 [[overview-http-verbs]] == HTTP 동사 본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다. |=== | 동사 | 용례 | `GET` | 리소스를 가져올 때 사용 | `POST` | 새 리소스를 만들 때 사용 | `PUT` | 기존 리소스를 수정할 때 사용 | `PATCH` | 기존 리소스의 일부를 수정할 때 사용 | `DELETE` | 기존 리소스를 삭제할 떄 사용 |=== [[overview-http-status-codes]] == HTTP 상태 코드 본 REST API에서 사용하는 HTTP 상태 코드는 가능한한 표준 HTTP와 REST 규약을 따릅니다. |=== | 상태 코드 | 용례 | `200 OK` | 요청을 성공적으로 처리함 | `201 Created` | 새 리소스를 성공적으로 생성함. 응답의 `Location` 헤더에 해당 리소스의 URI가 담겨있다. | `204 No Content` | 기존 리소스를 성공적으로 수정함. | `400 Bad Request` | 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다. | `404 Not Found` | 요청한 리소스가 없음. |=== [[overview-errors]] == 오류 에러 응답이 발생했을 때 (상태 코드 &gt;= 400), 본문에 해당 문제를 기술한 JSON 객체가 담겨있다. 에러 객체는 다음의 구조를 따른다. include::{snippets}/errors/response-fields.adoc[] 예를 들어, 잘못된 요청으로 이벤트를 만들려고 했을 때 다음과 같은 `400 Bad Request` 응답을 받는다. include::{snippets}/errors/http-response.adoc[] [[overview-hypermedia]] == 하이퍼미디어 본 REST API는 하이퍼미디어와 사용하며 응답에 담겨있는 리소스는 다른 리소스에 대한 링크를 가지고 있다. 응답은 http://stateless.co/hal_specification.html[Hypertext Application from resource to resource. Language (HAL)] 형식을 따른다. 링크는 `_links`라는 키로 제공한다. 본 API의 사용자(클라이언트)는 URI를 직접 생성하지 않아야 하며, 리소스에서 제공하는 링크를 사용해야 한다. [[resources]] = 리소스 [[resources-index]] == 인덱스 인덱스는 서비스 진입점을 제공한다. [[resources-index-access]] === 인덱스 조회 `GET` 요청을 사용하여 인덱스에 접근할 수 있다. operation::index[snippets=&#39;response-body,http-response,links&#39;] [[resources-events]] == 이벤트 이벤트 리소스는 이벤트를 만들거나 조회할 때 사용한다. [[resources-events-list]] === 이벤트 목록 조회 `GET` 요청을 사용하여 서비스의 모든 이벤트를 조회할 수 있다. operation::get-events[snippets=&#39;response-fields,curl-request,http-response,links&#39;] [[resources-events-create]] === 이벤트 생성 `POST` 요청을 사용해서 새 이벤트를 만들 수 있다. operation::create-event[snippets=&#39;request-fields,curl-request,http-response,links&#39;] [[resources-events-get]] === 이벤트 조회 `Get` 요청을 사용해서 기존 이벤트 하나를 조회할 수 있다. operation::get-event[snippets=&#39;request-fields,curl-request,http-response,links&#39;] [[resources-events-update]] === 이벤트 수정 `PUT` 요청을 사용해서 기존 이벤트를 수정할 수 있다. operation::update-event[snippets=&#39;request-fields,curl-request,http-response,links&#39;] </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>4) 오른쪽 maven 메뉴에서 package 클릭후 빌드하면 끝</strong></span></span></p><p><img alt="" width="503" height="452" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhTgfxcrDujCUEQwFWHf-J5MLyO6QG3zePbhfADOm6wNBmz4zrYgA-m-sb-lH3roqkJRU-zpUF3fYwxe9KmwzybdBCZ87k_FZsbxUxKd7GxIZ7_JsSMpQMpfplPzr3mJVAb0irDSKtcX6wVpL-YVJc9cY518293lXORPKTvdM-OlgiSIeYA0RmsYVN0OQ/s16000/2023-05-27%2016%2000%2045.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>5) 빌드에 성공하면 target&nbsp; 디렉토리에&nbsp; &nbsp;generated-docs 디렉토리가 생성 되며 index.html 파일 생성되게 된다.</strong></span></p><p><span style="font-size:20px"><strong>index.html&nbsp; &nbsp;파일 선택후 마우스 우클릭후 해당 디렉토리 경로&nbsp; 복사후 브라우저창에서 열면</strong></span></p><p><span style="font-size:20px"><strong>rest api 문서가 생성된것을 볼수 있다.</strong></span></p><p>file:///F:/Study/JAVA/%EB%B0%B1%EA%B8%B0%EC%84%A0/RestAPI/rest-api-width-spring/target/generated-docs/index.html#resources-events-update_request_fields</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="389" height="502" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7wleEsmvSjQBOL69pb90GCkS4gABWLTN9hAoDQRGLQOLp6sXmy1WjXZeZksfKSr64eBbnfcQgbsOw5IUGNJCAg4X9xFPOxSvDC6gUt1Y2OXXOm-Fv-cgHl-Q3r8NNY5PMpAnf9YIZJt-m5qqprZ8tBp2antOGQO5ApOHAIsV9Y1Q7lYYpItDxCi-Mww/s16000/2023-05-27%2016%2006%2047.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px">다음과 같이&nbsp; 간단하게&nbsp; API 문서가 생성 된다.</span></p><p>&nbsp;</p><p><span style="font-size:20px"><a target="_blank" href="http://localhost:8080/docs/index.html">http://localhost:8080/docs/index.html</a></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>6) profile 링크 추가</strong></span></p><p>EventController 에서 profile 링크 추가</p><pre>eventResource.add(Link.of(&quot;/docs/index.html#resource-events-create&quot;).withRel(&quot;profile&quot;)); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>7)테스트 코드에서 추가 후 빌드하면 진정한 REST API 개발&nbsp; 문서 완료&nbsp;</strong></span></p><p>&nbsp;</p><pre class="brush:as3;"> linkWithRel(&quot;profile&quot;).description(&quot;link to update an existing event&quot;) responseFields( fieldWithPath(&quot;_links.profile.href&quot;).description(&quot;link to profile event&quot;) ) </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>깃 소스</strong></p><p><a target="_blank" href="https://github.com/braverokmc79/rest-api-width-spring/commit/ac5df4a571c541d03f8332f59145990186beaa96">https://github.com/braverokmc79/rest-api-width-spring/commit/ac5df4a571c541d03f8332f59145990186beaa96</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>26. 테스트용 &nbsp;DB와 설정 분리하기</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16432&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16432&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>Scripts</p><p>Here, I memo scripts that I have used during development.</p><p>Postgres</p><p>Run Postgres Container</p><pre>docker run --name ndb -p 5432:5432 -e POSTGRES_PASSWORD=pass -d postgres </pre><p>&nbsp;</p><p>This cmdlet will create Postgres instance so that you can connect to a database with:</p><ul><li>database: postgres</li><li>username: postgres</li><li>password: pass</li><li>post: 5432</li></ul><p>Getting into the Postgres container</p><pre>docker exec -i -t ndb bash</pre><p>&nbsp;</p><p>Then you will see the containers bash as a root user.</p><p>Connect to a database</p><pre>psql -d postgres -U postgres</pre><p>&nbsp;</p><p>Query Databases</p><pre>\l</pre><p>&nbsp;</p><p>Query Tables</p><pre>\dt</pre><p>&nbsp;</p><p>Quit</p><pre>\q</pre><p>&nbsp;</p><p>application.properties</p><p>Datasource</p><pre>spring.datasource.username=postgres spring.datasource.password=pass spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.driver-class-name=org.postgresql.Driver</pre><p>&nbsp;</p><p>Hibernate</p><pre>spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.properties.hibernate.format_sql=true logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE</pre><p>&nbsp;</p><p>Test Database</p><pre>spring.datasource.username=sa spring.datasource.password= spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>애플리케이션 설정과 테스트 설정 중복 어떻게 줄일 것인가?</strong></span></span></p><ul><li><p><span style="font-size:20px"><strong>프로파일과 @ActiveProfiles 활용</strong></span></p></li></ul><p>&nbsp;</p><p><strong>application-test.properties</strong></p><p>&nbsp;</p><pre class="brush:as3;">spring.datasource.username=sa spring.datasource.password= spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect </pre><p>&nbsp;</p><p>&nbsp;</p><p>test 디렉토리에 다음 이미지처럼 resource&nbsp; 폴더를 생성후&nbsp;</p><p>application-test.properties&nbsp; 파일을 생성해 준다.</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEglUZBAN3ueW2uQd2Oh2Lc4DG04OK799IvNp5WsaYpZdbsNL1PKEkxH8oM3ufNBTqAKDWo0tX8JIvgoBvL2K4uM2Aepr6pJKgiMLhWSLieY4fwx8B-N1QXasKnYHjSjjtjVOj9nTvOae69vi9hEi9CjQnt4NV4kaeG5dR46-sudyPTQ4qA1vR7zQ3HN8A/s16000/2023-05-27%2018%2013%2050.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>인텔리제이에서 인식해 줄수 있도록 다음과 같이 설정해 준다.</p><p><img alt="" width="1555" height="737" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgr87wrdL3PfQG1CTtRmKYfSN6d1jmAxtkn7SsipTZdpaYD8krcmG3OcCKGNG2QSbOs9YCBiSPS6IjvAsL2r2z4M5cy-0Dx6sADE3Y5I5j6xvWV_ibMNRPVgauMHPjMAZR4ExzcUB8AESVVth2_617A4q7N46expq74Mrpr4974nbST9uDgcWKMdQ3PkQ/s16000/2023-05-27%2018%2015%2009.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>테스트&nbsp; 클래스에 어노테이션 추가</strong></p><pre class="brush:as3;">@ActiveProfiles(&quot;test&quot;) public class EventControllerTests {</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>27. API 인덱스 만들기</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16433&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16433&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:18px"><strong>1) IndexController 을 다음과 같이 생성해 준다.</strong></span></span></p><p><img alt="" width="259" height="587" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEizLVCfUWjXuCq4pYafMWXxBCQE1Fihs1gNqVC49M7GqR2674vU11bFoznjsRWaRpiew-KseztV5mrtRKXsYwIlZm4h5UFU_g3TOxHkN24KKUKUDnyYsLdOhp39lyIHaclXIyYmxV3iwuuqwPT6JTz-Uc97S9Z9GH4V5SinCIfvyA6eZmJLspgRQSnGsw/s16000/2023-05-28%2007%2051%2059.png" /></p><p>&nbsp;</p><p><strong>IndexController</strong></p><pre class="brush:as3;">package net.macaronics.restapi.index; import net.macaronics.restapi.events.EventController; import org.springframework.hateoas.RepresentationModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @RestController public class IndexController { @GetMapping(&quot;/api&quot;) public RepresentationModel index(){ RepresentationModel index=new RepresentationModel(); index.add(linkTo(EventController.class).withRel(&quot;events&quot;)); return index; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><span style="color:#8e44ad"><strong>2) IndexControllerTest 생성</strong></span></span></p><p><span style="font-size:16px"><span style="color:#8e44ad"><strong><img alt="" width="288" height="287" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbUCO_ajwi_qkrpNKVIhZiA_X88Jmuf_LRv4Vy9CBVljl5XXMkrvVmYS2YFEMORAddBDUfDtIpqZR7B8z3RJoKSNzteMzCP-yn-H6ijVHz11NYTDtDouo5CUum5KUd1sE4P43X030c-2OJUcv19lkjFKg08Wh9LMJQXcwoZHY55I6HJU4r-41ndnoegw/s16000/2023-05-28%2009%2010%2054.png" /></strong></span></span></p><pre class="brush:as3;">package net.macaronics.restapi.index; import com.fasterxml.jackson.databind.ObjectMapper; import net.macaronics.restapi.common.RestDocsConfiguration; import net.macaronics.restapi.events.Event; import net.macaronics.restapi.events.EventStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs @Import(RestDocsConfiguration.class) @ActiveProfiles(&quot;test&quot;) public class IndexControllerTest { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Test public void index() throws Exception{ this.mockMvc.perform( get(&quot;/api&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) ).andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath(&quot;_links.events&quot;).exists()); } @Test @DisplayName(&quot;인덱스 입력 값이 잘못된 경우에 에러가 발생하는 테스트&quot;) public void createEvent_Bad_Request_Wrong_Input() throws Exception{ Event event =Event.builder() .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(10000) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;4.강남역 D2 스타텀 팩토리&quot;) .eventStatus(EventStatus.PUBLISHED) .build(); this.mockMvc.perform(MockMvcRequestBuilders.post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event))) .andDo((print())) .andExpect(status().isBadRequest()) .andExpect(jsonPath(&quot;errors[0].objectName&quot;).exists()) .andExpect(jsonPath(&quot;errors[0].defaultMessage&quot;).exists()) .andExpect(jsonPath(&quot;errors[0].code&quot;).exists()) .andExpect(jsonPath(&quot;_links.index&quot;).exists()); // 추가 } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>에러처리시 메인화면으로 이동 처리를 위해</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:18px"><strong>ErrosResource</strong></span></span></p><p><strong>@Component&nbsp; 로 구성해 줘야 한다.</strong></p><pre class="brush:as3;">package net.macaronics.restapi.common; import net.macaronics.restapi.index.IndexController; import org.springframework.hateoas.EntityModel; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Component public class ErrorsResource{ public EntityModel addLink(Errors content) { EntityModel&lt;Errors&gt; entityModel = EntityModel.of(content); entityModel.add(linkTo(methodOn(IndexController.class).index()).withRel(&quot;index&quot;)); return entityModel; } } </pre><p>&nbsp;</p><p><span style="font-size:16px"><span style="color:#8e44ad"><strong>EventController</strong></span></span></p><pre class="brush:as3;"> private final ErrorsResource errorsResource; @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) throws Exception { if (errors.hasErrors()) { // return ResponseEntity.badRequest().body(errors); return badRequest(errors); } //커스텀 validate 검사 eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ // return ResponseEntity.badRequest().body(errors); return badRequest(errors); } ~ private ResponseEntity&lt;EntityModel&gt; badRequest(Errors errors) { return ResponseEntity.badRequest().body(errorsResource.addLink(errors)); } </pre><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.events; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import net.macaronics.restapi.common.ErrorsResource; import org.springframework.hateoas.*; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import java.net.URI; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor @Log4j2 public class EventController { private final EventRepository eventRepository; // private final ModelMapper modelMapper; private final EventValidator eventValidator; private final ErrorsResource errorsResource; /** * methodOn 사용 * @return */ // @PostMapping(&quot;/method&quot;) // public ResponseEntity createEventMethod(){ // WebMvcLinkBuilder webMvcLinkBuilder = linkTo(methodOn(EventController.class).createEvent(null)); // return ResponseEntity.created(webMvcLinkBuilder.toUri()).build(); // } @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) throws Exception { if (errors.hasErrors()) { // return ResponseEntity.badRequest().body(errors); return badRequest(errors); } //커스텀 validate 검사 eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ // return ResponseEntity.badRequest().body(errors); return badRequest(errors); } //modelMapper 오류 //Event event=modelMapper.map(eventDto, Event.class); Event event = eventDto.toEvent(); Integer eventId = event.getId(); //유료인지 무료인지 변경처리 event.update(); Event newEvent=this.eventRepository.save(event);//저장 /** * * ★ 링크 생성하기 * EntityModel.of(newEvent); Resource 객체를 가져와서 사용 * * **/ WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(eventId); URI createdUri = selfLinkBuilder.toUri(); log.info(&quot;* createdUri {} &quot; , createdUri); //출력 =&gt; * createdUri http://localhost/api/events EntityModel eventResource = EntityModel.of(newEvent); //셀프링크 추가 방법 eventResource.add(linkTo(EventController.class).slash(eventId).withSelfRel()); //1)링크추가방법 eventResource.add(linkTo(EventController.class).withRel(&quot;query-events&quot;)); //2)링크추가방법 eventResource.add(selfLinkBuilder.withRel(&quot;update-event&quot;)); eventResource.add(Link.of(&quot;/docs/index.html#resource-events-create&quot;).withRel(&quot;profile&quot;)); return ResponseEntity.created(createdUri).body(eventResource); } private ResponseEntity&lt;EntityModel&gt; badRequest(Errors errors) { return ResponseEntity.badRequest().body(errorsResource.addLink(errors)); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-22 21:02:25 스프링 기반 REST API 개발 - (백기선) - 1-2.REST API 및 프로젝트 소개 , 2. 이벤트 생성 API 개발 - ★ JUnit 5 Parameterized Tests http://macaronics.net/index.php/m01/spring/view/2130 2130 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드] 강의입니다.</strong></p><p>다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해</p><p>다양한 스프링 기술을 활용하여 REST API 개발</p><p>스프링 HATEOAS와 스프링 REST Docs 프로젝트 활용</p><p>테스트 주도 개발(TDD)</p><p>스프링으로 REST를 따르는 API를 만들어보자!<br />백기선의 스프링 기반 REST API 개발</p><p><img title="239527-img1-white.png" alt="" width="965" height="298" src="https://cdn.inflearn.com/public/files/courses/239527/a59979fe-29a4-481a-bdbc-a2150bccfdb6/239527-img1-white.png" /></p><p><strong>스프링 기반 REST API 개발</strong></p><p>이 강의에서는 다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발합니다.</p><p>그런&nbsp;<strong>REST API</strong>로 괜찮은가</p><p>2017년 네이버가 주관한 개발자 컨퍼런스 Deview에서&nbsp;<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a>라는 이응준님의 발표가 있었습니다. 현재 REST API로 불리는 대부분의 API가 실제로는 로이 필딩이 정의한 REST를 따르고 있지 않으며, 그 중에서도 특히 Self-Descriptive Message와 HATEOAS가 지켜지지 않음을 지적했고, 그에 대한 대안을 제시되었습니다.</p><p><strong>이번 강의는 해당 발표에 영감을 얻어 만들어졌습니다.</strong>&nbsp;2018년 11월에 KSUG에서 동일한 이름으로 세미나를 진행한 경험이 있습니다. 4시간이라는 짧지 않은 발표였지만, 빠르게 진행하느라 충분히 설명하지 못하고 넘어갔던 부분이 있었습니다. 내용을 더 보충하고, 또 해결하려는 문제에 대한 여러 선택지를 제공하는 것이 좋을 것 같아 이 강의를 만들게 되었습니다.<br />또한 이 강의에서는 제가 주로 사용하는&nbsp;<strong>IntelliJ 단축키</strong>도 함께 설명하고 있습니다.</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>인프런 :</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/spring_rest-api#">https://www.inflearn.com/course/spring_rest-api#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 자료 :&nbsp;&nbsp;<a target="_blank" href="https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit">https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 소스 :</p><p>&nbsp;</p><p><a target="_blank" href="https://gitlab.com/whiteship/natural">https://gitlab.com/whiteship/natural</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/keesun/study">https://github.com/keesun/study</a></p><p>ksug201811restapi</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌에서는 다음의 다양한 스프링 기술을 사용하여 REST API를 개발합니다.</strong></p><ul><li><p><strong>스프링 프레임워크</strong></p></li><li><p><strong>스프링 부트</strong></p></li><li><p><strong>스프링 데이터 JPA</strong></p></li><li><p><strong>스프링 HATEOAS</strong></p></li><li><p><strong>스프링 REST Docs</strong></p></li><li><p><strong>스프링 시큐리티 OAuth2</strong></p></li></ul><p>&nbsp;</p><p><strong>또한 개발은 테스트 주도 개발(TDD)로 진행하기 때문에 평소 테스트 또는 TDD에 관심있던 개발자에게도 이번 강좌가 도움이 될 것으로 기대합니다.</strong></p><p>&nbsp;</p><p><strong>사전 학습</strong></p><ul><li><p><strong>스프링 프레임워크 핵심 기술 (필수)</strong></p></li><li><p><strong>스프링 부트 개념과 활용 (필수)</strong></p></li><li><p><strong>스프링 데이터 JPA (선택)</strong></p></li></ul><p>&nbsp;</p><p><strong>학습 목표</strong></p><ul><li><p><strong>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해합니다.</strong></p></li><li><p><strong>다양한 스프링 기술을 활용하여 REST API를 개발할 수 있습니다.</strong></p></li><li><p><strong>스프링 HATEOAS와 스프링 REST Docs 프로젝트를 활용할 수 있습니다.</strong></p></li><li><p><strong>테스트 주도 개발(TDD)에 익숙해 집니다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>REST API 는 다음 두가지를 만족해야 한다.</strong></span></p><p><strong><span style="color:#2980b9">1) Self-Describtive Message</span></strong></p><p><strong><span style="color:#2980b9">2) HATEOAS</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>17. 비즈니스 로직 적용</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16423&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16423&amp;tab=curriculum</a></p><p>&nbsp;</p><p>테스트 할 것<br />비즈니스 로직 적용 됐는지 응답 메시지 확인<br />offline과 free 값 확인</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventController</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">@Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor @Log4j2 public class EventController { @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){ if(errors.hasErrors()){ log.info(&quot;첫번째 Bad Request 처리 {}&quot;, errors.getFieldErrors()); //return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().body(errors); } //커스텀 validate 검사 eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ log.info(&quot;두번째 Bad Request 처리&quot;); // return ResponseEntity.badRequest().build(); //errors 의 경우 기본적으로 Serialize 처리가 안되어 있어 에러 발생 //다음괴 같이 ErrorsSerializer 클래스를 만들어 처리해 준다. log.info(&quot; errors : {}&quot;, errors); return ResponseEntity.badRequest().body(errors); } //modelMapper 오류 //Event event=modelMapper.map(eventDto, Event.class); Event event = eventDto.toEvent(); //유료인지 무료인지 변경처리 event.update(); Event newEvent=this.eventRepository.save(event);//저장 URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri(); return ResponseEntity.created(createdUri).body(event); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventTest</strong></span></p><pre class="brush:as3;">@SpringBootTest @Transactional class EventTest { @Autowired private ModelMapper modelMapper; @Test public void testFree(){ //Given Event event=Event.builder() .basePrice(0) .maxPrice(0) .build(); //When event.update(); //Then Assertions.assertThat(event.isFree()).isTrue(); //Given event=Event.builder() .basePrice(100) .maxPrice(100) .build(); //When event.update(); //Then Assertions.assertThat(event.isFree()).isFalse(); } @Test public void testOffline(){ //Given Event event =Event.builder() .location(&quot;강남역 네이버 D2 스타텁 팩토리&quot;) .build(); //When event.update(); //Then Assertions.assertThat(event.isOffline()).isTrue(); //Given event =Event.builder() .build(); //When event.update(); //Then Assertions.assertThat(event.isOffline()).isFalse(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Event</strong></span></p><pre class="brush:as3;">import jakarta.persistence.*; import lombok.*; import org.springframework.util.StringUtils; import java.time.LocalDateTime; @Builder @AllArgsConstructor @NoArgsConstructor @Getter //@Setter @EqualsAndHashCode(of=&quot;id&quot;) @Entity public class Event { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; private String description; private LocalDateTime beginEnrollmentDateTime; private LocalDateTime closeEnrollmentDateTime; private LocalDateTime beginEventDateTime; private LocalDateTime endEventDateTime; private String location; // (optional) 이게 없으면 온라인 모임 private int basePrice; // (optional) private int maxPrice; // (optional)- private int limitOfEnrollment; private boolean offline; private boolean free; @Enumerated(EnumType.STRING) private EventStatus eventStatus; public void update() { //Update Free if(this.basePrice==0 &amp;&amp; this.maxPrice==0){ this.free=true; }else{ this.free=false; } //Update offline if(StringUtils.hasText(this.location)){ this.offline=true; }else { this.offline=false; } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br />&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>18. 매개변수를 이용한 테스트</strong></span></span></p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#8e44ad">JUnit 5 Parameterized Tests</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16424&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16424&amp;tab=curriculum</a></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;!-- https://mvnrepository.com/artifact/pl.pragmatists/JUnitParams --&gt; &lt;dependency&gt; &lt;groupId&gt;org.junit.jupiter&lt;/groupId&gt; &lt;artifactId&gt;junit-jupiter-params&lt;/artifactId&gt; &lt;version&gt;5.9.2&lt;/version&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;사용법</p><p>참조 :&nbsp;<a target="_blank" href="https://reflectoring.io/tutorial-junit5-parameterized-tests/">https://reflectoring.io/tutorial-junit5-parameterized-tests/</a></p><p>&nbsp;</p><pre class="brush:as3;"> @ParameterizedTest // @CsvSource({&quot;0,0,true&quot;, &quot;100,0,false&quot;}) // @MethodSource(&quot;paramsForTestFree&quot;) @MethodSource public void testFree(int basePrice, int maxPrice, boolean isFree){ //Given Event event=Event.builder() .basePrice(basePrice) .maxPrice(maxPrice) .build(); //When event.update(); //Then Assertions.assertThat(event.isFree()).isEqualTo(isFree); } static Object[] testFree(){ return new Object[]{ new Object[] {0, 0,true}, new Object[]{100,0, false}, new Object[]{0,100, false}, new Object[]{100, 200, false} }; } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @ParameterizedTest @MethodSource public void testOffline(String location, boolean isOffline){ //Given Event event =Event.builder() .location(location) .build(); //When event.update(); //Then Assertions.assertThat(event.isOffline()).isEqualTo((isOffline)); } static Object[] testOffline(){ return new Object[]{ new Object[]{&quot;강남&quot;, true}, new Object[]{null, false}, new Object[]{&quot; &quot;, false} }; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-19 22:10:19 SuperDisplay - Virtual Monitor http://macaronics.net/index.php/m05/computer/view/2129 2129 <p>&nbsp;</p><p>&nbsp;</p><p><strong>최근 업무용으로 활용하고 있는 Super Display(다중 모니터 확장 프로그램)을 소개합니다.</strong></p><p><strong>※ </strong><strong>제품 협찬 및 상업적 홍보가 아님</strong><strong>을 말씀드립니다.</strong></p><p><a data-linktype="img" data-linkdata="{&quot;id&quot; : &quot;SE-27fe8b18-47cb-433f-8f9d-cfdb1c50f6a9&quot;, &quot;src&quot; : &quot;https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMTY3/MDAxNjIxNTYxMjgyNjAw.haEuzscIQ-rakE2tUOEeHMgEk2M5VQFS40BCEqcnCEQg.qgYcT-yql8CFzHG-ZvB0fY3UXKl14tqJK1iU-GqHlUMg.JPEG.toughw0/%EC%BA%A1%EC%B2%981.JPG&quot;, &quot;linkUse&quot; : &quot;false&quot;, &quot;link&quot; : &quot;&quot;}" area-hidden="true" onclick="return false;"><img data-lazy-src="" data-width="693" data-height="368" alt="" id="img_1" data-top="375.5" src="https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMTY3/MDAxNjIxNTYxMjgyNjAw.haEuzscIQ-rakE2tUOEeHMgEk2M5VQFS40BCEqcnCEQg.qgYcT-yql8CFzHG-ZvB0fY3UXKl14tqJK1iU-GqHlUMg.JPEG.toughw0/%EC%BA%A1%EC%B2%981.JPG?type=w800" /></a></p><p><strong>노트북이나 화상회의 시 듀얼모니터 사용과 필기(터치)에 불편함을 느껴 Space Desk 앱을 설치하여 </strong></p><p><strong>사용하였으나 네트워크(Wifi)가 지원되지 않을 경우 사용이 제한되었고, 무엇보다도 딜레이(화면끊김)</strong></p><p><strong>현상이 심하여 다른 프로그램을 알아보다가 Super Display를 설치하여 잘 사용하고 있는 중입니다.</strong></p><p><strong>​</strong></p><p><strong>1. 장점</strong></p><p><strong>윈도우 OS를 지원하는 PC나 노트북에서 듀얼모니터 및 필기(터치)를 지원하고 있으며, 무엇보다도</strong></p><p><strong>딜레이 현상이 거의 없고, 유무선 모두 사용할 수 있어 좋습니다.</strong></p><p><strong>​</strong></p><p><strong>프로그램 다운로드 사이트</strong></p><p><a target="_blank" href="https://superdisplay.app/"><strong>https://superdisplay.app/</strong></a></p><p><a target="_blank" href="https://superdisplay.app/">&nbsp;</a><a target="_blank" href="https://superdisplay.app/"><strong>SuperDisplay</strong></a></p><p><a target="_blank" href="https://superdisplay.app/">About Blog Help Media Second monitor SuperDisplay turns your Android device into a portable USB display for your Windows 10 PC. Duplicate or extend your screen simply by plugging in your phone or tablet. Superb performance A laggy display is as good as no display. SuperDisplay was built with perform...</a></p><p><a target="_blank" href="https://superdisplay.app/">superdisplay.app</a></p><p><strong>※ 윈도우용 프로그램을 설치하면 아래의 드라이버 프로그램이 설치됨</strong></p><p><a data-linktype="img" data-linkdata="{&quot;id&quot; : &quot;SE-96051151-dda6-42fc-abbd-1c8c38a0d720&quot;, &quot;src&quot; : &quot;https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMzQg/MDAxNjIxNTYxNjU0NTk3.BLEdoBaMfa5ytjKL0ZbT7-1RnhedlTCzHjgOxv0UJjwg.RMNLj3FHoXb0Y1ufBzSj4yehq0I_mg5mcWoAflIXty8g.JPEG.toughw0/%EC%BA%A1%EC%B2%983.JPG&quot;, &quot;linkUse&quot; : &quot;false&quot;, &quot;link&quot; : &quot;&quot;}" area-hidden="true" onclick="return false;"><img data-lazy-src="" data-width="372" data-height="727" alt="" id="img_2" data-top="1245.34375" src="https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMzQg/MDAxNjIxNTYxNjU0NTk3.BLEdoBaMfa5ytjKL0ZbT7-1RnhedlTCzHjgOxv0UJjwg.RMNLj3FHoXb0Y1ufBzSj4yehq0I_mg5mcWoAflIXty8g.JPEG.toughw0/%EC%BA%A1%EC%B2%983.JPG?type=w800" /></a></p><p><strong>스마트폰(안드로이드계열) 프로그램 다운로드</strong></p><p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko"><strong>https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko</strong></a></p><p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko"><img alt="" data-top="2074.84375" src="https://dthumb-phinf.pstatic.net/?src=%22https%3A%2F%2Fplay-lh.googleusercontent.com%2FwA6vdvmz5tWaB0ZBYp1K9KU7ldBJdHCd7HNd4ca6PPzTt24tB8zxk382d-zxxQioPw%22&amp;type=ff120" /></a><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko">&nbsp;</a><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko"><strong>SuperDisplay - Virtual Monitor &amp; Graphics Tablet - Apps on Google Play</strong></a></p><p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko">Turn your device into a portable USB display and graphics tablet for Windows 10</a></p><p><a target="_blank" href="https://play.google.com/store/apps/details?id=com.kelocube.mirrorclient&amp;hl=ko">play.google.com</a></p><p><a data-linktype="img" data-linkdata="{&quot;id&quot; : &quot;SE-2219d0ff-f763-4630-95ce-e8740160df3a&quot;, &quot;src&quot; : &quot;https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMjgx/MDAxNjIxNTYxNTI5MjY2.t1XaaHEqY4X3-Ek5bDfBqcfhw5BnxvzzBd46Dj64SfAg.b0GOySvoA6DrA11KeVz1TMnvY2KS1l1xWDOf-WPVVWwg.JPEG.toughw0/%EC%BA%A1%EC%B2%984.JPG&quot;, &quot;linkUse&quot; : &quot;false&quot;, &quot;link&quot; : &quot;&quot;}" area-hidden="true" onclick="return false;"><img data-lazy-src="" data-width="693" data-height="1139" alt="" id="img_3" data-top="2214.84375" src="https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMjgx/MDAxNjIxNTYxNTI5MjY2.t1XaaHEqY4X3-Ek5bDfBqcfhw5BnxvzzBd46Dj64SfAg.b0GOySvoA6DrA11KeVz1TMnvY2KS1l1xWDOf-WPVVWwg.JPEG.toughw0/%EC%BA%A1%EC%B2%984.JPG?type=w800" /></a></p><p><a data-linktype="img" data-linkdata="{&quot;id&quot; : &quot;SE-0c4d8f9a-c78d-4042-96c7-72bb5af5c1c8&quot;, &quot;src&quot; : &quot;https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMTY1/MDAxNjIxNTYxOTc5NjMw.cXHbQjFKJ0zrgr1KLLEhhkW8M_VLKD95tnUKaqJ5WM4g.-6ASs2LJWEko3l7vZrMlXZWK7oTXR06edFwqYt3UEqYg.JPEG.toughw0/%EC%BA%A1%EC%B2%982.JPG&quot;, &quot;linkUse&quot; : &quot;false&quot;, &quot;link&quot; : &quot;&quot;}" area-hidden="true" onclick="return false;"><img data-lazy-src="" data-width="693" data-height="437" alt="" id="img_4" data-top="3337.59375" src="https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMTY1/MDAxNjIxNTYxOTc5NjMw.cXHbQjFKJ0zrgr1KLLEhhkW8M_VLKD95tnUKaqJ5WM4g.-6ASs2LJWEko3l7vZrMlXZWK7oTXR06edFwqYt3UEqYg.JPEG.toughw0/%EC%BA%A1%EC%B2%982.JPG?type=w800" /></a></p><p><strong>※ 네트워크(wifie) 및 USB 케이블 모두 연결 가능</strong></p><p><a data-linktype="img" data-linkdata="{&quot;id&quot; : &quot;SE-7f8f544d-8e22-4af1-a31f-cefee1312b61&quot;, &quot;src&quot; : &quot;https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMzEg/MDAxNjIxNTYxNzU4MTM0.sfa3AeVmULxvffYmV7eMwWt_EZsDoGqzGNQWV09Wkd4g.wSSooF_JrlWmPsA-bvT4O0qczed2Xsbdj_ygna5MTDog.JPEG.toughw0/%EC%BA%A1%EC%B2%985.JPG&quot;, &quot;linkUse&quot; : &quot;false&quot;, &quot;link&quot; : &quot;&quot;}" area-hidden="true" onclick="return false;"><img data-lazy-src="" data-width="693" data-height="312" alt="" id="img_5" data-top="3833.09375" src="https://mblogthumb-phinf.pstatic.net/MjAyMTA1MjFfMzEg/MDAxNjIxNTYxNzU4MTM0.sfa3AeVmULxvffYmV7eMwWt_EZsDoGqzGNQWV09Wkd4g.wSSooF_JrlWmPsA-bvT4O0qczed2Xsbdj_ygna5MTDog.JPEG.toughw0/%EC%BA%A1%EC%B2%985.JPG?type=w800" /></a></p><p><strong>※ 태블릿과 노트북을 연동한 모습</strong></p><p><strong>​</strong></p><p><strong>2. 단점</strong></p><p><strong>딜레이 현상이 적어 일반적인 작업 시 큰 제한사항이 없었으나 그래픽디자인 등 정밀작업 시에는 제한이</strong></p><p><strong>있을 수 있다고 생각되었고, PC나 노트북이 윈도우가 아닌 다른 OS(맥, 리눅스 등)일 경우 아직까지는</strong></p><p><strong>사용이 제한되는 것으로 확인하였습니다.</strong></p><p><strong>무엇보다 해당 프로그램은 유료이며, 유로로 구매하지 않을 경우 사용 간 구매 메시지가 뜨는 부분이</strong></p><p><strong>있으니 참고하시기 바랍니다.</strong></p><p><strong>​</strong></p><p><strong>3. 결론</strong></p><p><strong>태블릿이나 스마트폰을 PC나 노트북과 연동하여 필기(터치)가 가능한 다중 모니터 확장 프로그램이라</strong></p><p><strong>생각되고, 향후 다양한 OS 지원과 금액적인 부분만 뒷받침된다면 상당히 괜찮다고 생각됩니다.</strong></p> 2023-05-14 20:02:36 Spring Boot REST API CRUD , Hateoas 사용한 rest API 1시간 http://macaronics.net/index.php/m01/spring/view/2128 2128 <p>&nbsp;</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/y6R3reU1vWE" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/hateoas-rest-api-account/tree/main">https://github.com/braverokmc79/hateoas-rest-api-account/tree/main</a></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-u8nHevRUyzG7-56Kkt5nEYjgyXbzBiDrbbu88LICyy3BLzu0Qc2C6BaYIdEWflOWMPyR_XHZrBxUE95_QfzepyGZR0sZGIhchA0PEgbgPt2PJ7xLf6vtuCoWSh6SpD7Gt4OmtGS2vMb1YgAiZQVuvR43pj317lPFz62SxIj7CsW933AAU2MRfsvmDQ/s16000/2023-05-08%2016%2023%2013.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>pom.xml</strong></span></p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt; &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt; &lt;parent&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt; &lt;version&gt;3.0.6&lt;/version&gt; &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt; &lt;/parent&gt; &lt;groupId&gt;net.macaronics.account&lt;/groupId&gt; &lt;artifactId&gt;AccoutAPI&lt;/artifactId&gt; &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt; &lt;name&gt;AccoutAPI&lt;/name&gt; &lt;description&gt;Develop REST APis with HATOAS&lt;/description&gt; &lt;properties&gt; &lt;java.version&gt;17&lt;/java.version&gt; &lt;/properties&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-hateoas&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.h2database&lt;/groupId&gt; &lt;artifactId&gt;h2&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;build&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;/build&gt; &lt;/project&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>1. application.properties</strong></span></p><pre class="brush:as3;">spring.jpa.hibernate.ddl-auto=create #spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true #logging.level.org.hibernate.SQL=debug logging.level.org.hibernate.type.descriptor.sql=trace</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>2.Account</strong></span></p><pre class="brush:as3;">import org.springframework.hateoas.RepresentationModel; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Table(name=&quot;accounts&quot;) @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Builder public class Account extends RepresentationModel&lt;Account&gt;{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(length =20, nullable=false, unique=true) private String accountNumber; private float balance; public Account(String accountNumber, float balance) { this.accountNumber = accountNumber; this.balance = balance; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>3.AccountNotFoundException</strong></span></p><pre class="brush:as3;">package com.example.demo; public class AccountNotFoundException extends Exception { } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>4.AccountRepository</strong></span></p><pre class="brush:as3;">package com.example.demo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; public interface AccountRepository extends JpaRepository&lt;Account, Integer&gt; { @Query(&quot;UPDATE Account a SET a.balance= a.balance + :amount WHERE a.id =:id&quot;) @Modifying public void updateBalance(float amount, Integer id); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>5.AccountService</strong></span></p><pre class="brush:as3;">package com.example.demo; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Transactional public class AccountService { private AccountRepository repo; public AccountService(AccountRepository repo) { this.repo=repo; } public List&lt;Account&gt; listAll(){ return repo.findAll(); } public Account get(Integer id) { return repo.findById(id).get(); } public Account save(Account account) { return repo.save(account); } /** 입금 */ public Account deposit(float amount, Integer id) { repo.updateBalance(-amount, id); return repo.findById(id).get(); } /** 출금 */ public Account withdraw(float amount, Integer id) { repo.updateBalance(-amount, id); return repo.findById(id).get(); } public void delete(Integer id) throws AccountNotFoundException { if(!repo.existsById(id)) { throw new AccountNotFoundException(); } repo.deleteById(id); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>6.Amount</strong></span></p><pre class="brush:as3;">import lombok.Getter; import lombok.Setter; @Getter @Setter public class Amount { private float amout; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>7.DatabaseLoader</strong></span></p><pre class="brush:as3;">package com.example.demo; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component public class DatabaseLoader { private AccountRepository repo; public DatabaseLoader(AccountRepository repo) { this.repo=repo; } @Bean public CommandLineRunner initDatabase() { return args -&gt;{ Account account1=new Account(&quot;1234567891&quot;, 100); Account account2=new Account(&quot;1002003009&quot;, 50); Account account3=new Account(&quot;1223565893&quot;, 1000); //repo.saveAll(List.of(account1, account2, account3)); repo.save(account1); repo.save(account2); repo.save(account3); System.out.println(&quot;Sample database initialized&quot;); }; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>8.AccountModelAssember</strong></span></p><pre class="brush:as3;">package com.example.demo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.server.RepresentationModelAssembler; @Configuration public class AccountModelAssember implements RepresentationModelAssembler&lt;Account, EntityModel&lt;Account&gt;&gt; { @Override public EntityModel&lt;Account&gt; toModel(Account account) { EntityModel&lt;Account&gt; accountModel=EntityModel.of(account); accountModel.add(linkTo(methodOn(AccountApi.class).getOne(account.getId())).withSelfRel()); accountModel.add(linkTo(methodOn(AccountApi.class).deposit(account.getId(), null)).withRel(&quot;deposits&quot;)); accountModel.add(linkTo(methodOn(AccountApi.class).withdraw(account.getId(), null)).withRel(&quot;withdrawals&quot;)); accountModel.add(linkTo(methodOn(AccountApi.class).listAll()).withRel(IanaLinkRelations.COLLECTION)); return accountModel; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>9.AccountApi</strong></span></p><pre class="brush:as3;">package com.example.demo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; import java.util.List; import java.util.NoSuchElementException; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; //@RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RestController @RequestMapping(value=&quot;/api/accounts&quot;, produces=MediaTypes.HAL_JSON_VALUE) //@RequestMapping(value=&quot;/api/accounts&quot;) public class AccountApi { private AccountService service; private AccountModelAssember assember; public AccountApi(AccountService service, AccountModelAssember assember) { this.service=service; this.assember=assember; } @GetMapping public ResponseEntity&lt;?&gt; listAll(){ List&lt;Account&gt; listAccounts =service.listAll(); if(listAccounts.isEmpty()) { return ResponseEntity.noContent().build(); } /** List&lt;EntityModel&lt;Account&gt;&gt; accountModel =listAccounts.stream().map(assember::toModel).collect(Collectors.toList()); CollectionModel&lt;EntityModel&lt;Account&gt;&gt; collectionModel=CollectionModel.of(accountModel); //컬렉션에 링크 추가 collectionModel.add(linkTo(methodOn(AccountApi.class).listAll()).withSelfRel()); assember 에 내장된 toCollectionModel 을 사용 ==&gt; */ CollectionModel&lt;EntityModel&lt;Account&gt;&gt; collectionModel=assember.toCollectionModel(listAccounts); return ResponseEntity.status(HttpStatus.OK).body(collectionModel); } //return new ResponseEntity&lt;&gt;(collectionModel, HttpStatus.OK); /** * ====&gt;출력 { &quot;_embedded&quot;: { &quot;accountList&quot;: [ { &quot;id&quot;: 1, &quot;accountNumber&quot;: &quot;1234567891&quot;, &quot;balance&quot;: 100.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/1&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } }, { &quot;id&quot;: 2, &quot;accountNumber&quot;: &quot;1002003009&quot;, &quot;balance&quot;: 50.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/2&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } }, { &quot;id&quot;: 3, &quot;accountNumber&quot;: &quot;1223565893&quot;, &quot;balance&quot;: 1000.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/3&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } ] }, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } */ @GetMapping(&quot;/{id}&quot;) public ResponseEntity&lt;?&gt; getOne(@PathVariable(&quot;id&quot;) Integer id) { try { Account account=service.get(id); EntityModel&lt;Account&gt; accountModel=assember.toModel(account); return ResponseEntity.status(HttpStatus.OK).body(accountModel); }catch (NoSuchElementException ex) { return ResponseEntity.notFound().build(); } } /** * ===&gt;출력 * * { &quot;id&quot;: 1, &quot;accountNumber&quot;: &quot;1234567891&quot;, &quot;balance&quot;: 100.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/1&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } */ /** @PostMapping(&quot;/{id}&quot;) public ResponseEntity&lt;?&gt; createEvent(@PathVariable(&quot;id&quot;) Integer id){ Account account =service.get(id); URI createdUri = linkTo(AccountApi.class).slash(id).toUri(); System.out.println(&quot; createUri : &quot; + createdUri); return ResponseEntity.created(createdUri).body(account); } */ @PostMapping public ResponseEntity&lt;?&gt; add(@RequestBody Account account){ Account savedAccount =service.save(account); EntityModel&lt;Account&gt; accountModel=assember.toModel(account); return ResponseEntity.created(linkTo(methodOn(AccountApi.class).getOne(savedAccount.getId())).toUri()).body(accountModel); } /**==&gt; 출력 * { &quot;id&quot;: 8, &quot;accountNumber&quot;: &quot;987654321&quot;, &quot;balance&quot;: 2950.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/8&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } */ @PutMapping public ResponseEntity&lt;?&gt; replace(@RequestBody Account account){ Account updatedAccount =service.save(account); EntityModel&lt;Account&gt; accountModel=assember.toModel(updatedAccount); return ResponseEntity.status(HttpStatus.OK).body(accountModel); } /** * ==&gt; * * { &quot;id&quot;:1, &quot;accountNumber&quot;:&quot;1234567891&quot;, &quot;balance&quot; : 999 } 출력=&gt; { &quot;id&quot;: 1, &quot;accountNumber&quot;: &quot;1234567891&quot;, &quot;balance&quot;: 999.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/1&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } */ @PatchMapping(&quot;/{id}/deposit&quot;) public ResponseEntity&lt;?&gt; deposit(@PathVariable(&quot;id&quot;) Integer id, @RequestBody Amount amount){ Account updatedAccount=service.deposit(amount.getAmout(), id); EntityModel&lt;Account&gt; accountModel=assember.toModel(updatedAccount); return ResponseEntity.status(HttpStatus.OK).body(accountModel); } @PatchMapping(&quot;/{id}/withdraw&quot;) public ResponseEntity&lt;?&gt; withdraw(@PathVariable(&quot;id&quot;) Integer id, @RequestBody Amount amount){ Account updatedAccount=service.withdraw(amount.getAmout(), id); EntityModel&lt;Account&gt; accountModel=assember.toModel(updatedAccount); return ResponseEntity.status(HttpStatus.OK).body(accountModel); } /** * { &quot;id&quot;: 3, &quot;accountNumber&quot;: &quot;1223565893&quot;, &quot;balance&quot;: 1000.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/3&quot; }, &quot;deposits&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/3/deposit&quot; }, &quot;withdrawals&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/3/withdraw&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } * */ @DeleteMapping(&quot;/{id}&quot;) public ResponseEntity&lt;?&gt; delete(@PathVariable(&quot;id&quot;) Integer id){ try { service.delete(id); return ResponseEntity.noContent().build(); } catch (Exception e) { return ResponseEntity.notFound().build(); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Code REST API for Create Operation</strong><br /><strong>Request URI</strong>: /api/accounts<br />Add new account information<br />HTTP request method: POST<br />Request body: JSON document represents account information<br />Response status code:<br /><span style="color:#c0392b"><strong><em>201 Created for successful creatio</em>n</strong></span><br />Response body: JSON document of newly added account</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="536" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdEMN9itaIwgpH8Xx14LNpt52yEcQrTAa0gn8MuYJb3Sez3nY6MJGV2V9Va6n7Zsz0JzNLZFA-cz1jtRwsFBG4nKGBL1oJ01XUKFqHUik7B0AaIuo7teGDbTy1HSeAn0Mc4Usa9NcdujwH-NvsAnFgtYubAt8tcT-RGeZWWPZOBBJfb6zPWQsAl4dphg/s792/2023-05-07%2021%2041%2017.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @PostMapping public ResponseEntity&lt;?&gt; add(@RequestBody Account account){ Account savedAccount =service.save(account); savedAccount.add(linkTo(methodOn(AccountApi.class).getOne(account.getId())).withSelfRel()); savedAccount.add(linkTo(methodOn(AccountApi.class).listAll()).withRel(IanaLinkRelations.COLLECTION)); return ResponseEntity.created(linkTo(methodOn(AccountApi.class).getOne(savedAccount.getId())).toUri()).body(savedAccount); } /**==&gt; 출력 * { &quot;id&quot;: 8, &quot;accountNumber&quot;: &quot;987654321&quot;, &quot;balance&quot;: 2950.0, &quot;_links&quot;: { &quot;self&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts/8&quot; }, &quot;collection&quot;: { &quot;href&quot;: &quot;http://localhost:8080/api/accounts&quot; } } } */ </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-05-07 21:05:23 이클립스 eclipse sts툴 java / html / javascript / jsp 자동완성 http://macaronics.net/index.php/m05/computer/view/2127 2127 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://ondolroom.tistory.com/480">이클립스 eclipse sts툴 java / html / javascript / jsp 자동완성</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://ondolroom.tistory.com/480">https://ondolroom.tistory.com/480</a></p> 2023-04-27 08:01:03 스프링 dataSource 설정, HikraiCP 옵션 설정 http://macaronics.net/index.php/m01/spring/view/2126 2126 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>autoCommit</strong></span></span></p><p>이 속성은 풀에서 반환된 연결의 기본 자동 커밋 동작을 제어합니다.&nbsp;부울 값입니다.&nbsp;기본값: 참</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>connectionTimeout&nbsp;&nbsp;</strong></span></span>접속 시간 초과</p><p>이 속성은 클라이언트(귀하)가 풀에서 연결을 기다리는 최대 시간(밀리초)을 제어합니다.&nbsp;연결을 사용할 수 없는 상태에서 이 시간을 초과하면 SQLException이 발생합니다.&nbsp;허용 가능한 최저 연결 제한 시간은 250ms입니다.&nbsp;기본값: 30000(30초)</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>idleTimeout&nbsp; </strong></span></span>유휴 시간 초과</p><p>이 속성은 연결이 풀에서 유휴 상태로 있을 수 있는 최대 시간을 제어합니다.&nbsp;이 설정은 minimumIdle이 maximumPoolSize 미만으로 정의된 경우에만 적용됩니다.&nbsp;풀이 minimumIdle 연결에 도달하면 유휴 연결이 만료되지 않습니다.&nbsp;연결이 유휴 상태로 종료되는지 여부는 최대 +30초의 변동과 +15초의 평균 변동이 적용됩니다.&nbsp;이 시간 초과 전에는 연결이 유휴 상태로 만료되지 않습니다.&nbsp;값 0은 유휴 연결이 풀에서 제거되지 않음을 의미합니다.&nbsp;최소 허용 값은 10000ms(10초)입니다.&nbsp;기본값: 600000(10분)</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>maxLifetime&nbsp; </strong></span></span>최대 수명</p><p>이 속성은 풀에서 연결의 최대 수명을 제어합니다.&nbsp;사용 중인 연결은 절대 폐기되지 않으며 닫힐 때만 제거됩니다.&nbsp;풀에서 대량 소멸을 방지하기 위해 연결별로 약간의 음수 감쇠가 적용됩니다.&nbsp;이 값을 설정하는 것이 좋습니다. 연결 시간 제한이 있는 데이터베이스 또는 인프라보다 몇 초 더 짧아야 합니다.&nbsp;값 0은 물론 idleTimeout 설정에 따라 최대 수명(무한 수명)이 없음을 나타냅니다.&nbsp;기본값: 1800000(30분)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:20px">connectionTestQuery</span> </strong></span>연결 테스트 쿼리</p><p>드라이버가 JDBC4를 지원하는 경우 이 속성을 설정하지 않는 것이 좋습니다.&nbsp;이는 JDBC4 Connection.isValid() API를 지원하지 않는 &quot;레거시&quot; 드라이버용입니다.&nbsp;이것은 데이터베이스에 대한 연결이 여전히 활성 상태인지 확인하기 위해 풀에서 연결이 제공되기 직전에 실행되는 쿼리입니다.&nbsp;다시 말하지만, 이 속성 없이 풀을 실행해 보십시오. HikariCP는 드라이버가 JDBC4와 호환되지 않는 경우 오류를 기록하여 알려줍니다.&nbsp;기본값: 없음</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>minimumIdle&nbsp; </strong></span></span>최소유휴</p><p>이 속성은 HikariCP가 풀에서 유지하려는 최소 유휴 연결 수를 제어합니다.&nbsp;유휴 연결이 이 값 아래로 떨어지고 풀의 총 연결이 maximumPoolSize 미만인 경우 HikariCP는 추가 연결을 빠르고 효율적으로 추가하기 위해 최선을 다합니다.&nbsp;그러나 최대 성능과 스파이크 요구에 대한 응답성을 위해 이 값을 설정하지 않고 대신 HikariCP가 고정 크기 연결 풀로 작동하도록 허용하는 것이 좋습니다.&nbsp;기본값: maximumPoolSize와 동일</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#c0392b">maximumPoolSize&nbsp; &nbsp;</span></span></strong>최대 풀 크기</p><p>이 속성은 유휴 및 사용 중인 연결을 모두 포함하여 풀이 도달할 수 있는 최대 크기를 제어합니다.&nbsp;기본적으로 이 값은 데이터베이스 백엔드에 대한 실제 연결의 최대 수를 결정합니다.&nbsp;이에 대한 합리적인 값은 실행 환경에 따라 가장 잘 결정됩니다.&nbsp;풀이 이 크기에 도달하고 사용 가능한 유휴 연결이 없으면 getConnection()에 대한 호출이 시간 초과되기 전에 최대 connectionTimeout 밀리초 동안 차단됩니다.&nbsp;수영장 크기 조정에 대해 읽으십시오.&nbsp;기본값: 10</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>metricRegistry&nbsp; &nbsp;</strong></span></span>메트릭 레지스트리</p><p>이 속성은 프로그래밍 구성 또는 IoC 컨테이너를 통해서만 사용할 수 있습니다.&nbsp;이 속성을 사용하면 풀에서 다양한 메트릭을 기록하는 데 사용할 Codahale/Dropwizard MetricRegistry의 인스턴스를 지정할 수 있습니다.&nbsp;자세한 내용은 메트릭 위키 페이지를 참조하세요.&nbsp;기본값: 없음</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>healthCheckRegistry&nbsp; &nbsp;</strong></span></span>건강체크레지스트리</p><p>이 속성은 프로그래밍 구성 또는 IoC 컨테이너를 통해서만 사용할 수 있습니다.&nbsp;이 속성을 사용하면 현재 상태 정보를 보고하기 위해 풀에서 사용할 Codahale/Dropwizard HealthCheckRegistry의 인스턴스를 지정할 수 있습니다.&nbsp;자세한 내용은 상태 확인 위키 페이지를 참조하세요.&nbsp;기본값: 없음</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">poolName&nbsp; </span></strong></span>풀 이름</p><p>이 등록 정보는 연결 풀에 대한 사용자 정의 이름을 나타내며 풀 및 풀 구성을 식별하기 위해 주로 로깅 및 JMX 관리 콘솔에 나타납니다.&nbsp;기본값: 자동 생성</p><p><br /><br />&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:20px">prepStmtCacheSize</span></span></strong></p><p>이는 MySQL 드라이버가 연결당 캐시할 준비된 명령문의 수를 설정합니다.&nbsp;기본값은 보수적인 25입니다. 250-500 사이로 설정하는 것이 좋습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:20px">prepStmtCacheSqlLimit</span></span></strong></p><p>이것은 드라이버가 캐시할 준비된 SQL 문의 최대 길이입니다.&nbsp;MySQL 기본값은 256입니다. 경험상, 특히 Hibernate와 같은 ORM 프레임워크에서 이 기본값은 생성된 명령문 길이의 임계값보다 훨씬 낮습니다.&nbsp;권장 설정은 2048입니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:20px">cachePrepStmts</span></span></strong></p><p>기본적으로 캐시가 실제로 비활성화된 경우 위의 매개변수 중 어느 것도 영향을 미치지 않습니다.&nbsp;이 매개변수를 true로 설정해야 합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:20px">useServerPrepStmts</span></span></strong></p><p>최신 버전의 MySQL은 서버 측 준비된 명령문을 지원하므로 상당한 성능 향상을 제공할 수 있습니다.&nbsp;이 속성을 true로 설정합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>HikariCP의 일반적인 MySQL 구성</strong></span></span></p><pre class="brush:as3;">jdbcUrl=jdbc:mysql://localhost:3306/simpsons username=test password=test dataSource.cachePrepStmts=true dataSource.prepStmtCacheSize=250 dataSource.prepStmtCacheSqlLimit=2048 dataSource.useServerPrepStmts=true dataSource.useLocalSessionState=true dataSource.rewriteBatchedStatements=true dataSource.cacheResultSetMetadata=true dataSource.cacheServerConfiguration=true dataSource.elideSetAutoCommits=true dataSource.maintainTimeStats=false</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>HikariCP Datasource 설정 부분</strong></span></span></p><ul><li>기본적으로&nbsp;Datasource를 이용한 설정 방법</li><li>Springboot v2.x 이상부터 tomcat jdbc가 아닌 hikariCP 기본으로 지원</li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>스프링부트 지원 properties를 이용한 설정 방법</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">########################################################################## ############### Hikari -Setting ########################################## spring.datasource.hikari.minimumIdle=2 spring.datasource.hikari.maximumPoolSize=10 spring.datasource.hikari.idleTimeout=120000 spring.datasource.hikari.data-source-properties.cachePrepStmts=true spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250 spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048 spring.datasource.hikari.data-source-properties.useServerPrepStmts=true #Mysql spring.datasource.hikari.connectionTestQuery=select 1 from dual spring.datasource.hikari.connectionTimeout=300000 spring.datasource.hikari.leakDetectionThreshold=300000 #Oracle #spring.datasource.hikari.connectionTestQuery=select 1 from sys.dual #################### Hikari -Setting ###################################### ############################################################################</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Bean @ConfigurationProperties(prefix=&quot;spring.datasource.hikari&quot;) public HikariConfig hikariConfig() { return new HikariConfig(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>java 설정 방법</strong></span></span></p><pre class="brush:as3;">@Bean public HikariConfig hikariConfig() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(jdbcUrl); hikariConfig.setUsername(username); hikariConfig.setPassword(password); hikariConfig.setMaximumPoolSize(10); hikariConfig.addDataSourceProperty(&quot;cachePrepStmts&quot;, &quot;true&quot;); hikariConfig.addDataSourceProperty(&quot;prepStmtCacheSize&quot;, &quot;250&quot;); hikariConfig.addDataSourceProperty(&quot;prepStmtCacheSqlLimit&quot;, &quot;2048&quot;); hikariConfig.addDataSourceProperty(&quot;useServerPrepStmts&quot;, &quot;true&quot;); return hikariConfig; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>xml 설정 방법</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;bean id=&quot;hikariConfig&quot; class=&quot;com.zaxxer.hikari.HikariConfig&quot;&gt; &lt;property name=&quot;driverClassName&quot; value=&quot;${driver}&quot; /&gt; &lt;property name=&quot;jdbcUrl&quot; value=&quot;${url}&quot; /&gt; &lt;property name=&quot;username&quot; value=&quot;${user}&quot; /&gt; &lt;property name=&quot;password&quot; value=&quot;${password}&quot; /&gt; &lt;property name=&quot;minimumIdle&quot; value=&quot;5&quot; /&gt; &lt;property name=&quot;maximumPoolSize&quot; value=&quot;10&quot; /&gt; &lt;property name=&quot;connectionTestQuery&quot; value=&quot;select 1 from sys.dual&quot; /&gt; &lt;!-- 5분마다 요청 --&gt; &lt;property name=&quot;connectionTimeout&quot; value=&quot;300000&quot; /&gt; &lt;property name=&quot;leakDetectionThreshold&quot; value=&quot;300000&quot; /&gt; &lt;/bean&gt; &lt;!-- connectionTimeout=30 000 (30초), connectionTimeout=60 000 (60초) 이면 실제 90초 마다 idle connection 이 갱신됨 --&gt; &lt;bean id=&quot;dataSource&quot; class=&quot;com.zaxxer.hikari.HikariDataSource&quot; destroy-method=&quot;close&quot;&gt; &lt;constructor-arg ref=&quot;hikariConfig&quot; /&gt; &lt;/bean&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>HikariCP + MySQL 설정 시</p><ul><li>출처: https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration</li></ul><p>MySQL 권장 설정 일부</p><p>&nbsp;</p> 2023-04-20 13:12:08 ★ 스프링 구글로그인 처리, Sns google 로그인 연동 http://macaronics.net/index.php/m01/spring/view/2125 2125 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>0. 구글 개발자 설정</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://smpark1020.tistory.com/203">[SpringSecurity] 구글 로그인 연동하기 1 - 구글 서비스 등록</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>1. GoogleOAuthRequest</strong></span></span></p><pre class="brush:as3;">import lombok.Builder; import lombok.Data; @Data @Builder public class GoogleOAuthRequest { private String redirectUri; private String clientId; private String clientSecret; private String code; private String responseType; private String scope; private String accessType; private String grantType; private String state; private String includeGrantedScopes; private String loginHint; private String prompt; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>2.&nbsp;GoogleOAuthResponse</strong></span></span></p><pre class="brush:as3;">import lombok.Data; @Data public class GoogleOAuthResponse { private String accessToken; private String expiresIn; private String refreshToken; private String scope; private String tokenType; private String idToken; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">3. GoolgeVO</span></strong></span></p><pre class="brush:as3;">import lombok.Data; import lombok.ToString; /** * iss=https://accounts.google.com, azp=65465-scmetkg9rf7g0anodi1g4rfp434f81c7rfu.apps.googleusercontent.com, aud=2383873471124-scmetkg9rf7g0ano5di431g24rf32pf81c7rfu.apps.googleusercontent.com, sub=1012804324239237390221439, email=honggidong@gmail.com, email_verified=true, at_hash=Dv79dcE0q6Ydsadn2uxR5FJpHw, name=홍길동, picture=https://lh3.googleusercontent.com/a/AGNmyxZ-H_5vaMlSUoYZpdXFd134nFl63lJ-gCsJ7icmj3sA=s96-c, given_name=길동, family_name=홍, locale=ko, iat=16818033226, exp=16813806826, alg=RS4256, kid=9697180428796829a92372e7949d1a9fff14231cd61b1e3, typ=JWT * */ @Data @ToString public class GoolgeVO { private String id; private String email; private String name; private String picture ; private String given_name; private String family_name; private String oauth; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>4.GoogleOAuthURL</strong></span></span></p><pre class="brush:as3;"> public class GoogleOAuthURL { final static String GOOGLE_AUTH_BASE_URL = &quot;https://accounts.google.com/o/oauth2/v2/auth&quot;; final static String GOOGLE_TOKEN_BASE_URL = &quot;https://oauth2.googleapis.com/token&quot;; final static String GOOGLE_REVOKE_TOKEN_BASE_URL = &quot;https://oauth2.googleapis.com/revoke&quot;; ///login/oauth2/code/google final static String REDIRECT_URI = &quot;http://localhost:8080/login/google/auth&quot;; final static String CLIENT_ID = &quot;클라이언트 아이디&quot;; final static String CLIENT_SECRET = &quot;클라이언트 시크릿&quot;; public static String url(){ StringBuffer sb=new StringBuffer(); sb.append(GOOGLE_AUTH_BASE_URL); sb.append(&quot;?client_id=&quot;); sb.append(CLIENT_ID); sb.append(&quot;&amp;redirect_uri=&quot;); sb.append(REDIRECT_URI); sb.append(&quot;&amp;response_type=code&quot;); sb.append(&quot;&amp;scope=email profile openid https://www.googleapis.com/auth/drive.file&quot;); sb.append(&quot;&amp;access_type=offline&quot;); return sb.toString(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>5.GoogleOAuthController</strong></span></span></p><pre class="brush:as3;"> import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.korea.webtoon.dao.MemberDAO; @Controller @RequestMapping(&quot;/login/google/&quot;) public class GoogleOAuthController { @Autowired private MemberDAO memberDao; /** 로그인페이지 로그인 첫 화면 요청 메소드 */ @RequestMapping(value = &quot;login.do&quot;, method = { RequestMethod.GET, RequestMethod.POST }) public String join( Model model) { model.addAttribute(&quot;urlGoogle&quot;,GoogleOAuthURL.url()); return &quot;oauth/google/login&quot;; } /** * Authentication Code를 전달 받는 엔드포인트 **/ @GetMapping(&quot;auth&quot;) public String googleAuth(Model model, @RequestParam(value = &quot;code&quot;) String authCode) throws JsonProcessingException { // HTTP Request를 위한 RestTemplate RestTemplate restTemplate = new RestTemplate(); // Google OAuth Access Token 요청을 위한 파라미터 세팅 GoogleOAuthRequest googleOAuthRequestParam = GoogleOAuthRequest.builder().clientId(GoogleOAuthURL.CLIENT_ID) .clientSecret(GoogleOAuthURL.CLIENT_SECRET).code(authCode).redirectUri(&quot;http://localhost:8080/login/google/auth&quot;) .grantType(&quot;authorization_code&quot;).build(); // JSON 파싱을 위한 기본값 세팅 // 요청시 파라미터는 스네이크 케이스로 세팅되므로 Object mapper에 미리 설정해준다. ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); mapper.setSerializationInclusion(Include.NON_NULL); // AccessToken 발급 요청 ResponseEntity&lt;String&gt; resultEntity = restTemplate.postForEntity(GoogleOAuthURL.GOOGLE_TOKEN_BASE_URL, googleOAuthRequestParam, String.class); // Token Request GoogleOAuthResponse result = mapper.readValue(resultEntity.getBody(), new TypeReference&lt;GoogleOAuthResponse&gt;() {}); // ID Token만 추출 (사용자의 정보는 jwt로 인코딩 되어있다) String jwtToken = result.getIdToken(); String requestUrl = UriComponentsBuilder.fromHttpUrl(&quot;https://oauth2.googleapis.com/tokeninfo&quot;) .queryParam(&quot;id_token&quot;, jwtToken).encode().toUriString(); String resultJson = restTemplate.getForObject(requestUrl, String.class); Map&lt;String, String&gt; userInfo = mapper.readValue(resultJson, new TypeReference&lt;Map&lt;String, String&gt;&gt;() {}); System.out.println(&quot;userInfo 정보 &quot; +userInfo.toString()); GoolgeVO googleVO=new GoolgeVO(); googleVO.setId(&quot;google_&quot;+userInfo.get(&quot;kid&quot;)); googleVO.setEmail(userInfo.get(&quot;email&quot;)); googleVO.setName(userInfo.get(&quot;name&quot;)); googleVO.setPicture(userInfo.get(&quot;picture&quot;)); googleVO.setOauth(&quot;google&quot;); //DB 에 저장 및 업데이트 처리 memberDao.saveOauthGoole(googleVO); model.addAllAttributes(userInfo); model.addAttribute(&quot;token&quot;, result.getAccessToken()); System.out.println(userInfo); return &quot;oauth/google/loginSuccess&quot;; } /** * 토큰 무효화 **/ @GetMapping(&quot;revoke/token&quot;) @ResponseBody public Map&lt;String, String&gt; revokeToken(@RequestParam(value = &quot;token&quot;) String token) throws JsonProcessingException { Map&lt;String, String&gt; result = new HashMap&lt;&gt;(); RestTemplate restTemplate = new RestTemplate(); final String requestUrl = UriComponentsBuilder.fromHttpUrl(GoogleOAuthURL.GOOGLE_REVOKE_TOKEN_BASE_URL) .queryParam(&quot;token&quot;, token).encode().toUriString(); System.out.println(&quot;TOKEN ? &quot; + token); String resultJson = restTemplate.postForObject(requestUrl, null, String.class); result.put(&quot;result&quot;, &quot;success&quot;); result.put(&quot;resultJson&quot;, resultJson); return result; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>6.login.jsp</strong></span></span></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt; &lt;title&gt;google OAuth&lt;/title&gt; &lt;style type=&quot;text/css&quot;&gt; #container{width:500px} .sns_join a.google{border-color:#eb6155} .sns_join a{display:block;margin-top:10px;width:100%;height:50px;border:1px solid #000;font-size:15px;line-height:50px;text-align:center;background:#fff} .sns_join a.google&gt;.icon{background:url(https://cdn.jsdelivr.net/gh/braverokmc79/ouath2-img@v1.0.0/images/icon_google.png) no-repeat 0 0;background-size:18px auto} .sns_join a&gt;.icon{display:inline-block;margin:0 auto;padding-left:29px;width:188px;color:#666;font-size:15px;letter-spacing:-1px;line-height:20px;text-align:left} &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div id=&quot;container&quot;&gt; &lt;div class=&quot;sns_join&quot;&gt; &lt;a class=&quot;google&quot; href=&quot;${urlGoogle}&quot; id=&quot;googleLoginBtn&quot;&gt; &lt;span class=&quot;icon&quot;&gt;구글 로그인&lt;/span&gt; &lt;/a&gt; &lt;/div&gt; &lt;/div&gt; &lt;/body&gt; &lt;script&gt; const onClickGoogleLogin = (e) =&gt; { window.location.replace(&#39;${src}&#39;) } const googleLoginBtn = document.getElementById(&quot;googleLoginBtn&quot;); googleLoginBtn.addEventListener(&quot;click&quot;, onClickGoogleLogin) &lt;/script&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>7.loginSuccess.jsp</strong></span></span></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt; &lt;title&gt;Insert title here&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;h1&gt;Google Login 완료&lt;/h1&gt; &lt;div&gt;${token}&lt;/div&gt; &lt;div&gt;${email}&lt;/div&gt; &lt;div&gt; &lt;img src=&quot;${picture}&quot;&gt;&lt;/img&gt; &lt;/div&gt; &lt;div&gt; &lt;a href=&quot;/&quot;&gt;홈&lt;/a&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>db 저장 참조</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;insert id=&quot;saveOauthGoole&quot;&gt; MERGE INTO WEBTOON_USER USING DUAL ON (EMAIL = #{email} ) WHEN MATCHED THEN UPDATE SET OAUTH = #{oauth} WHEN NOT MATCHED THEN INSERT (USER_IDX, NAME, ID, EMAIL, OAUTH) VALUES( seq_user_idx.nextVal, #{name}, #{id}, #{email}, #{oauth} ) &lt;/insert&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><a href="https://macaronics.net/m01/spring/view/2124#">스프링 네이버 로그인 처리, Sns Naver 연동</a><a target="_blank" href="https://macaronics.net/m01/spring/view/2124">&nbsp;</a></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-17 17:07:01 ★ 스프링 네이버 로그인 처리, sns naver 연동 http://macaronics.net/index.php/m01/spring/view/2124 2124 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:20px">0. 네이버 개발자 설정</span></span></strong></p><p><a target="_blank" href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=wit_dgt&amp;logNo=221720763979">&nbsp;[SNS로그인 네이버 연동] 네아로 설정하는 방법</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>다음 네이버 api 사이트에서 js + html 로</p><p><a href="https://developers.naver.com/docs/login/api/api.md">https://developers.naver.com/docs/login/api/api.md</a></p><p>&nbsp;</p><p>1. APIExamNaverLogin.html&nbsp; 과&nbsp; &nbsp;callback.html&nbsp; 의 두페이지만 구현하면&nbsp; 끝이난다.</p><p>혹은&nbsp; naverlogin.jsp&nbsp; 와&nbsp; callback.jsp 페이지 이다.</p><p>&nbsp;</p><p><strong>나머지는 부수적으로 네이버에서 데이터를 가져오는 설정 값처리이며, 무엇보다&nbsp; 중요한것이&nbsp;</strong></p><p><strong>APIExamNaverLogin.html&nbsp; 에서는 다음과 같이 간단하게&nbsp; &nbsp; 자바스크르립트로 처리후&nbsp;</strong>callback.html&nbsp;&nbsp;페이지에서 데이터를 가져올수 있으나</p><pre class="brush:as3;">&lt;script type=&quot;text/javascript&quot;&gt; var naver_id_login = new naver_id_login(&quot;YOUR_CLIENT_ID&quot;, &quot;YOUR_CALLBACK_URL&quot;); var state = naver_id_login.getUniqState(); naver_id_login.setButton(&quot;white&quot;, 2,40); naver_id_login.setDomain(&quot;YOUR_SERVICE_URL&quot;); naver_id_login.setState(state); naver_id_login.setPopup(); naver_id_login.init_naver_id_login(); &lt;/script&gt;</pre><p>&nbsp;</p><p>js 가 아닌 자바로 구현시 다음&nbsp; naver 로그인 url 값이 생략되 있기 때문에&nbsp; url 생성을 해줘야 한다.</p><p>즉, 접근토크 발급 요청 api 과정이라고 보면 된다.&nbsp;</p><pre class="brush:as3;">&lt;a href=&quot;https://nid.naver.com/oauth2.0/authorize? response_type=code &amp;client_id=O9St1pC9EAPKQRlsYeWN &amp;state=state &amp;redirect_uri=http://localhost:8081/c3t2/Naver&quot; id=&quot;naverLogin&quot;&gt; Naver 로그인 &lt;/a&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="506" src="https://chaelin1211.github.io/img/posts/inPost/naver-opensource-06.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>따라서, 스크립트는 의미없으며 주석 처리를 해도 작동 된다. 다만 로그인 버튼 디자인 처리를 위해 .</p><p>다음과 같은 코드만 넣도된다.</p><p>naver_id_login.setButton(&quot;white&quot;, 2,40);&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;div class=&quot;naverLogin&quot; id=&quot;naver_id_login&quot;&gt; &lt;a href=&quot;https://nid.naver.com/oauth2.0/authorize? response_type=code &amp;client_id=네이버clinet_id &amp;state=state &amp;redirect_uri=http://localhost:8081/naver/callback&quot; id=&quot;naverLogin&quot;&gt; Naver 로그인 &lt;/a&gt; &lt;/div&gt; &lt;script type=&quot;text/javascript&quot;&gt; /* var naver_id_login = new naver_id_login(&quot;네이버clinet_id&quot;, &quot;http://localhost:8080/naver/callback&quot;); var state = naver_id_login.getUniqState(); &nbsp; naver_id_login.setButton(&quot;white&quot;, 2,40); naver_id_login.setDomain(&quot;http://localhost:8081/c3t2&quot;); naver_id_login.setState(state); naver_id_login.setPopup(); naver_id_login.init_naver_id_login(); */ &lt;/script&gt; &lt;/div&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>1. 라이브러리</strong></span></span></p><pre class="brush:as3;"> &lt;!--네이버 로그인 라이브러리--&gt; &lt;dependency&gt; &lt;groupId&gt;com.fasterxml.jackson.core&lt;/groupId&gt; &lt;artifactId&gt;jackson-databind&lt;/artifactId&gt; &lt;version&gt;2.12.3&lt;/version&gt; &lt;/dependency&gt; &lt;!-- OAuth2.0 --&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.scribejava&lt;/groupId&gt; &lt;artifactId&gt;scribejava-core&lt;/artifactId&gt; &lt;version&gt;2.8.1&lt;/version&gt; &lt;/dependency&gt; &lt;!-- 제이슨 파싱 --&gt; &lt;dependency&gt; &lt;groupId&gt;com.googlecode.json-simple&lt;/groupId&gt; &lt;artifactId&gt;json-simple&lt;/artifactId&gt; &lt;version&gt;1.1.1&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>2.NaverVO</strong></span></span></p><pre class="brush:as3;">package com.korea.webtoon.util.oauth.naver; import lombok.Data; import lombok.ToString; /** 네이버 JSON 반환 처리 샘플값 { &quot;resultcode&quot;: &quot;00&quot;, &quot;message&quot;: &quot;success&quot;, &quot;response&quot;: { &quot;id&quot;: &quot;11601227211&quot;, &quot;nickname&quot;: &quot;sample&quot;, &quot;profile_image&quot;: &quot;https:\/\test.jpg&quot;, &quot;age&quot;: &quot;20-29&quot;, &quot;gender&quot;: &quot;M&quot;, &quot;email&quot;: &quot;test1@naver.com&quot;, &quot;name&quot;: &quot;홍길도&quot;, &quot;birthday&quot;: &quot;07-28&quot; } } */ @Data @ToString @JsonIgnoreProperties(ignoreUnknown = true) public class NaverVO { private String resultcode; private String message; private String id; private String nickname; private String profile_image; private String age; private String gender; private String email; private String name; private String birthday; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>3. NaverOAuthApi</strong></span></span></p><pre class="brush:as3;">package com.korea.webtoon.util.oauth.naver; import org.springframework.stereotype.Component; import com.github.scribejava.core.builder.api.DefaultApi20; @Component public class NaverOAuthApi extends DefaultApi20 { @Override public String getAccessTokenEndpoint() { return &quot;https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&quot;; } @Override protected String getAuthorizationBaseUrl() { return &quot;https://nid.naver.com/oauth2.0/authorize&quot;; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>4.NaverLoginBO</strong></span></span></p><pre class="brush:as3;"> import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.OAuth20Service; @Component public class NaverOAuthLoginBO { /** [[ 인증 요청문을 구성하는 파라미터]] 1.client_id: 애플리케이션 등록 후 발급받은 클라이언트 아이디 2.response_type: 인증 과정에 대한 구분값, code로 값이 고정 3.redirect_uri: 네이버 로그인 인증의 결과를 전달받을 콜백 URL(URL 인코딩) 애플리케이션을 등록할 때 Callback URL에 설정한 정보 4.state: 애플리케이션이 생성한 상태 토큰 */ private final static String CLIENT_ID = &quot;클라이언트 아이디&quot;; // 클라이언트 아이디 private final static String CLIENT_SECRET = &quot;클라이언트 시크릿&quot;; // 클라이언트 시크릿 // private final static String REDIRECT_URI = &quot;http://localhost:8080/naver/naverRedirect&quot;; private final static String SESSION_STATE = &quot;oauth_state&quot;; /** 프로필 조회 API URL */ private final static String PROFILE_API_URL = &quot;https://openapi.naver.com/v1/nid/me&quot;; private final static String NAVER_REDIRECT = &quot;/naver/naverRedirect&quot;; @Autowired private NaverOAuthApi naverOAuthApi; @Autowired private HttpServletRequest request; /** 네이버 아이디로 인증 URL 생성 Method */ public String getAuthorizationUrl(HttpSession session) { /* 세션 유효성 검증을 위하여 난수를 생성 */ String state = UUID.randomUUID().toString(); /* 생성한 난수 값을 session에 저장 */ session.setAttribute(SESSION_STATE, state); String DOMAIN = request.getScheme()+&quot;://&quot;+ request.getServerName() +&quot;:&quot;+ request.getServerPort()+request.getContextPath(); String REDIRECT_URI=DOMAIN+NAVER_REDIRECT; // REDIRECT_URI= URLEncoder.encode(REDIRECT_URI, Charset.forName(&quot;UTF-8&quot;)); /* Scribe에서 제공하는 인증 URL 생성 기능을 이용하여 네아로 인증 URL 생성 */ OAuth20Service oauthService = new ServiceBuilder().apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET) .callback(REDIRECT_URI).state(state) // 앞서 생성한 난수값을 인증 URL생성시 사용함 .build(naverOAuthApi); return oauthService.getAuthorizationUrl(); } /** 네이버아이디로 Callback 처리 및 AccessToken 획득 Method */ public OAuth2AccessToken getAccessToken(HttpSession session, String code, String state) throws IOException { String DOMAIN = request.getScheme()+&quot;://&quot;+ request.getServerName() +&quot;:&quot;+ request.getServerPort()+request.getContextPath(); String REDIRECT_URI=DOMAIN+NAVER_REDIRECT; // REDIRECT_URI= URLEncoder.encode(REDIRECT_URI, Charset.forName(&quot;UTF-8&quot;)); /* Callback으로 전달받은 세선검증용 난수값과 세션에 저장되어있는 값이 일치하는지 확인 */ String sessionState = (String) session.getAttribute(SESSION_STATE); if (StringUtils.pathEquals(sessionState, state)) { OAuth20Service oauthService = new ServiceBuilder().apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET) .callback(REDIRECT_URI).state(state).build(naverOAuthApi); /* Scribe에서 제공하는 AccessToken 획득 기능으로 네아로 Access Token을 획득 */ return oauthService.getAccessToken(code); } return null; } /** Access Token을 이용하여 네이버 사용자 프로필 API를 호출 */ public String getUserProfile(OAuth2AccessToken oauthToken) throws IOException { String DOMAIN = request.getScheme()+&quot;://&quot;+ request.getServerName() +&quot;:&quot;+ request.getServerPort()+request.getContextPath(); String REDIRECT_URI=DOMAIN+NAVER_REDIRECT; // REDIRECT_URI= URLEncoder.encode(REDIRECT_URI, Charset.forName(&quot;UTF-8&quot;)); OAuth20Service oauthService = new ServiceBuilder().apiKey(CLIENT_ID).apiSecret(CLIENT_SECRET) .callback(REDIRECT_URI).build(naverOAuthApi); OAuthRequest request = new OAuthRequest(Verb.GET, PROFILE_API_URL, oauthService); oauthService.signRequest(oauthToken, request); return request.send().getBody(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>5.NaverController</strong></span></span></p><pre class="brush:as3;"> import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.scribejava.core.model.OAuth2AccessToken; import NaverOAuthLoginBO; import NaverVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Controller @RequiredArgsConstructor @RequestMapping(&quot;/naver/&quot;) public class NaverOAuthController { private final NaverOAuthLoginBO naverLoginBO; /** urlNaver * * */ @GetMapping(&quot;urlNaver&quot;) @ResponseBody public String login(Model model, HttpSession session) throws Exception { /* 네아로 인증 URL을 생성하기 위하여 naverLoginBO클래스의 getAuthorizationUrl메소드 호출 */ String naverAuthUrl = naverLoginBO.getAuthorizationUrl(session); /* 생성한 인증 URL을 View로 전달 */ return naverAuthUrl; } /** 네이버 로그인 성공시 callback호출 메소드 */ @GetMapping(&quot;/naverRedirect&quot;) public String callbackNaver(Model model, @RequestParam String code, @RequestParam String state, HttpServletRequest rqeust) throws Exception { OAuth2AccessToken oauthToken; oauthToken = naverLoginBO.getAccessToken(rqeust.getSession(), code, state); // 로그인 사용자 정보를 읽어온다. String apiResult = naverLoginBO.getUserProfile(oauthToken); log.info(&quot;1.로그인 사용자 정보를 읽어온다 {}&quot;,apiResult ); JSONParser jsonParser = new JSONParser(); JSONObject jsonObj=(JSONObject) jsonParser.parse(apiResult); JSONObject responseObj = (JSONObject) jsonObj.get(&quot;response&quot;); // objectMapper 로 json NaverVO 객체 매핑 처리리 ObjectMapper mapper=new ObjectMapper(); NaverVO naverVO = mapper.readValue(responseObj.toJSONString(), new TypeReference&lt;NaverVO&gt;() {}); naverVO.setResultcode(jsonObj.get(&quot;resultcode&quot;).toString()); naverVO.setMessage(jsonObj.get(&quot;message&quot;).toString()); log.info(&quot;2.로그인 사용자 정보를 읽어온다 1 {}&quot;,naverVO.toString() ); // 세션에 사용자 정보 등록 rqeust.getSession().setAttribute(&quot;naverVO&quot;, naverVO); /* 네이버 로그인 성공 페이지 View 호출 */ return &quot;redirect:/&quot;; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>다음과 같이&nbsp; ajax 로&nbsp;&nbsp;urlNaver 값을 가져오는 것이&nbsp; 좋다.</strong></span></p><pre class="brush:as3;"> /** urlNaver * * */ @GetMapping(&quot;urlNaver&quot;) @ResponseBody public String login(Model model, HttpSession session) throws Exception { /* 네아로 인증 URL을 생성하기 위하여 naverLoginBO클래스의 getAuthorizationUrl메소드 호출 */ String naverAuthUrl = naverLoginBO.getAuthorizationUrl(session); /* 생성한 인증 URL을 View로 전달 */ return naverAuthUrl; } </pre><p>&nbsp;</p><pre class="brush:as3;">$(function(){ $(&quot;#modal_btn_login_submit&quot;).on(&quot;click&quot;, function(e){ const HOME=$(&quot;#HOME&quot;).val(); const member_id=$(&quot;#modal_login_id&quot;).val(); const member_passwd=$(&quot;#modal_login_passwd&quot;).val(); const rememberId=$(&quot;#modal_login_rememberId&quot;).is(&quot;:checked&quot;); console.log(&quot; 로그인 파라미터 : &quot;, member_id, member_passwd , rememberId); if(!member_id){ alert(&quot;아이디를 입력해 주세요.&quot;); $(&quot;#id&quot;).focus(); return; } if(!member_passwd){ alert(&quot;비밀번호를 입력해 주세요.&quot;); $(&quot;#member_passwd&quot;).focus(); return; } $.ajax({ type : &quot;POST&quot;, data : { member_id, member_passwd, rememberId }, url : `${HOME}/login/LoginPro`, success : function(res) { console.log(&quot;성공 :&quot;, res); if(res==&quot;admin_login&quot;){ location.href=`${HOME}/admin/admin_login`; return; } if(res.status==&quot;failed&quot;){ alert(res.msg); return; }else if(res.status==&quot;success&quot;){ //alert(&quot;로그인 성공&quot;); location.href=`${HOME}/`; } }, error:function(res){ console.log(&quot;실패 :&quot;, res); } }); }); getNaverUrl(); }); //https://nid.naver.com/oauth2.0/authorize //?response_type=code&amp;client_id=esfsefse52324234 //&amp;redirect_uri=http://localhost:8080/naver/naverRedirect&amp;state=fsdfsdewwww function getNaverUrl(){ const HOME=$(&quot;#HOME&quot;).val(); $.ajax({ type : &quot;GET&quot;, url : `${HOME}/naver/urlNaver`, success : function(res) { console.log(&quot;urlNaver :&quot; ,res); $(&quot;#naverLogin&quot;).attr(&quot;href&quot;, res); }, error:function(res){ console.log(&quot;실패 :&quot;, res); } }); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>6.login.jsp</strong></span></span></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt; &lt;title&gt;로그인&lt;/title&gt; &lt;style type=&quot;text/css&quot;&gt; #container{width:500px} .sns_join a.naver{border-color:#2db400} .sns_join a{display:block;margin-top:10px;width:100%;height:50px;border:1px solid #000;font-size:15px;line-height:50px;text-align:center;background:#fff} .sns_join a.naver&gt;.icon{background:url(https://cdn.jsdelivr.net/gh/braverokmc79/ouath2-img@v1.0.0/images/icon_naver.png) no-repeat 0 0;background-size:18px auto} .sns_join a&gt;.icon{display:inline-block;margin:0 auto;padding-left:29px;width:188px;color:#666;font-size:15px;letter-spacing:-1px;line-height:20px;text-align:left} &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div id=&quot;container&quot;&gt; &lt;div class=&quot;sns_join&quot;&gt; &lt;a class=&quot;naver&quot; href=&quot;${urlNaver}&quot; id=&quot;naver_id_login_anchor&quot;&gt; &lt;span class=&quot;icon&quot;&gt;네이버로 로그인&lt;/span&gt; &lt;/a&gt; &lt;/div&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>7.loginSuccess.jsp</strong></span></span></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt; &lt;title&gt;Insert title here&lt;/title&gt; &lt;/head&gt; &lt;body&gt; &lt;div&gt; &lt;h1&gt;환영합니다!&lt;/h1&gt; &lt;p&gt; &lt;span&gt;${naverVO.name}&lt;/span&gt;님의 로그인 성공&lt;br&gt; 이메일 주소는 &lt;strong&gt;${naverVO.email}&lt;/strong&gt;입니다. &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;a href=&quot;/&quot;&gt;홈&lt;/a&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>DB 저장&nbsp; &nbsp;mybais</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;insert id=&quot;saveOauthNaver&quot;&gt; MERGE INTO WEBTOON_USER USING DUAL ON (EMAIL = #{email} ) WHEN MATCHED THEN UPDATE SET OAUTH = #{oauth} WHEN NOT MATCHED THEN INSERT (USER_IDX, NAME, ID, EMAIL, OAUTH) VALUES( seq_user_idx.nextVal, #{name}, #{id}, #{email}, #{oauth} ) &lt;/insert&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>참조 :&nbsp;&nbsp;<a target="_blank" href="https://cobook.tistory.com/31">https://cobook.tistory.com/31</a></p><p>[Spring] 네이버 로그인 Open API</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2125">스프링 구글로그인 처리, Sns Google 로그인 연동</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-16 19:52:44 mysql 행번호 붙이는 간단한 방법 http://macaronics.net/index.php/m04/mysql/view/2123 2123 <p>&nbsp;</p><p>row_number() over(order by&nbsp; &#39;정렬하고싶은컬럼값&#39;&nbsp; desc )&nbsp; as&nbsp; num</p><p>&nbsp;</p><p>셀렉트절에 넣는다.</p><p>&nbsp;</p><pre class="brush:as3;">row_number() over(order by id desc) as num </pre><p>&nbsp;</p><p>&nbsp;</p><p>num 번호가 오름차순으로 출력</p><pre class="brush:as3;">select row_number() over(order by id desc) as num, u.* from `user` as u</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>num 번호를 내림 차순으로 출력하고 싶으면&nbsp; 정렬하고싶은컬럼값에 대해&nbsp;</p><p>&nbsp;</p><p>&nbsp; row_number()&nbsp; &nbsp; over&nbsp; 에 오름차순으로&nbsp; 한다음</p><p>&nbsp;</p><p>&nbsp;내림차순으로 하면 된다.</p><p>&nbsp;</p><p><strong>힘들게 셀렉트 절로 두번 묶어 줄 필요가 없다.</strong></p><p>&nbsp;</p><pre class="brush:as3;"> SELECT row_number() over(order by id asc) AS num, u.* FROM `user` AS u ORDER BY num DESC </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-15 15:06:17 [Developer]/Android scrcpy: 안드로이드 화면을 PC에서 미러링 하기 http://macaronics.net/index.php/m05/computer/view/2122 2122 <p>&nbsp;</p><p>&nbsp;</p><p>개요</p><p>안드로이드 기기를 가지고 있다보면 PC에서 안드로이드 화면을 보고싶은 경우가 많이 있다.</p><p>그냥 보고싶은 경우라면 앱플레이어도 괜찮은 것들이 많이 나와있고, 퍼포먼스도 괜찮은 편이지만, 최신버전을 구동하고 싶다거나, 실제 기기의 것을 활용하고 싶은 경우도 많기 때문에 그 경우라면 앱플레이어 혹은 에뮬레이터로는 그 갈증을 해소하기는 어렵다.</p><p>사실 이러한 용도보다 개인적으로는 개발자로서 필요한 이유를 강조하고 싶다.</p><p>안드로이드 개발자로서 개발을 하다보면 특히 물리적인 기기를 내가 직접 컨트롤 할수 있는 환경은 아니지만(대표적으로 원격근무 환경), 실제 기기를 컨트롤 해야 하는 도구가 있었으면 좋겠다는 생각을 하였다. 그러던 중 개발자 지인을 통해 알게된 툴이 있는데 바로 scrcpy이다.</p><p>소개</p><p>scrcpy의 이름의 의미는 strcpy에서 유래하였다. strcpy는 string을 copy하듯이, scrcpy는 screen을 copy 한다는 의미로 담았다고 한다. 즉, 안드로이드의 화면을 복사하여 띄운다는 의미이다. 특히 발음하기 어려운 이름을 찾다가 발견하였다고 한다.</p><p>이 툴은 오픈소스로 개발된 툴로 Github에 소스가 공개되어 있다.(Apache License 2.0)</p><p>또한 그 뿐 아니라 설치 과정도 여러 경로로 편하게 배포되어 있어서 실제 설치도 편하다.</p><p>일단 작동화면은 다음과 같다.</p><p>&nbsp;</p><p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDih0c%2FbtrtH0ZBtW8%2FZz3WBVZdxz0BOEjwrwHicK%2Fimg.png" data-origin-width="427" data-origin-height="600" alt="" src="https://blog.kakaocdn.net/dn/cDih0c/btrtH0ZBtW8/Zz3WBVZdxz0BOEjwrwHicK/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p><p>&nbsp;</p><p>이 툴이 지원하는 범위는 무려 앱이 잠금화면에 들어갔을 경우라도 그 상태에서 해제할 수 있는 부분까지 지원하고 있다. 기기의 상태에 따라서는 기기 자체가 리부팅 된 상황에서라도 바로 사용이 가능하다.(항상 그런것은 아니기 때문에 본인의 기기 상태를 체크하고 리부팅 하는것을 권장한다. 만일 지원하지 않는데 원격으로 리부팅하여 낭패를 본다면 아찔하다..)</p><p>지원 범위</p><p>런타임 환경</p><ul><li>Linux<ul><li>Debian, Ubuntu, Arch Linux, Fedora, Gentoo, Others..</li></ul></li><li>Windows<ul><li>Windows 10, 11까지 지원하는 것을 확인하였다. 아마도 Windows 7은 당연히 될 듯하고 그 이하는 잘 모르겠지만 중요하지는 않을듯?</li></ul></li><li>macOS<ul><li>Monterey 지원 확인. 그 이하의 버전들도 확인하였으나 정확히 어느버전까지 지원하는지는 확인하지 않음</li></ul></li></ul><p>설치 가능 루트</p><ul><li>Linux<ul><li>apt, pacman, snap, copr, ebuild, 등..</li></ul></li><li>Windows<ul><li>zip 파일 수동 다운로드 및 실행 지원(portable)</li><li>chocolatey</li><li>scoop</li></ul></li><li>macOS<ul><li>Homebrew</li><li>MacPorts</li></ul></li><li>위의 OS 모두 수동빌드가 가능하기 때문에 위의 방법이 마음에 들지 않는다면 직접 빌드해서 사용해도 된다!</li></ul><p>Android OS</p><ul><li>낮은 버전은 adb가 지원되는 한 제한없이 가능할 것이다.</li><li>최고버전은 현재 Android 12까지 되는 것을 확인하였다.(scrcpy 1.21 버전 기준)</li></ul><p>설치 및 실행 방법</p><p>이 곳에 모든 버전을 기재할 수는 없으니 위의 설치 가능 루트를 참고하여 설치하면 되며, Windows 환경에서의 chocolatey를 통한 설치 기준으로는 다음과 같이 하면 가능하다.</p><p>chocolatey 설치는 이 곳을 참고하면 된다.(&nbsp;<a href="https://chocolatey.org/install">https://chocolatey.org/install</a>&nbsp;)</p><ol><li>이 후 chocolatey에서의 진행을 원활하게 하기 위해서는 명령 프롬프트 실행시 관리자 권한으로 실행을 권장한다. cmd창에서 우클릭하여, 명령프롬프트를 다시 우클릭하면 관리자 권한으로 실행을 할 수 있다.<p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwFd7g%2FbtrtJEuHeEE%2F3kHEmawFH8zbmDoDv0GG80%2Fimg.png" data-origin-width="535" data-origin-height="463" alt="" src="https://blog.kakaocdn.net/dn/wFd7g/btrtJEuHeEE/3kHEmawFH8zbmDoDv0GG80/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p></li><li>adb 툴이 설치되어 있어야 한다. 설치되어 있는지는 Windows에서 Command Window(Window키 +&nbsp;R&nbsp;&rarr;&nbsp;cmd&nbsp;입력 및 실행) 이후 나오는 명령 프롬프트 창에서&nbsp;adb&nbsp;입력 후 엔터를 실행했을 때 아래와 같이 나와야 한다.<p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEE7OU%2FbtrtIUxPC6e%2FIh4TND2MyhAfzeTGKknyu0%2Fimg.png" data-origin-width="812" data-origin-height="438" alt="" src="https://blog.kakaocdn.net/dn/cEE7OU/btrtIUxPC6e/Ih4TND2MyhAfzeTGKknyu0/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p></li><li>만약 설치가 되어 있는것이 분명한데,&nbsp;내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.&nbsp;라고 나온다면, 시스템 환경변수에 PATH를 추가해 주면 된다. Android Studio를 설치한 피씨라면 설치는 되어있을 터이니 PATH만 지정해주어도 무방하다.</li><li>만약 설치되어 있지 않다면, 아래의 커맨드로도 설치가 가능하다. 만약 위에서 이미 설치되어 있으나 PATH 지정이 귀찮다면 아래의 명령으로 설치하는 것도 괜찮다.<br />choco install adb</li><li>그리고 마지막으로 간단하게 아래의 명령을 주면 설치가 된다.<br />choco install scrcpy이미 설치가 되어있기 때문에 이렇게 나오지만, 아니라면 Y를 눌러 설치를 진행하고 이와는 다른 화면이 나온다.<img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF5dO0%2FbtrtNRT8Udm%2F3qokAS7XqzXZRyaNPkHwN1%2Fimg.png" data-origin-width="803" data-origin-height="446" alt="" src="https://blog.kakaocdn.net/dn/cF5dO0/btrtNRT8Udm/3qokAS7XqzXZRyaNPkHwN1/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /><p>이미 설치가 되어있기 때문에 이렇게 나오지만, 아니라면 Y를 눌러 설치를 진행하고 이와는 다른 화면이 나온다.</p></li><li>부록으로.. 만약 또 기존에 설치된 버전이 있고 업그레이드를 원한다면&nbsp;choco upgrade scrcpy&nbsp;라고 입력하면 된다.</li><li>실행을 위해서는 연결하고자 하는 안드로이드 기기에 개발자옵션 - USB 디버깅을 ON 해주고, USB로 PC에 연결하고, 권한을 요청할 경우 권한 승인을 해주면 된다.</li><li>그리고, 아래의 명령을 주면 기본적으로 실행이 완료된다. ????<br />scrcpy<p><img srcset="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKK4cs%2FbtrtMKgFirG%2FnImxjzlIcnRMsCfxDzH6G1%2Fimg.png" data-origin-width="1214" data-origin-height="683" alt="" src="https://blog.kakaocdn.net/dn/bKK4cs/btrtMKgFirG/nImxjzlIcnRMsCfxDzH6G1/img.png" onerror="this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';" /></p></li></ol><p>일단, 글이 길어지므로 기본 소개는 여기에서 멈추고 추가 옵션 활용은 다음 글을 참고하면 된다.</p><p>&nbsp;</p><p><a target="_blank" rel="noopener" href="https://blog.soobinpark.com/243">(1) scrcpy 제대로 활용하기 - 사이즈, 렌더링, 캡처 등</a></p><p><a target="_blank" rel="noopener" href="https://blog.soobinpark.com/244">(2) scrcpy 제대로 활용하기 - 연결, 윈도우, 컨트롤, 파일드롭, 단축키 등</a></p><p>참고링크</p><ul><li>Scrcpy open repository:&nbsp;<a href="https://github.com/Genymobile/scrcpy">https://github.com/Genymobile/scrcpy</a></li><li>Chocolatey:&nbsp;<a href="https://chocolatey.org/">https://chocolatey.org/</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">출처 :&nbsp; &nbsp; &nbsp;<a href="https://blog.soobinpark.com/">happy 빈이 라이프스토리</a></span></strong></p><p><strong><span style="font-size:26px"><a target="_blank" href="https://blog.soobinpark.com/242">https://blog.soobinpark.com/242</a></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-15 09:53:11 스프링 기반 REST API 개발 - (백기선) - 1.REST API 및 프로젝트 소개 http://macaronics.net/index.php/m01/spring/view/2121 2121 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드] 강의입니다.</strong></p><p>다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발하는 강의입니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해</p><p>다양한 스프링 기술을 활용하여 REST API 개발</p><p>스프링 HATEOAS와 스프링 REST Docs 프로젝트 활용</p><p>테스트 주도 개발(TDD)</p><p>스프링으로 REST를 따르는 API를 만들어보자!<br />백기선의 스프링 기반 REST API 개발</p><p><img title="239527-img1-white.png" alt="" width="965" height="298" src="https://cdn.inflearn.com/public/files/courses/239527/a59979fe-29a4-481a-bdbc-a2150bccfdb6/239527-img1-white.png" /></p><p><strong>스프링 기반 REST API 개발</strong></p><p>이 강의에서는 다양한 스프링 기술을 사용하여 Self-Descriptive Message와 HATEOAS(Hypermedia as the engine of application state)를 만족하는 REST API를 개발합니다.</p><p>그런&nbsp;<strong>REST API</strong>로 괜찮은가</p><p>2017년 네이버가 주관한 개발자 컨퍼런스 Deview에서&nbsp;<a target="_blank" rel="noopener" href="https://www.youtube.com/watch?v=RP_f5dMoHFc">그런 REST API로 괜찮은가</a>라는 이응준님의 발표가 있었습니다. 현재 REST API로 불리는 대부분의 API가 실제로는 로이 필딩이 정의한 REST를 따르고 있지 않으며, 그 중에서도 특히 Self-Descriptive Message와 HATEOAS가 지켜지지 않음을 지적했고, 그에 대한 대안을 제시되었습니다.</p><p><strong>이번 강의는 해당 발표에 영감을 얻어 만들어졌습니다.</strong>&nbsp;2018년 11월에 KSUG에서 동일한 이름으로 세미나를 진행한 경험이 있습니다. 4시간이라는 짧지 않은 발표였지만, 빠르게 진행하느라 충분히 설명하지 못하고 넘어갔던 부분이 있었습니다. 내용을 더 보충하고, 또 해결하려는 문제에 대한 여러 선택지를 제공하는 것이 좋을 것 같아 이 강의를 만들게 되었습니다.<br />또한 이 강의에서는 제가 주로 사용하는&nbsp;<strong>IntelliJ 단축키</strong>도 함께 설명하고 있습니다.</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>인프런 :</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/spring_rest-api#">https://www.inflearn.com/course/spring_rest-api#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 자료 :&nbsp;&nbsp;<a target="_blank" href="https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit">https://docs.google.com/document/d/1GFo3W6XxqhxDVVqxiSEtqkuVCX93Tdb3xzINRtTIx10/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 소스 :</p><p>&nbsp;</p><p><a target="_blank" href="https://gitlab.com/whiteship/natural">https://gitlab.com/whiteship/natural</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/keesun/study">https://github.com/keesun/study</a></p><p>ksug201811restapi</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌에서는 다음의 다양한 스프링 기술을 사용하여 REST API를 개발합니다.</strong></p><ul><li><p><strong>스프링 프레임워크</strong></p></li><li><p><strong>스프링 부트</strong></p></li><li><p><strong>스프링 데이터 JPA</strong></p></li><li><p><strong>스프링 HATEOAS</strong></p></li><li><p><strong>스프링 REST Docs</strong></p></li><li><p><strong>스프링 시큐리티 OAuth2</strong></p></li></ul><p>&nbsp;</p><p><strong>또한 개발은 테스트 주도 개발(TDD)로 진행하기 때문에 평소 테스트 또는 TDD에 관심있던 개발자에게도 이번 강좌가 도움이 될 것으로 기대합니다.</strong></p><p>&nbsp;</p><p><strong>사전 학습</strong></p><ul><li><p><strong>스프링 프레임워크 핵심 기술 (필수)</strong></p></li><li><p><strong>스프링 부트 개념과 활용 (필수)</strong></p></li><li><p><strong>스프링 데이터 JPA (선택)</strong></p></li></ul><p>&nbsp;</p><p><strong>학습 목표</strong></p><ul><li><p><strong>Self-Describtive Message와 HATEOAS를 만족하는 REST API를 이해합니다.</strong></p></li><li><p><strong>다양한 스프링 기술을 활용하여 REST API를 개발할 수 있습니다.</strong></p></li><li><p><strong>스프링 HATEOAS와 스프링 REST Docs 프로젝트를 활용할 수 있습니다.</strong></p></li><li><p><strong>테스트 주도 개발(TDD)에 익숙해 집니다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>REST API 는 다음 두가지를 만족해야 한다.</strong></span></p><p><strong><span style="color:#2980b9">1) Self-Describtive Message</span></strong></p><p><strong><span style="color:#2980b9">2) HATEOAS</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>&nbsp; &nbsp; &nbsp;1. 강좌 소개</strong></span></span></p><p><strong>첫 페이지 참고</strong></p><p>&nbsp;</p><p><strong>소스 코드</strong></p><ul><li><p><strong><a href="https://github.com/keesun/study/tree/master/rest-api-with-spring">https://github.com/keesun/study/tree/master/rest-api-with-spring</a></strong></p></li></ul><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>&nbsp; &nbsp; &nbsp;2. 강사 소개</strong></span></span></p><p><strong>백기선</strong></p><ul><li><p><strong>현재 마이크로소프트 미국 본사에 근무 중. (그전에는 네이버와 아마존에서 일을 했습니다.)</strong></p></li><li><p><strong>2007년부터 개발자로 일했으며 이제 막 경력 10년이 조금 넘었네요.</strong></p></li><li><p><strong>자바, 스프링 프레임워크, JPA, 하이버네이트를 주로 공부하고 공유해 왔습니다.</strong></p></li><li><p><strong>Youtube/백기선 채널에서 코딩 관련 정보를 영상으로 공유하고 있습니다.</strong></p></li><li><p><strong>(예전에는 Whiteship.me 라는 블로그에 글도 많이 올렸지만 요즘은 잘 안써요.)</strong></p></li><li><p><strong>(더 예전에는 책도 쓰고 번역도 하고 발표도 많이 했었지만 역시나.. 요즘은 안합니다.)</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:36px">[1. REST API 및 프로젝트 소개]</span></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>3. REST API</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16409&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16409&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>API</strong></p><ul><li><p><strong>Application Programming Interface</strong></p></li></ul><p>&nbsp;</p><p><strong>REST</strong></p><ul><li><p><strong>REpresentational State Transfer</strong></p></li><li><p><strong>인터넷 상의 시스템 간의 상호 운용성(interoperability)을 제공하는 방법중 하나</strong></p></li><li><p><strong>시스템 제각각의 독립적인 진화를 보장하기 위한 방법</strong></p></li><li><p><strong>REST API: REST 아키텍처 스타일을 따르는 API</strong></p></li></ul><p>&nbsp;</p><p><strong>REST 아키텍처 스타일 (<a href="https://www.youtube.com/watch?v=RP_f5dMoHFc">발표 영상 </a>11분)</strong></p><ul><li><p><strong>Client-Server</strong></p></li><li><p><strong>Stateless</strong></p></li><li><p><strong>Cache</strong></p></li><li><p><strong>Uniform Interface</strong></p></li><li><p><strong>Layered System</strong></p></li><li><p><strong>Code-On-Demand (optional)</strong></p></li></ul><p>&nbsp;</p><p><strong>Uniform Interface (발표 영상 11분 40초)</strong></p><ul><li><p><strong>Identification of resources</strong></p></li><li><p><strong>manipulation of resources through represenations</strong></p></li><li><p><strong>self-descrive messages</strong></p></li><li><p><strong>hypermedia as the engine of appliaction state (HATEOAS)</strong></p></li></ul><p>&nbsp;</p><p><strong>두 문제를 좀 더 자세히 살펴보자. (발표 영상 37분 50초)</strong></p><ul><li><p><strong>Self-descriptive message</strong></p><ul><li><p><strong>메시지 스스로 메시지에 대한 설명이 가능해야 한다.</strong></p></li><li><p><strong>서버가 변해서 메시지가 변해도 클라이언트는 그 메시지를 보고 해석이 가능하다.</strong></p></li><li><p><strong>확장 가능한 커뮤니케이션</strong></p></li></ul></li><li><p><strong>HATEOAS</strong></p><ul><li><p><strong>하이퍼미디어(링크)를 통해 애플리케이션 상태 변화가 가능해야 한다.</strong></p></li><li><p><strong>링크 정보를 동적으로 바꿀 수 있다. (Versioning 할 필요 없이!)</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong>Self-descriptive message 해결 방법&nbsp;</strong></p><ul><li><p><strong>방법 1: 미디어 타입을 정의하고 IANA에 등록하고 그 미디어 타입을 리소스 리턴할 때 Content-Type으로 사용한다.</strong></p></li><li><p><strong>방법 2: profile 링크 헤더를 추가한다. (발표 영상 41분 50초)</strong></p><ul><li><p><strong><a href="http://test.greenbytes.de/tech/tc/httplink/">브라우저들이 아직 스팩 지원을 잘 안해</a></strong></p></li><li><p><strong>대안으로 <a href="http://stateless.co/hal_specification.html">HAL</a>의 링크 데이터에 <a href="https://tools.ietf.org/html/draft-wilde-profile-link-04">profile 링크</a> 추가</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong>HATEOAS 해결 방법&nbsp;</strong></p><ul><li><p><strong>방법1: 데이터에 링크 제공</strong></p><ul><li><p><strong>링크를 어떻게 정의할 것인가? HAL</strong></p></li></ul></li><li><p><strong>방법2: 링크 헤더나 Location을 제공</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>4. Event REST API</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16410&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16410&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>이벤트 등록, 조회 및 수정 API</strong></p><p><strong>GET /api/events</strong></p><p><strong>이벤트 목록 조회 REST API (로그인 안 한 상태)</strong></p><ul><li><p><strong>응답에 보여줘야 할 데이터</strong></p><ul><li><p><strong>이벤트 목록</strong></p></li><li><p><strong>링크</strong></p><ul><li><p><strong>self</strong></p></li><li><p><strong>profile: 이벤트 목록 조회 API 문서로 링크</strong></p></li><li><p><strong>get-an-event: 이벤트 하나 조회하는 API 링크</strong></p></li><li><p><strong>next: 다음 페이지 (optional)</strong></p></li><li><p><strong>prev: 이전 페이지 (optional)</strong></p></li></ul></li></ul></li><li><p><strong>문서?</strong></p><ul><li><p><strong>스프링 REST Docs로 만들 예정</strong></p></li></ul></li></ul><p><strong>이벤트 목록 조회 REST API (로그인 한 상태)</strong></p><ul><li><p><strong>응답에 보여줘야 할 데이터</strong></p><ul><li><p><strong>이벤트 목록</strong></p></li><li><p><strong>링크</strong></p><ul><li><p><strong>self</strong></p></li><li><p><strong>profile: 이벤트 목록 조회 API 문서로 링크</strong></p></li><li><p><strong>get-an-event: 이벤트 하나 조회하는 API 링크</strong></p></li><li><p><strong>create-new-event: 이벤트를 생성할 수 있는 API 링크</strong></p></li><li><p><strong>next: 다음 페이지 (optional)</strong></p></li><li><p><strong>prev: 이전 페이지 (optional)</strong></p></li></ul></li></ul></li><li><p><strong>로그인 한 상태???? (stateless라며..)</strong></p><ul><li><p><strong>아니, 사실은 Bearer 헤더에 유효한 AccessToken이 들어있는 경우!</strong></p></li></ul></li></ul><p><strong>POST /api/events</strong></p><ul><li><p><strong>이벤트 생성</strong></p></li></ul><p>&nbsp;</p><p><strong>GET /api/events/{id}</strong></p><ul><li><p><strong>이벤트 하나 조회</strong></p></li></ul><p>&nbsp;</p><p><strong>PUT /api/events/{id}</strong></p><ul><li><p><strong>이벤트 수정</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>5. Events API 사용 예제</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16410&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16410&amp;tab=curriculum</a></p><p>&nbsp;</p><p><a target="_blank" href="https://restlet.talend.com/downloads/current/">https://restlet.talend.com/downloads/current/</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>6. Postman &amp; Restlet</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16411&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16411&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>(토큰 없이) 이벤트 목록 조회</strong></p><ol><li><p><strong>create 안 보임</strong></p></li></ol></li><li><p><strong>access token 발급 받기 (A 사용자 로그인)</strong></p></li><li><p><strong>(유효한 A 토큰 가지고) 이벤트 목록 조회</strong></p><ol><li><p><strong>create event 보임</strong></p></li></ol></li><li><p><strong>(유효한 A 토큰 가지고) 이벤트 만들기</strong></p></li><li><p><strong>(토큰 없이) 이벤트 조회</strong></p><ol><li><p><strong>update 링크 안 보임</strong></p></li></ol></li><li><p><strong>(유효한 A 토큰 가지고) 이벤트 조회</strong></p><ol><li><p><strong>update 링크 보임</strong></p></li></ol></li><li><p><strong>access token 발급 받기 (B 사용자 로그인)</strong></p></li><li><p><strong>(유효한 B 토큰 가지고) 이벤트 조회</strong></p><ol><li><p><strong>update 안 보임</strong></p></li></ol></li></ol><p>&nbsp;</p><p><strong>REST API 테스트 클라이언트 애플리케이션</strong></p><ul><li><p><strong>크롬 플러그인</strong></p><ul><li><p><strong>Restlet</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong><img width="493" height="484" alt="" src="https://lh6.googleusercontent.com/CJfFQ5a42PTrdFTg35MnGqgMZQ-PjMxzfpPd79uKapS5pDAacFZFAUENXnXU5k07kCdJte9GS_EYgE3C-IcTmAuA4NCcZG82AiyhW9F1cGXTV5gE1BzeK4n8gQj0iqZp0jilytoN-hzeGmMLgjwNtA" /></strong></p><ul><li><p><strong>애플리케이션</strong></p><ul><li><p><strong>Postman</strong></p></li></ul></li></ul><p><strong><img width="616" height="495" alt="" src="https://lh6.googleusercontent.com/8g1IxTCgDrknrNl-V0xSwccCn0JH7f3aTDokZ-ygiG8EzyQOmh2ISHwHXQQCCYYdGUm9wCCamQMAmp2YRpwd2SXC2QxBod8sFoMLRW1bndpAOLj6KG_T1hfC3mJnKbwzomHPhgXiE5xm_kKAvgITjg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>7. Project 만들기</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16412&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16412&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>추가할 의존성</strong></p><ul><li><p><strong>Web</strong></p></li><li><p><strong>JPA</strong></p></li><li><p><strong>HATEOAS</strong></p></li><li><p><strong>REST Docs</strong></p></li><li><p><strong>H2</strong></p></li><li><p><strong>PostgreSQL</strong></p></li><li><p><strong>Lombok</strong></p></li></ul><p>&nbsp;</p><p><strong>자바 버전 11로 시작</strong></p><ul><li><p><strong><a href="https://itnext.io/java-is-still-free-c02aef8c9e04">자바는 여전히 무료다.</a></strong></p></li></ul><p>&nbsp;</p><p><strong>스프링 부트 핵심 원리</strong></p><ul><li><p><strong>의존성 설정 (pom.xml)</strong></p></li><li><p><strong>자동 설정 (@EnableAutoConfiguration)</strong></p></li><li><p><strong>내장 웹 서버 (의존성과 자동 설정의 일부)</strong></p></li><li><p><strong>독립적으로 실행 가능한 JAR (pom.xml의 플러그인)</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>pom.xml</strong></p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt; &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt; &lt;parent&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt; &lt;version&gt;3.0.5&lt;/version&gt; &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt; &lt;/parent&gt; &lt;groupId&gt;net.macaronics&lt;/groupId&gt; &lt;artifactId&gt;restapi&lt;/artifactId&gt; &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt; &lt;name&gt;restapi&lt;/name&gt; &lt;description&gt;spring boot rest-api&lt;/description&gt; &lt;properties&gt; &lt;java.version&gt;17&lt;/java.version&gt; &lt;/properties&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-hateoas&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.h2database&lt;/groupId&gt; &lt;artifactId&gt;h2&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.postgresql&lt;/groupId&gt; &lt;artifactId&gt;postgresql&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; &lt;artifactId&gt;spring-restdocs-mockmvc&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;build&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.asciidoctor&lt;/groupId&gt; &lt;artifactId&gt;asciidoctor-maven-plugin&lt;/artifactId&gt; &lt;version&gt;2.2.1&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;generate-docs&lt;/id&gt; &lt;phase&gt;prepare-package&lt;/phase&gt; &lt;goals&gt; &lt;goal&gt;process-asciidoc&lt;/goal&gt; &lt;/goals&gt; &lt;configuration&gt; &lt;backend&gt;html&lt;/backend&gt; &lt;doctype&gt;book&lt;/doctype&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.restdocs&lt;/groupId&gt; &lt;artifactId&gt;spring-restdocs-asciidoctor&lt;/artifactId&gt; &lt;version&gt;${spring-restdocs.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;/plugin&gt; &lt;plugin&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt; &lt;/plugin&gt; &lt;plugin&gt; &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt; &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt; &lt;version&gt;2.22.2&lt;/version&gt; &lt;configuration&gt; &lt;skipTests&gt;true&lt;/skipTests&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;/build&gt; &lt;/project&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>스프링부트 3.0 이상 패키지 생성 오류시&nbsp; &nbsp;다음을 추가 할것</strong></span></p><pre class="brush:as3;">&lt;plugin&gt; &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt; &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt; &lt;version&gt;2.22.2&lt;/version&gt; &lt;configuration&gt; &lt;skipTests&gt;true&lt;/skipTests&gt; &lt;/configuration&gt; &lt;/plugin&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>8. 이벤트 도메인 구현</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16413&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16413&amp;tab=curriculum</a></p><p>&nbsp;</p><pre class="brush:as3;">public class Event { private String name; private String description; private LocalDateTime beginEnrollmentDateTime; private LocalDateTime closeEnrollmentDateTime; private LocalDateTime beginEventDateTime; private LocalDateTime endEventDateTime; private String location; // (optional) 이게 없으면 온라인 모임 private int basePrice; // (optional) private int maxPrice; // (optional) private int limitOfEnrollment; } </pre><p>&nbsp;</p><p><strong>추가 필드</strong></p><pre class="brush:as3;">private Integer id; private boolean offline; private boolean free; private EventStatus eventStatus = EventStatus.DRAFT; </pre><p>&nbsp;</p><p><strong>EventStatus 이늄 추가</strong></p><pre class="brush:as3;">public enum EventStatus { DRAFT, PUBLISHED, BEGAN_ENROLLMEND, CLOSED_ENROLLMENT, STARTED, ENDED } </pre><p>&nbsp;</p><p><strong>롬복 애노테이션 추가</strong></p><p>&nbsp;</p><pre class="brush:as3;">@Getter @Setter @EqualsAndHashCode(of = &quot;id&quot;) @Builder @NoArgsConstructor @AllArgsConstructor public class Event { </pre><p>&nbsp;</p><ul><li><p><strong>왜 @EqualsAndHasCode에서 of를 사용하는가</strong></p></li><li><p><strong>왜 @Builder를 사용할 때 @AllArgsConstructor가 필요한가</strong></p></li><li><p><strong>@Data를 쓰지 않는 이유</strong></p></li><li><p><strong>애노테이션 줄일 수 없나</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>9. 이벤트 비즈니스 로직</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16414&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16414&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Event 생성 API</strong></p><ul><li><p><strong>다음의 입력 값을 받는다.</strong></p><ul><li><p><strong>name</strong></p></li><li><p><strong>description</strong></p></li><li><p><strong>beginEnrollmentDateTime</strong></p></li><li><p><strong>closeEnrollmentDateTime</strong></p></li><li><p><strong>beginEventDateTime</strong></p></li><li><p><strong>endEventDateTime</strong></p></li><li><p><strong>location (optional) 이게 없으면 온라인 모임</strong></p></li><li><p><strong>basePrice (optional)&nbsp;</strong></p></li><li><p><strong>maxPrice (optional)</strong></p></li><li><p><strong>limitOfEnrollment</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><strong>basePrice와 maxPrice 경우의 수와 각각의 로직</strong></p><p><strong><img alt="" width="622" height="270" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjiAC6DWxWTFZ72_ansWI20K20zpUKz2ellKtD1pLeFrxoacY0mAK2fCMqNGU9VukDZgbRplT0XjyWykux3g5bZdiudZWgUKSxsa4Ij9_PB-bJpVlZVXOwqF3EH7txJGMcfHaXPPrpTyo3KrgJVsROZZt5vMvDpESpG1o4JklOwfOk9MqClj_6yhCsr1g/s622/2023-05-06%2016%2001%2030.png" /></strong></p><p>&nbsp;</p><ul><li><p><strong>결과값</strong></p><ul><li><p><strong>id</strong></p></li><li><p><strong>name</strong></p></li><li><p><strong>...</strong></p></li><li><p><strong>eventStatus: DRAFT, PUBLISHED, ENROLLMENT_STARTED, ...</strong></p></li><li><p><strong>offline</strong></p></li><li><p><strong>free</strong></p></li><li><p><strong>_links</strong></p><ul><li><p><strong>profile (for the self-descriptive message)</strong></p></li><li><p><strong>self&nbsp;</strong></p></li><li><p><strong>publish</strong></p></li><li><p><strong>...</strong></p></li></ul></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:36px">[2. 이벤트 생성 API 개발]</span></span></strong></p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>10. 이벤트 API 테스트 클래스 생성</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16416&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16416&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.events; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; //@SpringBootTest @WebMvcTest public class EventControllerTests { @Autowired MockMvc mockMvc; @Test public void createEvent() throws Exception{ mockMvc.perform( post(&quot;/api/events/&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8).accept(MediaTypes.HAL_JSON) ).andExpect(status().isCreated()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>11. 201 응답 받기</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16417">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16417</a></p><p>&nbsp;</p><p><strong>EventController</strong></p><pre class="brush:as3;">package net.macaronics.restapi.events; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import java.net.URI; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; @Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) public class EventController { /** * methodOn 사용 * @return */ // @PostMapping(&quot;/method&quot;) // public ResponseEntity createEventMethod(){ // WebMvcLinkBuilder webMvcLinkBuilder = linkTo(methodOn(EventController.class).createEvent(null)); // return ResponseEntity.created(webMvcLinkBuilder.toUri()).build(); // } @PostMapping public ResponseEntity createEvent(@RequestBody Event event){ URI createdUri = linkTo(EventController.class).slash(&quot;{id}&quot;).toUri(); event.setId(11); return ResponseEntity.created(createdUri).body(event); } } </pre><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>EventControllerTests</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi.events; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest public class EventControllerTests { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Test public void createEvent() throws Exception{ Event event =Event.builder() .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .eventStatus(EventStatus.DRAFT) .build(); //MediaType.APPLICATION_JSON_UTF8 --&gt; 버전 문제 다음 사용할것 //MediaTypes.HAL_JSON_VALUE mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>12. 이벤트 Repository</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16418&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16418&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 JPA</strong><br /><strong>JpaRepository 상속 받아 만들기</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Enum을 JPA 맵핑시 주의할 것</strong><br /><strong>@Enumerated(EnumType.STRING)</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>@MockBean</strong><br /><strong>Mockito를 사용해서 mock 객체를 만들고 빈으로 등록해 줌.<br />(주의) 기존 빈을 테스트용 빈이 대체 한다.</strong><br />&nbsp;</p><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><p><br />입력값들을 전달하면 JSON 응답으로 201이 나오는지 확인.<br />Location 헤더에 생성된 이벤트를 조회할 수 있는 URI 담겨 있는지 확인.<br />id는 DB에 들어갈 때 자동생성된 값으로 나오는지 확인<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">EventController</span></strong></p><pre class="brush:as3;">package net.macaronics.restapi.events; import lombok.RequiredArgsConstructor; import org.springframework.hateoas.MediaTypes; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import java.net.URI; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; @Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) @RequiredArgsConstructor public class EventController { private final EventRepository eventRepository; /** * methodOn 사용 * @return */ // @PostMapping(&quot;/method&quot;) // public ResponseEntity createEventMethod(){ // WebMvcLinkBuilder webMvcLinkBuilder = linkTo(methodOn(EventController.class).createEvent(null)); // return ResponseEntity.created(webMvcLinkBuilder.toUri()).build(); // } @PostMapping public ResponseEntity createEvent(@RequestBody Event event){ Event newEvent=this.eventRepository.save(event); URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri(); return ResponseEntity.created(createdUri).body(event); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventControllerTests</strong></span></p><pre class="brush:as3;">package net.macaronics.restapi.events; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest public class EventControllerTests { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @MockBean EventRepository eventRepository; @Test public void createEvent() throws Exception{ Event event =Event.builder() .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .eventStatus(EventStatus.DRAFT) .build(); event.setId(10); Mockito.when(eventRepository.save(event)).thenReturn(event); //MediaType.APPLICATION_JSON_UTF8 --&gt; 버전 문제 다음 사용할것 //MediaTypes.HAL_JSON_VALUE // Headers = [Location:&quot;http://localhost/api/events/10&quot;, Content-Type:&quot;application/hal+json&quot;] mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>13. 입력값 제한하기</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16419&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16419&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>입력값 제한</strong></p><ul><li><p><strong>id 또는 입력 받은 데이터로 계산해야 하는 값들은 입력을 받지 않아야 한다.</strong></p></li><li><p><strong>EventDto 적용</strong></p></li></ul><p>&nbsp;</p><p><strong>DTO -&gt; 도메인 객체로 값 복사</strong></p><ul><li><p><strong>ModelMapper</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.modelmapper&lt;/groupId&gt; &lt;artifactId&gt;modelmapper&lt;/artifactId&gt; &lt;version&gt;3.1.1&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p><strong>통합 테스트로 전환</strong></p><ul><li><p><strong>@WebMvcTest 빼고 다음 애노테이션 추가</strong></p><ul><li><p><strong>@SpringBootTest</strong></p></li><li><p><strong>@AutoConfigureMockMvc</strong></p></li></ul></li><li><p><strong>Repository @MockBean 코드 제거</strong></p></li><li><p>&nbsp;</p></li></ul><p>&nbsp;</p><p>&nbsp;</p><ul><li><p><strong>입력값으로 누가 id나 eventStatus, offline, free 이런 데이터까지 같이 주면?</strong></p></li></ul><p><strong>Bad_Request로 응답 vs 받기로 한 값 이외는 무시</strong></p><p>&nbsp;</p><p>==&gt;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package net.macaronics.restapi; import net.macaronics.restapi.events.Event; import net.macaronics.restapi.events.EventDto; import org.modelmapper.ModelMapper; import org.modelmapper.TypeMap; import org.modelmapper.config.Configuration; import org.modelmapper.convention.MatchingStrategies; import org.modelmapper.convention.NameTokenizers; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import javax.print.attribute.standard.Destination; @SpringBootApplication public class RestapiApplication { public static void main(String[] args) { SpringApplication.run(RestapiApplication.class, args); } @Bean public ModelMapper modelMapper() { ModelMapper modelMapper=new ModelMapper(); //setter 아닌 필드로 주입 modelMapper.getConfiguration() .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE) .setFieldMatchingEnabled(true) .setSkipNullEnabled(true); return modelMapper; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Event</strong></span></p><pre class="brush:as3;">import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; @Builder @AllArgsConstructor @NoArgsConstructor @Getter //@Setter @EqualsAndHashCode(of=&quot;id&quot;) @Entity public class Event { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; private String description; private LocalDateTime beginEnrollmentDateTime; private LocalDateTime closeEnrollmentDateTime; private LocalDateTime beginEventDateTime; private LocalDateTime endEventDateTime; private String location; // (optional) 이게 없으면 온라인 모임 private int basePrice; // (optional) private int maxPrice; // (optional)- private int limitOfEnrollment; private boolean offline; private boolean free; @Enumerated(EnumType.STRING) private EventStatus eventStatus; }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventDto</strong></span></p><pre class="brush:as3;">import lombok.*; import java.time.LocalDateTime; @Builder @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of=&quot;id&quot;) @Getter @ToString public class EventDto { private String name; private String description; private LocalDateTime beginEnrollmentDateTime; private LocalDateTime closeEnrollmentDateTime; private LocalDateTime beginEventDateTime; private LocalDateTime endEventDateTime; private String location; // (optional) 이게 없으면 온라인 모임 private int basePrice; // (optional) private int maxPrice; // (optional)- private int limitOfEnrollment; public Event toEvent(){ return Event.builder() .name(this.name) .description(this.description) .beginEnrollmentDateTime(this.beginEnrollmentDateTime) .closeEnrollmentDateTime(this.closeEnrollmentDateTime) .beginEventDateTime(this.beginEventDateTime) .endEventDateTime(this.endEventDateTime) .location(this.location) .basePrice(this.basePrice) .maxPrice(this.maxPrice) .limitOfEnrollment(this.limitOfEnrollment) .build(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>EventController</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">~ @Controller @RequestMapping(value = &quot;/api/events&quot;, produces = MediaTypes.HAL_JSON_VALUE) public class EventController { private final EventRepository eventRepository; @Autowired private ModelMapper modelMapper; public EventController(EventRepository eventRepository){ this.eventRepository=eventRepository; } ~ @PostMapping public ResponseEntity createEvent(@RequestBody EventDto eventDto){ Event event=modelMapper.map(eventDto, Event.class); //Event event = eventDto.toEvent(); try{ modelMapper.validate(); }catch (ValidationException e){ e.printStackTrace(); System.out.println(&quot; ValidationException : &quot; + e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); } Event newEvent=this.eventRepository.save(event); System.out.println(&quot; newEvent 저장후 &quot; +newEvent.getId()); URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri(); return ResponseEntity.created(createdUri).body(event); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>EventControllerTests</strong></span></p><pre class="brush:as3;">import com.fasterxml.jackson.databind.ObjectMapper; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; //@WebMvcTest @SpringBootTest @AutoConfigureMockMvc public class EventControllerTests { @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; /** * Mock은 껍데기만 있는 객체를 얘기합니다. * 인터페이스의 추상메소드가 메소드 바디는 없고 파라미터 타입과 리턴타입만 선언된 것처럼, Mock Bean은 * 기존에 사용되던 Bean의 껍데기만 가져오고 내부의 구현 부분은 모두 사용자에게 위임한 형태입니다. */ // @MockBean // EventRepository eventRepository; @Test public void createEvent() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(true) .offline(false) .build(); //Mockito.when(eventRepository.save(event)).thenReturn(event); //MediaType.APPLICATION_JSON_UTF8 --&gt; 버전 문제 다음 사용할것 //MediaTypes.HAL_JSON_VALUE // Headers = [Location:&quot;http://localhost/api/events/10&quot;, Content-Type:&quot;application/hal+json&quot;] System.out.println(&quot;event: &quot;+objectMapper.writeValueAsString(event)); mockMvc.perform( post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON_UTF8) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event)) ) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath(&quot;id&quot;).exists()) .andExpect(header().exists(HttpHeaders.LOCATION)) .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) .andExpect(jsonPath(&quot;id&quot;).value(Matchers.not(100))) .andExpect(jsonPath(&quot;free&quot;).value(Matchers.not(true)) ); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>위와같이 설정은 좋았다.</p><p>강좌에서는 특별히 설정을 안해도 정상작동했으나,&nbsp;</p><p>그러나&nbsp; 다음과 같은 이&nbsp;<strong><span style="color:#8e44ad">Unmapped destination properties found in TypeMap 에 대한 해당 필드가 없어서&nbsp;&nbsp;매핑이 안되어서 오류가났다.</span></strong></p><p>setFieldMatchingEnabled(true) 해 주었음에도 불구하고 오류 발생</p><p>버전을 변경하고&nbsp; 기타 여러방법을 시도했는데.&nbsp;</p><p>properties found in TypeMap 처리를 해결할 수없었다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">ModelMapper validation errors: 1) Unmapped destination properties found in TypeMap[EventDto -&gt; Event ]: net.macaronics.restapi.events.Event.id net.macaronics.restapi.events.Event.offline net.macaronics.restapi.events.Event.free net.macaronics.restapi.events.Event.eventStatus 1 error</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad">결론 =&gt;&nbsp; 다음과 같이 직접&nbsp; builder 매핑 처리를 해주었다.</span></p><p>&nbsp;</p><pre class="brush:as3;"> public Event toEvent(){ return Event.builder() .name(this.name) .description(this.description) .beginEnrollmentDateTime(this.beginEnrollmentDateTime) .closeEnrollmentDateTime(this.closeEnrollmentDateTime) .beginEventDateTime(this.beginEventDateTime) .endEventDateTime(this.endEventDateTime) .location(this.location) .basePrice(this.basePrice) .maxPrice(this.maxPrice) .limitOfEnrollment(this.limitOfEnrollment) .build(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>14. 입력값 이외에 에러 발생</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16420&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16420&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>ObjectMapper 커스터마이징</strong></span></p><ul><li><p><span style="color:#8e44ad"><strong>spring.jackson.deserialization.fail-on-unknown-properties=true</strong></span></p></li></ul><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><p>&nbsp;</p><pre class="brush:as3;">입력값으로 누가 id나 eventStatus, offline, free 이런 데이터까지 같이 주면? Bad_Request로 응답 vs 받기로 한 값 이외는 무시 </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">서버구동시에는 정상작동하나, 다음과 같이 테스코드 작성후 테스트시에 오류 발생</span></strong></p><pre class="brush:as3;"> @Test public void createEvent_Bad_Request() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(true) .offline(false) .eventStatus(EventStatus.PUBLISHED) .build(); mockMvc.perform(post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event))) .andDo((print())) .andExpect(status().isBadRequest()); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>15. Bad Request 처리</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16421&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16421&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>@Valid와 BindingResult (또는 Errors)</strong></p><ul><li><p><strong>BindingResult는 항상 @Valid 바로 다음 인자로 사용해야 함. (스프링 MVC)</strong></p></li><li><p><strong>@NotNull, @NotEmpty, @Min, @Max, ... 사용해서 입력값 바인딩할 때 에러 확인할 수 있음</strong></p></li></ul><p>&nbsp;</p><p><strong>도메인 Validator 만들기</strong></p><ul><li><p><strong><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/Validator.html">Validator</a> 인터페이스 없이 만들어도 상관없음</strong></p></li></ul><p>&nbsp;</p><p><strong>테스트 설명 용 애노테이션 만들기</strong></p><ul><li><p><strong>@Target, @Retention</strong></p></li></ul><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><p>&nbsp;</p><p>&nbsp;</p><ul><li><p><strong>입력 데이터가 이상한 경우 Bad_Request로 응답</strong></p><ul><li><p><strong>입력값이 이상한 경우 에러</strong></p></li><li><p><strong>비즈니스 로직으로 검사할 수 있는 에러</strong></p></li><li><p><strong>에러 응답 메시지에 에러에 대한 정보가 있어야 한다.</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>EventValidator</strong></span></p><pre class="brush:as3;">import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import java.time.LocalDateTime; @Component public class EventValidator { public void validate(EventDto eventDto, Errors errors){ if(eventDto.getBasePrice() &gt; eventDto.getMaxPrice() &amp;&amp; eventDto.getMaxPrice() !=0){ errors.rejectValue(&quot;basePrice&quot;,&quot;wrongValue&quot;, &quot;BasePrice is wrong.&quot; ); errors.rejectValue(&quot;maxPrice&quot;, &quot;wrongValue&quot;, &quot;MaxPrice is wrong.&quot;); } LocalDateTime endEventDateTime = eventDto.getEndEventDateTime(); if(endEventDateTime.isBefore(eventDto.getBeginEventDateTime()) || endEventDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) || endEventDateTime.isBefore(eventDto.getBeginEnrollmentDateTime() ) ){ errors.rejectValue(&quot;endEventDateTime&quot;, &quot;wrongValue&quot;, &quot;EndEventDateTime is wrong&quot;); } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>EventController</strong></span></p><pre class="brush:as3;"> @PostMapping public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){ if(errors.hasErrors()){ System.out.println(&quot;첫번째 Bad Request 처리&quot;); return ResponseEntity.badRequest().build(); } eventValidator.validate(eventDto, errors); if(errors.hasErrors()){ System.out.println(&quot;두번째 Bad Request 처리&quot;); return ResponseEntity.badRequest().build(); } //Event event=modelMapper.map(eventDto, Event.class); Event event = eventDto.toEvent(); Event newEvent=this.eventRepository.save(event); URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri(); return ResponseEntity.created(createdUri).body(event); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>TEST</strong></span></p><pre class="brush:as3;"> @Test @DisplayName(&quot;입력 받을 수 없는 값을 사용한 경우에 에러가 발생하는 테스트&quot;) //spring.jackson.deserialization.fail-on-properties=true public void createEvent_Bad_Request() throws Exception{ Event event =Event.builder() .id(100) .name(&quot;Spring&quot;) .description(&quot;REST API Development with Spring&quot;) .beginEnrollmentDateTime(LocalDateTime.of(2023, 05, 06, 19 , 20 )) .closeEnrollmentDateTime(LocalDateTime.of(2023, 05, 20, 20 , 20)) .beginEventDateTime(LocalDateTime.of(2023, 05, 20, 20, 20)) .endEventDateTime(LocalDateTime.of(2023, 11, 26, 20, 20)) .basePrice(100) .maxPrice(200) .limitOfEnrollment(100) .location(&quot;강남역 D2 스타텀 팩토리&quot;) .free(true) .offline(false) .eventStatus(EventStatus.PUBLISHED) .build(); mockMvc.perform(post(&quot;/api/events&quot;) .contentType(MediaType.APPLICATION_JSON) .accept(MediaTypes.HAL_JSON) .content(objectMapper.writeValueAsString(event))) .andDo((print())) .andExpect(status().isBadRequest()); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Bad Request 응답</p><p>비즈니스 로직 적용</p><p>매개변수를 이용한 테스트</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><span style="color:#c0392b"><strong>16. Bad Request 응답</strong></span></span></p><p>&nbsp;</p><p>강의 :&nbsp; &nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16422&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=spring_rest-api&amp;unitId=16422&amp;tab=curriculum</a></p><p>&nbsp;</p><p><br />커스텀 JSON Serializer 만들기<br />extends JsonSerializer&lt;T&gt; (Jackson JSON 제공)<br />@JsonComponent (스프링 부트 제공)<br />&nbsp;</p><p>&nbsp;</p><p><strong>BindingError</strong><br />&nbsp; FieldError 와 GlobalError (ObjectError)가 있음<br />&nbsp; objectName<br />&nbsp; defaultMessage<br />&nbsp; code<br />&nbsp; field<br />&nbsp; rejectedValue</p><p>&nbsp;</p><p><strong>테스트 할 것</strong></p><pre class="brush:as3;">입력 데이터가 이상한 경우 Bad_Request로 응답 입력값이 이상한 경우 에러 비즈니스 로직으로 검사할 수 있는 에러 에러 응답 메시지에 에러에 대한 정보가 있어야 한다. </pre><p><br />&nbsp;</p><p><span style="font-size:24px"><a target="_blank" href="https://pupupee9.tistory.com/68">https://pupupee9.tistory.com/68</a></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>결론은&nbsp; 응답처리는 다음과 같이&nbsp; Errors 를 Serializer&nbsp; 로 만들어 처리하면된다.</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.springframework.boot.jackson.JsonComponent; import org.springframework.validation.Errors; import java.io.IOException; //ObjectMapper 에 Custom Serializer를 등록해 주어야하는데 // Spring Boot에서 제공하는 @JsonComponent를 사용하면 손쉽게 등록이 가능하다. @JsonComponent public class ErrorsSerializer extends JsonSerializer&lt;Errors&gt; { @Override public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartArray(); errors.getFieldErrors().forEach(e-&gt;{ try{ gen.writeStartObject(); gen.writeStringField(&quot;objectName&quot;,e.getObjectName()); gen.writeStringField(&quot;field&quot;,e.getField()); gen.writeStringField(&quot;defaultMessage&quot;,e.getDefaultMessage()); gen.writeStringField(&quot;code&quot;, e.getCode()); Object rejectedValue =e.getRejectedValue(); if(rejectedValue!=null){ gen.writeStringField(&quot;rejectedValue&quot;, rejectedValue.toString()); } gen.writeEndObject(); }catch (IOException e1){ e1.printStackTrace(); } }); errors.getGlobalErrors().stream().forEach(e-&gt;{ try{ gen.writeStartObject(); gen.writeStringField(&quot;objectName&quot;,e.getObjectName()); gen.writeStringField(&quot;defaultMessage&quot;,e.getDefaultMessage()); gen.writeStringField(&quot;code&quot;, e.getCode()); gen.writeEndObject(); }catch (IOException e1){ e1.printStackTrace(); } }); gen.writeEndArray(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-12 12:39:40 스프링 데이터 JPA (백기선) -2-3.스프링 데이터 JPA 활용 JpaRepository, ,JPA 쿼리 메소드, Sort, Named Parameter와 SpEL,Update 쿼리,EntityGraph,Projection,Specifications,Query by Example,트랜잭션,Auditing http://macaronics.net/index.php/m01/spring/view/2120 2120 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[웹 개발, 백엔드] 강의입니다.</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>ORM에 대한 이해</p><p>JPA 프로그래밍</p><p>Bean 생성 방법</p><p><strong>스프링 JPA가 어렵게 느껴졌다면?<br />개념과 원리, 실제까지 확실하게 학습해 보세요.</strong></p><p>제대로 배우는<br /><strong>백기선의 스프링 데이터 JPA</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>&nbsp;</p><p><strong>강의 :</strong></p><p><a target="_blank" href="https://www.inflearn.com/course/스프링-데이터-jpa#reviews">https://www.inflearn.com/course/스프링-데이터-jpa#reviews</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료 :</p><p><a target="_blank" href="https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit">https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 코드</p><p><a target="_blank" href="https://github.com/braverokmc79/springdatajpa">https://github.com/braverokmc79/springdatajpa</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/demojpa3">https://github.com/braverokmc79/demojpa3</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강좌 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>Application -&gt; 스프링 데이터 JPA (-&gt; JPA -&gt; JDBC) -&gt; Database</strong></p><p><strong><img width="516" height="408" alt="" src="https://lh6.googleusercontent.com/XVN0FUf452XRm-sey7YfF0CZt-J7qg9mkylNPjhd10OQrE7_ef2PcG9yrdExK6-WUDKIbHnXF5nlGj9KwObhsD7p2zrPL4qYekNZqeyr-onHOMom2vf3ogYXk2v5utDO0wMgtiJxj2WCXoSXi4OmQg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강사 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>백기선</strong></p><p>&nbsp;</p><p><strong>마이크로소프트(2+) &lt;- 아마존(1) &lt;- 네이버(4.5) &lt;- SLT(2.5) ...</strong></p><p>&nbsp;</p><p><strong>강좌</strong></p><ul><li><p><strong>스프링 프레임워크 입문 (Udemy)</strong></p></li><li><p><strong>백기선의 스프링 부트 (인프런)</strong></p></li></ul><p>&nbsp;</p><p><strong>특징</strong></p><ul><li><p><strong>스프링 프레임워크 중독자</strong></p></li><li><p><strong>JPA 하이버네이트 애호가</strong></p></li><li><p><strong>유튜브 / 백기선</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:36px"><strong>[2부: 스프링 데이터 JPA 활용]</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">31.스프링 데이터 JPA 1. JpaRepository</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13774&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13774&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>@EnableJpaRepositories</strong></p><ul><li><p><strong>스프링 부트 사용할 때는 사용하지 않아도 자동 설정 됨.</strong></p></li><li><p><strong>스프링 부트 사용하지 않을 때는 @Configuration과 같이 사용.</strong></p></li></ul><p>&nbsp;</p><p><strong>@Repository 애노테이션을 붙여야 하나 말아야 하나...</strong></p><ul><li><p><strong>안붙여도 됩니다.</strong></p></li><li><p><strong>이미 붙어 있어요. 또 붙인다고 별일이 생기는건 아니지만 중복일 뿐입니다.</strong></p></li></ul><p>&nbsp;</p><p><strong>스프링 @Repository</strong></p><ul><li><p><strong>SQLExcpetion 또는 JPA 관련 예외를 스프링의 DataAccessException으로 변환 해준다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import org.assertj.core.api.Assertions; import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional @Rollback(value = false) class PostRepositoryTest { @Autowired private PostRepository postRepository; @Test public void crud(){ Post post =new Post(); post.setTitle(&quot;jpa&quot;); postRepository.save(post); // List&lt;Post&gt; all=postRepository.findAll(); // Assertions.assertThat(all.size()).isEqualTo(1); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">32.스프링 데이터 JPA 2. JpaRepository.save() 메소드</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13775&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13775&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>JpaRepository의 save()는 단순히 새 엔티티를 추가하는 메소드가 아닙니다.</strong></p><ul><li><p><strong>Transient 상태의 객체라면 EntityManager.persist()</strong></p></li><li><p><strong>Detached 상태의 객체라면 EntityManager.merge()</strong></p></li></ul><p>&nbsp;</p><p><strong>Transient인지 Detached 인지 어떻게 판단 하는가?</strong></p><ul><li><p><strong>엔티티의 @Id 프로퍼티를 찾는다. 해당 프로퍼티가 null이면 Transient 상태로 판단하고 id가 null이 아니면 Detached 상태로 판단한다.</strong></p></li><li><p><strong>엔티티가 Persistable 인터페이스를 구현하고 있다면 isNew() 메소드에 위임한다.</strong></p></li><li><p><strong>JpaRepositoryFactory를 상속받는 클래스를 만들고 getEntityInfomration()을 오버라이딩해서 자신이 원하는 판단 로직을 구현할 수도 있습니다.</strong></p></li></ul><p>&nbsp;</p><p><strong>EntityManager.persist()</strong></p><ul><li><p><strong><a href="https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)">https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#persist(java.lang.Object)</a></strong></p></li><li><p><strong>Persist() 메소드에 넘긴 그 엔티티 객체를 Persistent 상태로 변경합니다.</strong></p></li></ul><p><strong><img width="247" height="157" alt="" src="https://lh4.googleusercontent.com/LKV1ohfhptJTQPZbowATLoF3rY7V2tr5m3cWTVbdNpfRidBTNuj9bwHlIyNGlyn0TIh_QHC_FEKnlgArmBjJxfDhYhuyBg4R864p3vYhBUZi_DOrSnFqqLyqbqeBaFL6AURgg7LnKnISYVoqT80y6g" /></strong></p><p><strong>EntityManager.merge()</strong></p><ul><li><p><strong><a href="https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#merge(java.lang.Object)">https://docs.oracle.com/javaee/6/api/javax/persistence/EntityManager.html#merge(java.lang.Object)</a></strong></p></li><li><p><strong>Merge() 메소드에 넘긴 그 엔티티의 복사본을 만들고, 그 복사본을 다시 Persistent 상태로 변경하고 그 복사본을 반환합니다.</strong></p></li></ul><p><strong><img width="269" height="181" alt="" src="https://lh5.googleusercontent.com/kt8VcuNmKVp8gXgM47XzFVMEHF_fppoDdReE7SNDP_mWGdlfP_-_cpSbcB8X1lVCp9jdc00KfyObWWuhc1d9RB9aTsEQEypylERyjBvQLXPC5jDtrYYyOOQUetrvjQw4vXZvo8FLnUMgYJBuk7ppzA" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>영속성이 적용된&nbsp; 항상 반환된 값을 이용 해라</strong></span></p><p><span style="color:#2980b9">Post savedPost</span></p><p><span style="color:#2980b9">savedPost 값 사용</span></p><p><span style="color:#2980b9">updatePost 값 사용</span></p><pre class="brush:as3;"> @Test public void save(){ Post post=new Post(); post.setId(1l); post.setTitle(&quot;jpa&quot;); Post savedPost=postRepository.save(post); //persist Assertions.assertThat(entityManager.contains(post)).isFalse(); Assertions.assertThat(entityManager.contains(savedPost)).isTrue(); Assertions.assertThat(savedPost==post); Post postUpdate=new Post(); postUpdate.setId(1l); postUpdate.setTitle(&quot;hibernate&quot;); Post updatePost= postRepository.save(postUpdate); //update Assertions.assertThat(entityManager.contains(updatePost)).isTrue(); Assertions.assertThat(entityManager.contains(postUpdate)).isFalse(); Assertions.assertThat(updatePost==postUpdate); List&lt;Post&gt; all=postRepository.findAll(); Assertions.assertThat(all.size()).isEqualTo(1); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">33.스프링 데이터 JPA 3. JPA 쿼리 메소드</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13776&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13776&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 생성하기</strong></p><ul><li><p><strong><a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation">https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation</a></strong></p></li><li><p><strong>And, Or</strong></p></li><li><p><strong>Is, Equals</strong></p></li><li><p><strong>LessThan, LessThanEqual, GreaterThan, GreaterThanEqual</strong></p></li><li><p><strong>After, Before</strong></p></li><li><p><strong>IsNull, IsNotNull, NotNull</strong></p></li><li><p><strong>Like, NotLike</strong></p></li><li><p><strong>StartingWith, EndingWith, Containing</strong></p></li><li><p><strong>OrderBy</strong></p></li><li><p><strong>Not, In, NotIn</strong></p></li><li><p><strong>True, False</strong></p></li><li><p><strong>IgnoreCase</strong></p></li></ul><p>&nbsp;</p><p><strong>쿼리 찾아쓰기</strong></p><ul><li><p><strong>엔티티에 정의한 쿼리 찾아 사용하기 JPA Named 쿼리</strong></p><ul><li><p><strong>@NamedQuery</strong></p></li><li><p><strong>@NamedNativeQuery</strong></p></li></ul></li><li><p><strong>리포지토리 메소드에 정의한 쿼리 사용하기</strong></p><ul><li><p><strong>@Query</strong></p></li><li><p><strong>@Query(nativeQuery=true)</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>PostRepository</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface PostRepository extends JpaRepository&lt;Post, Long&gt; { List&lt;Post&gt; findByTitleStartsWith(String title); @Query(&quot;SELECT p FROM Post AS p WHERE p.title =:title&quot;) List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Post</strong></span></p><p>@NamedQuery 쿼리 사용시</p><pre class="brush:as3;">@Entity @Data @ToString(of={&quot;title&quot;, &quot;content&quot;}) @NoArgsConstructor(access = AccessLevel.PUBLIC) //@NamedQuery(name=&quot;Post.findByTitle&quot;, query=&quot;SELECT p FROM Post AS p WHERE p.title =:title&quot;) public class Post { ~</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>PostRepositoryTest</strong></p><pre class="brush:as3;"> @Test public void findByTitleStartWidth(){ savePost(); List&lt;Post&gt; all =postRepository.findByTitleStartsWith(&quot;Spring&quot;); Assertions.assertThat(all.size()).isEqualTo(1); all.forEach(p-&gt; System.out.println(&quot;p :getTitle : &quot; +p.getTitle())); } private void savePost() { Post post =new Post(); post.setTitle(&quot;Spring Data Jpa&quot;); postRepository.save(post); //persist } @Test public void findByTitleTest(){ savePost(); List&lt;Post&gt; all =postRepository.findByTitle(&quot;Spring&quot;); Assertions.assertThat(all.size()).isEqualTo(1); all.forEach(p-&gt; System.out.println(&quot;findByTitleTest =&gt; : &quot; +p.getTitle())); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">34.Sort</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13777&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13777&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>이전과 마찬가지로 Pageable이나 Sort를 매개변수로 사용할 수 있는데, @Query와 같이 사용할 때 제약 사항이 하나 있습니다.</strong></p><p>&nbsp;</p><p><strong>Order by 절에서 함수를 호출하는 경우에는 Sort를 사용하지 못합니다. 그 경우에는 JpaSort.unsafe()를 사용 해야 합니다.</strong></p><ul><li><p><strong>Sort는 그 안에서 사용한 프로퍼티 또는 alias가 엔티티에 없는 경우에는 예외가 발생합니다.</strong></p></li><li><p><strong>JpaSort.unsafe()를 사용하면 함수 호출을 할 수 있습니다.</strong></p><ul><li><p><strong>JpaSort.unsafe(&ldquo;LENGTH(firstname)&rdquo;);</strong></p></li><li><p>&nbsp;</p></li></ul></li></ul><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; public interface PostRepository extends JpaRepository&lt;Post, Long&gt; { List&lt;Post&gt; findByTitleStartsWith(String title); @Query(&quot;SELECT p FROM Post AS p WHERE p.title =:title&quot;) List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title, Sort sort); }</pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void findByTitleTest(){ savePost(); List&lt;Post&gt; all =postRepository.findByTitle(&quot;Spring&quot; , Sort.by(&quot;title&quot;)); System.out.println(&quot;all.size() = &quot; + all.size()); all.forEach(p-&gt; System.out.println(&quot;findByTitleTest =&gt; : &quot; +p.getTitle())); } /** * 함수를 이용한 길이 정렬 */ @Test public void findByTitleTest2(){ savePost(); List&lt;Post&gt; all =postRepository.findByTitle(&quot;Spring&quot; , JpaSort.unsafe(&quot;LENGTH(title)&quot;)); System.out.println(&quot;2.all.size() = &quot; + all.size()); all.forEach(p-&gt; System.out.println(&quot;2.findByTitleTest =&gt; : &quot; +p.getTitle())); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">35.Named Parameter와 SpEL</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13778&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13778&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Named Parameter</strong></p><ul><li><p><strong>@Query에서 참조하는 매개변수를 ?1, ?2 이렇게 채번으로 참조하는게 아니라 이름으로 :title 이렇게 참조하는 방법은 다음과 같습니다.</strong></p></li></ul><p>&nbsp;</p><p><strong>&nbsp; &nbsp; @Query(&quot;SELECT p FROM Post AS p WHERE p.title = :title&quot;)</strong></p><p><strong>&nbsp;&nbsp;&nbsp;&nbsp;List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title, Sort sort);</strong></p><p>&nbsp;</p><p><strong>SpEL</strong></p><ul><li><p><strong>스프링 표현 언어</strong></p></li><li><p><strong><a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions">https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions</a></strong></p></li><li><p><strong>@Query에서 엔티티 이름을 #{#entityName} 으로 표현할 수 있습니다.</strong></p></li></ul><p>&nbsp;</p><p><strong>&nbsp; &nbsp; @Query(&quot;SELECT p FROM #{#entityName} AS p WHERE p.title = :title&quot;)</strong></p><p><strong>&nbsp;&nbsp;&nbsp;&nbsp;List&lt;Post&gt; findByTitle(@Param(&quot;title&quot;) String title, Sort sort);</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">36.Update 쿼리</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13779&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13779&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 생성하기</strong></p><ul><li><p><strong>find...</strong></p></li><li><p><strong>count...</strong></p></li><li><p><strong>delete...</strong></p></li><li><p><strong>흠.. update는 어떻게 하지?</strong></p></li></ul><p>&nbsp;</p><p><strong>Update 또는 Delete 쿼리 직접 정의하기</strong></p><ul><li><p><strong>@Modifying @Query</strong></p></li><li><p><strong>추천하진 않습니다.</strong></p></li></ul><p>&nbsp;</p><p><strong>&nbsp; &nbsp; @Modifying(clearAutomatically = true, flushAutomatically = true)</strong></p><p><strong>&nbsp;&nbsp;&nbsp;&nbsp;@Query(&quot;UPDATE Post p SET p.title = ?2 WHERE p.id = ?1&quot;)</strong></p><p><strong>&nbsp;&nbsp;&nbsp;&nbsp;int updateTitle(Long id, String title);</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @Modifying(clearAutomatically = true) @Query(&quot;UPDATE Post p Set p.title =:title WHERE p.id =:id&quot;) int updateTitle(@Param(&quot;title&quot;) String title, @Param(&quot;id&quot;) Long id); </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>테스트</strong></span></p><pre class="brush:as3;"> private Post savePost() { Post post =new Post(); post.setTitle(&quot;Spring Data Jpa&quot;); return postRepository.save(post); //persist } @Test public void updateTitle(){ Post spring =savePost(); String hibernate=&quot;hibernate&quot;; int update=postRepository.updateTitle(&quot;hibernate&quot;, spring.getId()); Assertions.assertThat(update).isEqualTo(update); Optional&lt;Post&gt; byId = postRepository.findById(spring.getId()); Assertions.assertThat(byId.get().getTitle()).isEqualTo(hibernate); } /** * * ==&gt; * 업데이트 는 다음과 같이 더티 체킹 */ @Test public void updateTitle2(){ Post spring =savePost(); spring.setTitle(&quot;hibernate&quot;); List&lt;Post&gt; all=postRepository.findAll(); Assertions.assertThat(all.get(0).getTitle()).isEqualTo(&quot;hibernate&quot;); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">37.EntityGraph</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13780&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13780&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 메소드 마다 연관 관계의 Fetch 모드를 설정 할 수 있습니다.</strong></p><p>&nbsp;</p><p><strong>@NamedEntityGraph</strong></p><ul><li><p><strong>@Entity에서 재사용할 여러 엔티티 그룹을 정의할 때 사용.</strong></p></li></ul><p>&nbsp;</p><p><strong>@EntityGraph</strong></p><ul><li><p><strong>@NamedEntityGraph에 정의되어 있는 엔티티 그룹을 사용 함.</strong></p></li><li><p><strong>그래프 타입 설정 가능</strong></p><ul><li><p><strong>&nbsp;(기본값) FETCH: 설정한 엔티티 애트리뷰트는 EAGER 패치 나머지는 LAZY 패치.</strong></p></li><li><p><strong>LOAD: 설정한 엔티티 애트리뷰트는 EAGER 패치 나머지는 기본 패치 전략 따름.</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#c0392b"><strong>=&gt; EntityGraph 는&nbsp;&nbsp;다음을 참조해서 볼것</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2079">https://macaronics.net/index.php/m01/spring/view/2079</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84&amp;unitId=28019&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&amp;unitId=28019&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><pre>#spring.jpa.properties.hibernate.default_batch_fetch_size=100</pre><p>&nbsp;</p><p><strong>batch_fetch 를 사용하면&nbsp; 지연로딩에서&nbsp;&nbsp;entityGraph 인 페치 조인을 사용안하면 in 으로 데이터를 한번에 가져온다.</strong></p><p><strong>앞 강의 확인 할것.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>연관된 엔티티들을 SQL 한번에 조회하는 방법<br />member team은 지연로딩 관계이다. 따라서 다음과 같이 team의 데이터를 조회할 때 마다 쿼리가<br />실행된다. (N+1 문제 발생)</p><p>&nbsp;</p><pre class="brush:as3;">@Test public void findMemberLazy() throws Exception { //given //member1 -&gt; teamA //member2 -&gt; teamB Team teamA = new Team(&quot;teamA&quot;); Team teamB = new Team(&quot;teamB&quot;); teamRepository.save(teamA); teamRepository.save(teamB); memberRepository.save(new Member(&quot;member1&quot;, 10, teamA)); memberRepository.save(new Member(&quot;member2&quot;, 20, teamB)); em.flush(); em.clear(); //when List&lt;Member&gt; members = memberRepository.findAll(); //then for (Member member : members) { System.out.println(&quot;시작============================================================&quot;); member.getTeam().getName(); // 다음과 같이 지연 로딩 여부를 확인할 수 있다 //Hibernate 기능으로 확인 Hibernate.initialize(member.getTeam()); //JPA 표준 방법으로 확인 PersistenceUnitUtil util = em.getEntityManagerFactory().getPersistenceUnitUtil(); System.out.println( &quot;isLoaded : &quot; + util.isLoaded(member.getTeam()) ); System.out.println(&quot;끝============================================================&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>참고: 다음과 같이 지연 로딩 여부를 확인할 수 있다.</strong></p><p>&nbsp;</p><pre class="brush:as3;">//Hibernate 기능으로 확인 Hibernate.isInitialized(member.getTeam()) //JPA 표준 방법으로 확인 PersistenceUnitUtil util = em.getEntityManagerFactory().getPersistenceUnitUtil(); util.isLoaded(member.getTeam());</pre><p>&nbsp;</p><p><strong>연관된 엔티티를 한번에 조회하려면 페치 조인이 필요하다.</strong></p><p>&nbsp;</p><p><strong>JPQL 페치 조인</strong></p><p>&nbsp;</p><pre class="brush:as3;">@Query(&quot;select m from Member m left join fetch m.team&quot;) List &lt;Member&gt; findMemberFetchJoin();</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하게 도와준다. 이 기능을<br />사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능)</strong></p><p>&nbsp;</p><pre class="brush:as3;">//공통 메시더 오버라이드 @Override @EntityGraph(attributePaths = {&quot;team&quot;}) List&lt;Member&gt; findAll(); //JPQL + 엔티티 그래프 @EntityGraph(attributePaths = {&quot;team&quot;}) @Query(&quot;select m from Member m&quot;) List&lt;Member&gt; findMemberEntityGraph(); @EntityGraph(attributePaths = {&quot;team&quot;}) List&lt;Member&gt; findEntityGraphByUsername(String username); }</pre><p>&nbsp;</p><p><strong>EntityGraph 정리&nbsp; &nbsp;사실상 페치 조인(FETCH JOIN)의 간편 버전&nbsp; &nbsp; LEFT OUTER JOIN 사용</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:16px"><strong>NamedEntityGraph 사용 방법</strong></span></span></p><p>@EntityGraph&nbsp;<span style="color:#8e44ad">Member.all&nbsp;은&nbsp;&nbsp;@NamedEntityGraph 호출 하며 이것은&nbsp;&nbsp;<strong>FETCH JOIN 으로 설정되어 진다.</strong></span></p><pre class="brush:as3;">@NamedEntityGraph(name = &quot;Member.all&quot;, attributeNodes = @NamedAttributeNode(&quot;team&quot;)) @Entity public class Member {} @EntityGraph(&quot;Member.all&quot;) @Query(&quot;select m from Member m&quot;) List &lt;Member&gt; findMemberEntityGraph();</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">★★★</span></strong></span><span style="color:#c0392b"><strong><span style="font-size:28px"> 38.스프링 데이터 JPA: Projection&nbsp; </span></strong></span><span style="color:#8e44ad"><strong><span style="font-size:28px">★★★</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13781&amp;category=questionDetail&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13781&amp;category=questionDetail&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>엔티티의 일부 데이터만 가져오기.</strong></p><p>&nbsp;</p><p><strong>인터페이스 기반 프로젝션</strong></p><ul><li><p><strong>Nested 프로젝션 가능.</strong></p></li></ul><ul><li><p><strong>Closed 프로젝션</strong></p><ul><li><p><strong>쿼리를 최적화 할 수 있다. 가져오려는 애트리뷰트가 뭔지 알고 있으니까.</strong></p></li><li><p><strong>Java 8의 디폴트 메소드를 사용해서 연산을 할 수 있다.</strong></p></li></ul></li><li><p><strong>Open 프로젝션</strong></p><ul><li><p><strong>@Value(SpEL)을 사용해서 연산을 할 수 있다. 스프링 빈의 메소드도 호출 가능.</strong></p></li><li><p><strong>쿼리 최적화를 할 수 없다. SpEL을 엔티티 대상으로 사용하기 때문에.</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong>클래스 기반 프로젝션</strong></p><ul><li><p><strong>DTO</strong></p></li><li><p><strong>롬복 @Value로 코드 줄일 수 있음</strong></p></li></ul><p>&nbsp;</p><p><strong>다이나믹 프로젝션</strong></p><ul><li><p><strong>프로젝션 용 메소드 하나만 정의하고 실제 프로젝션 타입은 타입 인자로 전달하기.</strong></p></li></ul><p>&nbsp;</p><p><strong>&lt;T&gt; List&lt;T&gt; findByPost_Id(Long id, Class&lt;T&gt; type);</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="200" height="127" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgI8B7wGZXn7ZTiDDiwYZQSnEOpWpM-_kCjI1qtsycIBsAECj7XMNt5cY87FJWzU3G-K7WUTqiyBKcFcX3GDqHca1CgeEFfxPvafY57zR5Pj5vJuofZz6U-zvs8aGI3mIwkqAXyiQJAShPpgyijmHesO0NlxIdAxVR_4gD74nzjFr3aSDv5Vpj3bgLJoA/s16000/2023-04-11%2010%2004%2058.png" /></p><p>&nbsp;</p><p><strong>Post</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import com.example.demojap3.comment.Comment; import jakarta.persistence.*; import lombok.*; import java.util.ArrayList; import java.util.Date; import java.util.List; @Entity @Data @ToString(of={&quot;title&quot;, &quot;content&quot;}) @NoArgsConstructor(access = AccessLevel.PUBLIC) //@NamedQuery(name=&quot;Post.findByTitle&quot;, query=&quot;SELECT p FROM Post AS p WHERE p.title =:title&quot;) public class Post { @Id @GeneratedValue @Column(name = &quot;post_id&quot;) private Long id; private String title; @Lob private String content; @Temporal(TemporalType.TIMESTAMP) private Date created; public Post(String content) { this.content = content; } @OneToMany(mappedBy = &quot;post&quot;) public List&lt;Comment&gt; commentList=new ArrayList&lt;&gt;(); } </pre><p>&nbsp;</p><p><strong>Comment</strong></p><pre class="brush:as3;">package com.example.demojap3.comment; import com.example.demojap3.post.Post; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; @Entity @Data //@NamedEntityGraph(name = &quot;Comment.post&quot;, // attributeNodes = @NamedAttributeNode(&quot;post&quot;) //) public class Comment { @Id @GeneratedValue @Column(name = &quot;comment_id&quot;) private Long id; private String comment; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = &quot;post_id&quot;) private Post post; private int up; private int down; private boolean best; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>CommentRepository</strong></span><br />&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.comment; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; public interface CommentRepository extends JpaRepository&lt;Comment, Long&gt; { // @EntityGraph(value = &quot;Comment&quot;, type = EntityGraph.EntityGraphType.LOAD) // Optional&lt;Comment&gt; loadCommentById(Long id); List&lt;CommentSummary2&gt; findByPost_Id(Long id); /** * ==&gt; 다음과 같이 제네릭으로 변경 */ &lt;T&gt;List&lt;T&gt; findByPost_Id(Long id, Class&lt;T&gt; type); } </pre><p>&nbsp;</p><p><strong>CommentSummary&nbsp; <span style="color:#8e44ad">(인터페이스 방식 )&nbsp;</span></strong></p><pre class="brush:as3;">package com.example.demojap3.comment; public interface CommentSummary { String getComment(); int getUp(); int getDown(); /** * Open Projection 방법 * @return @Value(&quot;#{target.up + &#39; &#39; +target.down}&quot;) String getVotes(); Hibernate: select c1_0.comment_id, c1_0.best, c1_0.comment, c1_0.down, c1_0.post_id, c1_0.up from comment c1_0 where c1_0.post_id=? 호출시 ==================================== 10 1 ==================================== =====&gt; 아래 같은 메서드 방법으로 커스텀화 되어 최적화된 쿼리로 출력 가능 */ default String getVotes(){ return getUp() + &quot; &quot; +getDown(); } /** ★ ★ ★최적화 되어 쿼리 원하는 것만 출력 * select * c1_0.comment, * c1_0.up, * c1_0.down * from * comment c1_0 * where * c1_0.post_id=? */ } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>CommentSummary2<span style="color:#8e44ad"> (클래스 방식)&nbsp;</span></strong></p><pre class="brush:as3;">package com.example.demojap3.comment; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; /** * 클래스방식 커스텀 */ @Data @AllArgsConstructor(access = AccessLevel.PROTECTED) public class CommentSummary2 { private String comment; private int up; private int down; public String getVotes(){ return getUp() + &quot; : &quot; +getDown(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>CommentOnly&nbsp;<span style="color:#8e44ad">(클래스 방식)&nbsp;</span></strong></p><pre class="brush:as3;">package com.example.demojap3.comment; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor(access = AccessLevel.PROTECTED) public class CommentOnly { private String comment; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>테스트</strong></span></p><pre class="brush:as3;">package com.example.demojap3.comment; import com.example.demojap3.post.Post; import com.example.demojap3.post.PostRepository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @SpringBootTest @Transactional class CommentRepositoryTest { @Autowired CommentRepository commentRepository; @Autowired PostRepository postRepository; @Test public void getCommentTest1(){ savePost(); Optional&lt;Comment&gt; byId = commentRepository.findById(1l); System.out.println(&quot;제목 = ? &quot; +byId.get().getPost().getTitle()); } private Comment savePost() { Post post = new Post(); post.setTitle(&quot;jpa&quot;); Post savePost=postRepository.save(post); Comment comment=new Comment(); comment.setComment(&quot;comment- hello &quot;); comment.setPost(savePost); comment.setUp(10); comment.setDown(1); return commentRepository.save(comment); } @Test public void getCommentTest2(){ savePost(); commentRepository.findByPost_Id(1l).forEach(c-&gt;{ System.out.println(&quot;====================================&quot;); System.out.println(c.getVotes()); System.out.println(&quot;====================================&quot;); }); } @Test public void getCommentOnelyTest(){ savePost(); commentRepository.findByPost_Id(1l, CommentOnly.class).forEach(c-&gt;{ System.out.println(&quot;====================================&quot;); System.out.println(c.getComment()); System.out.println(&quot;====================================&quot;); }); } }</pre><p>&nbsp;</p><pre class="brush:as3;">Hibernate: select c1_0.comment_id, c1_0.best, c1_0.comment, c1_0.down, c1_0.post_id, c1_0.up from comment c1_0 where c1_0.post_id=? ★인터페이스 페이스 값으로 가져옴 ★ 원하는 컬럼만 가져오며 query 가 최적화 됨 select c1_0.comment, c1_0.up, c1_0.down from comment c1_0 where c1_0.post_id=? ==================================== 10 1 ==================================== </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>결론</strong>&nbsp;</span></span></p><p><span style="color:#e67e22"><span style="font-size:22px">1)&nbsp;<strong>CommentSummary2&nbsp; 와 같은 클래스 방식에&nbsp; &nbsp; </strong></span></span></p><p><span style="color:#e67e22"><span style="font-size:22px"><strong>2) 아래처럼 제네릭 메소드 추천</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> /** * ==&gt; 다음과 같이 제네릭으로 변경 */ &lt;T&gt;List&lt;T&gt; findByPost_Id(Long id, Class&lt;T&gt; type); </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">★ 반드시&nbsp; 참조해서 볼것&nbsp; :&nbsp; &nbsp;=&gt;</span></strong></span></p><p>&nbsp;</p><p>링크클릭</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2103"><strong>★32.동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용</strong></a></p><p>&nbsp;</p><p>예)</p><pre class="brush:as3;">package study.querydsl.dto; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; @Data public class MemberTeamDto { private Long memberId; private String username; private int age; private Long teamId; private String teamName; @QueryProjection public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) { this.memberId = memberId; this.username = username; this.age = age; this.teamId = teamId; this.teamName = teamName; } }</pre><p>&nbsp;</p><pre class="brush:as3;">public List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition){ return queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : nu</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">39.Specifications (99%사용안함)</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13782&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13782&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>에릭 에반스의 책 DDD에서 언급하는 Specification 개념을 차용 한 것으로 QueryDSL의 Predicate와 비슷합니다.</strong></p><p>&nbsp;</p><p><strong>설정 하는 방법</strong></p><ul><li><p><strong><a href="https://docs.jboss.org/hibernate/stable/jpamodelgen/reference/en-US/html_single/">https://docs.jboss.org/hibernate/stable/jpamodelgen/reference/en-US/html_single/</a></strong></p></li><li><p><strong>의존성 설정</strong></p></li><li><p><strong>플러그인 설정</strong></p></li><li><p><strong>IDE에 애노테이션 처리기 설정</strong></p></li><li><p><strong>코딩 시작</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.hibernate&lt;/groupId&gt; &lt;artifactId&gt;hibernate-jpamodelgen&lt;/artifactId&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;plugin&gt; &lt;groupId&gt;org.bsc.maven&lt;/groupId&gt; &lt;artifactId&gt;maven-processor-plugin&lt;/artifactId&gt; &lt;version&gt;2.0.5&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;id&gt;process&lt;/id&gt; &lt;goals&gt; &lt;goal&gt;process&lt;/goal&gt; &lt;/goals&gt; &lt;phase&gt;generate-sources&lt;/phase&gt; &lt;configuration&gt; &lt;processors&gt; &lt;processor&gt;org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor&lt;/processor&gt; &lt;/processors&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.hibernate&lt;/groupId&gt; &lt;artifactId&gt;hibernate-jpamodelgen&lt;/artifactId&gt; &lt;version&gt;${hibernate.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;/plugin&gt; </pre><p>&nbsp;</p><p><strong>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</strong></p><p><strong><img width="602" height="439" alt="" src="https://lh5.googleusercontent.com/nZIS1AWwkFwMJZ3xpOYxzUgyIAczoTmkEH9rrrdRdC_RRDAAu_ZDeskYZcxAoIXt0Xjs7914iB54SWpe15lmEiocoBVDkoWon9cM6WGsHPb1672VNCe9Bb6eKf9QETWOU6LbtkpjumxukW8yTi4MvQ" /></strong></p><p>&nbsp;</p><pre class="brush:as3;">public interface CommentRepository extends JpaRepository&lt;Comment, Long&gt;, JpaSpecificationExecutor&lt;Comment&gt; { } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:22px">참조 :</span></span></strong></p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2082">https://macaronics.net/index.php/m01/spring/view/2082</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Specifications (명세) 는</strong></p><p>&nbsp;</p><p><strong>1) 코드를 해독하는데 있어서 어려움이 많아 쿼리가 조금이라도 복잡해지면 사용하기가 어렵다.</strong></p><p><strong>&nbsp;</strong></p><p><strong>2) 생성해야 하는 Predicate 가 많아진다면 관리하기 어려워지고 직관적으로 이해하기 힘들다는 단점이 발생.</strong></p><p>&nbsp;</p><p><strong>3)&nbsp;JPA&nbsp;Specification&nbsp;와&nbsp;&nbsp;Criteria&nbsp;&nbsp;는&nbsp; &nbsp;</strong><strong>99% 사용&nbsp; 안하며,&nbsp; &nbsp;대신에 QueryDSL을 사용한다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">40.Query by Example(99%사용안함)</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13783&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13783&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>QBE는 필드 이름을 작성할 필요 없이(뻥) 단순한 인터페이스를 통해 동적으로 쿼리를 만드는 기능을 제공하는 사용자 친화적인 쿼리 기술입니다. (감이 1도 안잡히는거 이해합니다.. 코드를 봐야 이해하실꺼에요.)</strong></p><p>&nbsp;</p><p><strong>Example = Probe + ExampleMatcher</strong></p><ul><li><p><strong>Probe는 필드에 어떤 값들을 가지고 있는 도메인 객체.</strong></p></li><li><p><strong>ExampleMatcher는 Prove에 들어있는 그 필드의 값들을 어떻게 쿼리할 데이터와 비교할지 정의한 것.</strong></p></li><li><p><strong>Example은 그 둘을 하나로 합친 것. 이걸로 쿼리를 함.</strong></p></li></ul><p>&nbsp;</p><p><strong>장점</strong></p><ul><li><p><strong>별다른 코드 생성기나 애노테이션 처리기 필요 없음.</strong></p></li><li><p><strong>도메인 객체 리팩토링 해도 기존 쿼리가 깨질 걱정하지 않아도 됨.(뻥)</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><ul><li><p><strong>데이터 기술에 독립적인 API</strong></p></li></ul><p><strong>단점</strong></p><ul><li><p><strong>nested 또는 프로퍼티 그룹 제약 조건을 못 만든다.</strong></p></li><li><p><strong>조건이 제한적이다. 문자열은 starts/contains/ends/regex 가 가능하고 그밖에 property는 값이 정확히 일치해야 한다.</strong></p></li></ul><p>&nbsp;</p><p><strong>QueryByExampleExecutor</strong></p><p>&nbsp;</p><p><strong><a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example">https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">41.트랜잭션</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13784&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13784&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 JPA가 제공하는 Repository의 모든 메소드에는 기본적으로 @Transaction이 적용되어 있습니다.</strong></p><p>&nbsp;</p><p><strong>스프링 @Transactional</strong></p><ul><li><p><strong>클래스, 인터페이스, 메소드에 사용할 수 있으며, 메소드에 가장 가까운 애노테이션이 우선 순위가 높다.</strong></p></li><li><p><strong><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html</a> (반드시 읽어볼 것, 그래야 뭘 설정해서 쓸 수 있는지 알죠..)</strong></p></li></ul><p>&nbsp;</p><p><strong>JPA 구현체로 Hibernate를 사용할 때 트랜잭션을 readOnly를 사용하면 좋은 점</strong></p><ul><li><p><strong>Flush 모드를 NEVER로 설정하여, Dirty checking을 하지 않도록 한다.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">42.Auditing</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13785&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13785&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 데이터 JPA의 Auditing</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @CreatedDate private Date created; @LastModifiedDate private Date updated; @CreatedBy @ManyToOne private Account createdBy; @LastModifiedBy @ManyToOne private Account updatedBy; </pre><p>&nbsp;</p><p><strong>엔티티의 변경 시점에 언제, 누가 변경했는지에 대한 정보를 기록하는 기능.</strong></p><p>&nbsp;</p><p><strong>아쉽지만 이 기능은 스프링 부트가 자동 설정 해주지 않습니다.</strong></p><ol><li><p><strong>메인 애플리케이션 위에 @EnableJpaAuditing 추가</strong></p></li><li><p><strong>엔티티 클래스 위에 @EntityListeners(AuditingEntityListener.class) 추가</strong></p></li><li><p><strong>AuditorAware 구현체 만들기</strong></p></li><li><p><strong>@EnableJpaAuditing에 AuditorAware 빈 이름 설정하기.</strong></p></li></ol><p>&nbsp;</p><p><strong>JPA의 라이프 사이클 이벤트</strong></p><ul><li><p><strong><a href="https://docs.jboss.org/hibernate/orm/4.0/hem/en-US/html/listeners.html">https://docs.jboss.org/hibernate/orm/4.0/hem/en-US/html/listeners.html</a></strong></p></li><li><p><strong>@PrePersist</strong></p></li><li><p><strong>@PreUpdate</strong></p></li><li><p><strong>...</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><strong>Account</strong></p><pre class="brush:as3;">package com.example.demojap3.account; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.Data; @Entity @Data public class Account { @Id @GeneratedValue private Long id; private String username; private String firstName; private String lastName; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>AccountAuditAware</strong></p><pre class="brush:as3;">package com.example.demojap3.account; import org.springframework.data.domain.AuditorAware; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class AccountAuditAware implements AuditorAware&lt;Account&gt; { @Override public Optional&lt;Account&gt; getCurrentAuditor() { System.out.println(&quot;\n\n\n********** looking for current user\n\n\n&quot;); return Optional.empty(); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Comment</strong></p><pre class="brush:as3;">package com.example.demojap3.comment; import com.example.demojap3.account.Account; import com.example.demojap3.post.Post; import jakarta.persistence.*; import jakarta.persistence.Id; import lombok.Data; import lombok.Getter; import org.springframework.data.annotation.*; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Entity @Data //@NamedEntityGraph(name = &quot;Comment.post&quot;, // attributeNodes = @NamedAttributeNode(&quot;post&quot;) //) @EntityListeners(AuditingEntityListener.class) public class Comment { @Id @GeneratedValue @Column(name = &quot;comment_id&quot;) private Long id; private String comment; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = &quot;post_id&quot;) private Post post; private int up; private int down; private boolean best; @CreatedBy @ManyToOne(fetch = FetchType.LAZY) private Account createBy; @LastModifiedBy @ManyToOne(fetch = FetchType.LAZY) private Account updateBy; @CreatedDate private LocalDateTime created; @LastModifiedDate private LocalDateTime updated; @PrePersist public void prePersist(){ System.out.println(&quot;Pre Persist is called&quot;); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Application</strong></p><pre class="brush:as3;">package com.example.demojap3; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @SpringBootApplication @EnableJpaRepositories(repositoryBaseClass =SimpleMyRepository.class) @EnableJpaAuditing(auditorAwareRef = &quot;accountAuditAware&quot;)//빈 이름으로 설정해야 한다. public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } </pre><p>&nbsp;</p><p><span style="color:#c0392b"><strong>* Auditing 사용하려면</strong></span></p><p><strong><span style="color:#16a085">1.Application 에 @EnableJpaAuditing 추가 *</span></strong></p><p><span style="color:#16a085"><strong>2.Auditing 을 사용할 Entity 에서도 @EntityListeners(AuditingEntityListener.class) 추가</strong></span></p><p><strong><span style="color:#16a085">&nbsp;3.@EnableJpaAuditing(auditorAwareRef = &quot;accountAuditAware&quot;)//빈 이름으로 설정해야 한다.</span></strong></p><p>&nbsp;</p><p><strong>테스트</strong></p><pre class="brush:as3;"> /** * Auditing 사용하려면 * 1.Application 에 @EnableJpaAuditing 추가 * 2.Auditing 을 사용할 Entity 에서도 @EntityListeners(AuditingEntityListener.class) 추가 * 3.@EnableJpaAuditing(auditorAwareRef = &quot;accountAuditAware&quot;)//빈 이름으로 설정해야 한다. */ @Test //@Rollback(value = false) public void auditingTest(){ savePost(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>다음을 참조할것</strong></p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2081">https://macaronics.net/index.php/m01/spring/view/2081</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong><span style="color:#8e44ad">Spring Boot + JPA + Audit Listener</span></strong></span></p><p>&nbsp;</p><p>Spring Boot는 이미 이 솔루션을 제공하고 있습니다.</p><p><strong>0단계 &mdash; pom.xml</strong>&nbsp;에&nbsp;<strong>JPA가</strong>&nbsp;있는지 확인</p><p>&nbsp;</p><pre class="brush:as3;">&lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>1단계 - Auditor.java&nbsp;생성</p><p>&nbsp;</p><pre class="brush:as3;">import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import java.util.Optional; public class Auditor implements AuditorAware&lt;String&gt; { @Override public Optional&lt;String&gt; getCurrentAuditor() { SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); return authentication != null ? Optional.of((String) authentication.getPrincipal()) : Optional.of(&quot;0&quot;); } }</pre><p>&nbsp;</p><blockquote><p>사용하는 Principal 개체 유형에 따라 다릅니다.<br />이 예에서는 문자열을 사용하여 사용자 ID를&nbsp; Auditor 으로 나타냅니다.</p></blockquote><p><strong>2단계 -&nbsp;Spring Boot 애플리케이션 클래스에서&nbsp;@EnableJpaAuditing을 추가하고&nbsp;Audit Bean을&nbsp;정의합니다 .</strong></p><p>&nbsp;</p><pre class="brush:as3;">import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing(auditorAwareRef = &quot;auditor&quot;) public class MyAwesomeApplication { ... @Bean public AuditorAware&lt;String&gt; auditor() { return new Auditor(); } ... }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3단계 &mdash; 기본 엔터티에 @EntityListeners 및 @MappedSuperclass 추가</strong></p><p>&nbsp;</p><pre class="brush:as3;">import lombok.Data; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.Instant; @Data @EntityListeners(AuditingEntityListener.class) @MappedSuperclass public class BaseEntity { ... @CreatedBy @Column(length = 36) private String createdBy; @CreatedDate private Instant created; @LastModifiedBy @Column(length = 36) private String updatedBy; @LastModifiedDate private Instant updated; ... }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>4단계 - BaseEntity 확장</strong></p><pre class="brush:as3;">import lombok.Data; import lombok.EqualsAndHashCode; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @EqualsAndHashCode(callSuper = true) @Data @Entity(name = &quot;m_user&quot;) public class User extends BaseEntity { @Column(length = 60, nullable = false, unique = true) private String username; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">43.JPA: 마무리</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13786&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13786&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>이번 강좌가 여러분이 앞으로 스프링 데이터 JPA를 사용하고 학습하시는데 도움이 되었길 바랍니다.</strong></p><p><strong>감사합니다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-10 17:52:49 스프링 데이터 JPA (백기선) -2-2.스프링 데이터 JPA 활용 ( ★ Rest 페이징 처리 2. hateoas) http://macaronics.net/index.php/m01/spring/view/2119 2119 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[웹 개발, 백엔드] 강의입니다.</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>ORM에 대한 이해</p><p>JPA 프로그래밍</p><p>Bean 생성 방법</p><p><strong>스프링 JPA가 어렵게 느껴졌다면?<br />개념과 원리, 실제까지 확실하게 학습해 보세요.</strong></p><p>제대로 배우는<br /><strong>백기선의 스프링 데이터 JPA</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>&nbsp;</p><p><strong>강의 :</strong></p><p><a target="_blank" href="https://www.inflearn.com/course/스프링-데이터-jpa#reviews">https://www.inflearn.com/course/스프링-데이터-jpa#reviews</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료 :</p><p><a target="_blank" href="https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit">https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 코드</p><p><a target="_blank" href="https://github.com/braverokmc79/springdatajpa">https://github.com/braverokmc79/springdatajpa</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/demojpa3">https://github.com/braverokmc79/demojpa3</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강좌 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>Application -&gt; 스프링 데이터 JPA (-&gt; JPA -&gt; JDBC) -&gt; Database</strong></p><p><strong><img width="516" height="408" alt="" src="https://lh6.googleusercontent.com/XVN0FUf452XRm-sey7YfF0CZt-J7qg9mkylNPjhd10OQrE7_ef2PcG9yrdExK6-WUDKIbHnXF5nlGj9KwObhsD7p2zrPL4qYekNZqeyr-onHOMom2vf3ogYXk2v5utDO0wMgtiJxj2WCXoSXi4OmQg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강사 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>백기선</strong></p><p>&nbsp;</p><p><strong>마이크로소프트(2+) &lt;- 아마존(1) &lt;- 네이버(4.5) &lt;- SLT(2.5) ...</strong></p><p>&nbsp;</p><p><strong>강좌</strong></p><ul><li><p><strong>스프링 프레임워크 입문 (Udemy)</strong></p></li><li><p><strong>백기선의 스프링 부트 (인프런)</strong></p></li></ul><p>&nbsp;</p><p><strong>특징</strong></p><ul><li><p><strong>스프링 프레임워크 중독자</strong></p></li><li><p><strong>JPA 하이버네이트 애호가</strong></p></li><li><p><strong>유튜브 / 백기선</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:36px"><strong>[2부: 스프링 데이터 JPA 활용]</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">26.스프링 데이터 Common 11. 웹 기능 1부 소개</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13769&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13769&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 웹 지원 기능 설정</strong></p><ul><li><p><strong>스프링 부트를 사용하는 경우에.. 설정할 것이 없음. (자동 설정)</strong></p></li><li><p><strong>스프링 부트 사용하지 않는 경우?</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;">@Configuration @EnableWebMvc @EnableSpringDataWebSupport class WebConfiguration {} </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>제공하는 기능</strong></p><ul><li><p><strong>도메인 클래스 컨버터</strong></p></li><li><p><strong>@RequestHandler 메소드에서 Pageable과 Sort 매개변수 사용&nbsp;</strong></p></li><li><p><strong>Page 관련 HATEOAS 기능 제공</strong></p><ul><li><p><strong>PagedResourcesAssembler</strong></p></li><li><p><strong>PagedResoure</strong></p></li></ul></li><li><p><strong>Payload 프로젝션</strong></p><ul><li><p><strong>요청으로 들어오는 데이터 중 일부만 바인딩 받아오기</strong></p></li><li><p><strong>@ProjectedPayload, @XBRead, @JsonPath</strong></p></li></ul></li><li><p><strong>요청 쿼리 매개변수를 QueryDSLdml Predicate로 받아오기</strong></p><ul><li><p><strong>?firstname=Mr&amp;lastname=White =&gt; Predicate</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">27.스프링 데이터 Common 12. 웹 기능 2부 DomainClassConverter</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13770&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13770&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 Converter</strong></p><ul><li><p><strong><a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html</a></strong></p></li><li><p><strong>Formatter도 들어 본 것 같은데...&nbsp;</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/posts/{id}&quot;) public String getAPost(@PathVariable Long id) { Optional&lt;Post&gt; byId = postRepository.findById(id); Post post = byId.get(); return post.getTitle(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/posts/{id}&quot;) public String getAPost(@PathVariable(&ldquo;id&rdquo;) Post post) { return post.getTitle(); } </pre><p>&nbsp;</p><p><strong>테스트</strong></p><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc class PostControllerTest { @Autowired MockMvc mockMvc; @Autowired PostRepository postRepository; @Test public void getPost() throws Exception{ Post post=new Post(); post.setTitle(&quot;jpa&quot;); postRepository.save(post); mockMvc.perform(get(&quot;/posts/&quot;+post.getId())) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().string(&quot;jpa&quot;)); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">28.스프링 데이터 Common 13. 웹 기능 3부 Pageable과 Sort</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13771&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13771&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 MVC HandlerMethodArgumentResolver</strong></p><ul><li><p><strong>스프링 MVC 핸들러 메소드의 매개변수로 받을 수 있는 객체를 확장하고 싶을 때 사용하는 인터페이스</strong></p></li><li><p><strong><a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/method/support/HandlerMethodArgumentResolver.html">https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/method/support/HandlerMethodArgumentResolver.html</a></strong></p></li></ul><p>&nbsp;</p><p><strong>페이징과 정렬 관련 매개변수</strong></p><ul><li><p><strong>page: 0부터 시작.</strong></p></li><li><p><strong>size: 기본값 20.</strong></p></li><li><p><strong>sort: property,property(,ASC|DESC)</strong></p></li><li><p><strong>예) sort=created,desc&amp;sort=title (asc가 기본값)</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/posts&quot;) public Page&lt;Post&gt; getPosts(Pageable pageable){ return postRepository.findAll(pageable); } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc class PostControllerTest { @Autowired MockMvc mockMvc; @Autowired PostRepository postRepository; @Test public void getPosts() throws Exception{ Post post =new Post(); post.setTitle(&quot;jpa&quot;); postRepository.save(post); mockMvc.perform( get(&quot;/posts&quot;) .param(&quot;page&quot;, &quot;0&quot;) .param(&quot;size&quot;, &quot;10&quot;) .param(&quot;sort&quot;, &quot;created,desc&quot;) .param(&quot;sort&quot;, &quot;title&quot;) ) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath(&quot;$.content[0].title&quot;, is(&quot;jpa&quot;))); } }</pre><p>&nbsp;</p><p><strong>출력&nbsp;</strong></p><pre class="brush:as3;">MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:&quot;application/json&quot;] Content type = application/json Body = {&quot;content&quot;:[{&quot;id&quot;:1,&quot;title&quot;:&quot;jpa&quot;,&quot;content&quot;:null,&quot;created&quot;:null}],&quot;pageable&quot;:{&quot;sort&quot;:{&quot;empty&quot;:false,&quot;unsorted&quot;:false,&quot;sorted&quot;:true},&quot;offset&quot;:0,&quot;pageNumber&quot;:0,&quot;pageSize&quot;:10,&quot;unpaged&quot;:false,&quot;paged&quot;:true},&quot;last&quot;:true,&quot;totalElements&quot;:1,&quot;totalPages&quot;:1,&quot;size&quot;:10,&quot;number&quot;:0,&quot;sort&quot;:{&quot;empty&quot;:false,&quot;unsorted&quot;:false,&quot;sorted&quot;:true},&quot;first&quot;:true,&quot;numberOfElements&quot;:1,&quot;empty&quot;:false} Forwarded URL = null Redirected URL = null Cookies = []</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">29.스프링 데이터 Common 14. 웹 기능 4부 HATEOAS</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13772&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13772&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>라이브러리 추가</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-hateoas&lt;/artifactId&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>스프링 HATEOAS의 낮은 버전에서는&nbsp;ResourceSupport / Resource / Resources / PagedResources와 같은 클래스를 제공해줬지만 버전이 올라가면서&nbsp;ResourceSupport / Resource / Resources / PagedResources 클래스의 위치와 이름이 변경되었습니다.</p><p>HATEOAS에서 위와 같은 클래스들을 사용할 수 없다면 밑에 변경된 클래스들을 확인하고 변경해줘야 합니다.&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>첫 번째</p><p>&nbsp;</p><p>첫 번째로 linkTo, methodOn의 메소드의 경우에는 옛날 버전에서는&nbsp;ControllerLinkBuilder안에 포함되었지만 지금은&nbsp;WebMvcLinkBuilder 안에 존재합니다.</p><p>&nbsp;</p><ul><li>import&nbsp;static&nbsp;org.springframework.hateoas.mvc.ControllerLinkBuilder.*; 를&nbsp;</li><li>import&nbsp;static&nbsp;org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 로 변경&nbsp;</li></ul><p>&nbsp;</p><ul><li><p>ResourceSupport&nbsp;-&gt;&nbsp;RepresentationModel</p></li><li><p>Resource&nbsp;-&gt;&nbsp;EntityModel</p></li><li><p>Resources&nbsp;-&gt;&nbsp;CollectionModel</p></li><li><p>PagedResources&nbsp;-&gt;&nbsp;PagedModel</p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">1. 기본&nbsp;</span></strong></span></p><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/posts&quot;) public Page&lt;Post&gt; getPosts(Pageable pageable){ return postRepository.findAll(pageable); } </pre><p>=&gt;출력</p><pre class="brush:as3;">{ &quot;content&quot;:[ ... { &quot;id&quot;:111, &quot;title&quot;:&quot;jpa&quot;, &quot;created&quot;:null } ], &quot;pageable&quot;:{ &quot;sort&quot;:{ &quot;sorted&quot;:true, &quot;unsorted&quot;:false }, &quot;offset&quot;:20, &quot;pageSize&quot;:10, &quot;pageNumber&quot;:2, &quot;unpaged&quot;:false, &quot;paged&quot;:true }, &quot;totalElements&quot;:200, &quot;totalPages&quot;:20, &quot;last&quot;:false, &quot;size&quot;:10, &quot;number&quot;:2, &quot;first&quot;:false, &quot;numberOfElements&quot;:10, &quot;sort&quot;:{ &quot;sorted&quot;:true, &quot;unsorted&quot;:false } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>&nbsp;★2.&nbsp;hateoas 로 할경우</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> @GetMapping(&quot;/posts-ha&quot;) public PagedModel&lt;EntityModel&lt;Post&gt;&gt; getPosts(Pageable pageable, PagedResourcesAssembler&lt;Post&gt; assembler) { Post post=new Post(); post.setTitle(&quot;hello&quot;); post.setContent(&quot;jpa&quot;); postRepository.save(post); return assembler.toModel(postRepository.findAll(pageable)); }</pre><p>&nbsp;</p><pre class="brush:as3;"> &quot;_embedded&quot;:{ &quot;postList&quot;:[ { &quot;id&quot;:140, &quot;title&quot;:&quot;jpa&quot;, &quot;created&quot;:null }, ... { &quot;id&quot;:109, &quot;title&quot;:&quot;jpa&quot;, &quot;created&quot;:null } ] }, &quot;_links&quot;:{ &quot;first&quot;:{ &quot;href&quot;:&quot;http://localhost/posts?page=0&amp;size=10&amp;sort=created,desc&amp;sort=title,asc&quot; }, &quot;prev&quot;:{ &quot;href&quot;:&quot;http://localhost/posts?page=1&amp;size=10&amp;sort=created,desc&amp;sort=title,asc&quot; }, &quot;self&quot;:{ &quot;href&quot;:&quot;http://localhost/posts?page=2&amp;size=10&amp;sort=created,desc&amp;sort=title,asc&quot; }, &quot;next&quot;:{ &quot;href&quot;:&quot;http://localhost/posts?page=3&amp;size=10&amp;sort=created,desc&amp;sort=title,asc&quot; }, &quot;last&quot;:{ &quot;href&quot;:&quot;http://localhost/posts?page=19&amp;size=10&amp;sort=created,desc&amp;sort=title,asc&quot; } }, &quot;page&quot;:{ &quot;size&quot;:10, &quot;totalElements&quot;:200, &quot;totalPages&quot;:20, &quot;number&quot;:2 } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">30.스프링 데이터 Common: 마무리</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13774&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13774&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>지금까지 살펴본 내용</strong></p><ul><li><p><strong>스프링 데이터 Repository</strong></p></li><li><p><strong>쿼리 메소드</strong></p><ul><li><p><strong>메소드 이름 보고 만들기</strong></p></li><li><p><strong>메소드 이름 보고 찾기</strong></p></li></ul></li><li><p><strong>Repository 정의하기</strong></p><ul><li><p><strong>내가 쓰고 싶은 메소드만 골라서 만들기</strong></p></li><li><p><strong>Null 처리</strong></p></li></ul></li><li><p><strong>쿼리 메소드 정의하는 방법</strong></p></li><li><p><strong>리포지토리 커스터마이징</strong></p><ul><li><p><strong>리포지토리 하나 커스터마이징</strong></p></li><li><p><strong>모든 리포지토리의 베이스 커스터마이징</strong></p></li></ul></li><li><p><strong>도메인 이벤트 Publish</strong></p></li><li><p><strong>스프링 데이터 확장 기능</strong></p><ul><li><p><strong>QueryDSL 연동</strong></p></li><li><p><strong>웹 지원</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-10 14:04:21 ★★★스프링부트 3.0 QueryDsl 설정 (maven , gradle) http://macaronics.net/index.php/m01/spring/view/2118 2118 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:26px">1. Maven&nbsp; 설정</span></strong></span></p><p>&nbsp;</p><p><strong>pom.xml</strong></p><p><strong>1)libray</strong></p><p>&nbsp;</p><pre class="brush:as3;"> &lt;properties&gt; &lt;java.version&gt;17&lt;/java.version&gt; &lt;querydsl.version&gt;5.0.0&lt;/querydsl.version&gt; &lt;/properties&gt;</pre><pre class="brush:as3;">&lt;!-- querydsl 설정 --&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-core&lt;/artifactId&gt; &lt;version&gt;${querydsl.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-jpa&lt;/artifactId&gt; &lt;version&gt;${querydsl.version}&lt;/version&gt; &lt;classifier&gt;jakarta&lt;/classifier&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-sql&lt;/artifactId&gt; &lt;version&gt;${querydsl.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-sql-spring&lt;/artifactId&gt; &lt;version&gt;${querydsl.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-apt&lt;/artifactId&gt; &lt;version&gt;${querydsl.version}&lt;/version&gt; &lt;classifier&gt;jakarta&lt;/classifier&gt; &lt;/dependency&gt;</pre><p>querydsl-jpa 와&nbsp; querydsl-apt&nbsp; 추가하면 되지만&nbsp; &nbsp; 부수적으로&nbsp;querydsl-core,&nbsp;querydsl-sql-spring 추가했다.</p><p>그리고 3.0 이상에서은&nbsp;</p><p><strong>&lt;classifier&gt;jakarta&lt;/classifier&gt;&nbsp; 추가해 야 한다</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2)plugin</strong></p><pre class="brush:as3;"> &lt;plugin&gt; &lt;groupId&gt;com.mysema.maven&lt;/groupId&gt; &lt;artifactId&gt;apt-maven-plugin&lt;/artifactId&gt; &lt;version&gt;1.1.3&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;goals&gt; &lt;goal&gt;process&lt;/goal&gt; &lt;/goals&gt; &lt;configuration&gt; &lt;outputDirectory&gt;target/generated-sources/java&lt;/outputDirectory&gt; &lt;processor&gt;com.querydsl.apt.jpa.JPAAnnotationProcessor&lt;/processor&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;/plugin&gt; </pre><p>&nbsp;</p><p>만약에&nbsp;</p><p>clean -&gt;&nbsp; &nbsp; compile 시에 다음과 같은 오류가 나오면,&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Compilation failure<br />Attempt to recreate a file for type com.shop.entity.QItem</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg0W6FYuXd7dRqmZ3e-WuXUDy5E7MA5bHRrkALhHMzSjiNmlFNvzMJ6qb9QRJQb_2chQBywG5jvnEIShcJhUoc9sSAA2a4vZ2pJzavOPUPukc0aDzoJfza7uD0IdtismiqyRZgeNgTXCIz1qmS0omJ41mheC86TO3Z8iVUQ8MGZQ8NkQfhRMv-RWYYZjMUw/s16000/2024-01-11%2022%2007%2037.png" /></p><p>&nbsp;</p><p>clean -&gt;&nbsp;compile&nbsp; 다시 시도 하거나</p><p>&nbsp;</p><p>다음 플러그인 코드에서 다음 내용을 삭제해야 한다.</p><pre class="brush:as3;">&lt;outputDirectory&gt;target/generated-sources/java&lt;/outputDirectory&gt;--&gt;</pre><p>=&gt;</p><pre class="brush:as3;"> &lt;plugin&gt; &lt;groupId&gt;com.mysema.maven&lt;/groupId&gt; &lt;artifactId&gt;apt-maven-plugin&lt;/artifactId&gt; &lt;version&gt;1.1.3&lt;/version&gt; &lt;executions&gt; &lt;execution&gt; &lt;goals&gt; &lt;goal&gt;process&lt;/goal&gt; &lt;/goals&gt; &lt;configuration&gt; &lt;processor&gt;com.querydsl.apt.jpa.JPAAnnotationProcessor&lt;/processor&gt; &lt;/configuration&gt; &lt;/execution&gt; &lt;/executions&gt; &lt;/plugin&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOURMQWx24zXelGLb4qUxbe5VrpQsJW6vLR-jiTctneXGyHazd8FElSht9TONrWDSp6SxMWOlKb8pLvXZomq8I__S2M0tszWIbpwxBvDnG_Nw1xlUYFLeFY2mQGBM9cP5XlWXGXTCDu2pmjasWyLsdAvnX46gOuMAiCp8cxQKKzlZXt2gNS3nBENQiCQ/s16000/2023-04-09%2013%2027%2022.png">이미지크게</a></p><p><img alt="" width="900" height="488" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOURMQWx24zXelGLb4qUxbe5VrpQsJW6vLR-jiTctneXGyHazd8FElSht9TONrWDSp6SxMWOlKb8pLvXZomq8I__S2M0tszWIbpwxBvDnG_Nw1xlUYFLeFY2mQGBM9cP5XlWXGXTCDu2pmjasWyLsdAvnX46gOuMAiCp8cxQKKzlZXt2gNS3nBENQiCQ/s16000/2023-04-09%2013%2027%2022.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:26px">2. Gradle 설정</span></strong></span></p><p>&nbsp;</p><p><strong>build.gradle</strong></p><pre class="brush:as3;">plugins { id &#39;java&#39; id &#39;org.springframework.boot&#39; version &#39;3.2.2&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.1.4&#39; } group = &#39;com.shop&#39; version = &#39;0.0.1-SNAPSHOT&#39; java { sourceCompatibility = &#39;17&#39; } repositories { mavenCentral() } 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-validation&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39; implementation group: &#39;com.github.gavlyukovskiy&#39;, name: &#39;p6spy-spring-boot-starter&#39;, version: &#39;1.9.1&#39; implementation group: &#39;nz.net.ultraq.thymeleaf&#39;, name: &#39;thymeleaf-layout-dialect&#39;, version: &#39;3.3.0&#39; implementation group: &#39;org.modelmapper&#39;, name: &#39;modelmapper&#39;, version: &#39;3.2.0&#39; compileOnly &#39;org.projectlombok:lombok&#39; developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39; runtimeOnly &#39;com.h2database:h2&#39; runtimeOnly &#39;org.mariadb.jdbc:mariadb-java-client&#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; // ⭐ Spring boot 3.x이상에서 QueryDsl 패키지를 정의하는 방법 implementation &#39;com.querydsl:querydsl-jpa:5.0.0:jakarta&#39; annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot; annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot; annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot; } tasks.named(&#39;test&#39;) { useJUnitPlatform() } def querydslSrcDir = &#39;src/main/generated&#39; clean { delete file(querydslSrcDir) } tasks.withType(JavaCompile) { options.generatedSourceOutputDirectory = file(querydslSrcDir) options.compilerArgs.add(&quot;-parameters&quot;) } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre>Optional&lt;Integer&gt; page</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>1) Optional 사용시&nbsp;&nbsp;@PathVaiable 를 사용해야 한다.</strong></span></p><p>&nbsp;</p><p><strong><span style="font-size:18px">2)jpql 에서 파라미터는 항&nbsp;@Param을 생략해서는 안된다.</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-shop-maven-and-gradle">https://github.com/braverokmc79/jpa-shop-maven-and-gradle</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgavtxIt_XAaukclveltsEKOK9T76cD3RXZiUzS-p2iSIe00CFZnLxFhL3dajcnpGWG-pCgFTpCeLZ7Q3gXxnwIrcb6CmfzIxtARdDoGVQTMO0-CfV87zmLCoZBvvbGAZaKE3DbxECpShzZDAAhdXloCRtqyh-8Jmlt3ZfR-cm3igR2FKO1FjLHJrIj6Q/s1904/2023-03-22%2020%2003%2025.png">이미지 크게</a></p><p><img alt="" width="900" height="475" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgavtxIt_XAaukclveltsEKOK9T76cD3RXZiUzS-p2iSIe00CFZnLxFhL3dajcnpGWG-pCgFTpCeLZ7Q3gXxnwIrcb6CmfzIxtARdDoGVQTMO0-CfV87zmLCoZBvvbGAZaKE3DbxECpShzZDAAhdXloCRtqyh-8Jmlt3ZfR-cm3igR2FKO1FjLHJrIj6Q/s1904/2023-03-22%2020%2003%2025.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>오류시</p><p>Querydsl Q클래스 생성 오류 (Attempt to recreate a file for type)</p><p>Attempt&nbsp;to&nbsp;recreate&nbsp;a&nbsp;file&nbsp;for&nbsp;type</p><p>&nbsp;</p><p>어플리케이션 시작 시 Q클래스를 생성할 수 없다고 나온다.</p><p>이미 존재하는 Q클래스 삭제해도 똑같은 오류,</p><p><span style="color:#c0392b"><strong>인텔리제이 build 방식을 gradle-&gt;인텔리제이로 변경</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:28px"><strong>3.설정 테스트</strong></span></span></p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/demojpa3">https://github.com/braverokmc79/demojpa3</a></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhhnSIUIRi7hPjpwj1WSgOWki7r3KVnWCqR4VhSPyqg6OTXIovQxcZ296z73Znybiy4JdisgituRg0ZDNeNXsgk--cpZ-G7shYc5qMLrowiWvafdheTYKr62KzEFfU2yV-7d461oSXTjlRwzBME3SXGduy3WX802jjNi2f6KBqsfXkWxGJ2gTu6cnzZKA/s16000/2023-04-09%2014%2026%2030.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1) AppConfig</strong></p><pre class="brush:as3;">package com.example.demojap3; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory(){ return new JPAQueryFactory(em); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2)Post</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import jakarta.persistence.*; import lombok.Data; import lombok.ToString; import java.util.Date; @Entity @Data @ToString(of={&quot;title&quot;, &quot;content&quot;}) public class Post { @Id @GeneratedValue private Long id; private String title; @Lob private String content; @Temporal(TemporalType.TIMESTAMP) private Date created; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3) PostDto</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import com.querydsl.core.annotations.QueryProjection; import jakarta.persistence.Lob; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; import lombok.Data; import lombok.ToString; import org.springframework.data.querydsl.binding.QuerydslPredicate; import java.util.Date; @Data @ToString public class PostDto { private Long id; private String title; private String content; private Date created; @QueryProjection public PostDto(Long id, String title, String content, Date created) { this.id = id; this.title = title; this.content = content; this.created = created; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>4) PostSearchCondition</strong><br />&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import lombok.Data; @Data public class PostSearchCondition { private String title; private String content; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>5) PostCustomRepository</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface PostCustomRepository { public Page&lt;PostDto&gt; searchPostList(PostSearchCondition condition, Pageable pageable) ; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>6) PostCustomRepositoryImpl</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import java.util.List; import static com.example.demojap3.post.QPost.post; import static org.springframework.util.StringUtils.hasText; @RequiredArgsConstructor public class PostCustomRepositoryImpl implements PostCustomRepository{ private final JPAQueryFactory queryFactory; @Override public Page&lt;PostDto&gt; searchPostList(PostSearchCondition condition, Pageable pageable) { List&lt;PostDto&gt; postDtoList = queryFactory.select( new QPostDto(post.id, post.title, post.content, post.created) ).where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .from(post).fetch(); JPAQuery&lt;Long&gt; countQuery = queryFactory .select(post.count()) .from(post) .where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ); return PageableExecutionUtils.getPage(postDtoList, pageable, countQuery::fetchOne); } private BooleanExpression titleEq(String title) { return hasText(title) ? post.title.eq(title) :null; } private BooleanExpression contentEq(String content) { return hasText(content) ? post.content.eq(content) :null; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>7) PostCustomRepository</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface PostRespository extends JpaRepository&lt;Post, Long&gt; , PostCustomRepository { } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>8)&nbsp;&nbsp;PostRespositoryTest</strong></p><pre class="brush:as3;">package com.example.demojap3.post; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @SpringBootTest @Transactional @Rollback(value = false) class PostRespositoryTest { @Autowired PostRespository postRespository; @Test public void crud(){ Post post=new Post(); post.setTitle(&quot;hello1&quot;); post.setContent(&quot;content1&quot;); post.setCreated(new Date()); postRespository.save(post); Post post2=new Post(); post2.setTitle(&quot;hello1&quot;); post2.setContent(&quot;content2&quot;); post2.setCreated(new Date()); postRespository.save(post2); Post post3=new Post(); post3.setTitle(&quot;hello3&quot;); post3.setContent(&quot;content3&quot;); post3.setCreated(new Date()); postRespository.save(post3); PostSearchCondition condition =new PostSearchCondition(); condition.setTitle(&quot;hello1&quot;); Page&lt;PostDto&gt; postList =postRespository.searchPostList(condition, PageRequest.of(0, 10)); System.out.println(&quot;postList.getTotalElements() = &quot; + postList.getTotalElements()); postList.forEach(postDto -&gt; System.out.println(&quot;postDto.toString() = &quot; + postDto.toString())); Assertions.assertThat(postList.getNumberOfElements()).isEqualTo(2); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><br />&nbsp;</p><pre class="brush:as3;"> select p1_0.id, p1_0.title, p1_0.content, p1_0.created from post p1_0 where p1_0.title=? offset ? rows fetch first ? rows only postList.getTotalElements() = 2 postDto.toString() = PostDto(id=1, title=hello1, content=content1, created=2023-04-09 14:24:50.018) postDto.toString() = PostDto(id=2, title=hello1, content=content2, created=2023-04-09 14:24:50.052)</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-09 14:30:33 스프링 데이터 JPA (백기선) -2-1. 스프링 데이터 JPA 활용 http://macaronics.net/index.php/m01/spring/view/2117 2117 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[웹 개발, 백엔드] 강의입니다.</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>ORM에 대한 이해</p><p>JPA 프로그래밍</p><p>Bean 생성 방법</p><p><strong>스프링 JPA가 어렵게 느껴졌다면?<br />개념과 원리, 실제까지 확실하게 학습해 보세요.</strong></p><p>제대로 배우는<br /><strong>백기선의 스프링 데이터 JPA</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>&nbsp;</p><p><strong>강의 :</strong></p><p><a target="_blank" href="https://www.inflearn.com/course/스프링-데이터-jpa#reviews">https://www.inflearn.com/course/스프링-데이터-jpa#reviews</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료 :</p><p><a target="_blank" href="https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit">https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 코드</p><p><a target="_blank" href="https://github.com/braverokmc79/springdatajpa">https://github.com/braverokmc79/springdatajpa</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/braverokmc79/demojpa3">https://github.com/braverokmc79/demojpa3</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강좌 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>Application -&gt; 스프링 데이터 JPA (-&gt; JPA -&gt; JDBC) -&gt; Database</strong></p><p><strong><img width="516" height="408" alt="" src="https://lh6.googleusercontent.com/XVN0FUf452XRm-sey7YfF0CZt-J7qg9mkylNPjhd10OQrE7_ef2PcG9yrdExK6-WUDKIbHnXF5nlGj9KwObhsD7p2zrPL4qYekNZqeyr-onHOMom2vf3ogYXk2v5utDO0wMgtiJxj2WCXoSXi4OmQg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강사 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>백기선</strong></p><p>&nbsp;</p><p><strong>마이크로소프트(2+) &lt;- 아마존(1) &lt;- 네이버(4.5) &lt;- SLT(2.5) ...</strong></p><p>&nbsp;</p><p><strong>강좌</strong></p><ul><li><p><strong>스프링 프레임워크 입문 (Udemy)</strong></p></li><li><p><strong>백기선의 스프링 부트 (인프런)</strong></p></li></ul><p>&nbsp;</p><p><strong>특징</strong></p><ul><li><p><strong>스프링 프레임워크 중독자</strong></p></li><li><p><strong>JPA 하이버네이트 애호가</strong></p></li><li><p><strong>유튜브 / 백기선</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:36px"><strong>[2부: 스프링 데이터 JPA 활용]</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">15.스프링 데이터 JPA 활용 파트 소개</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13758&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13758&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><img width="475" height="284" alt="" src="https://lh6.googleusercontent.com/0ZalyGRyhRBvc-EmQ2ty2tgA27jh9d8Fdb0izBsPJBFh8H_re54F6ug5bQQjLaaXVKoyrbkeH4MUsbkCJyOuvblvUMIqSn8WfZgLVMA-BTUlhNth0j84ucKV2oh8502d4yQf2Xme2kozsaIFo8dSPA" /></strong></p><p>&nbsp;</p><p><strong>스프링 데이터</strong></p><p><strong>SQL &amp; NoSQL 저장소 지원 프로젝트의 묶음.</strong></p><p><strong>스프링 데이터 Common</strong></p><p><strong>여러 저장소 지원 프로젝트의 공통 기능 제공.</strong></p><p><strong>스프링 데이터 REST</strong></p><p><strong>저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트.</strong></p><p><strong>스프링 데이터 JPA</strong></p><p><strong>스프링 데이터 Common이 제공하는 기능에 JPA 관련 기능 추가.</strong></p><p>&nbsp;</p><p><strong><a href="http://projects.spring.io/spring-data/">http://projects.spring.io/spring-data/</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">16.스프링 데이터 Common: Repository</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13759&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13759&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/">https://docs.spring.io/spring-data/jpa/docs/current/reference/html/</a></p><p>&nbsp;</p><p><img alt="" width="284" height="488" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj1Q03gd6EQ2pSpctk9oPCF9DiPtSA-PJXn9WEWSK5bVQZDl-N47qEvu9Y4BblDLBVOkpy_hadpJrezJJt6ArHWUz2BR6NUFZOkEHCGcP4TUGxVgSIWH2lRfqSMLIruStMD3YJsuzmJmRIK4vRWug-r8sSFwgb4MweOGW5Tv8Fr0yn9Ja-lccHlVBsalA/s16000/2023-04-08%2013%2057%2018.png" /></p><p>&nbsp;</p><p><strong>application.properties</strong></p><pre class="brush:as3;">#console color spring.output.ansi.enabled=always #Springboot auto build spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true #Datasource Configuration #spring.datasource.hikari.maximum-pool-size=4 spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:tcp://localhost/~/querydsl spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create #spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true #logging.level.org.hibernate.SQL=debug #logging.level.org.hibernate.type.descriptor.sql=trace </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><img width="510" height="329" alt="" src="https://lh4.googleusercontent.com/iSyVVKlZdwFR9p4EjNeDtrsxW6jzlagjuwEKwHDExvZS-K-Ur_OdReIULno-uGFkSX5FyE2zjV0FsgVY5ZuOWMZYzm9LzX2eZN9uaItF6_iGjpnZvhe3sbaqpkpb4SmsslvbG1djfzvZV3LucKA__A" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>PostRepository</strong></p><pre class="brush:as3;">package com.jpa.spring; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRepository extends JpaRepository&lt;Post, Long&gt; { Page&lt;Post&gt; findByTitleContains(String title, Pageable pageable); long countByTitleContains(String title) ; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>PostRepositoryTest</strong></p><pre class="brush:as3;">package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.List; @SpringBootTest @Transactional @Rollback(value = false) class PostRepositoryTest { @Autowired PostRepository postRepository; @Test public void crudRepository(){ //Given Post post =new Post(); post.setTitle(&quot;hello spring boot common&quot;); Assertions.assertThat(post.getId()).isNull(); //when Post newPost=postRepository.save(post); //Then Assertions.assertThat(newPost.getId()).isNotNull(); //When List&lt;Post&gt; posts =postRepository.findAll(); Assertions.assertThat(posts.size()).isEqualTo(1); Assertions.assertThat(posts).contains(newPost); //when Page&lt;Post&gt; page = postRepository.findAll(PageRequest.of(0, 10)); Assertions.assertThat(page.getTotalElements()).isEqualTo(1); Assertions.assertThat(page.getNumber()).isEqualTo(0); Assertions.assertThat(page.getSize()).isEqualTo(10); Assertions.assertThat(page.getNumberOfElements()).isEqualTo(1); //when page= postRepository.findByTitleContains(&quot;spring&quot;, PageRequest.of(0, 10)); Assertions.assertThat(page.getTotalElements()).isEqualTo(1); Assertions.assertThat(page.getNumber()).isEqualTo(0); Assertions.assertThat(page.getSize()).isEqualTo(10); Assertions.assertThat(page.getNumberOfElements()).isEqualTo(1); //when long spring=postRepository.countByTitleContains(&quot;spring&quot;); //Then } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">17.스프링 데이터 Common 2. 인터페이스 정의하기&nbsp;</span></strong></span></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">(중요 x )</span></strong></p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13760&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13760&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Repository 인터페이스로 공개할 메소드를 직접 일일히 정의하고 싶다면</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.jpa.spring; import org.springframework.data.repository.RepositoryDefinition; import java.util.List; @RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository { Comment save(Comment comment); List&lt;Comment&gt; findAll(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest class CommentRepositoryTest { @Autowired CommentRepository commentRepository; @Test public void crud(){ Comment comment=new Comment(); comment.setComment(&quot;Hello Comment&quot;); commentRepository.save(comment); List&lt;Comment&gt; all=commentRepository.findAll(); Assertions.assertThat(all.size()).isEqualTo(1); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>특정 리포지토리 당</strong></p><ul><li><p><strong>@RepositoryDefinition</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;">@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository { Comment save(Comment comment); List&lt;Comment&gt; findAll(); } </pre><p>&nbsp;</p><p><strong>공통 인터페이스 정의</strong></p><ul><li><p><strong>@NoRepositoryBean</strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;">@NoRepositoryBean public interface MyRepository&lt;T, ID extends Serializable&gt; extends Repository&lt;T, ID&gt; { &lt;E extends T&gt; E save(E entity); List&lt;T&gt; findAll(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">18.스프링 데이터 Common 3. Null 처리</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13761&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13761&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 데이터 2.0 부터 자바 8의 Optional 지원.</strong></p><ul><li><p><strong>Optional&lt;Post&gt; findById(Long id);</strong></p></li></ul><p>&nbsp;</p><p><strong>콜렉션은 Null을 리턴하지 않고, 비어있는 콜렉션을 리턴합니다.</strong></p><p>&nbsp;</p><p><strong>스프링 프레임워크 5.0부터 지원하는 Null 애노테이션 지원.</strong></p><ul><li><p><strong>@NonNullApi, @NonNull, @Nullable.</strong></p></li><li><p><strong>런타임 체크 지원 함.</strong></p></li><li><p><strong>JSR 305 애노테이션을 메타 애노테이션으로 가지고 있음. (IDE 및 빌드 툴 지원)</strong></p></li></ul><p>&nbsp;</p><p><strong>인텔리J 설정</strong></p><ul><li><p><strong>Build, Execution, Deployment</strong></p><ul><li><p><strong>Compiler</strong></p><ul><li><p><strong>Add runtime assertion for notnull-annotated methods and parameters</strong></p></li></ul></li></ul></li></ul><p><strong>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img width="602" height="435" alt="" src="https://lh5.googleusercontent.com/oikIY9mcdYY2lOsOfWT9YyhgChSSvQQs_qfo0BF7cUrVR-sUG_MHZB9PLUdYDeFOXqFbLqKK3ulswv_u-LgJf2ihbdbCxYvcKX-VDlVeH8-ZfkozECel3cO2kJC01TbnYjn4u0ZoxbyvMcDROGVnoA" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">19.스프링 데이터 Common: 쿼리 만들기 개요</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13761&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13761&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>스프링 데이터 저장소의 메소드 이름으로 쿼리 만드는 방법</strong><br />메소드 이름을 분석해서 쿼리 만들기 (CREATE)<br />미리 정의해 둔 쿼리 찾아 사용하기 (USE_DECLARED_QUERY)<br />미리 정의한 쿼리 찾아보고 없으면 만들기 (CREATE_IF_NOT_FOUND)</p><p><strong>쿼리 만드는 방법</strong><br />리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)]{정렬 조건} (매개변수)<br />&nbsp;</p><p>&nbsp;</p><p><img alt="" width="680" height="271" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgga74a38wtv0lg06HyjDCYJoIbtLmWpko3CFIMS_YZm6hF0KAcvtKiAEapfpJi4MwSVIAB2q0tahdIr3qQ6uadNiEnwDarqpqTcaOJljve-RQ1sVGETUjIKp4toUsSorORF3q5_LvQ6zGNXJhY292xkrHzLo5NrsaWLEpvn1Z54g50Gq3QGkYIslh9dw/s16000/2023-04-08%2016%2018%2001.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 찾는 방법</strong></p><ul><li><p><strong>메소드 이름으로 쿼리를 표현하기 힘든 경우에 사용.</strong></p></li><li><p><strong>저장소 기술에 따라 다름.</strong></p></li><li><p><strong>JPA: @Query @NamedQuery</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package com.jpa.spring; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; //@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class) public interface CommentRepository extends JpaRepository&lt;Comment, Long&gt; { Comment save(Comment comment); List&lt;Comment&gt; findAll(); @Query(&quot;select c.comment from Comment as c &quot;) List&lt;String&gt; findByCommentContains(String comment); Page&lt;Comment&gt; findByCommentContains(String comment, Pageable pageable); Page&lt;Comment&gt; findByLikeCountGreaterThanAndPost(int likeCount, Post post, Pageable pageable); } </pre><p>&nbsp;</p><p><strong>CommentRepositoryTest</strong></p><pre class="brush:as3;">package com.jpa.spring; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import java.util.List; import java.util.Optional; @SpringBootTest class CommentRepositoryTest { @Autowired CommentRepository commentRepository; @Test public void crud(){ Comment comment=new Comment(); comment.setComment(&quot;Hello Comment&quot;); commentRepository.save(comment); List&lt;Comment&gt; all=commentRepository.findAll(); Assertions.assertThat(all.size()).isEqualTo(1); Optional&lt;Comment&gt; byId=commentRepository.findById(100l); Assertions.assertThat(byId).isEmpty(); Comment comment1= byId.orElseThrow((IllegalArgumentException::new)); } @Test public void findByTitleContainsTest(){ Comment comment=new Comment(); comment.setComment(&quot;aaa&quot;); commentRepository.save(comment); Comment comment2=new Comment(); comment2.setComment(&quot;bbb&quot;); commentRepository.save(comment2); List&lt;String&gt; aaa = commentRepository.findByCommentContains(&quot;aaa&quot;); aaa.forEach(a-&gt; System.out.println(&quot;a = &quot; + a)); Page&lt;Comment&gt; commentPage = commentRepository.findByCommentContains(&quot;aaa&quot;, PageRequest.of(0, 10)); System.out.println(&quot;commentPage.getTotalElements() = &quot; + commentPage.getTotalElements()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">20.스프링 데이터 Common: 쿼리 만들기 실습</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13763&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13763&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>기본 예제</strong></p><pre class="brush:as3;">List&lt;Person&gt; findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // distinct List&lt;Person&gt; findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List&lt;Person&gt; findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // ignoring case List&lt;Person&gt; findByLastnameIgnoreCase(String lastname); // ignoring case List&lt;Person&gt; findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>정렬</strong></p><pre class="brush:as3;">List&lt;Person&gt; findByLastnameOrderByFirstnameAsc(String lastname); List&lt;Person&gt; findByLastnameOrderByFirstnameDesc(String lastname);</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>페이징</strong></p><pre class="brush:as3;">Page&lt;User&gt; findByLastname(String lastname, Pageable pageable); Slice&lt;User&gt; findByLastname(String lastname, Pageable pageable); List&lt;User&gt; findByLastname(String lastname, Sort sort); List&lt;User&gt; findByLastname(String lastname, Pageable pageable); </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>스트리밍</strong></p><p><strong>Stream&lt;User&gt; readAllByFirstnameNotNull();</strong></p><ul><li><p><strong>try-with-resource 사용할 것. (Stream을 다 쓴다음에 close() 해야 함)</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">21.스프링 데이터 Common: 비동기 쿼리</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13764&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13764&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>비동기 쿼리</strong></p><p><strong>@Async Future&lt;User&gt; findByFirstname(String firstname);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</strong></p><p><strong>@Async CompletableFuture&lt;User&gt; findOneByFirstname(String firstname);&nbsp;</strong></p><p><strong>@Async ListenableFuture&lt;User&gt; findOneByLastname(String lastname);&nbsp;</strong></p><ul><li><p><strong>해당 메소드를 스프링 TaskExecutor에 전달해서 별도의 쓰레드에서 실행함.</strong></p></li><li><p><strong>Reactive랑은 다른 것임</strong></p></li></ul><p>&nbsp;</p><p><strong>권장하지 않는 이유</strong></p><ul><li><p><strong>테스트 코드 작성이 어려움.</strong></p></li><li><p><strong>코드 복잡도 증가.</strong></p></li><li><p><strong>성능상 이득이 없음.&nbsp;</strong></p><ul><li><p><strong>DB 부하는 결국 같고.</strong></p></li><li><p><strong>메인 쓰레드 대신 백드라운드 쓰레드가 일하는 정도의 차이.</strong></p></li><li><p><strong>단, 백그라운드로 실행하고 결과를 받을 필요가 없는 작업이라면 @Async를 사용해서 응답 속도를 향상 시킬 수는 있다.</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">22.스프링 데이터 Common: 커스텀 리포지토리</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13765&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13765&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.0.5&amp;packaging=jar&amp;jvmVersion=17&amp;groupId=com.example&amp;artifactId=demojap3&amp;name=demojap3&amp;description=Demo%20project%20for%20Spring%20Boot&amp;packageName=com.example.demojap3&amp;dependencies=devtools,lombok,data-jpa,postgresql,h2">프로젝트 생성</a></p><p>&nbsp;</p><p>&nbsp;</p><p>링크&nbsp; :&nbsp;&nbsp;<a target="_blank" href="https://github.com/braverokmc79/demojpa3">https://github.com/braverokmc79/demojpa3</a></p><p>&nbsp;</p><p>&nbsp;</p><p>참조 :</p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2104#">실전! * Querydsl - 7. 스프링 데이터 JPA와 Querydsl,스프링 데이터 JPA 리포지토리로 변경,사용자 정의 리포지토리, Querydsl 페이징 연동,CountQuery 최적화 ,컨트롤러 개발</a></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2104">https://macaronics.net/index.php/m01/spring/view/2104</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 메소드(쿼리 생성과 쿼리 찾아쓰기)로 해결이 되지 않는 경우 직접 코딩으로 구현 가능.</strong></p><ul><li><p><strong>스프링 데이터 리포지토리 인터페이스에 기능 추가.</strong></p></li><li><p><strong>스프링 데이터 리포지토리 기본 기능 덮어쓰기 가능.</strong></p></li><li><p><strong>구현 방법</strong></p><ol><li><p><strong>커스텀 리포지토리 인터페이스 정의&nbsp;</strong></p></li><li><p><strong>인터페이스 구현 클래스 만들기 (기본 접미어는 Impl)</strong></p></li><li><p><strong>엔티티 리포지토리에 커스텀 리포지토리 인터페이스 추가</strong></p></li></ol></li></ul><p>&nbsp;</p><p><strong>기능 추가하기</strong></p><p>&nbsp;</p><p><strong>기본 기능 덮어쓰기</strong></p><p>&nbsp;</p><p><strong>접미어 설정하기</strong></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhhnSIUIRi7hPjpwj1WSgOWki7r3KVnWCqR4VhSPyqg6OTXIovQxcZ296z73Znybiy4JdisgituRg0ZDNeNXsgk--cpZ-G7shYc5qMLrowiWvafdheTYKr62KzEFfU2yV-7d461oSXTjlRwzBME3SXGduy3WX802jjNi2f6KBqsfXkWxGJ2gTu6cnzZKA/s16000/2023-04-09%2014%2026%2030.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1) AppConfig</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory(){ return new JPAQueryFactory(em); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>2 )Post</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import jakarta.persistence.*; import lombok.Data; import lombok.ToString; import java.util.Date; @Entity @Data @ToString(of={&quot;title&quot;, &quot;content&quot;}) public class Post { @Id @GeneratedValue private Long id; private String title; @Lob private String content; @Temporal(TemporalType.TIMESTAMP) private Date created; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>3)PostDto</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; import lombok.ToString; import java.util.Date; @Data @ToString public class PostDto { private Long id; private String title; private String content; private Date created; @QueryProjection public PostDto(Long id, String title, String content, Date created) { this.id = id; this.title = title; this.content = content; this.created = created; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">4) PostSearchCondition</span></strong></p><pre class="brush:as3;">package com.example.demojap3.post; import lombok.Data; @Data public class PostSearchCondition { private String title; private String content; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>5) PostCustomRepository</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; public interface PostCustomRepository { public Page&lt;PostDto&gt; searchPostList(PostSearchCondition condition, Pageable pageable) ; public List&lt;Post&gt; findMyPost(); public void deletePost(Post entity); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>6)&nbsp;&nbsp;PostCustomRepositoryImpl</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import java.util.List; import static com.example.demojap3.post.QPost.post; import static org.springframework.util.StringUtils.hasText; //@Repository , @Transactional 생략 가능 //@Repository //@Transactional @RequiredArgsConstructor public class PostCustomRepositoryImpl implements PostCustomRepository{ private final JPAQueryFactory queryFactory; private final EntityManager entityManager; @Override public Page&lt;PostDto&gt; searchPostList(PostSearchCondition condition, Pageable pageable) { List&lt;PostDto&gt; postDtoList = queryFactory.select( new QPostDto(post.id, post.title, post.content, post.created) ).where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .from(post).fetch(); JPAQuery&lt;Long&gt; countQuery = queryFactory .select(post.count()) .from(post) .where(titleEq(condition.getTitle()), contentEq(condition.getContent()) ); return PageableExecutionUtils.getPage(postDtoList, pageable, countQuery::fetchOne); } @Override public List&lt;Post&gt; findMyPost() { return entityManager.createQuery(&quot;SELECT p FROM Post AS p &quot; , Post.class).getResultList(); } @Override public void deletePost(Post entity) { System.out.println(&quot;custom delete&quot;); entityManager.remove(entity); } private BooleanExpression titleEq(String title) { return hasText(title) ? post.title.eq(title) :null; } private BooleanExpression contentEq(String content) { return hasText(content) ? post.content.eq(content) :null; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>7)PostRespository</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import org.springframework.data.jpa.repository.JpaRepository; public interface PostRespository extends JpaRepository&lt;Post, Long&gt; , PostCustomRepository { } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>8) 테스트&nbsp;</strong></span></p><pre class="brush:as3;">package com.example.demojap3.post; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @SpringBootTest @Transactional @Rollback(value = false) class PostRepositoryTest { @Autowired PostRespository postRespository; @Test public void crud(){ Post post=new Post(); post.setTitle(&quot;hello1&quot;); post.setContent(&quot;content1&quot;); post.setCreated(new Date()); postRespository.save(post); Post post2=new Post(); post2.setTitle(&quot;hello1&quot;); post2.setContent(&quot;content2&quot;); post2.setCreated(new Date()); postRespository.save(post2); Post post3=new Post(); post3.setTitle(&quot;hello3&quot;); post3.setContent(&quot;content3&quot;); post3.setCreated(new Date()); postRespository.save(post3); PostSearchCondition condition =new PostSearchCondition(); condition.setTitle(&quot;hello1&quot;); Page&lt;PostDto&gt; postList =postRespository.searchPostList(condition, PageRequest.of(0, 10)); System.out.println(&quot;postList.getTotalElements() = &quot; + postList.getTotalElements()); postList.forEach(postDto -&gt; System.out.println(&quot;postDto.toString() = &quot; + postDto.toString())); Assertions.assertThat(postList.getNumberOfElements()).isEqualTo(2); } @Test public void findMyPostTest(){ System.out.println(&quot;custom findMyPostTest ===&gt;&quot;); postRespository.findMyPost(); } @Test public void crud2(){ Post post=new Post(); post.setTitle(&quot;hello1&quot;); post.setContent(&quot;content1&quot;); post.setCreated(new Date()); postRespository.save(post); postRespository.deletePost(post); postRespository.flush(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">★</span></strong></span><span style="color:#c0392b"><strong><span style="font-size:28px">23.스프링 데이터 Common: 기본 리포지토리 커스터마이징</span></strong></span></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">공통적으로 구현 클해스&nbsp;MyRepository</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;&nbsp;<a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13765&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13765&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있다면&nbsp;</strong></p><p>&nbsp;</p><ol><li><p><strong>JpaRepository를 상속 받는 인터페이스 정의</strong></p><ul><li><p><strong>@NoRepositoryBean</strong></p></li></ul></li><li><p><strong>기본 구현체를 상속 받는 커스텀 구현체 만들기</strong></p></li><li><p><strong>@EnableJpaRepositories에 설정</strong></p><ul><li><p><strong>repositoryBaseClass</strong></p></li></ul></li></ol><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@NoRepositoryBean public interface MyRepository&lt;T, ID extends Serializable&gt; extends JpaRepository&lt;T, ID&gt; { boolean contains(T entity); } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">public class SimpleMyRepository&lt;T, ID extends Serializable&gt; extends SimpleJpaRepository&lt;T, ID&gt; implements MyRepository&lt;T, ID&gt; { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation&lt;T, ?&gt; entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public boolean contains(T entity) { return entityManager.contains(entity); } } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@EnableJpaRepositories(repositoryBaseClass = SimpleMyRepository.class) </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">public interface PostRepository extends MyRepository&lt;Post, Long&gt; { } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">&nbsp;</span></strong></span><span style="color:#c0392b"><strong><span style="font-size:28px">24.스프링 데이터 Common: 도메인 이벤트</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;<a href="http:// https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13767&amp;tab=curriculum">&nbsp;https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13767&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">강의 내용 이해 안가면, 다음 내용을 보세요.</span></strong></p><p>&nbsp;</p><p>출처:&nbsp;</p><p><a target="_blank" href="https://parkadd.tistory.com/141">https://parkadd.tistory.com/141</a></p><p>&nbsp;</p><p><strong>모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있다면&nbsp;</strong></p><p>&nbsp;</p><p><strong>JpaRepository를 상속 받는 인터페이스 정의</strong><br />@NoRepositoryBean</p><p><br /><strong>기본 구현체를 상속 받는 커스텀 구현체 만들기</strong><br />@EnableJpaRepositories에 설정</p><p><br /><strong>repositoryBaseClass</strong><br />&nbsp;</p><pre class="brush:as3;">@NoRepositoryBean public interface MyRepository&lt;T, ID extends Serializable&gt; extends JpaRepository&lt;T, ID&gt; { boolean contains(T entity); } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">public class SimpleMyRepository&lt;T, ID extends Serializable&gt; extends SimpleJpaRepository&lt;T, ID&gt; implements MyRepository&lt;T, ID&gt; { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation&lt;T, ?&gt; entityInformation, EntityManager entityManager) { super(entityInformation, entityManager); this.entityManager = entityManager; } @Override public boolean contains(T entity) { return entityManager.contains(entity); } } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@EnableJpaRepositories(repositoryBaseClass = SimpleMyRepository.class) </pre><p>&nbsp;</p><pre class="brush:as3;">public interface PostRepository extends MyRepository&lt;Post, Long&gt; { } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px">Spring 에서 지원하는 이벤트 관련 기능</span></p><p><br />Spring은 이벤트 관련 기능을 지원해줍니다.</p><p>ApplicationEventPublisher - 이벤트 발행자<br />ApplicationEvent - 이벤트 객체<br />ApplicationListener - 이벤트 리스너<br />@EventListener<br />&nbsp;</p><p>위의 기능을 활용하면 이벤트를 발행하고, 이벤트 리스너가 이벤트에 대한 처리를 담당할 수 있습니다.</p><p>&nbsp;</p><p>먼저 간단하게 순서를 설명하면 아래와 같습니다.</p><p>&nbsp;</p><p>1. ApplicationEvent(이벤트) 객체를 생성</p><p>2. 1에서 생성한 ApplicationEvent 객체를 ApplicationEventPublisher(이벤트 발행자)에게 발행을 요청.</p><p>3. 이벤트가 발행되면 ApplicationListener 가 이벤트를 처리</p><p>&nbsp;</p><p>그럼 간단한 예시로 먼저 알아보겠습니다.</p><p>&nbsp;</p><p>먼저 예시용 엔티티인 Post 클래스입니다. Post는 하나의 글을 의미합니다.</p><p>PostRepository도 함께 사용하겠습니다. (JpaRepository 사용)</p><p>&nbsp;</p><pre class="brush:as3;">@Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Lob private String content; protected Post() { } public Post(String content) { this.content = content; } ... Getter } public interface PostRepository extends JpaRepository&lt;Post, Long&gt; { }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>여기까지 예시에서 사용할 도메인 객체를 만들었습니다.</p><p>다음으로 Post의 이벤트 객체를 생성합니다.</p><p>Post의 발행(저장)할 때의 이벤트를 만듭니다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">public class PostPublishedEvent extends ApplicationEvent { private final Post post; public PostPublishedEvent(Object source) { super(source); this.post = (Post) source; } public Post getPost() { return post; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>ApplicationEvent를 상속받으며 필드로 발행된 Post를 가집니다.</p><p>다음으로 이벤트를 받아서 처리하는 이벤트 리스너를 만들겠습니다.</p><p>이벤트 리스너는 두 가지 스타일로 만들 수 있습니다. (스프링 버전에 따라 애너테이션 방식은 안될 수 있습니다.)</p><ul><li>생성한 Listener 클래스가 ApplicationListener 를 implements(구현) 한다.</li><li>이벤트 처리 메서드에 @EventListener 애너테이션을 추가한다.</li></ul><p>아래 예시에서 두 가지 방법을 모두 보시고 마음에 드시는 방법을 사용하시면 됩니다!</p><p>단, 두 가지 방법 모두 빈으로 등록되어야 합니다. @Component 또는 설정 클래스(@Configuration)에서 빈으로 등록해주셔야 합니다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Component public class PostListener { @EventListener public void onApplicationEvent(PostPublishedEvent event) { System.out.println(&quot;======================&quot;); System.out.println(&quot;Post 이벤트 발행, Post Id = &quot; + event.getPost().getId() + &quot;, Content = &#39;&quot; + event.getPost().getContent() + &quot;&#39;&quot;); System.out.println(&quot;======================&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Post의 이벤트 객체와 이벤트 리스너를 모두 만들었으니 이벤트를 발행하고 처리하는 테스트를 작성해보겠습니다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@SpringBootTest class PostEventTest { @Autowired ApplicationEventPublisher applicationEventPublisher; @Test void eventListener() { Post post = new Post(&quot;hello world&quot;); PostPublishedEvent event = new PostPublishedEvent(post); applicationEventPublisher.publishEvent(event); } } // 실행결과 // ====================== // Post 이벤트 발행, Post Id = null, Content = &#39;hello world&#39; // ======================</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>여기까지 스프링에서 지원하는 이벤트 관련 기능 예시였습니다.</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>Spring Data 에서 지원하는 이벤트 자동 발행 기능</strong></span></p><p>Spring Data에서는 Repository에서 save할 때 이벤트 자동 발행 기능을 제공합니다.</p><p>AbstractAggregateRoot를 사용해서 구현할 수 있습니다.</p><p>&nbsp;</p><p>AbstractAggregateRoot를 사용하면 다음과 같은 것들이 가능합니다.</p><ul><li>Post 객체를 저장 시(save 메서드 호출 시) 이벤트를 발행, 처리할 수 있다.</li><li>Post 객체에 이벤트 객체를 여러개 저장해두고 저장 시(save 메서드 호출 시) 여러개의 이벤트를 발행, 처리할 수 있다.</li><li>이벤트 발행 후 모아놨던 모든 이벤트를 제거한다. (메모리 누수 차단)</li></ul><p>&nbsp;</p><p>AbstractAggregateRoot의 내부에 대해서 좀 더 자세히 알아보겠습니다.</p><ul><li>List&lt;Object&gt; domainEvents - 이벤트 객체를 모아놓는 필드입니다.</li><li>&lt;T&gt; T registerEvent(T event) - domainEevents 에 이벤트를 추가하고 추가한 이벤트를 반환합니다.</li><li>A andEvent(Object event) -&nbsp;domainEevents 에 이벤트를 추가하고 현재 엔티티 객체(Aggregate)를 반환합니다.</li><li>clearDomainEvents() - 이벤트 발행 후 모아놨던 모든 이벤트를 제거합니다.</li><li>domainEvents() - 현재 쌓여있는 모든 이벤트(domainEvents)를 반환합니다.</li></ul><p>이해가 가지 않는다면 예시와 함께 봐주세요!</p><p>엔티티 클래스가 AbstractAggregateRoot를 상속하도록 합니다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Entity public class Post extends AbstractAggregateRoot&lt;Post&gt; { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Lob private String content; protected Post() { } public Post(String content) { this.content = content; } // 이벤트 추가 후 자기 자신을 반환 public Post publish() { return this.andEvent(new PostPublishedEvent(this)); } ... Getter }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>publish() 메서드를 보면 이벤트를 추가합니다.</p><p>이제 해당 Post 객체를 save() 메서드로 영속화할 때 이벤트가 발행됩니다.</p><p>&nbsp;</p><pre class="brush:as3;">@SpringBootTest class PostEventTest { @Autowired PostRepository postRepository; @Test void domainEvent1() { Post newPost = new Post(&quot;hello world&quot;); postRepository.save(newPost.publish()); System.out.println(&quot;newPost 저장!!!&quot;); Post newPost2 = new Post(&quot;hello charlie!!&quot;); postRepository.save(newPost2.publish()); System.out.println(&quot;newPost2 저장!!!&quot;); } } // 실행 결과 // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 1, Content = &#39;hello world&#39; // ====================== // newPost 저장!!! // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 2, Content = &#39;hello charlie!!&#39; // ====================== // newPost2 저장!!!</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>다음의 순서로 이벤트가 발행, 처리됩니다.</p><p>1. 이벤트 객체(PostPublishedEvent)를 생성, 엔티티 객체의 domainEvents 에 추가합니다. (Post의 publis() 메서드에서 일어남)</p><p>2. Repository에서 해당 엔티티 객체로 save 메서드를 실행할 때, domainEvents의 모든 이벤트 발행(정확히는 데이터가 영속화 된 후)</p><p>3. EventListener(PostListener) 에서 이벤트를 처리.</p><p>&nbsp;</p><p>그럼 여러개의 이벤트를 추가해놓으면 Repository의 save 메서드 호출시 한번에 이벤트가 발행될까요?</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @Test void domainEvent2() { Post newPost = new Post(&quot;hello world&quot;); newPost.addPublishEvent(); newPost.addPublishEvent(); System.out.println(&quot;newPost save 메서드 호출!!!&quot;); postRepository.save(newPost); System.out.println(&quot;newPost 저장!!!&quot;); } // 실행결과 // ... insert 쿼리 생략 // ====================== // Post 이벤트 발행, Post Id = 1, Content = &#39;hello world&#39; // ====================== // ====================== // Post 이벤트 발행, Post Id = 1, Content = &#39;hello world&#39; // ====================== // newPost 저장!!!</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>위 예시처럼 여러개의 이벤트를 넣어 놓고 save를 호출하면 여러개의 이벤트가 발행됩니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">&nbsp;</span></strong></span><span style="color:#c0392b"><strong><span style="font-size:28px">25.스프링 데이터 Common 10.QueryDSL 연동</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;<a href="http:// https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13768&amp;tab=curriculum">&nbsp;https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13768&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>다음 참조 :</strong></p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2105#">실전! *&nbsp;&nbsp;Querydsl -8.스프링 데이터 JPA가 제공하는 Querydsl 기능, ★4.3 스프링 부트 2.6 이상, Querydsl 5.0 지원 방법 , Querydsl4RepositorySupport 사용 코드</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">22.번 볼것</span></strong></span></p><p>&nbsp;</p><p>findByFirstNameIngoreCaseAndLastNameStartsWithIgnoreCase(String firstName, String lastName)&nbsp;<br />이게 이게 뭐냐... @_@ 어지러우시죠?? 이 정도 되면 그냥 한글로 주석을 달아 두시는게...</p><p>여러 쿼리 메소드는 대부분 두 가지 중 하나.<br />Optional&lt;T&gt; findOne(Predicate): 이런 저런 조건으로 무언가 하나를 찾는다.<br />List&lt;T&gt;|Page&lt;T&gt;|.. findAll(Predicate): 이런 저런 조건으로 무언가 여러개를 찾는다.<br />QuerydslPredicateExecutor 인터페이스</p><p>QueryDSL<br />http://www.querydsl.com/<br />타입 세이프한 쿼리 만들 수 있게 도와주는 라이브러리<br />JPA, SQL, MongoDB, JDO, Lucene, Collection 지원<br />QueryDSL JPA 연동 가이드</p><p>스프링 데이터 JPA + QueryDSL<br />인터페이스: QuerydslPredicateExecutor&lt;T&gt;<br />구현체: QuerydslPredicateExecutor&lt;T&gt;</p><p><strong>연동 방법</strong></p><ul><li><p><strong>기본 리포지토리 커스터마이징 안 했을 때. (쉬움)</strong></p></li><li><p><strong>기본 리포지토리 커스타마이징 했을 때. (해맬 수 있으나... 제가 있잖습니까)</strong></p></li></ul><p>&nbsp;</p><p><strong>의존성 추가</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>SimpleMyRepository</strong></p><pre class="brush:as3;">package com.example.demojap3; import jakarta.persistence.EntityManager; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.QuerydslJpaRepository; import java.io.Serializable; public class SimpleMyRepository&lt;T, ID extends Serializable&gt; extends QuerydslJpaRepository&lt;T, ID&gt; implements MyRepository&lt;T, ID&gt; { private EntityManager entityManager; public SimpleMyRepository(JpaEntityInformation&lt;T, ?&gt; entityInformation, EntityManager entityManager) { super((JpaEntityInformation&lt;T, ID&gt;) entityInformation, entityManager); this.entityManager=entityManager; } @Override public boolean contains(T entity){ return entityManager.contains(entity); } } </pre><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.post; import com.example.demojap3.MyRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; public interface PostRepository extends MyRepository&lt;Post, Long&gt;, PostCustomRepository, QuerydslPredicateExecutor&lt;Post&gt;{ } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong><span style="font-size:28px">&nbsp;</span></strong></span><span style="color:#c0392b"><strong><span style="font-size:28px">26.QueryDSL 연동 보강</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :&nbsp;<a href="http:// https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=38371&amp;tab=curriculum">&nbsp;https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=38371&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>다음 강의 를 볼것</strong></span></p><p><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20Querydsl/page/1">https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20Querydsl/page/1</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-07 21:33:22 Vue PWA 푸시 알림 서비스 구현 - 웹 푸시 알림(Web Push Notification) - 5 ★ http://macaronics.net/index.php/m04/vue/view/2116 2116 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ul><li><p><img alt="Do it! 프로그레시브 웹앱 만들기 대표 이미지" width="320" height="439" src="https://contents.kyobobook.co.kr/sih/fit-in/458x0/pdt/9791163031765.jpg" /></p></li><li>&nbsp;</li></ul><p>&nbsp;</p><p>출처:&nbsp;</p><p><a target="_blank" href="https://product.kyobobook.co.kr/detail/S000001817978">https://product.kyobobook.co.kr/detail/S000001817978</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>소스</strong></p><p><a target="_blank" href="https://github.com/codedesign-webapp">https://github.com/codedesign-webapp</a></p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/codedesign-webapp/pwa-about">https://github.com/codedesign-webapp/pwa-about</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>푸쉬&nbsp; 프로젝트&nbsp; ex13</strong></span></p><p><a target="_blank" href="https://github.dev/codedesign-webapp/pwa-example">https://github.dev/codedesign-webapp/pwa-example</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong><span style="font-size:20px">★★ 웹 푸시 알림(Web Push Notification&nbsp;★</span></strong></span></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong><span style="font-size:20px">=&gt;&nbsp;</span></strong></span><a target="_blank" href="https://geundung.dev/114">https://geundung.dev/114</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:26px"><strong>★파이어베이스 공식 문서</strong></span></span></p><p><span style="font-size:16px"><span style="color:#8e44ad"><strong>=</strong></span><span style="color:#8e44ad"><strong>&gt;</strong></span></span></p><p><span style="font-size:16px"><a target="_blank" href="https://firebase.google.com/docs/web/setup?authuser=0&amp;hl=ko"><span style="color:#8e44ad"><strong>https://firebase.google.com/docs/web/setup?authuser=0&amp;hl=ko</strong></span></a></span></p><p>=&gt;</p><p><a target="_blank" href="https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko">https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://console.firebase.google.com/project/my-project-49bf8/messaging/onboarding?hl=ko">https://console.firebase.google.com/project/my-project-49bf8/messaging/onboarding?hl=ko</a></p><p><img alt="" width="900" height="463" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiiY7vR_Ys9RfHQJINTOTSgplAsZymf5guqhC6p2zGBQm-3JMgiRKEtoIhYP2GTOH19dND8iFxI-treZO2u3ooodJCdffmdiTRlR5vSOZvnt0j-auruZIe3_7FaO0wWnKwBji7DtObKIldnYPjwmwHM4CnMVnkgJh1fzSmccwEfuxXIWASqs-QHEPrZGQ/s16000/2023-04-09%2021%2055%2057.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>Vuefire</strong></span></span></p><p><a target="_blank" href="https://v2.vuefire.vuejs.org/"><strong>Realtime bindings between Vue/Vuex and Firebase</strong></a></p><p><span style="color:#c0392b"><strong>공시문서 반드시 참조 할것</strong></span></p><p>&nbsp;</p><p><a target="_blank" href="https://vuefire.vuejs.org/">https://vuefire.vuejs.org/</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:28px">1.푸시 알림 서비스 구경하기</span></span></strong></p><p>&nbsp;</p><p>푸시알림이란 공지가 필요한 메시지를 발송하여 사용자가 모바일 기기에서 확인 할 수 있는 서비스입니다. 전톡적으로 네이티브 모바일 앱에서 고객 관리를 목적으로</p><p>중요하게 활용되고 있습니다. PWA 에서도 이러한 푸시 알림 기능을 지원하며 웹 푸시 알림 이라는 명칭으로 구분합니다.</p><p>&nbsp;</p><p>웹 푸시 알림은 네이티브 앱과 똑같은 방식으로 모바일 기기뿐만 아니라 데스크톱에서도 제공함으로써 플랫폼 종류를 더욱 확장하고 사용자의 편의석을 높입니다.</p><p>그리고 웹 푸시 알림은&nbsp; 운영체제 차원에서 지원하므로 웹 브라우저 활성화 여부와 관계없이 언제든지 알림을 전달합니다.&nbsp;</p><p>따라서 네이티브 앱과 같은 경험을 제공할 수 있습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>푸시 알림 서비스의 실행 구조</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=dilrong&amp;logNo=221532597601">https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&amp;blogId=dilrong&amp;logNo=221532597601</a></p><p><img alt="" src="https://mblogthumb-phinf.pstatic.net/MjAxOTA1MDhfMTgy/MDAxNTU3MzAzOTg0OTA4.MMhcrgMHMPTMJ47qC9gh6HEIR3gJP1beSzFvgn64iqwg.Y6ZoF-RPOtjFC_LbH-P2SvQr9Ob2Dx1b0G309xWDGpMg.PNG.dilrong/messaging-overview.png?type=w800" /></p><p>&nbsp;</p><p>&nbsp;</p><p>구독자의 수신 주소라 할 수 있는 종단점(endpoint) 정보를 관리하는 DB 서버와 푸시 알림을&nbsp; 발송하도록 푸시 서비스에 요청하는 애플리케이션 서버를 파이어베이스 DB,</p><p>파이어베이스 함수로 각각 구축할 것입니다. 푸시 서비스는 웹 브라우저에서 자동으로 지원하는데&nbsp; 크롬 브라우저는 자동으로 FCM 을 사용하므로 별도로 구축할 필요는</p><p>없습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>공개 키와 비공개 키</strong></span></span></p><p>&nbsp;</p><p>공개 키와&nbsp; 비공개 키는 하나의&nbsp; 쌍으로 생성되며 인증받은 사용자만 중요한 데이터에 접근할 수 있습니다.&nbsp; &nbsp;</p><p>공개 키는 일반적으로 신뢰할 수 있는 기관에 제공하며 열려 있으므로 누구나 열람하고 사용할 수 있습니다. 반면에 비공개 키는 암호화된 데이터를</p><p>해독할 때 공개 키와 함께 사용해야 합니다. 그러므로 절대로 외부에 유출되어서는 안 되는 중요한 키입니다.</p><p>&nbsp;</p><p>또한 두 키의 역할을 보면 차이점을 이해 할 수 있습니다. 공개 키가 있다는 것은 신뢰할 만한 외부 기관에 데이터를 보관하고 있다는 의미입니다.</p><p>그리고 비공개 키가 있다는 것은 데이터를 소유한 주인이라는 의미입니다. 공개 키와 비공개 키를 함께 사용한다는 것은 데이터의 주인이 신뢰할 만한</p><p>기관을 통해 데이터에 접근하고 있다는 뜻이므로 데이터를 해독해 사용해도 됩니다.</p><p>&nbsp;</p><p>다음 그림은 이러한 관계를 친구 사인</p><p>&nbsp;</p><p><img alt="" src="https://blog.kakaocdn.net/dn/czMAVX/btrbW2Mk0aQ/kCOKvkqq6UCO4NFPC93Skk/img.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>이러한 관계를 이번 예제에 적용하면 비공개 키를 소유한 것은 푸시 알림 PWA 가 되고, 공개키는 신뢰할 수 있는 알림 서비스인 FCM 에서만 관리합니다.&nbsp;</p><p>다음 그림은 푸시 알림 서비스에서 공개 키와 비공개 키가 어떻게 사용되는지 정리한 것입니다.&nbsp; 종단점에는 어떤 푸시 서버를 이용해서 어떤 사용자</p><p>기기에 발송할 것인지에 관한 정보가 담겨 있습니다.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">$ npm install $ npm rub build $ server dist</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>먼저&nbsp;윈도우&nbsp;&nbsp;검색창에&nbsp; &quot;알림 및 작업&quot; 입력후&nbsp;&nbsp; 알림이 켜져 있는지 확인해 보자.&nbsp; &nbsp;</strong></span></p><p>&nbsp;</p><p><img alt="" width="600" height="361" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEieSFhVVh8UHMIkHblxv6hq2xix_41fnbn7vMDhvsBYW_XC-fU4dAqzrLhIOdazFAWiix0FygBIvsMXIVViYbtJ3-UIN8u37lSVtzvwDn4pSQ201kxRO4WWF6YEzm0OJ5K-2eK_8BnEuZGckSvXgyuV1jtL-ck1yd1LaymAluz2H0M2FYXHcFxf5EH6Cg/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>완성된 프로젝트 파일 ex13 에서 첫 화면에서&nbsp; 알림 허용을 클릭해서 푸시 알림을 등록합니다. 그러면 웹 브라우저 위쪽에 팝업 메뉴가 나타나서</p><p>알림 표시 여부를 묻습니다. 여기서 &lt;허용&gt; 을 클릭하면 푸시 알림 서비스에 등록됩니다. 즉, 이 컴퓨터는 지금부터 푸시 알림 서비스를 받습니다.</p><p>&nbsp;</p><p>구글 계정에 로그인 된 브라우저만 적용이 된다.</p><p><img alt="" width="600" height="641" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQ95IS8_78Kdyhc9oxf4nKgZNRR59PIj-mlmeaUCt0_SOXLXUZHKsrlKTg-iczyw2w-45AhjKVaLrwriA267ZPBpwn0llTRSCLCNIim-OmUYDnWzvFuGOiAaAiVkpE84Ef_l86fK2nL5eQrN_Jd04bMyykr28SJLOlRq3m_KbQvbKNmG-kCLgAPzCPhw/s16000/1.png" /></p><p>&nbsp;</p><p>구글 계정에 로그인 되어 있지 않으면 다음과 같이 콘솔창에&nbsp; 푸시 창에 푸시알림 기능이 허용되지 않았다는&nbsp;</p><p>메시지가 나온다.</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi1JxI5D8hFExzoGcML3MGlRwwD25zbj3zo_5zPniBvn-rivozy5eZYV9LZR1zGJKWPXDnqQKkTM0u8scG7KKXnL0ZfhCheGuUgUPqEp3DebRnqvtuqUNOLCDjJsDQuFTGrlzS6FxOZIWJEm1wJzbLjBJaqtwDUQeRBUEDfyCgJeKXVWj8Ej2z7gM0f1Q/s16000/2.png" /></p><p>알림 버튼을 클릭하면 잠시 후 컴퓨터 바탕화면 오른쪽 아래에 알림 서비스 등록을 환영하는 메시창이 나타난다.</p><p>&nbsp;</p><p><img alt="" width="900" height="420" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyXOM9Jisxxg1HJXaV73yE4a0oRu96fh_OMitPBC4CuSAgHke89l_PYyWzm30qID9ETMDZxy64mzhVoowGuytEs_m4esQiZEpETWCjOf9eprpsK3Jy_VkUq8Oow8ozm2eLRoiQI3zsJfd98OasDv_KPZj1gsfp56Z5Cs2BoWTooOaXo65qhikcf7b4qA/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>푸시 알림을 허용한 후 보이는&nbsp; 알림 창에서 아래쪽에 커피 아이콘이나 &lt;커피를 좋아하시면 링크를 클릭하세요.&gt; 를 클릭하면 스타벅스 홈페이지가 나타납니다.</p><p>이 처럼 푸시 알림은 링크를 사용해 정보를 전달하는 웹 사이트에 연결할 수도 있다.</p><p>&nbsp;</p><p>&nbsp;</p><p>알림을 해제하고 싶다면 언제든&nbsp; 알림해제를 누르면 됩니다.</p><p><img alt="" width="450" height="688" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjFqDsRI-gajZaAb9ysBaomUVyEiB888gIqN8yfmUVmDk_gWYfyNvY5lNGL7-KzDQ-WjhEiWHEldcGMQmgPjuG2VaNhnwzVA_AQUmNHOEEVTD2U7opRnKjZ-1NsCdM0TrGfsaEX4kHt2gZLVR0JFprkbVCIjnK4vLG6Lwfw4lf_JYv1xA5z9oUoIcvDZw/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>이어서 푸시 알림 메시지를 보내겠습니다. 햄버거 아이콘(=)을 눌러 메뉴 목록을 표시하고 , [푸시 알림 메시지 보내기]를 선택해서 운영자 화면으로 변경합니다.</p><p>제목과 내용에 발송할 내용을 입력하고 발송을 클립합니다.</p><p>&nbsp;</p><p><img alt="" width="600" height="548" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEix9Ce3jEIs2rnnURqfFRLe3CFbAqB0r0xHvqv7C9JU_N4p1owqnKZBzFwkwbyj5O-PlpZD2m5R5gvD98kmK2hJ_Ct5-y-Qkw4a_2Ei6ku8Y0vnrtu_0ZKRXp9jtEvXOtOhDoh4V2f2Y0LhnrNea_pxHqi8piD4zps7bM37pC1OXzmCf4DP92ry6C9atA/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="626" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgK3HuoIaFTCWYNdWKXqiwgv_BiXgkzcFe2ds1ZEz6kaemZARsX36oteFzau6j-nQ1Ll7XXqWSeHkwuJPxRdU6uZRsc9OK-LgxSCtWoPGTbq6JISPW6zNxWprvXbqrEQ8OvGEXjVD5Al4IWNaCy3BZClnNJwlfmxd8kGSfCb-3oMhTwzcLopltTQSNDQg/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>그러면 등록된 사용자에게 메시지가 발송되므로 현재 컴퓨터로 확인할 수 있습니다. 알림창이 뜨면 &lt;이 링크는 상세 정보페이지로 이동합니다.&gt;를</strong></p><p><strong>클릭해 봅니다. 그러면 새로운 웹 브라우저 창이 열리면서 CODE-DESIGN 웹 앱&#39;&nbsp; 사이트에 접속하는 것을 알 수 있습니다.</strong></p><p><strong>또한 안드로이드 기기로드 테스트해 보면 화면이 잠겨 있어도 메시지를 잘 받고 아이콘에는 메시지 개수에 따라 배지 숫자도 표시되는 것을 확인할 수 있습니다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><span style="color:#2980b9"><strong>크롬 브라우저에서 푸시 알림 해제하기</strong></span></span></p><p>&nbsp;</p><p>크롬 브라우저에서 오른쪽 위에 메쥬 버튼을 누르고 [설정]을 선택합니다. 설정 화면이 보이면 오른쪽 위에 돋보기 아이콘을 누르고 &quot;알림&quot; 을 입력합니다.</p><p>검색 결과 중[사이트 설정]을 클릭합니다.</p><p>&nbsp;</p><p><img alt="" width="900" height="492" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgznimyqTxT3n19nIzv5Y6EBk5tA25BSxyhXOfFG29d5G4gUG2lF9W7PVLEa1Kl0Q1HdoF6PvCzuTPYmGuOXTs2KEtdmNQpgsbAezjyZ6xG2LGt22qTh_99yb0_9Nkw73KpJqtKOI3mU9fSAr2HYaVhnGDb3yj908nal2d2GtRUPwdrN2Z7BbbJDSBY7Q/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>사이트 설정 화면에서 [알림]을 선택합니다.&nbsp; 이어서 허용 목록에서&nbsp; &quot;http://localhost:3000&quot; 오른쪽에 메뉴 버튼을 누르고[삭제]를 선택합니다.</p><p>&nbsp;</p><p><strong>또한, 안드로이드폰에서는 [설정 -&gt; 사이트 설정 -&gt; 알림] 메뉴를 선택한 후 등록된 해당 사이트를 삭제합니다.</strong></p><p>&nbsp;</p><p><img alt="" width="600" height="703" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhQLNzAA-6ACGFDuVa0SzJfTcVevREFenbsxUJk-4cuD3FuQTgWtMypsrohQ5LPW6iwwxBIQMZ46UL0lEfVr97dZK4LqM4eGmX94uS7FR-PebcxdEJRRog4ejAJI4pnSSDx4PUP7iWTiV1cJjlbzBXDXavz39oPNwOOZQQkKo6gskLkPOWZiYEXumYfLQ/s16000/2023-04-05%2022%2037%2023.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="692" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBkpFGUBmMw_glgC2DWIHCadNyJRq8kKFWL6ULa_idXpapcMPSlf2GNoSQe71DGgJaQfkqcaGjmqf1V3aCuellxlTwQgDGJXXPkY8GE6gBedLMBtL7PWYj3f9Cy05RyIPY570NVM2HN7Tz4qL3qiCmEOWAKrfMP7EaOSPiJbyFs6Q_d8ogh_KsGd3t0A/s16000/2023-04-05%2022%2037%2047.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:28px">2.매니페스트 작성하기</span></span></strong></p><p>&nbsp;</p><p>운영자로서 푸시 알림 메시지를 보내려면 먼저 공개 키와 비공개 키가 있어야 합니다.&nbsp; web-push 모듈의 web-push 명령을 사용하면 되는데, 먼저 package.json 의 scripts 항목에</p><p>&quot;web-push&quot; : &quot;web-push&quot; 를 추가합니다.</p><p>&nbsp;</p><p><strong>package.json</strong></p><pre class="brush:as3;"> &quot;private&quot;: true, &quot;scripts&quot;: { &quot;serve&quot;: &quot;vue-cli-service serve&quot;, &quot;build&quot;: &quot;vue-cli-service build&quot;, &quot;web-push&quot; : &quot;web-push&quot; },</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">다음으로 web-push 를 포함해 이 프로젝트에서 사용하는 모듈들을 설치하고 공개키와 비공개 키를 생성합니다.</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>4개의 모듈 설치</strong></p><pre class="brush:as3;">$ npm install web-push firebase vuefire firebase-tools </pre><p>&nbsp;</p><p><strong>공개키와&nbsp; 비공개키</strong></p><pre class="brush:as3;">$ npm run web-push generate-vapid-keys </pre><p>&nbsp;</p><p><strong>생성된 키는 web-push-key.txt 파일을 만들어서 임시로 저장</strong></p><p>&nbsp;</p><pre class="brush:as3;">Public Key: BLq3ycYqetXwk3qCkK-rF-------샘플-----------hs51tze3TGqe6gwcTB0dsad9wbTBnac_sdfsdfsdfFPMnTvsAr2ZaJxrK8JaR2AZiB77bCJ4XI Private Key: 7cvGgYJVsfds--------샘플----------d352r32PR1WgVsE6fM-fsdfBUk8CwoAL4</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>매니페스트&nbsp; 작성하기</strong></span></p><p><strong>public/manifest.json</strong></p><pre class="brush:as3;">{ &quot;name&quot;: &quot;푸시알림 서비스&quot;, &quot;short_name&quot;: &quot;푸시알림 서비스&quot;, &quot;icons&quot;: [ { &quot;src&quot;: &quot;./img/icons/android-chrome-192x192.png&quot;, &quot;sizes&quot;: &quot;192x192&quot;, &quot;type&quot;: &quot;image/png&quot; }, { &quot;src&quot;: &quot;./img/icons/android-chrome-512x512.png&quot;, &quot;sizes&quot;: &quot;512x512&quot;, &quot;type&quot;: &quot;image/png&quot; } ], &quot;start_url&quot;: &quot;./index.html&quot;, &quot;display&quot;: &quot;standalone&quot;, &quot;orientation&quot;: &quot;portrait&quot;, &quot;background_color&quot;: &quot;#ffffff&quot;, &quot;theme_color&quot;: &quot;#ffffff&quot; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:28px">3.파이어베이스 준비하기</span></span></strong></p><p>&nbsp;</p><p>실전용 푸시 알림 서비스를 제공하려면 구독자를 관리하는 DB 서버, 푸시 알림을 보내는 애플리케이션 서버를 구축해야 합니다.&nbsp; 이러한 백엔드 서버는&nbsp;</p><p>파이어베이스를 활용하면 강력한 서비스를 쉽게 제공할 수 있습니다.&nbsp; 어떻게 연결하여 사용하는지 살펴보겠습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>파이어베이스 프로젝트를 만들고 웹앱에 등록 후 데이터베이스 생성까지 과정은 앞에서 다루었으니 생략합니다. 다음 과정을 참고해</p><p>pwa-notification-push 라는 이름으로 새로운 프로젝트를 만듭니다.</p><p>&nbsp;</p><p><strong>1. firebase.google.com 에 접속해서 pwa-notification-push 라는 이름으로 새프로젝트를 만들기</strong></p><p>&nbsp;</p><p><strong>2.파이어베이스 프로젝트 설정 화면에서 웹앱에 파이어베이스 추가하기 (닉네임 pwa-notification-push 로 등록)&nbsp;</strong></p><p>&nbsp;</p><p><strong>3.파이어베이스 SDK 추가에서 databaseURL 값을 복사해서 기록해 두기</strong></p><p>&nbsp;</p><p><strong>4.Realtime Database 만들기 -&gt; 테스트 모드로 시작</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:16px"><strong>src/datasource/firebase.js</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-05 21:04:10 스프링 데이터 JPA (백기선) -1.핵심 개념 이해 (ORM 개요, ORM 패러다임 불일치, Value 타입 맵핑,관계 맵핑, 엔티티 상태와 Cascade,Fetch) http://macaronics.net/index.php/m01/spring/view/2115 2115 <p>&nbsp;</p><p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[웹 개발, 백엔드] 강의입니다.</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>ORM에 대한 이해</p><p>JPA 프로그래밍</p><p>Bean 생성 방법</p><p><strong>스프링 JPA가 어렵게 느껴졌다면?<br />개념과 원리, 실제까지 확실하게 학습해 보세요.</strong></p><p>제대로 배우는<br /><strong>백기선의 스프링 데이터 JPA</strong></p><p>JPA(Java Persistence API)를 보다 쉽게 사용할 수 있도록 여러 기능을 제공하는 스프링 데이터 JPA에 대해 학습합니다.</p><p>&nbsp;</p><p><strong>강의 :</strong></p><p><a target="_blank" href="https://www.inflearn.com/course/스프링-데이터-jpa#reviews">https://www.inflearn.com/course/스프링-데이터-jpa#reviews</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료 :</p><p><a target="_blank" href="https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit">https://docs.google.com/document/d/1IjSKwMEsLdNXhRLvFk576VTR03AKTED_3jMsk0bHANg/edit</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 코드</p><p><a target="_blank" href="https://github.com/braverokmc79/springdatajpa">https://github.com/braverokmc79/springdatajpa</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강좌 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>Application -&gt; 스프링 데이터 JPA (-&gt; JPA -&gt; JDBC) -&gt; Database</strong></p><p><strong><img width="516" height="408" alt="" src="https://lh6.googleusercontent.com/XVN0FUf452XRm-sey7YfF0CZt-J7qg9mkylNPjhd10OQrE7_ef2PcG9yrdExK6-WUDKIbHnXF5nlGj9KwObhsD7p2zrPL4qYekNZqeyr-onHOMom2vf3ogYXk2v5utDO0wMgtiJxj2WCXoSXi4OmQg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><ol><li><p><strong>강사 소개</strong></p></li></ol><p>&nbsp;</p><p><strong>백기선</strong></p><p>&nbsp;</p><p><strong>마이크로소프트(2+) &lt;- 아마존(1) &lt;- 네이버(4.5) &lt;- SLT(2.5) ...</strong></p><p>&nbsp;</p><p><strong>강좌</strong></p><ul><li><p><strong>스프링 프레임워크 입문 (Udemy)</strong></p></li><li><p><strong>백기선의 스프링 부트 (인프런)</strong></p></li></ul><p>&nbsp;</p><p><strong>특징</strong></p><ul><li><p><strong>스프링 프레임워크 중독자</strong></p></li><li><p><strong>JPA 하이버네이트 애호가</strong></p></li><li><p><strong>유튜브 / 백기선</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:36px"><strong>[1부: 핵심 개념 이해]</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">3.관계형 데이터베이스와 자바</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13745&amp;tab=community&amp;q=5647&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13745&amp;tab=community&amp;q=5647&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>본격적인 스프링 데이터 JPA 활용법을 학습하기에 앞서, ORM과 JPA에 대한 이론적인 배경을 학습합니다.&nbsp;</strong></p><p>&nbsp;</p><ol><li><p><strong>관계형 데이터베이스와 자바</strong></p></li></ol><p>&nbsp;</p><p><strong>JDBC</strong></p><ul><li><p><strong>(관계형) 데이터베이스와 자바의 연결 고리</strong></p></li></ul><p><strong><img width="462" height="148" alt="" src="https://lh6.googleusercontent.com/v6di_qcVlIjQiWyKnUt9hks7Xm9qfq7mDaIZ6SzO-xjQakwXv-tRJz8n2eQ_sWAh6LkqxMv-f5Wpr9s6tFVeHBBUkNoDZ8rfhYKvvEYMdOn2t_UmjfHeUs74VZ-tYpdx9I4rPYqoRXmoojwWLC5l2Q" /></strong></p><p><strong>JDBC</strong></p><ul><li><p><strong>DataSource / DriverManager</strong></p></li><li><p><strong>Connection</strong></p></li><li><p><strong>PreparedStatement</strong></p></li></ul><p>&nbsp;</p><p><strong>SQL</strong></p><ul><li><p><strong>DDL</strong></p></li><li><p><strong>DML</strong></p></li></ul><p>&nbsp;</p><p><strong>무엇이 문제인가?</strong></p><ul><li><p><strong>SQL을 실행하는 비용이 비싸다.</strong></p></li><li><p><strong>SQL이 데이터베이스 마다 다르다.</strong></p></li><li><p><strong>스키마를 바꿨더니 코드가 너무 많이 바뀌네...</strong></p></li><li><p><strong>반복적인 코드가 너무 많아.</strong></p></li><li><p><strong>당장은 필요가 없는데 언제 쓸 줄 모르니까 미리 다 읽어와야 하나...</strong></p></li></ul><p>&nbsp;</p><p><strong>의존성 추가</strong></p><pre class="brush:as3;"> org.postgresql postgresql 42.6.0 </pre><p>&nbsp;</p><p><strong>PostgreSQL 설치 및 서버 실행 (docker)</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=keesun -e POSTGRES_DB=springdata --name postgres_boot -d postgres docker exec -i -t postgres_boot bash su - postgres psql springdata 데이터베이스 조회 \list 테이블 조회 \dt 쿼리 SELECT * FROM account; </pre><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:18px"><span style="color:#c0392b"><strong>★ 윈도우에서 도터 컨테이너 접속후 &nbsp; psql&nbsp; 접속시 다음과 같이 유저명과&nbsp; DB 명을 입력해야한다.</strong></span></span></p><p><span style="color:#2980b9"><strong>psql --username keesun --dbname springdata</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package me.whiteship; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class Application { public static void main(String[] args) throws Exception { String url =&quot;jdbc:postgresql://localhost:5432/springdata&quot;; String username=&quot;keesun&quot;; String password=&quot;pass&quot;; try(Connection connection= DriverManager.getConnection(url, username, password)){ System.out.println(&quot;connection = &quot; + connection); // String sql =&quot;CREATE TABLE ACCOUNT(id int , username varchar(255), password varchar(255)); &quot;; String sql =&quot;INSERT INTO ACCOUNT VALUES(1 , &#39;test&#39;, &#39;pass&#39;)&quot;; try(PreparedStatement statement = connection.prepareStatement(sql)){ statement.execute(); } } } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">4.ORM: Object-Relation Mapping</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13746&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13746&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>JDBC 사용</strong></p><pre class="brush:as3;"> try(Connection connection = DriverManager.getConnection(url, username, password)) { System.out.println(&quot;Connection created: &quot; + connection); String sql = &quot;INSERT INTO ACCOUNT VALUES(1, &#39;keesun&#39;, &#39;pass&#39;);&quot;; try(PreparedStatement statement = connection.prepareStatement(sql)) { statement.execute(); } } </pre><p>&nbsp;</p><p><strong>도메인 모델 사용</strong></p><pre class="brush:as3;">Account account = new Account(&ldquo;keesun&rdquo;, &ldquo;pass&rdquo;); accountRepository.save(account); </pre><p>&nbsp;</p><p><strong>JDBC 대신 도메인 모델 사용하려는 이유?</strong></p><ul><li><p><strong>객체 지향 프로그래밍의 장점을 활용하기 좋으니까.</strong></p></li><li><p><strong>각종 디자인 패턴</strong></p></li><li><p><strong>코드 재사용</strong></p></li><li><p><strong>비즈니스 로직 구현 및 테스트 편함.</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>ORM은 애플리케이션의 클래스와 SQL 데이터베이스의 테이블 사이의 맵핑 정보를 기술한 메타데이터를 사용하여, </strong></p><p><strong>자바 애플리케이션의 객체를 SQL 데이터베이스의 테이블에 자동으로 (또 깨끗하게) 영속화 해주는 기술입니다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">장점 :</span>&nbsp; 생산성 , 유지보수성 ,성능 ,밴더 독립성</strong></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>단점 :&nbsp; &nbsp;</strong></span><strong>학습비용</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">5.ORM 패러다임 불일치</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13747&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13747&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p><img alt="" width="900" height="1469" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhpQv7YGXrovCTm_RnP-qFpHVp8NeW5t7WAP1EYgmdvUBzOypwwrVmeOv9V9XXz9KRPZYOHUFGgootxo4Qot5REZaXBeHzYPqeF-vfo5fBz-DFX3YLkdHA8GKPKGbnSeW3dCpv9Ae7xOv-bCM7Gn47AElZy38tnlq6_jH08Ae1iVz1-etf7oR2kVwgTA/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">6.ORM 패러다임 불일치</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13748&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13748&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p>참조 :</p><p><a target="_blank" href="https://velog.io/@jwpark06/SpringBoot에-JDBC로-Postgresql-연동하기">https://velog.io/@jwpark06/SpringBoot에-JDBC로-Postgresql-연동하기</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>데이터베이스 실행</strong></p><ul><li><p><strong>PostgreSQL 도커 컨테이너 재사용</strong></p></li><li><p><strong>docker start postgres_boot</strong></p></li></ul><p>&nbsp;</p><p><strong>스프링 부트</strong></p><ul><li><p><strong>스프링 부트 v2.*</strong></p></li><li><p><strong>스프링 프레임워크 v5.*</strong></p></li></ul><p>&nbsp;</p><p><strong>스프링 부트 스타터 JPA</strong></p><ul><li><p><strong>JPA 프로그래밍에 필요한 의존성 추가</strong></p><ul><li><p><strong>JPA v2.*</strong></p></li><li><p><strong>Hibernate v5.*</strong></p></li></ul></li><li><p><strong>자동 설정: HibernateJpaAutoConfiguration</strong></p><ul><li><p><strong>컨테이너가 관리하는 EntityManager (프록시) 빈 설정</strong></p></li><li><p><strong>PlatformTransactionManager 빈 설정</strong></p></li></ul></li></ul><p>&nbsp;</p><p><strong>JDBC 설정</strong></p><ul><li><p><strong>jdbc:postgresql://localhost:5432/springdata</strong></p></li><li><p><strong>keesun</strong></p></li><li><p><strong>pass</strong></p></li></ul><p>&nbsp;</p><p><strong>application.properties</strong></p><ul><li><p><strong>spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true</strong></p></li><li><p><strong>spring.jpa.hibernate.ddl-auto=create</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>=================================================&nbsp;=================================================&nbsp;=================================================&nbsp;</p><p>=================================================&nbsp;=================================================&nbsp;=================================================&nbsp;</p><p>=================================================&nbsp;=================================================&nbsp;=================================================&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:16px">스프링부트 3.0.5&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;설정 파일&nbsp; :&nbsp; jPA&nbsp; &nbsp; +&nbsp;&nbsp;&nbsp; dev 툴 +&nbsp; &nbsp; &nbsp; postsql +&nbsp; &nbsp; &nbsp; lombok</span></strong></p><p>&nbsp;</p><p><span style="font-size:16px"><strong>설정 파일 다운로드</strong></span></p><p><a target="_blank" href="https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.0.5&amp;packaging=jar&amp;jvmVersion=17&amp;groupId=com.jpa&amp;artifactId=springdatajpa&amp;name=springdatajpa&amp;description=Demo%20project%20for%20Spring%20Boot&amp;packageName=com.jpa.spring&amp;dependencies=devtools,postgresql,lombok,data-jpa">https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.0.5&amp;packaging=jar&amp;jvmVersion=17&amp;groupId=com.jpa&amp;artifactId=springdatajpa&amp;name=springdatajpa&amp;description=Demo%20project%20for%20Spring%20Boot&amp;packageName=com.jpa.spring&amp;dependencies=devtools,postgresql,lombok,data-jpa</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>=&gt;&nbsp;<a target="_blank" href="https://goddaehee.tistory.com/208">롬복 적용</a></strong></p><p>&nbsp;</p><p><strong>=&gt;&nbsp;<a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/1809#">인텔리제이 스프링부트 자동빌드 적용</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>콘솔 색깔 설정</strong></p><p><strong>application.properties</strong></p><pre class="brush:as3;">spring.output.ansi.enabled=always</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Account</strong></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import lombok.Data; import org.springframework.boot.autoconfigure.domain.EntityScan; @Entity @Data public class Account { @Id @GeneratedValue private Long id; private String username; private String password; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>JpaRunner</strong></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.hibernate.Session; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component @Transactional public class JpaRunner implements ApplicationRunner { @PersistenceContext EntityManager entityManager; @Override public void run(ApplicationArguments args) throws Exception { Account account=new Account(); account.setUsername(&quot;HongGilDong&quot;); account.setPassword(&quot;jpa&quot;); //entityManager.persist(account); Session session =entityManager.unwrap(Session.class); session.save(account); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>spring.datasource.platform=postgres&nbsp; 를 추가하면&nbsp;&nbsp;spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 제거해도 된다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><span style="font-size:20px"><strong>쿼리 파라미터 로그 남기기 설정하기</strong></span></span></p><p>&nbsp;</p><p>참조 :<a target="_blank" href="http:// https://macaronics.net/index.php/m01/spring/view/2055">&nbsp;https://macaronics.net/index.php/m01/spring/view/2055</a></p><p><br />로그에 다음을 추가하기: SQL 실행 파라미터를 로그로 남긴다.<br />주의! 스프링 부트 3.x를 사용한다면 영상 내용과 다르기 때문에 다음 내용을 참고하자.</p><p><br /><strong>1)스프링 부트 2.x, hibernate5</strong><br /><strong>org.hibernate.type: trace</strong></p><p><br /><br />외부 라이브러리 사용<br /><a target="_blank" href="https://github.com/gavlyukovskiy/spring-boot-data-source-decorator">https://github.com/gavlyukovskiy/spring-boot-data-source-decorator</a></p><p><br />스프링 부트를 사용하면 이 라이브러리만 추가하면 된다.<br /><strong>implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6</strong>&#39;<br />&gt; 참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는<br />편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.</p><p>&nbsp;</p><p><br /><strong>2) 쿼리 파라미터 로그 남기기 - 스프링 부트 3.0</strong></p><p>스프링 부트 3.x, hibernate6<br /><strong>org.hibernate.orm.jdbc.bind: trace</strong></p><p><br />p6spy-spring-boot-starter 라이브러리는 현재 스프링 부트 3.0을 정상 지원하지 않는다.<br />스프링 부트 3.0에서 사용하려면 다음과 같은 추가 설정이 필요하다</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>1. org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 추가</strong></p><pre class="brush:as3;">src/resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration</pre><p>&nbsp;</p><p><strong>폴더명:</strong>&nbsp;src/resources/META-INF/spring</p><p><strong>파일명:</strong>&nbsp;org.springframework.boot.autoconfigure.AutoConfiguration.imports</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2. spy.properties 파일 추가</strong></p><p>src/resources/spy.properties<br />appender=com.p6spy.engine.spy.appender.Slf4JLogger</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>spy.properties 에 입력하지 말것</strong></span></p><pre class="brush:as3;">#appender=com.p6spy.engine.spy.appender.Slf4JLogger</pre><p>&nbsp;</p><p>이렇게 2개의 파일을 추가하면 정상 동작한다</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>application.properties 추가 설정</strong></span></p><pre class="brush:as3;">spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>메인븐&nbsp; 라이브러리</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;com.github.gavlyukovskiy&lt;/groupId&gt; &lt;artifactId&gt;p6spy-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.9.0&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">7.JPA 프로그래밍: 엔티티 맵핑</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13749&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13749&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>@Entity</strong><br />&ldquo;엔티티&rdquo;는 객체 세상에서 부르는 이름.<br />보통 클래스와 같은 이름을 사용하기 때문에 값을 변경하지 않음.<br />엔티티의 이름은 JQL에서 쓰임.</p><p>&nbsp;</p><p><strong>@Table</strong><br />&ldquo;릴레이션&quot; 세상에서 부르는 이름.</p><p><br />@Entity의 이름이 기본값.<br />테이블의 이름은 SQL에서 쓰임.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>@Id</strong><br />엔티티의 주키를 맵핑할 때 사용.<br />자바의 모든 primitive 타입과 그 랩퍼 타입을 사용할 수 있음<br />Date랑 BigDecimal, BigInteger도 사용 가능.<br />복합키를 만드는 맵핑하는 방법도 있지만 그건 논외로..</p><p>&nbsp;</p><p><strong>@GeneratedValue</strong><br />주키의 생성 방법을 맵핑하는 애노테이션<br />생성 전략과 생성기를 설정할 수 있다.<br />기본 전략은 AUTO: 사용하는 DB에 따라 적절한 전략 선택<br />TABLE, SEQUENCE, IDENTITY 중 하나.</p><p>&nbsp;</p><p><strong>@Column<br />unique<br />nullable<br />length<br />columnDefinition</strong><br />...</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>@Temporal</strong><br />현재 JPA 2.1까지는 Date와 Calendar만 지원.</p><p>&nbsp;</p><p><strong>@Transient</strong><br />컬럼으로 맵핑하고 싶지 않은 멤버 변수에 사용.<br />&nbsp;</p><pre class="brush:as3;">application.properties spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true</pre><p>&nbsp;</p><p><strong>Account</strong></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.*; import jdk.jfr.Timestamp; import lombok.Data; import java.time.LocalDateTime; import java.util.Date; @Entity @Data public class Account { @Id @GeneratedValue private Long id; @Column(nullable = false, unique = true) private String username; private String password; @Temporal(TemporalType.TIMESTAMP) private Date created=new Date(); @Transient private String yes; private String no; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">8.JPA 프로그래밍: Value 타입 맵핑 (값 타입 매핑)</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13750&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13750&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p><strong>엔티티 타입과 Value 타입 구분&nbsp; 식별자가 있어야 하는가.&nbsp; 독립적으로 존재해야 하는가.</strong></p><p>&nbsp;</p><p><strong>Value 타입 종류&nbsp; &nbsp;기본 타입 (String, Date, Boolean, ...)<br />&nbsp;Composite Value 타입<br />&nbsp; Collection Value 타입<br />기본 타입의 콜렉션<br />컴포짓 타입의 콜렉션</strong></p><p>&nbsp;</p><p>Composite Value 타입 맵핑<br />@Embeddable<br />@Embedded<br />@AttributeOverrides<br />@AttributeOverride<br />&nbsp;</p><p>&nbsp;</p><p><strong>Address</strong>&nbsp;</p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.Embeddable; @Embeddable public class Address { private String street; private String city; private String state; private String zipCode; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Account</strong></p><pre class="brush:as3;"> @Embedded @AttributeOverrides({ @AttributeOverride(name=&quot;street&quot;, column = @Column(name=&quot;home_street&quot;) ) }) private Address address; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">9.JPA 프로그래밍 4. 관계 맵핑</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13751&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13751&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>관계에는 항상 두 엔티티가 존재 합니다.</strong>&nbsp;</p><p><strong>둘 중 하나는 그 관계의 주인(owning)이고 다른 쪽은 종속된(non-owning) 쪽입니다.</strong></p><p><br />해당 관계의 반대쪽 레퍼런스를 가지고 있는 쪽이 주인.</p><p><span style="color:#8e44ad"><strong>단방향에서의 관계의 주인은 명확합니다.&nbsp; 관계를 정의한 쪽이 그 관계의 주인입니다.</strong></span></p><p>&nbsp;</p><p><strong>단방향 @ManyToOne</strong><br />기본값은 FK 생성</p><p>&nbsp;</p><p><strong>단방향 @OneToMany</strong><br />기본값은 조인 테이블 생성</p><p>&nbsp;</p><p><strong>양방향</strong><br />FK 가지고 있는 쪽이 오너 따라서 <strong>기본값은 @ManyToOne</strong> 가지고 있는 쪽이 주인.<br />주인이 아닌쪽<strong>(@OneToMany쪽)에서 mappedBy </strong>사용해서 관계를 맺고 있는 필드를 설정해야 합니다.</p><p>&nbsp;</p><p>양방향<br /><strong>@ManyToOne (<span style="color:#c0392b">이쪽이 주인</span>)</strong></p><p><br /><strong>@OneToMany(mappedBy)</strong><br />주인한테 관계를 설정해야 DB에 반영이 됩니다.<br />&nbsp;</p><p>&nbsp;</p><p><strong>Account</strong></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import java.util.Date; import java.util.HashSet; import java.util.Set; @Entity @Data public class Account { @Id @GeneratedValue private Long id; @Column(nullable = false, unique = true) private String username; private String password; @Temporal(TemporalType.TIMESTAMP) private Date created=new Date(); @Transient private String yes; private String no; @Embedded @AttributeOverrides({ @AttributeOverride(name=&quot;street&quot;, column = @Column(name=&quot;home_street&quot;) ) }) private Address address; //CascadeType.ALL 는 order // persist(ownerA); // persist(ownerB); // persist(ownerB); // persist(ownerB); // persist(study); // =&gt;persist를 각각 해줘야 하는데 CascadeType.ALL 적용하면 persist(study); 한번에 적용된다. @OneToMany(mappedBy =&quot;owner&quot;, cascade = CascadeType.ALL) private Set&lt;Study&gt; studies=new HashSet&lt;&gt;(); public void addStudy(Study study){ this.getStudies().add(study); study.setOwner(this); } public void removeStudy(Study study){ this.getStudies().remove(study); study.setOwner(null); } } </pre><p>&nbsp;</p><p><strong>Study</strong></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import lombok.Getter; @Entity @Data public class Study { @Id @GeneratedValue private Long id; private String name; @ManyToOne(fetch = FetchType.LAZY) private Account owner; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong><span style="font-size:28px">10. JPA 프로그래밍: Cascade</span></strong></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13752&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13752&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p><strong>영속성전이(CASCADE)란 ?</strong></p><p>부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미합니다.</p><p>&nbsp;</p><p>각각의 operations에 대한 설명은 다음과 같습니다.</p><ul><li><span style="font-size:18px"><span style="color:#c0392b"><strong>CascadeType.ALL: 모든 Cascade를 적용</strong></span></span></li><li>&nbsp;</li><li><strong>CascadeType.PERSIST: 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지</strong></li><li>&nbsp;</li><li><strong>CascadeType.MERGE: 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합</strong></li><li>&nbsp;</li><li><strong>CascadeType.REMOVE: 엔티티를 제거할 때, 연관된 엔티티도 모두 제거</strong></li><li>&nbsp;</li><li><strong>CascadeType.DETACH: 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()상태가 되어 변경 사항 반영 X</strong></li><li>&nbsp;</li><li><strong>CascadeType.REFRESH: 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침</strong></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:22px">엔티티의 상태 변화를 전파 시키는 옵션.</span></strong></p><p>&nbsp;</p><p>잠깐? 엔티티의 상태가 뭐지?</p><p><br />Transient: JPA가 모르는 상태</p><p><br />Persistent: JPA가 관리중인 상태 (1차 캐시, Dirty Checking, Write Behind, ...)</p><p><br />Detached: JPA가 더이상 관리하지 않는 상태.</p><p><br />Removed: JPA가 관리하긴 하지만 삭제하기로 한 상태.<br />&nbsp;</p><p>&nbsp;</p><p><strong><img width="497" height="281" alt="" src="https://lh3.googleusercontent.com/f1wcx8iTzi_yZI_2CbJCulXOkQufoy9aY3C_3QuyiPCmda6zW2rxKUbnS59VVwwzNMKbNCHlt7ryvn3e5FMfqLLwyddr6_74G7rhsbVtRWBOB6uKtdh4SfAKhZsuQlBM48XJCMbfWl-GJxAQw_XlXg" /></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Post</strong></span></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; import java.util.ArrayList; import java.util.List; @Entity @Data public class Post { @Id @GeneratedValue @Column(name=&quot;post_id&quot;) private Long id; private String title; //CascadeType.ALL 는 comment // persist(commentA); // persist(commentB); // persist(commentB); // persist(commentB); // persist(post); // =&gt;persist를 각각 해줘야 하는데 CascadeType.ALL 적용하면 persist(study); 한번에 적용된다. @OneToMany(mappedBy = &quot;post&quot;, cascade = CascadeType.ALL) private List&lt;Comment&gt; comments =new ArrayList&lt;&gt;(); /** Set 으로 할경우 hashCode 에 대한 StackOverflowError 나와서 id 에 대한 hashCode 를 설정해 줘야 한다. // StackOverflowError hashCode **/ public void addComment(Comment comment){ this.getComments().add(comment); comment.setPost(this); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Comment</strong></span></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.*; import lombok.Data; @Entity @Data public class Comment { @Id @GeneratedValue @Column(name=&quot;comment_id&quot;) private Long id; private String comment; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name=&quot;post_id&quot;) private Post post; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>PostDTO</strong></span></p><pre class="brush:as3;">package com.jpa.spring; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.ToString; @Data @AllArgsConstructor @RequiredArgsConstructor @ToString public class PostDTO { private Long id; private String title; private Long commentId; private String comment; } </pre><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#c0392b"><strong>CascadeType.ALL: 모든 Cascade를 설정으로 연속해서 데이터 등록및 삭제 처리가 가능하다.</strong></span></span></p><p>&nbsp;</p><p>만약에 설정을 하지 않는다면, 다음과 같은 코드를 추가해 줘야 한다.</p><p>entityManager.persist(comment1);</p><p>entityManager.persist(comment2);</p><p>entityManager.persist(comment3)</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>JpaRunner</strong></span></p><pre class="brush:as3;">package com.jpa.spring; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Component @Transactional public class JpaRunner implements ApplicationRunner { @PersistenceContext EntityManager entityManager; // public void run2(ApplicationArguments args) throws Exception { // Account account=new Account(); // account.setUsername(&quot;HongGilDong&quot;); // account.setPassword(&quot;jpa&quot;); // // //entityManager.persist(account); // Study study =new Study(); // study.setName(&quot;Spring Data JPA&quot;); // // //보조적인 설정 //// account.getStudies().add(study); //// //항상 관계의 주인에 넣어줘야 한다. //// study.setOwner(account); // // //====&gt; // account.addStudy(study); // // Session session =entityManager.unwrap(Session.class); // session.save(account); // session.save(study); // // Account kessun = session.load(Account.class, account.getId()); // System.out.println(&quot; ====================================&quot; ); // System.out.println(&quot;1.kessun = &quot; + kessun.getUsername()); // // //더티 체킹 // kessun.setUsername(&quot;whiteship&quot;); // kessun.setUsername(&quot;whiteship&quot;); // System.out.println(&quot; ====================================&quot; ); // System.out.println(&quot;2.kessun = &quot; + kessun.getUsername()); // } @Override public void run(ApplicationArguments args) throws Exception{ Post post=new Post(); post.setTitle(&quot;Spring Data JPA 언제 보나...&quot;); Comment comment =new Comment(); comment.setComment(&quot;빨리 보고 싶어요.&quot;); post.addComment(comment); Comment comment1 =new Comment(); comment1.setComment(&quot;곧 보여드릴께요&quot;); post.addComment(comment1); // Session session =entityManager.unwrap(Session.class); // session.save(post); entityManager.persist(post); /** cascade = CascadeType.ALL 설정 되어 있기 때문에 다음 코드를 넣지 않아도 * comment data 가 들어간다. * * **/ // entityManager.persist(comment); // entityManager.persist(comment1); Post entityManager.flush(); entityManager.close(); List&lt;PostDTO&gt; resultList = entityManager.createQuery(&quot;select new com.jpa.spring.PostDTO( p.id, p.title, c.id, c.comment ) from Post p join p.comments c on p.id=1&quot;, PostDTO.class).getResultList(); for(PostDTO p :resultList){ System.out.println(&quot;resultList = &quot; + p); } /** 출력 p.getComments() = PostDTO(id=1, title=Spring Data JPA 언제 보나..., commentId=1, comment=빨리 보고 싶어요.) p.getComments() = PostDTO(id=1, title=Spring Data JPA 언제 보나..., commentId=2, comment=곧 보여드릴께요) * */ //삭제 Post singleResult = entityManager.createQuery(&quot;select p from Post p where p.id = 1l &quot;, Post.class).getSingleResult(); System.out.println(&quot;singleResult = &quot; + singleResult.getTitle()); entityManager.remove(singleResult); entityManager.flush(); entityManager.clear(); List&lt;Comment&gt; commentList = entityManager.createQuery(&quot;select c from Comment c where c.post.id = 1l &quot;, Comment.class).getResultList(); System.out.println(&quot;CommentList = &quot; + commentList.size()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>11. Fetch</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13753&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13753&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p><strong>연관 관계의 엔티티를 어떻게 가져올 것이냐... 지금 (Eager)? 나중에(Lazy)?</strong></p><ul><li><p><strong>@OneToMany의 기본값은 Lazy</strong></p></li><li><p><strong>@ManyToOne의 기본값은 Eager</strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>참조 :&nbsp; &nbsp;=&gt;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2054">자바 ORM 표준 JPA 프로그래밍 - 10. 객체지향 쿼리 언어2 - 중급 문법 ★ 페치 조인(fetch join)</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>12. JPA 프로그래밍: Query</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13754&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13754&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><pre class="brush:as3;">@Entity @Data @ToString(of = {&quot;id&quot;, &quot;title&quot;}) public class Post { ~</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>JpaRunner</strong></p><pre class="brush:as3;"> @Override public void run(ApplicationArguments args) throws Exception{ Post post = new Post(); post.setTitle(&quot;Spring Data JPA 언제 보나...&quot;); Comment comment = new Comment(); comment.setComment(&quot;빨리 보고 싶어요.&quot;); post.addComment(comment); Comment comment1 = new Comment(); comment1.setComment(&quot;곧 보여드릴께요&quot;); post.addComment(comment1); entityManager.persist(post); List&lt;Post&gt; resultList = entityManager.createQuery(&quot;select p from Post p join p.comments &quot;, Post.class).getResultList(); System.out.println(&quot;=========================================================&quot;); resultList.forEach(System.out::println); System.out.println(&quot;=========================================================&quot;); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>JPQL (HQL)</strong></p><ul><li><p><strong>Java Persistence Query Language / Hibernate Query Language</strong></p></li><li><p><strong>데이터베이스 테이블이 아닌, 엔티티 객체 모델 기반으로 쿼리 작성.</strong></p></li><li><p><strong>JPA 또는 하이버네이트가 해당 쿼리를 SQL로 변환해서 실행함.</strong></p></li><li><p><strong><a href="https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql">https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql</a></strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><strong>Criteria</strong></p><ul><li><p><strong>타입 세이프 쿼리</strong></p></li><li><p><strong><a href="https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#criteria">https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#criteria</a></strong></p></li></ul><p>&nbsp;</p><pre class="brush:as3;">CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery&lt;Post&gt; criteria = builder.createQuery(Post.class); Root&lt;Post&gt; root = criteria.from(Post.class); criteria.select(root); List&lt;Post&gt; posts = entityManager.createQuery(criteria).getResultList();</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Native Query</strong></p><ul><li><p><strong>SQL 쿼리 실행하기</strong></p></li><li><p><strong><a href="https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#sql">https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#sql</a></strong></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">List&lt;Post&gt; posts = entityManager .createNativeQuery(&quot;SELECT * FROM Post&quot;, Post.class) .getResultList(); </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>13. 스프링 데이터 JPA 소개 및 원리</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13755&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13755&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>JpaRepository&lt;Entity, Id&gt; 인터페이스</strong></p><ul><li><p><strong>매직 인터페이스</strong></p></li><li><p><strong>@Repository가 없어도 빈으로 등록해 줌.</strong></p></li></ul><p>&nbsp;</p><p><strong>@EnableJpaRepositories</strong></p><ul><li><p><strong>매직의 시작은 여기서 부터</strong></p></li></ul><p>&nbsp;</p><p><strong>매직은 어떻게 이뤄지나?</strong></p><ul><li><p><strong>시작은 @Import(JpaRepositoriesRegistrar.class)</strong></p></li></ul><p><strong>핵심은 ImportBeanDefinitionRegistrar 인터페이스</strong></p><p>&nbsp;</p><pre class="brush:as3;">package com.jpa.spring; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component @Transactional public class 스프링데이터JPA소개및원리13 implements ApplicationRunner { @Autowired PostRepository postRepository; @Override public void run(ApplicationArguments args) throws Exception { System.out.println(&quot;=================================================&quot;); postRepository.findAll().forEach(System.out::println); System.out.println(&quot;=================================================&quot;); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>14. 핵심 개념 마무리</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13756&amp;tab=curriculum&amp;category=questionDetail">https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-jpa&amp;unitId=13756&amp;tab=curriculum&amp;category=questionDetail</a></p><p>&nbsp;</p><pre class="brush:as3;"> @Autowired PostRepository postRepository; @Override public void run(ApplicationArguments args) throws Exception { Post post =new Post(); post.setTitle(&quot;spring&quot;); Comment comment =new Comment(); comment.setComment(&quot;hello&quot;); postRepository.save(post); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>데이터베이스와 자바</strong></p><p><strong>패러다임 불일치</strong></p><p><strong>ORM이란?</strong></p><p><strong>JPA 사용법 (엔티티, 벨류 타입, 관계 맵핑)</strong></p><p><strong>JPA 특징 (엔티티 상태 변화, Cascade, Fetch, 1차 캐시, ...)</strong></p><p><strong>주의할 점</strong></p><ul><li><p><strong>반드시 발생하는 SQL을 확인할 것.</strong></p></li><li><p><strong>팁: &ldquo;?&rdquo;에 들어있는 값 출력하기</strong></p><ul><li><p><strong>logging.level.org.hibernate.SQL=debug</strong></p></li><li><p><strong>logging.level.org.hibernate.type.descriptor.sql=trace</strong></p></li></ul></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>로그 설정</p><p>=&gt;</p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2088">https://macaronics.net/index.php/m01/spring/view/2088</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-05 14:52:49 ★ 스프링부트 풀스택 완전정복을 위한 추천강의 와 교재 http://macaronics.net/index.php/m05/computer/view/2114 2114 <p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>1. 김영한</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px"><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%9E%90%EB%B0%94%20ORM%20%ED%91%9C%EC%A4%80%20JPA%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20-/page/1"><strong><span style="color:#2980b9">1)&nbsp;자바 ORM 표준 - JPA 프로그래밍 -</span></strong></a></span></p><p><span style="font-size:16px"><strong><span style="color:#2980b9">2)&nbsp;</span><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%B6%80%ED%8A%B8%EC%99%80%20JPA%20%ED%99%9C%EC%9A%A91/page/1"><span style="color:#2980b9">실전! 스프링 부트와&nbsp;</span></a></strong><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%9E%90%EB%B0%94%20ORM%20%ED%91%9C%EC%A4%80%20JPA%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20-/page/1"><strong><span style="color:#2980b9">-</span></strong></a><strong><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%B6%80%ED%8A%B8%EC%99%80%20JPA%20%ED%99%9C%EC%9A%A91/page/1"><span style="color:#2980b9"> JPA 활용1</span></a></strong></span></p><p><span style="font-size:16px"><strong><span style="color:#2980b9">3)&nbsp;</span><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%B6%80%ED%8A%B8%EC%99%80%20JPA%20%ED%99%9C%EC%9A%A92/page/1">실전! 스프링 부트와&nbsp;</a></strong><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%9E%90%EB%B0%94%20ORM%20%ED%91%9C%EC%A4%80%20JPA%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20-/page/1"><strong><span style="color:#2980b9">-</span></strong></a><strong><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%B6%80%ED%8A%B8%EC%99%80%20JPA%20%ED%99%9C%EC%9A%A92/page/1"> JPA 활용2</a></strong></span></p><p><span style="font-size:16px"><span style="color:#2980b9"><strong>4)&nbsp;<a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20JPA/page/1">실전! 스프링 데이터&nbsp; </a></strong></span><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20JPA/page/1"><strong><span style="color:#2980b9">-</span></strong><span style="color:#2980b9"><strong>JPA</strong></span></a></span></p><p><span style="font-size:16px"><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20Querydsl/page/1"><span style="color:#2980b9"><strong>5)&nbsp;실전!&nbsp;</strong></span></a><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%9E%90%EB%B0%94%20ORM%20%ED%91%9C%EC%A4%80%20JPA%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%20-/page/1"><strong><span style="color:#2980b9">-</span></strong></a><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8B%A4%EC%A0%84%21%20Querydsl/page/1"><span style="color:#2980b9"><strong> Querydsl</strong></span></a></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#000000"><strong>2. 최범균</strong></span></span></p><p><span style="font-size:16px"><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/JPA%20%26%20Spring%20Data%20JPA%20%EA%B8%B0%EC%B4%88/page/1"><strong>1)&nbsp;JPA &amp; Spring Data&nbsp; &nbsp;&amp; JPA 기초</strong></a></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>3. 백기선</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px"><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8A%A4%ED%94%84%EB%A7%81%20%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%20%EC%9E%85%EB%AC%B8%20%28%EB%B0%B1%EA%B8%B0%EC%84%A0%29%20/page/1"><strong><span style="color:#2980b9">1)&nbsp;스프링 프레임워크 입문 (백기선)&nbsp;</span></strong></a></span></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8A%A4%ED%94%84%EB%A7%81%20%EB%8D%B0%EC%9D%B4%ED%84%B0%20JPA%20%28%EB%B0%B1%EA%B8%B0%EC%84%A0%29/page/1"><span style="font-size:16px">2)&nbsp;스프링 데이터 JPA (*백기선)</span></a></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%EC%8A%A4%ED%94%84%EB%A7%81%20%EA%B8%B0%EB%B0%98%20REST%20API%20%EA%B0%9C%EB%B0%9C/page/1"><strong>3)&nbsp;스프링 기반 REST API 개발</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>4.최주호</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px"><a target="_blank" href="https://www.youtube.com/playlist?list=PL93mKxaRDidG_OIfRQ4nztPQ13y74lCYg"><strong>1)&nbsp;스프링부트 개념정리 with JPA</strong></a></span></p><p><span style="font-size:16px"><a target="_blank" href="https://www.youtube.com/watch?v=GEv_hw0VOxE&amp;list=PL93mKxaRDidERCyMaobSLkvSPzYtIk0Ah&amp;ab_channel=%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9"><strong>2)&nbsp;스프링부트 시큐리티</strong></a></span></p><p><span style="font-size:16px"><a target="_blank" href="https://www.youtube.com/playlist?list=PL93mKxaRDidECgjOBjPgI3Dyo8ka6Ilqm"><strong><span style="color:#2980b9">3)&nbsp;Springboot - 나만의 블로그 만들기</span></strong></a></span></p><p><span style="font-size:16px"><strong><a target="_blank" href="https://www.youtube.com/playlist?list=PL93mKxaRDidHRYNYYFr1x3mLKIx1wFeFc">4)스프링부트와 몽고DB - 채팅서버</a></strong></span></p><p><span style="font-size:16px"><a target="_blank" href="https://www.youtube.com/playlist?list=PL93mKxaRDidEfLM0I_FFb-98vfAQgXT82"><strong><span style="color:#2980b9">5)&nbsp;&nbsp;react &amp; springboot</span></strong></a></span></p><p><strong><a target="_blank" href="https://easyupclass.e-itwill.com/course/course_view.jsp?id=27&amp;rtype=0&amp;ch=course">6)&nbsp;스프링부트 SNS프로젝트 - 포토그램 만들기</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>5.온노트</strong></span></p><p><span style="color:#2980b9"><span style="font-size:16px"><strong>1) 실전 스프링 부트 REST API 개발 JPA + MySQL</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong><span style="color:#000000">6.</span><a 1113168501="" href="javascript:void(0);" onclick="goClickSearchPage('chrc',"><span style="color:#000000">채규태</span></a></strong></span></p><p><strong><span style="color:#2980b9"><span style="font-size:16px">1) JPA 퀵스타트</span></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>7.&nbsp;&nbsp;호돌맨</strong></span></p><p><strong><span style="font-size:16px"><span style="color:#2980b9">1)&nbsp;호돌맨의 요절복통 개발쇼 (SpringBoot, Vue.JS, AWS)&nbsp;</span></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>8.김다정</strong></span></p><p><span style="font-size:16px"><span style="color:#2980b9"><strong>1) React.js, 스프링 부트, AWS로 배우는 웹 개발 101</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>9.<a href="https://search.kyobobook.co.kr/search?keyword=%EC%A3%BC%ED%95%98%20%ED%9E%8C%EC%BF%A8%EB%9D%BC&amp;chrcCode=2014408501">주하 힌쿨라</a>&nbsp;</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px"><a target="_blank" href="http://www.yes24.com/product/goods/117878070">1) 실전! 스프링 부트와 리액트로 시작하는 모던 웹 애플리케이션 개발</a></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>10.구멍가게 코딩</strong></span></p><p>&nbsp;</p><p><a target="_blank" href="http://www.yes24.com/Product/Goods/96051853"><span style="font-size:16px"><strong>1) 코드로 배우는 스프링 부트 웹 프로젝트</strong></span></a></p><p>&nbsp;</p><p><strong>1. 스프링부트 파트1 동영상 강의(완)</strong></p><p><a target="_blank" href="https://cafe.naver.com/gugucoding">https://cafe.naver.com/gugucoding</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>2.스프링부트 파트2 동영상 강의(완)</strong></p><p><a target="_blank" href="https://cafe.naver.com/gugucoding">https://cafe.naver.com/gugucoding</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>3.스프링부트 파트3 동영상 강의(미완)</strong></p><p><a target="_blank" href="https://cafe.naver.com/gugucoding">https://cafe.naver.com/gugucoding</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-04 18:52:38 스프링 프레임워크 입문 (백기선) 스프링부트 3.0 JPA 펫클리닉 (애완 동물 진료소) 프로젝트 , 스프링부트 JPA CRUD http://macaronics.net/index.php/m01/spring/view/2113 2113 <p>&nbsp;</p><p>&nbsp;</p><p><strong>초급자</strong>를 위해 준비한<br /><strong>[웹 개발, 백엔드] 강의입니다.</strong></p><p>Spring-PetClinic이라는 스프링 공식 예제 프로젝트의 코드를 보며, 다음의 스프링의 핵심 기능을 쉽고 빠르게 이해할 수 있습니다.</p><p><a target="_blank" rel="noopener noreferrer" href="https://www.inflearn.com/course/spring_revised_edition"><img title="spring_revised_edition2.png" alt="" width="150" height="98" src="https://cdn.inflearn.com/public/files/courses/221292/0f53edb0-f380-4bbf-b200-514ae6729aa1/spring_revised_edition2.png" /></a></p><p>개정판 강좌를 수강해볼까요?&nbsp;<br /><a target="_blank" rel="noopener noreferrer" href="https://www.inflearn.com/course/spring_revised_edition"><strong>예제로 배우는 스프링 입문(개정판) 바로가기 &gt;&gt;</strong></a>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/spring#">https://www.inflearn.com/course/spring#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>예제로 배우는 스프링 입문&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/spring_revised_edition">https://www.inflearn.com/course/spring_revised_edition</a></p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/spring-projects/spring-petclinic">https://github.com/spring-projects/spring-petclinic</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료 :&nbsp;<a target="_blank" href="https://drive.google.com/file/d/1Q8LGrUKzV_EX6MToQaynhv0CnPDCTgbM/view">https://drive.google.com/file/d/1Q8LGrUKzV_EX6MToQaynhv0CnPDCTgbM/view</a></p><p><a target="_blank" href="https://github.com/braverokmc79/course-petclinic/blob/main/예제로배우슨%20스프링입문.pdf">https://github.com/braverokmc79/course-petclinic/blob/main/예제로배우슨%20스프링입문.pdf</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogExecutionTime { } </pre><p>&nbsp;</p><pre class="brush:as3;">@Component @Aspect public class LogAspect { Logger logger = LoggerFactory.getLogger(LogAspect.class); @Around(&quot;@annotation(LogExecutionTime)&quot;) public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object proceed = joinPoint.proceed(); stopWatch.stop(); logger.info(stopWatch.prettyPrint()); return proceed; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>1. 프로젝트 구조</strong></span></span></p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN5qpFnKPdNtgptCUiB4cOfZ7JDH4o4uyq47N_hX8dci4H0e0H0VB-Q6JFvGWwLqUkAq2k8Xt8FTGPj4wDvii6slBeWT0CDuahVFS8mh5tK7ARiXIpySvxkVHr7DHrYvo0btFBTJoH2Xm4EL7XnPbF8AYDNXIpc8hTtpTBBINYm8R4RQX7xYrxhosNpw/s16000/2023-04-03%2020%2039%2029.png">이미지크게</a></p><p><img alt="" width="744" height="589" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN5qpFnKPdNtgptCUiB4cOfZ7JDH4o4uyq47N_hX8dci4H0e0H0VB-Q6JFvGWwLqUkAq2k8Xt8FTGPj4wDvii6slBeWT0CDuahVFS8mh5tK7ARiXIpySvxkVHr7DHrYvo0btFBTJoH2Xm4EL7XnPbF8AYDNXIpc8hTtpTBBINYm8R4RQX7xYrxhosNpw/s16000/2023-04-03%2020%2039%2029.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>2. 테이블 구조</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBkT9lTk1DBefA2fLy9wIfhw1Kvgkq8BR_1Jxh-i_x7AyWTMW09E2Zh9HxBxeQ39aQ9SUoTno4uS7JzPyz2jyuT5-1bWZ_Aboi4maADARY77wpVNUYm_rfPwrxVEmY5WMKIIBN0N_oTz6Dcl4vhWu6_eBOVc3UmALSIhTNdHfN5JMqBdmmm39cCKKzsg/s16000/2023-04-03%2020%2038%2045.png">이미지 크게</a></p><p><img alt="" width="1059" height="832" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBkT9lTk1DBefA2fLy9wIfhw1Kvgkq8BR_1Jxh-i_x7AyWTMW09E2Zh9HxBxeQ39aQ9SUoTno4uS7JzPyz2jyuT5-1bWZ_Aboi4maADARY77wpVNUYm_rfPwrxVEmY5WMKIIBN0N_oTz6Dcl4vhWu6_eBOVc3UmALSIhTNdHfN5JMqBdmmm39cCKKzsg/s16000/2023-04-03%2020%2038%2045.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>2. 공통모델</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYuT5B0owY6ozxZ3aO0VkTwDNosRsWxV5bVAeZs3dz80d1JgSzx7YpFuQDhemVDpybXiAmn6m3mSI1QIkLvWw-5qOb2fAzNaXmYDS6NbC2-Gcd2VhwBgvRNo4tsS5yusvrG1dzc4iaV8kd_RCmInmoAndxaIssgrRjMSdljpzmUaDz5rEPuKAORMFUtg/s16000/3.png">이미지크게</a></p><p><img alt="" width="900" height="789" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgYuT5B0owY6ozxZ3aO0VkTwDNosRsWxV5bVAeZs3dz80d1JgSzx7YpFuQDhemVDpybXiAmn6m3mSI1QIkLvWw-5qOb2fAzNaXmYDS6NbC2-Gcd2VhwBgvRNo4tsS5yusvrG1dzc4iaV8kd_RCmInmoAndxaIssgrRjMSdljpzmUaDz5rEPuKAORMFUtg/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>4. 엔티티와&nbsp; &nbsp;DB 관계&nbsp;</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpSqG7JkL6Em_VKT9nhxSz1anx5iyxBFEy1tPy8QKyz36GNbjsYXM3lUqKnsHjQoAkaxokZoJifF1QaUeyOhU6rzDAy-lPj2nmWPFLkKHSSTATEbJOhLM-R9q6xF2HlqwJHB7I3usV81ljF1vysUbdYQM83FePwetrjMafL6F_CgDP5iyFWlEYrTcfgQ/s16000/1.png">이미지크게</a></p><p><img alt="" width="900" height="891" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpSqG7JkL6Em_VKT9nhxSz1anx5iyxBFEy1tPy8QKyz36GNbjsYXM3lUqKnsHjQoAkaxokZoJifF1QaUeyOhU6rzDAy-lPj2nmWPFLkKHSSTATEbJOhLM-R9q6xF2HlqwJHB7I3usV81ljF1vysUbdYQM83FePwetrjMafL6F_CgDP5iyFWlEYrTcfgQ/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:36px"><strong>5 . 화면</strong></span></span></p><p><span style="color:#2980b9"><strong>1)메인화면</strong></span></p><p><img alt="" width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEidWQ9fvqFC4ecCuGPEyqiR2vg7BIkbEmKCrAf3WGEQX3nqd5SY0MpupfVTMsauO1GkTE-h2s0hl4zMKj-Y1f84sI340N8Ir6G6tyT16uai1VsjqKASM5bItflRkicDt2RDwUrs5VoY57Vk3YYjQrwPPMnhYBAK-ywTk_3qKjk6p9zHUTu-mjJfKmkFCA/s16000/5.png" /></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>2)펫 소유자 찾기 화면</strong></span></p><p><img alt="" width="600" height="403" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKA0ElPgFjC7oN-RSm0oq17MOjIR4uQVGd8vO96QlCa4d3tGNllF-2gDSh8PpDMYXORsksoSubVRh2kMLz6TRX4yrGt0MZQBjSlp74RHVAO6n4CS_NiG19HKiJxfNo5Biin-oK8o_yYwAauN0l5Xjp_lr_r_kQ4xURhofctAIGsCk1g6u-H48FtmGeLg/s16000/6.png" /></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>3)펫 소유자 등록화면</strong></span></p><p><img alt="" width="600" height="475" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrAh2zRWhtrJEdNsp1jf8WYXfSHmiSDDj5oKmownJItyLdYOuMXIemXEgBTj5Y9w9cJLbhr79ldbtDw4PlB9DYHrw49DBDF41rVZk3-dwctHHJyAkK2cpt104GwH7kwYoeXMSMxWkXFmLwrNrAE4vVCb8Qs75v_htorCl_QFRFvelQgtN2YExn8XpALw/s16000/7.png" /></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>4)펫 소유자 목록화면</strong></span></p><p><img alt="" width="600" height="390" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBR8S9k2G0s-YDrSbCAYTl8Fa7u_kLTMhbXGW2BzGbVqfB0FCVvBW_1rnwoRnEyM-7tLBlwzawbWhTGVR1OgsC1X9RxwMqfM-tb6ibTcEcnOzBuQdHAxfS49X_O42X9WW6mdw-z4qIjBUi-34hQokKvhqPmOL1eudAKBZAXklQx4Fa626crj4YkaTOrg/s16000/2023-04-03%2021%2014%2019.png" /></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>5)펫 소유자 상세화면</strong></span></p><p><span style="color:#2980b9"><strong><img alt="" width="600" height="485" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjae0Dgz1NbmaTOyWhN383PI-3jiroNx_WIURpgjKvMav_2nXj0idnCeJ7wkJ5COOfIodiEKr0vr90SqpsbWnxoNHzGetn2U6m6Ifj5ndJnqxywhGU2vk_nJSSrzbanWjKmumqAwXSeaNKNakbNiPZ0EiIZuGa3YbRzFFP-CODeSKI2WS17_0SBSzF5KA/s16000/88.png" /></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>6)수의사 목록화면</strong></span></p><p><span style="color:#2980b9"><strong><img alt="" width="600" height="354" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_0uWnqR026ULOP3mLAkmTd5dR08f0Y9kahQILh9xWFZhqsvxQpeLwP5LzLo_4gElnR1kawNt3kRJALGNK4gnPHM_8SeAe4QWUf3agM2PatPHywRv1Vv_2BpnzIYQCm3NQ8YNTTyUfUPFnTzvyKAPGMieBDG0Z4QQ9X4M3NF27e6tP97B3_4BYVFsJmQ/s16000/8.png" /></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>7)에러 화면</strong></span></p><p><span style="color:#2980b9"><strong><img alt="" width="600" height="353" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg72gh23F344_n9WmAnrn8mNX9Bk5eT9hnaCQSrBvkG3FwSGUl7hKVudKjbzT-A6_fqMwMfIHBOyVY494vNoxOHC9hSKNLm4jrH4WLHniFH2oQjJ2pN8v_6vz6T_3g1923eG2QC5BVKdqksTBM9CR4PsxhRq673GpbHSL8q5SSozZYv4mmAw_UkrjvIzg/s16000/9.png" /></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">수의사</span></strong></p><p>&nbsp;</p><p><strong>VetRepository</strong></p><pre class="brush:as3;">/* * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an &quot;AS IS&quot; BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.samples.petclinic.vet; import org.springframework.cache.annotation.Cacheable; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.Repository; import org.springframework.transaction.annotation.Transactional; import java.util.Collection; /** * &lt;code&gt;Vet&lt;/code&gt; 도메인 개체에 대한 리포지토리 클래스 모든 메서드 이름은 호환됩니다. * 이 인터페이스를 Spring용으로 쉽게 확장할 수 있도록 Spring Data 명명 규칙 사용 * 데이터. 보다: * https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation * * @author Ken Krebs * @author Juergen Hoeller * @author Sam Brannen * @author Michael Isvy */ public interface VetRepository extends Repository&lt;Vet, Integer&gt; { /** * 데이터 저장소에서 모든 &lt;code&gt;Vet&lt;/code&gt;를 검색합니다. * @return &lt;code&gt;Vet&lt;/code&gt;의 &lt;code&gt;컬렉션&lt;/code&gt; */ @Transactional(readOnly = true) @Cacheable(&quot;vets&quot;) Collection&lt;Vet&gt; findAll() throws DataAccessException; /** * 페이지의 데이터 저장소에서 모든 &lt;code&gt;Vet&lt;/code&gt; 검색 * @param pageable * @return * @throws DataAccessException */ @Transactional(readOnly = true) @Cacheable(&quot;vets&quot;) Page&lt;Vet&gt; findAll(Pageable pageable) throws DataAccessException; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>VetController</strong></p><pre class="brush:as3;">/* * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an &quot;AS IS&quot; BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.samples.petclinic.vet; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * @author Juergen Hoeller * @author Mark Fisher * @author Ken Krebs * @author Arjen Poutsma */ @Controller class VetController { private final VetRepository vetRepository; public VetController(VetRepository clinicService) { this.vetRepository = clinicService; } @GetMapping(&quot;/vets.html&quot;) public String showVetList(@RequestParam(defaultValue = &quot;1&quot;) int page, Model model) { // Here we are returning an object of type &#39;Vets&#39; rather than a collection of Vet // objects so it is simpler for Object-Xml mapping Vets vets = new Vets(); Page&lt;Vet&gt; paginated = findPaginated(page); vets.getVetList().addAll(paginated.toList()); return addPaginationModel(page, paginated, model); } private String addPaginationModel(int page, Page&lt;Vet&gt; paginated, Model model) { List&lt;Vet&gt; listVets = paginated.getContent(); model.addAttribute(&quot;currentPage&quot;, page); model.addAttribute(&quot;totalPages&quot;, paginated.getTotalPages()); model.addAttribute(&quot;totalItems&quot;, paginated.getTotalElements()); model.addAttribute(&quot;listVets&quot;, listVets); return &quot;vets/vetList&quot;; } private Page&lt;Vet&gt; findPaginated(int page) { int pageSize = 5; Pageable pageable = PageRequest.of(page - 1, pageSize); return vetRepository.findAll(pageable); } @GetMapping({ &quot;/vets&quot; }) public @ResponseBody Vets showResourcesVetList() { // Here we are returning an object of type &#39;Vets&#39; rather than a collection of Vet // objects so it is simpler for JSon/Object mapping Vets vets = new Vets(); vets.getVetList().addAll(this.vetRepository.findAll()); return vets; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-03 21:02:09 JPA & Spring Data JPA 기초 - 4.Spring Data JPA http://macaronics.net/index.php/m01/spring/view/2112 2112 <p>&nbsp;</p><p><strong>초급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>JPA와 스프링 데이터 JPA의 기본 사용법을 알아봅니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>JPA 기본 매핑</p><p>스프링 데이터 JPA 기본 사용법</p><p>DB 연동의 열쇠 JPA!&nbsp;<br />실무 중심의 핵심 기본기를 빠르게 ????</p><p>백엔드 실무자를 위한&nbsp;<br /><strong>JPA &amp; 스프링 데이터 JPA</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>인프런 강의<br /><a target="_blank" href="https://www.inflearn.com/course/jpa-spring-data-기초">https://www.inflearn.com/course/jpa-spring-data-기초</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>유튜브<br /><a target="_blank" href="https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX">https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#2980b9"><strong>[4] Spring Data JPA</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>21. JPQL 소개</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/1Q3Qtd5HZy4" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p></p><p><strong><span style="font-size:20px">JPA를 쌩으로 사용하진 않음</span></strong></p><p><br /><span style="font-size:20px">・Spring Boot + Spring Data JPA (거의) 설정 없이 사용</span></p><p>&nbsp;</p><p><strong><span style="font-size:20px">자동 설정</span></strong><br />persistence.xml<br />EntityManagerFactory</p><p><br /><br /><span style="font-size:20px"><strong>스프링 연동</strong></span><br />스프링 트랜잭션 연동<br />EntityManager 연동</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">사용법</span></strong><br />・spring-boot-starter-data-jpa 의존</p><p>&nbsp; &nbsp; &nbsp;필요한 설정 자동 처리</p><p><br />・스프링 부트 설정<br />・엔티티 단위로 Repository 인터페이스를 상속 받은 인터페이스 생성 또는 그 하위 인터페이스</p><p><br />&bull;지정한 규칙에 맞게 메서드 추가<br />&bull;필요한 곳에 해당 인터페이스 타입을 주입해서 사용</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><br />spring-boot-starter-data-jpa</span></strong></p><p><br /><strong>&bull;메이븐/그레이들 설정에 spring-boot-starter-data-jpa 의존 추가</strong></p><p>&nbsp;</p><pre class="brush:as3;">&lt;parent&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt; &lt;version&gt;2.7.0&lt;/version&gt; &lt;relativePath /&gt; &lt;!-- lookup parent from repository --&gt; &lt;/parent&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;mysql&lt;/groupId&gt; &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;/dependencies&gt;</pre><p>&nbsp;</p><p><span style="font-size:20px"><strong>스프링 부트 설정</strong></span></p><pre class="brush:as3;"> spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc: mysql://localhost/jpabegin? characterEncoding=utf8 spring.datasource.username=jpauser spring.datasource.password=j papass spring.datasource.hikari.maximum-pool-size=5 spring.jpa.database=mysql spring.jpa.open-in-view=false logging. level.org.hibernate.SQL=DEBUG </pre><p>&nbsp;</p><p><strong><span style="font-size:20px">* &#39;스프링 부트 버전에 따라 설정은 달라질 수 있으니 버전에 따른 문서 참고</span></strong></p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">엔티티 단위로 Repository 상속한 타입 추가</span></strong></p><p><br />&bull;Repository 인터페이스<br />&bull; 스프링 데이터 JPA가 제공하는 특별한 타입으로<br />이 인터페이스를 상속한 인터페이스를 이용해 빈(bean) 객체를 생성<br />&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">//T: 엔티티 타입 // ID: 엔티티의 식별자 타입 public interface Repository &lt;T, ID&gt; {}</pre><p>&nbsp;</p><pre class="brush:as3;">import org.springframework.data.repository.Repository; public interface UserRepository extends Repository&lt;User, String&gt;{ }</pre><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">규칙에 맞게 메서드 정의</span></strong><br /><strong>&bull; save(), findById(), delete()</strong></p><p>&nbsp;</p><pre class="brush:as3;">public interface UserRepository extends Repository&lt;User, String&gt; { Optional&lt;User&gt; findById(String email); void save (User user); void delete (User user); }</pre><p>&nbsp;</p><pre class="brush:as3;">@Service 리포지토리를 주입 받아 사용 public class NewUserService { private UserRepository userRepository; public NewUserService(UserRepository userRepository) {} this.userRepository = userRepository; @Transactional public void saveUser(SaveRequest saveRequest) {} Optional &lt;User&gt; userOpt = userRepository.findById(saveRequest.getEmail()); if (userOpt.isPresent()) throw new DupException(); User newUser = new User(saveRequest.getEmail(), saveRequest.getName(), LocalDateTime.now()); userRepository.save(newUser); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>22. Spring Data JPA 02 리포지터리 메서드 작성 규칙</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/qTiHaxVc6GY" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="656" height="222" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvh_0eZLl01d9TXafiJ5YB07psO3i1S8LcjsfnbsNczLT7HuviKBm8pMGMQxnUixLjJT7mxxnKgNkgft5Lcpd_exrGp_TozvSPQG-a94xJxM9a0THZWw0HBvqlQAL8GYxss-I7jYUlKoRpa7nPdRYw_vIow_QoZHMaFdk6mTljcR3cHbKoH08O-cUWNw/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYXldegK0MDhA1VSonknMe5BPBHtNOdCVCNEEvHhhNtuMuU1eQZn-x2v7DsxR1wEdkV5fcnRGAkx-dMpj0eJDkEEL4AWT96uReiiJVABpTZnqXAkWLnQuZ9nGjU02uOaWsPAnhhSZ0Dqmd2dqNANjtMEelmrcGEobYnHEAX4RpfH-6I6Q9zXjWARBtVQ/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgTv-zh1xBf6kfKyQS8XmW3XWkw5up5vufPRPSALWWutkQyUDK4KcRC3x3-kYIEqc11LAbH7QvB1ZFt36sFvmK2nSgBs-P-Supg_0_sGLNmD7zydvBMjydApKw6ZiaSUhU6UW9_ZGf_o8vLb691bTabsulQONZ5et3jsgszsc0HSXPwn6lhqMetOJhgYA/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvDKPneJ3naCSGLUmeGTrmP_lLzYfDTNi5sw1CBjUaIkYrjxV5WzSXrH7dD2wdH2I8_hD4oPwZYqqT0NsRpsETkt3g-x1Pp-FtdASxXHupL6yh-ZL1iDll8nR-81ISCwcv-SU-G32raom_wGBQ3rs_8YQQxedhv0qRPIZUSkxnpT1LnC5bIhF9Y4ZWWA/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj63LKCS9FyOPQWRvH3_EUP_QiKrVlEt1aUO40M2ewjLzhkKgLcoYGIB87_9teiDtrZCqcqyoExUMdyst7K4wDgLcP74ZiBdJcSt9lQvpV21kms2crh_1DgtaN-i_zxS9uN9nWNYm71BxxuKB_jzteNWAosm4xHeOPVAy2hgakH-fFy6iGTnmwVCYBNPw/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjYaGwOW8CG7ULw2Aw0HTv_436CvA0vflf9cDS57KEtYo66uzJ4BV5KK7HuFs4jtwZd6-W-8NE3gksTyaHit71SGid_vTd_5wX459k3-JWAPtT-iDH_sKuPQAgSBX7iDX-2VR5aUNajhSVV1Bg_q4Kq-b3WMCBzziE4xLR6sQHvFEsIfdarMgN_5GVu2Q/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjj5WaRUPGMSDu6LlMSo9oZZPQmwKjXuFP4tRzY9LKaXlGlbYqWIVjEVCn4Ceb2_XzfTCTPrfWmbaz-OXdLjaPo68mtarp1-joOGPT84wfuOPlgldEa-ZAFBNY9O15vDb_TGfa3-m_JYPBrqE1YeJdWLyapM4v_Fh47mFUu6v1rE03NT6sp2kwWCWVx_w/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgG5y03hPh5-K6Vwj3daC3tdL_apgs1l02_nz-kJDSfyotmRgDrVkH2TrSSPFo6M1rD7o8qQ5-8TWMmwqjFrtbALUqDwXKzEcfi7hJf7IjvbMiY8GRVVtodtcKZ2n6cbuI2k_c8comkHCab7aGx_TMLR5EF7azGc3JHNci7-McfHv5nMI148Wuq5M6j5w/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgS0V5a1GFtWvZjC_TSlsjEw7oUeXau_YS6-vjX8OY2ZO0aYie1shRf-zJR4e6Igw3NyxvSUEaoacLWAlJagSDJb427clw1gni7i7ZHn4jJCRUKdpUImW89gNSFLVku3-3mUc9rNHkPNkThU5Gw8GVfBZN8PBHjT4mV2YE5pzbV8wrA_SOoLcy0pi9CdA/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>★★23. Spring Data JPA 03 정렬 페이징 @Query</strong></span></span></p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/2-f9RFCT9Ik" frameborder="0" allowfullscreen=""></iframe></p><p><br />&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXZplLJJJgTPbQZydCs_QD4H0ncX9sWjvvdPEyZADOfe-SMdTtwC1A3qtH3S-acLrxFcGoJr_BYMQti1oka3kbK7fdMsZcO3bg34U6lf8Vn0uGDyapY7UkWVZc1GiyNVtwdXqkJCUuOXd-62Zf3sZsU9KNuZXFy3OcZDl95sSF_dytMg3hRV2rVRHuxQ/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjE9RRGQd40SDgmzBmlIS9Cp68tZ232S_Rr5PSDRckoGzNBUttTGRUymXP0-MfauKxp2gic6_81JZ_aH0TYT-2zSJF8t0d8pBwDsXQ0wgzF3QcE9iuwTnRqvOJN2bg-vDr1ZtAXyCXxnlq050wfH-y-loaZEndUhRUjA4O0ws-Zr__7dxnSRCMuYcR67g/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwF7DllJpGtYosQ8oYYa6p_lfbhEi_0fxcYvQ29dyQKpbvqO4oK9fVUUm5Nk3W4mbqR4_yGlo9oBaiQ1a3wvpMM0VN7nJ-RZwUphrHL80Xy-gLAT4HHzSOht06hXnDheRBBYcQQLIaPhUa2NkrVXZVxBTK6SlnGi0og9jo9Y2TDn0lfOggXNcFSd8mxA/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3ouc_EoLBWBBm6TOZ1HGshQV1SLcZ39hmScKF-6T911xccD9hLZI6bxLFc5msSVrTzOufVLFPZoM8iUQih0SFGr59xnMCseFKRSCgcgI0TAo_a020-5GaLr28dDrKn2Kjx-0YHYfdfeUxPdgJ6SLjZdXASlAhnmYIgD6dsaZD5AsmCTUVh7Ek7UYASw/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2hj6mFFVx3JBEVdC8BAHMaBPIdJCNCXULhFbBgyVRE5mfAkWtyvpjZ5PiMFOnOi-49eKQNyrdWV-rOIFmKeIFIPkgicpIeV4uLtYeISAoCIWjz4r8_UgmZxW4xyD4bXLECnUys6TEW5abNIlneGCRkThFm84_BY43HaMnvO-xkwA3ow61PKwGAKNV-Q/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="742" height="366" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIvrB03lvOkQrU5Zaoq-8bTzT5fbJCaHhWpnWK1Lv59oGASG_OzjCekzVfQG041K6uIQW1OpDvTW91LRYAFFMKEPT9dgLZWFZFvM8F6FCvavQXdjNnh78krIXXE2LxhRKMaXfzbcqUJwB2GYZt-3JFnzzkRoICTLLbc356u7Mx_fOafHKfFSwfnKCrtw/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>24. Spring Data JPA 04 Specification을 이용한 검색 조건 지정</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/-znDGy-BQJk" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#2980b9"><strong>Specifications (명세) 는</strong></span></span></p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:16px">1) 코드를 해독하는데 있어서 어려움이 많아 쿼리가 조금이라도 복잡해지면 사용하기가 어렵다.</span></span></strong></p><p><strong><span style="color:#c0392b"><span style="font-size:16px">&nbsp;</span></span></strong></p><p><strong><span style="color:#c0392b"><span style="font-size:16px">2) 생성해야 하는 Predicate 가 많아진다면 관리하기 어려워지고 직관적으로 이해하기 힘들다는 단점이 발생.</span></span></strong></p><p>&nbsp;</p><p><strong><span style="color:#c0392b"><span style="font-size:16px">3) </span></span><span style="color:#8e44ad"><span style="font-size:16px">JPA </span></span><span style="color:#16a085"><span style="font-size:16px">Specification&nbsp;</span></span><span style="color:#c0392b"><span style="font-size:16px">와&nbsp;&nbsp;</span></span><span style="color:#16a085"><span style="font-size:16px">Criteria&nbsp; </span></span><span style="color:#c0392b"><span style="font-size:16px">는&nbsp; &nbsp;</span></span></strong><strong><span style="color:#c0392b"><span style="font-size:16px">99% 사용&nbsp; 안하며,&nbsp; &nbsp;대신에 QueryDSL을 사용한다.</span></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>25. Spring Data JPA 05 기타</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/SYqknEb0wag" frameborder="0" allowfullscreen=""></iframe></p><p><br />&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiT_jkz1LpgL0lgq_su5vetPNvTiI7BGzM5js-m0pnS_FxX3rx9Oxhgz_0wPDMU5_PoBJxGUmAJigzWT9BR-70xu6Hr8R4RCa-bZ5zhP0ZTV4efwFBEZALbyHyVdKrbQwayhDd7KeadZKD9yNlcJhMl1aG3zwc3gVw5rvhMbb2rpz7rlNv4JrOYJ54akA/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqaFpw1HzM179uDJMvfism_yK9ykgw9k0-O7YRH_erGQ3DhoPJ7TcRFBhD_wGsN6QmBYLAnLEW3RNFnFibHrGMM28kOylzGAOiZmMnrDCZUflivRbHKtxtSviOzez1L4oqYlaKKkLjj5D0hhcJzu12Yo_I4RDRSqe3Qm7gQXsEBsmjHOp8L2zWzq54Jg/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="673" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRptyrC9KDRB3UkqMkY0gEsa_hnWjeKJJXrsn5rK28nUktvLhW5y1zBEhk6hGYQcGMSWoSPrhKgZbP34z9oDYs-Ao0oD2CxjFZMznFEgVQup8BAftXxxI6VTwcdsASvurFb_6iWruuRI_ZuJNCnDYguaPs6GkFzQKV0AgGrXqTdQajX8vhr8QOtxDjNA/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfrhVocsbMnJJf3kDKIPlZoVNmR5VigRjSoNfDHer5CsSP2U9KaS6avGVof8aA2y-StTv7UyTd-09GFn6Xsqw7GcocY7Waa7KpkqMi9HUCzNtJUJf1nSCEahN58PRxl-08giFnOzsWUkPxIQZvXwA0Iq5uxf5AMHIm4aioYDOCVxNVAolwcaUFS8y1zQ/s940/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgU6qhvhhrbW6xz_5MEHT5pqz5uiDK7jHq8Fwvll1CJwhkBiA5dEd8tieDYkRQr-vdJCerD4EQuaSe1FnoLgVCf2KWGPO703R0uZf9t1Rznnk2anTfUXnJGmzsSyhRePKIojiPfiVlAT89qQcZnmh27aN90IzCod44ARINy8-vxKfj1pZvAVSjLHnUrcg/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="736" height="262" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg2aBV5AQYeBqVPa-Vq1hs8zJnXQqGWDHzHRxTU9Ayu-l8l7vKaiZLBUlVhWpndjIj8YnM8j55MH1Gs-ttX3DaE0l2qN599Q2MM-wfJNfKn-zO-97QSp5DCU7BQTjZaTvqM_kLb2zyqiJbHxqKXWJxSCv5GBYuyN4zD1-Y76u8tEtqx93MTUJNnB0GlOg/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-03 15:18:12 JPA & Spring Data JPA 기초 - 3. JPQL, Criteria, 기타 특징 http://macaronics.net/index.php/m01/spring/view/2111 2111 <p>&nbsp;</p><p><strong>초급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>JPA와 스프링 데이터 JPA의 기본 사용법을 알아봅니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>JPA 기본 매핑</p><p>스프링 데이터 JPA 기본 사용법</p><p>DB 연동의 열쇠 JPA!&nbsp;<br />실무 중심의 핵심 기본기를 빠르게 ????</p><p>백엔드 실무자를 위한&nbsp;<br /><strong>JPA &amp; 스프링 데이터 JPA</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>인프런 강의<br /><a target="_blank" href="https://www.inflearn.com/course/jpa-spring-data-기초">https://www.inflearn.com/course/jpa-spring-data-기초</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>유튜브<br /><a target="_blank" href="https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX">https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#2980b9"><strong>[3] JPQL, Criteria, 기타 특징</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>18. JPQL 소개</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/lQ4-kVeHVGk" frameborder="0" allowfullscreen=""></iframe></p><p></p><p><br /><br /><strong><span style="font-size:16px">&bull; JPA Query Language</span></strong></p><p><br /><span style="font-size:16px"><strong>&bull; SQL 쿼리와 유사</strong></span></p><p><br /><span style="font-size:16px"><strong>테이블 대신 엔티티 이름, 속성 사용</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">TypedQuery &lt;Review&gt; query = em.createQuery( &quot;select r from Review r where r.hotelId hotelId order by r.id desc&quot;, Review.class); query.setParameter(&quot;hotelId&quot;, &quot;H-001&quot;); List &lt;Review&gt; reviews = query.getResultList();</pre><p>&nbsp;</p><pre class="brush:as3;">@Entity public class Review { @Id @Column(name = &quot;review_id&quot;) @GeneratedValue(strategy GenerationType.IDENTITY) private Long id; @Column(name = &quot;hotel_id&quot;) private String hotelId;</pre><p>&nbsp;</p><p>&nbsp;</p><p>기본 사용법</p><p><br />JPQL 기본 구조</p><p><br /><strong><span style="font-size:20px">select 별칭 from 엔티티명 별칭..</span></strong><br />&nbsp;</p><pre class="brush:as3;">select r from Review r select r from Review as r</pre><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>쿼리 생성</strong></span></p><p><strong>TypedQuery&lt;T&gt;EntityManager#createQuery(String ql, Class&lt;T&gt; resultClass)</strong></p><pre class="brush:as3;">TypedQuery&lt;Review&gt; query = em.createQuery( &quot;select r from Review r&quot;, // 2 Review.class); // List&lt;Review&gt; reviews = query.getResultList();</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj4mRoJFSK-QxXcjRj7gqojJYqn5_lduQ897FGGaUlrW6KIWTxVQh4m7lLRNIVE6RHYwI22MKSX9GZqMypDwKG7oPHiS9BOpoNpPA_ja4H8MK7fGZ9HHqDtDNz_ci0rERYtiEyrckjNmJuDiaf6XeJzjgOlwmIwT7ScNEugKE9_2sx2pg-eL3uYZSUqoQ/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBcwaIcgD8DJ9FUY9RwDoydlYWJ2VE6-s1U6XJ4bgUSYagHcob1QV3aQsUG8k2MhncSJJwJqtaXIf2h3v_0tFusxbcqlTVPNRJc9AqGsO_FPnS8kTlB2_pV5a2y1MV-4Khb_NyyCgBLurl4QM1XNmSo22DKsv4nv03_rYsdnyhA6fMwiGp8kwsTuzftA/s16000/4.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU_1vd800W4PsShYcVnj83itAZo-GMWSKpJPm-U2p2RtJwFgtF70s1krFQyCsSQsHUXv2EBwSV_qoQKbvx7eIZL4kKwT7k791vjxhx1c6y_OyFFPovULfZV-3qIoft5Cd-W7ZT0MPBzEstuvdQhB1fJ9pxy8FdoWRVuIFbZM1vLpSf1lj6vKrQbXWwrg/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="790" height="388" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVPqq7sBsJ98OE9WnB6_UEMAHzrXBQVIDOW79gzmEUaE5LG9_GQtCyXl15s_XhabHZMf40HS0YnaEcQ3SorohunotE4Xz3f2hCosQ0cRWMnT_sOB8I8UXw_9Bx52h8O5cS8Wh2ya1BYw4M2Mxjsp7Gn3gJfpoLbW0Ecdc49jG0KRieBzgfCKJLuwFghw/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="344" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicA--kRjy0xPmjxUMkaSlezbCuAifjurjhOv5wBsaCNP8ZoKWTXaopFM1ii1EaAhOjhH3nXeAUkaoP_NGFZt6VP0jDgKzN7ymnC7ftYj1Qnv7d58Eov6lK_WQGOVda0jqBwMDwrlNQhirC-EadYgCGICWU48Kcb95KO-ZX0UujhTwyE0RB320Boly2vQ/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="293" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjj96G8H8t7SlvOu_RRqvLJ6mqFStQEQI0wP5sN3t-PkhOYAnYCo6WkvfTuxNbJDhW5j4JLV9Cn9h5FM3pNPv0eZ8ZN1RpJEwG9hxelUO9qJKHIY6ZJNir-b63d7Pugf9QSVQgzAkG4KB2uBkhfm3_6RzgLLoQM1KYIGZuYDf-JqlgayELvu_t50oU61g/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>19. Criteria 소개</strong></span></span></p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>사용 안한다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/ZAiH382rUF0" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>20. AttributeConverter, @Formula, </strong></span></span></p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>@DynamicUpdate(@DynamicInsert), @Immutable, @Subselect</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/deJnCTkjLyU" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7C083zHbHdhDiAfiUWqlrqfZwlPu6pGr183des1qoR5KzqcvIcoNHXdry8crwKPqmzmjwCHXZ9tf81wwNmUXYh1Nj91j1odDkrTeJctNZuqTghRzxU6BXlFvPC4Yt0mlg92cmW_X-_mskRRX3FNaKaQXV9b6Y3ISIjrITAAol7HBt8h9kGtVralB5JQ/s16000/1.png" /></p><p>&nbsp;</p><p><img alt="" width="980" height="412" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiL2kZfbB21B-DKbDNNVqFm98fxv6-dy1gXo4pknKzCIHcAFcfR5FdM6x6s2vx5T7IV46WqFwPNoyJMRlh6uk3UrIyhPa8xwY8sffMGmFW5e5VlIn2i8lMKUfu-uT3j09HAP0KDNWk0-tUNzEavaFFwsOXYeiFa-rQ3bTrQbKN3sl6GDnPp3KTzXTkYRQ/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="397" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiAY3JcgvECHkNhQZoKfGMuRv3aaIpUYtiwBVR7T2-DkHIt_RU7Bpd4s_QOBIq-x4aK3Mhk4M3-tNqdPwCzKY9_6SLwhGDsoElsAs6mKsdmNqgVXBmR-5uRojV7F7XXI_3Z1P5dIdXnMs8DTUBU3dzdfJ-BX9-bX_EYsdwRZoiim8n4qsvhCSCv9womXg/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqBxKMLveHPtbJXEegX-b4cJmGhIy1fLXCWsaKAqFjgCs7sC2KzxNUNLi7QCa8voVoe05kk1_44TYJd9VdOwEauaJGal3JTyY7xIjE0SAp_WbGizmBDk82u4Djyk6S81rkWYlQ9xqnVADP-WeYbjcuDPTp00pf-sztgWuNRHcHi_tvgomYKzSx6PrwkQ/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkAeoJQD-JcZExm7EUe9CqmryLLNwMOd6-gepygL3Kp7xUvgtJt1q7CKsuMSVaGcP0E-1Z6FEIpN44aEzMbm4wYR80JbAsR452gWmH-5KoK-J3EmSD0FcmFtc0pS3SiO1N2kx5Uu23OUgV3rYYEmvjCAwYJ9Dt6EA2pjJISwvXNDQEsfi0I5OTuKIpCw/s16000/5.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUW9v9DogR4qYMw3DhvvsPb7m84g9gA-MfLvL9oxsXuLARXeG3FjQpD9e1uKqxoC30ZWlnn3WeWwPG9pIC_GYpbHBPN60ehOeNi3tNjeQPS8OV1Uts9EZ1UMh68kNcAtZ6umfNr3kWIFKmJivT-gdsUv6LGxxe8rCYiygi1TpvLMlTw8g1levwv1hwUA/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfIyIfl2j1c_hpuUkhfKh-m7XVhCPgdKWIkksjLTUWUXY34z6cp88i4WwYXXdrAR8nikNPg4bQxV1EyzACPhed2oIIeG70YS2qy8-h4yWh0W4LCVp9eWBVBK4Zao0c8PlR9Rxlc3wVprxGk6eTJOEfPErpdBi-GlccwZgeUddGf1l1p0LfUCwfxddQfA/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmSJrOWWC1QZYTPt067PgUzXgWxV6Q628CQkt1TRJULcrdBuoW_LwlrqmJ68ajOTTgT-ZeDhUBL9aiD3piWu0X8U1ya2dL1FzTxhEkx60GH7zmstDQtxtpaA8xokHLOunGbYT1lac7K-wqpIWkA9AXH8P4UM_JNfa94-LtD9HD6F9Ce1vuIAwUM4RDFQ/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-03 14:18:30 항상 비밀로 지켜야 할 6가지 http://macaronics.net/index.php/itcommunity/life/view/2110 2110 <p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/dF-5cwKnXqM" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>플라톤이 말하는 인간관계의 지혜 나의 사생활을 필요 이상 말하지 마라 사람의 이기적 본성은 타인의 고통에 둔감하게 만든다 따라서 나의 사생활을 진심으로 걱정하고 위로해주는 사람은 거의 없다 오히려 내 이야기가 주변에 퍼져서 심심풀이 주제로 소비되거나 언젠가 비수가 되어 나에게 돌아올 것이다 또한 자신에 대해 과도하게 털어놓으면 사람들과 더 가까워지기보다 오히려 멀어지는 경험을 하게 된다 인간관계는 시간이 갈수록 계속해서 변화한다</p><p><br />&nbsp;지음은 아주 가깝지만 몇 년 뒤는 어떻게 될지 모르는게 사람 사이다 어떤 관계도 영원할 수 없다 만약 나의 깊은 사생활을 잘하는 사람과<br />&nbsp;관계가 안 좋아지면 쓸데없이&nbsp; 신경 써야 하는 일이 많이 생길 것이다</p><p><br />&nbsp;특히 나의 안 좋은 습관들이나 불행한 가정사는 더욱 타인의 판단과 비판에 노출되지 않게 조심해야 한다 삶의 특정 부분을 비밀로 유지해야 내가 더 품이 있고 남들에게 존중받아야 할 사람이라는 인식을 심어줄 수 있다 .</p><p>&nbsp;</p><p>드러내지 말아야 할 내 사생활에 대한 이야기를 실컷 하고 집에 돌아와 말하지 않았더라면 좋았을 텐데라고 후회하는 것은 어리석은 행동이다 자신을 자랑하지 마라 남들이 나를 인정하기도 전에 내가 먼저<br />&nbsp;떠벌리는 것은 그 일의 가치를 떨어뜨리고 나를 점점 더 볼품없게 만든다 만날 때마다 자기 자각만 늘어놓는 사람을 보면 탕감이 생기지 않았는가 당장은<br />&nbsp;필요에 의해 곁에 있더라도 원래 떠날 사람들을 포함해 좋은 사람도&nbsp;&nbsp;당신을 떠나게 될 것이다</p><p>&nbsp;</p><p>겸손함을 길러 불필요한 시기질투를 피해라.</p><p><br />&nbsp;진실하고 안정된 사람들은 그들의 성공에 대해 끊임없이 자랑할 필요를 느끼지 않는다 .</p><p>&nbsp;</p><p>자신의 가치를 알고 있고 다른 사람들로부터 검증을 요구하지 않는다 계속해서 나의 선행이나 법적에 대해 이야기하는 것은 나를 거만하게 보이게 할 수 있고 사람들이 나를 피하기 시작할지도 모른다 말로만 자신을 증명하려 하기보다는 행동을 통해 진정한 인격이 빛나도록 하는 것이 좋다 나의 친절이나 관대함에 진심으로 감사하는 사람들은 그것을 기억할 것이고 내가 그것을 누구에게도 밝히지 않았더라도 언젠가는 그 호의를 돌려줄 것이다 겸손과 진정한 친절은 다른 사람들이 나를 진정으로 사랑하게 하는 것이며 이러한 자질들을 자기 자랑에 의존하기보다는 나 자신 속에서 배양하는 것이 중요하다 내가 가진 돈에 대해 이야기하지 마라 세상에서 사람을 가장 크게 상처 입히는 세 가지는 번민과 말다툼 그리고 텅 빈 지갑이다 그 중에서 텅 빈 지갑이 가장 크게 사람을 상처 입힌다.</p><p>&nbsp;</p><p><br />&nbsp;그러니 아무리 친한 친구에게도 내가 돈을 얼마나 많이 가졌는지에 대해 말하지 마라 인간은&nbsp;&nbsp;다른이가 나보다 더 많은 것을 가졌다는 이유만으로도 상처받는 존재이다<br />&nbsp;</p><p>앞에서 다들 박수쳐 주면서 함께<br />&nbsp;기뻐하는 것 같아도 속마음은 그렇지 못하다 나보다 돈이 적으면 무시하고 많으면 질투하는게 인간의 본능이다 특히 나와 별 다를 바 없이 살던 지인이 갑자기 부자가 된 경우라면 그 시기질투의 강도가 세진다 그러면서도 당연한 듯 무언가를 요구하거나 기대하며 내가 더 받는 것이 당연한 듯 행동하기 쉽다 결국 관계는 끝을 맺는다 그렇기에 아무리 친한 친구나 가족이라도 내가 가진 돈에 대한 이야기는&nbsp;절대 하지 마라 다른 사람의 단점을 험담하지 마라 누군가를 욕하는 것은 나를 부정적으로 보이게하고나 자신의 명성을 손상시킨다 비록 험담을함으로써 다른 사람과 유대감을 형성하고 있다고 느끼더라도 특히&nbsp;&nbsp;신뢰가 중요한 사회적 상황에서 나에 대한 의심과 불신으로 이어질 수 있다 .</p><p>&nbsp;</p><p>내가<br />&nbsp;다른 사람들에 대해 말하는 것은 나를 되돌아보게 할 수 있고 부정적인 이야기들은 빠르게 퍼져나가 자신의 사회적 지위를 손상시킬 수 있다는 것을 기억해야 한다 내가 누군가에게 반대하거나 잘못되었음을 알려주어야 할 때도 직접적인 비판 없이 재치있게 접근하는 것이 중요하다 그들의 관점에 귀를 기울이고 그들의 상황에 공감하며 그들이 성장할 수 있도록 건설적인 방법으로 이야기하라 불필요한 조언이나 비난을 하지 마라이는<br />&nbsp;관계를 손상시키고 신뢰나<br />&nbsp;존중의 부족으로 이어지기 때문이다 대신<br />&nbsp;진정한 지지와 격려를 제공하여 긍정적인 관계를 구축하라 우리 각자는 다 개인의 사정이 있기 마련이다<br />&nbsp;타인의 행동에 대한 의견이나 조언을 주는 것이 항상<br />&nbsp;필요하거나 도움이 되는 것은 아니다 그저 말을 공감해주고 들어주는 것만으로도 충분하다 나의 약점을 말하지 마라 사람은 모두 입안에 도끼를 가지고 태어난다는 말이 있다 어리석은 사람은 말을 함부로 하여 그 도끼로 자신을 찢고 만다.</p><p><br />&nbsp;말은 한 사람의 입에서 나와 천 사람의 귀로 들어간다 질병은 이물조차 들어가고 화근은 이물조차 나온다 어쩔 수 없이 약점을 얘기해야 할 때도 결코 전부를 드러낼 필요는 없다 우정이<br />&nbsp;깊지 않은 사랑과 깊은 대화를 나누지 말라 말은 한번 입으로 내뱉어 버리면 두 번 다시 주워담을 수 없기 때문에 항상 조심해야 한다 내가이 말을 해도 적절한다 내가 후회하지 않을 것인가 하고 한번 또 생각하라.</p><p><br />&nbsp;수다쟁이처럼 말을 많이 한다고 인기가 좋아지는 것도 아니고 인정을 더 받을 수 있는 것도 아니다 태난 말 한마디에 실수로 그동안 쌓아온 명성과 이미지를 쓸어내 버릴지도 모른다 들려주어도 괜찮은 말이 있고 감추는 것이 더 나은 이야기도 있다 상대에게 말을 해야 할지 말아야 할지 판단이 서지 않을 때는 먼저 상대를 주의 깊게 살펴보면서 그 사람의 인격에 대한 확신이 설 때까지 기다려라.</p><p><br />&nbsp;그러면 말해도 좋을지 비밀로 해야 할지가<br />&nbsp;분명해지기 마련이다 인간은 타인의 약점을 통해 위안을 얻고 타인의 불행을 은근히 즐긴다 내 약점을 다른 이들에게 말한다고 해서 도움을 받을 것이라고 생각하지 마라 약점은 누군가의 도움을 받아서가 아닌 스스로 극복해야 하는 것이며 스스로 극복할 수 있음을 명심해라 당신의<br />&nbsp;목표에 대해 말하지 마라 타인의 조언과 지지를 구하는 것이 도움이 될 수 있지만 사람들의 의견과 관점은 종종 서로 다르고 각자의 경험에 따라 상대적이다 세상에서 가장 위험한 것은 해보지 않은 자의 조언이다</p><p>&nbsp;</p><p>나를 이해하지 못하는 사람들의 근거 없는 판단과 평가에 쓸데없이<br />&nbsp;발목을 잡힐 필요가 없다 결국 나의 목표를 만들고 성취하는 것은 나에게 달려 있다 목표를 향해 어느 정도 진전을 이룰 때까지<br />&nbsp;목표를 혼자 알고 있는 것이 더 나을 수도 있다 목표를 너무 일찍 논의하는 것은 불필요한<br />&nbsp;에너지를 소비하고 목표를 성취하는데 필요한 진정한 노력을 기울이는 것을 방해할 수 있다 나의 목표를 완전히 이해하지 못하는 사람들로부터 비판이나 의심을 받을 수도 있다 자재는 최대의 승리다</p><p><br />&nbsp;누군가 나에 대해 나쁜 말을 하고 있다면 아무도 그 말을 믿지 않을 것이라는<br />&nbsp;믿음을 가지고 살아가라 깨끗한 곳은 건드리지 않고 오직 상처만을 노리는 파리떼처럼 악한 자는 타인의 장점은 무시하고 단점만을 찾아내려 편안히 된다 그러니<br />&nbsp;악한 자들과 어울리지 말라 악한 자들은 당신을 방패막이로 삼을 것이다 그리고 당신 또한 그러한 파리떼와 같은 존재가 되지 말라 진실을 말하는 사람만큼 미움을 받는 사람도 없다.</p><p>&nbsp;</p><p>아무리 설움이 크더라도 동정은 받지 말라 왜냐하면 그 동정 속에 경멸의 생각도 함께<br />&nbsp;들어 있기 때문이다 자기 기만을 조심하라 우리는 결국 우리가 생각하는 그대로가 된다 무의식적으로 자기기만에 빠지지 않도록 항상 주의하라 인간은<br />&nbsp;육체의 속박을 받고 있는 존재이다.</p><p>따라서<br />&nbsp;욕망으로부터 자유로워지지 않는 한 인간에게 결코 만족이란 없다 한 사람의 됨됨이를 보고 싶다면 그가 권력을 가졌을 때 무엇을 하는지를 보면 된다 인간의 행동 바탕이 되는 세 가지 요인이 있다 욕망과<br />&nbsp;감정 그리고 지식이다<br />&nbsp;</p><p>자신의 부정적인 생각과 감정을 통제하라 그것이 개인으로 쌓일 수 있는 가장 위대한 승리이다 최초이자 최고의 승리는 바로 자기 자신을 정복하는 것이다 자기 자신에게 정복 당하는 것이야말로 최대의 수치이다<br />&nbsp;현명한 사람은 해야 할 말이 있을 때 말을 한다 하지만 어리석은 자는 무엇이든 말을 해야 하기 때문에 말을 한다 빈 수레가 가장 요란한 법이다 지혜 가족을수록 수다가 많아진다 때로는<br />&nbsp;침묵이 최상의 대답일 수 있다.</p><p>&nbsp;</p><p>내가이 세상에서 가장 현명한 이유는 단 한 가지 사실을 알고 있기 때문이다 내가 아무것도 알지 못한다는이 한 가지 사실을 말이다 올바른<br />&nbsp;행동은 자기 자신에게 힘을 줄뿐만 아니라 타인도 올바른 방향으로 이끌어주는 힘이 있다 교육이란 우리 아이들이 올바른 것을 추구하도록 가르치는 것을 말한다 교육의<br />&nbsp;시작 방향이 훗날 그의 삶을 결정한다 마음이 행복한 사람만이 행복을 얻을 수 있다 마음이<br />&nbsp;곧 현실을 결정짓는다 .</p><p>&nbsp;</p><p>따라서&nbsp;&nbsp;당신의 마음을 바꾸면 현실도 바꿀 수 있다 행복하고 싶다면 먼저 당신의 마음을 행복하게 만들라 자신의 마음을 행복하게 만들 수 있는 사람만이<br />&nbsp;행복을 얻을 수 있다 마치 놀이처럼 즐기며 인생을 살아가라 세상을 지루하게 바라보는 자에게이 세상은 지루한 것이다.</p><p>&nbsp;</p><p>아름다움은 그것을 바라보는 그 사람의 눈에 달려 있다 겉모습은 그저 속임수에 불과하다 현실 너머에 진리를 볼 줄 아는 눈을 길노라 무지는 모든 악의 뿌리이자 줄기이다 그리고 모든 선의 시작은<br />&nbsp;진리로부터 나온다 정의는 자신에게 어울리는 것을 가지고 자신에게 어울리는 행동을 하는 것이다 각자 자신의 위치에서 해야 할 일에 최선을 다하고 타인을 방해하거나 간섭하지 않을 때 정의가 구현되는 것이다.</p><p><br />&nbsp;만나는 모든 사람들에게 친절하라 우리 모두는 각자 자기 나름의 힘든 전투를 벌이고 있다.</p><p>&nbsp;</p><p>나이가 들면<br />&nbsp;평온함과 자유에 대한 지각이 생겨난다 .</p><p>&nbsp;</p><p>그때서야 비로소 집착이 조금씩 누그러지고 온갖 지배역에서 벗어날 수 있게 된다 본성이 평온하고 행복한 사람은 나에 대한 압박을 거의 느끼지 않는다 감사함을 느낄 때 그대는 위대해질 수 있다 그리고 다른 위대한 것들을 끌어당기게 될 것이다</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-03 08:31:54 JPA & Spring Data JPA 기초 - 2. 콜렉션과 연관 매핑 http://macaronics.net/index.php/m01/spring/view/2109 2109 <p>&nbsp;</p><p><strong>초급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>JPA와 스프링 데이터 JPA의 기본 사용법을 알아봅니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>JPA 기본 매핑</p><p>스프링 데이터 JPA 기본 사용법</p><p>DB 연동의 열쇠 JPA!&nbsp;<br />실무 중심의 핵심 기본기를 빠르게 ????</p><p>백엔드 실무자를 위한&nbsp;<br /><strong>JPA &amp; 스프링 데이터 JPA</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>인프런 강의<br /><a target="_blank" href="https://www.inflearn.com/course/jpa-spring-data-기초">https://www.inflearn.com/course/jpa-spring-data-기초</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>유튜브<br /><a target="_blank" href="https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX">https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>8.JPA 기초 08 값 콜렉션 Set 매핑</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/lQ4-kVeHVGk" frameborder="0" allowfullscreen=""></iframe></p><p></p><p><br />&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEixfkLZHOavLqXC3cRWyiQacgW7gJppYSJmp_UefZtvCj2gLjVvA1HNYySKEdxhjH02bJFvj5qB7lXdyNX_XhYfXOaStd5LlvfpjuzgbpOQOyS67bLzKSxGrmXul-EBGViLecDlxKJ-Abu5Ph2Rx66EA6cEtJ1ePT2bc7EFWAZ9q3nh_fHGKc6fE9Mqsw/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="820" height="434" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoTE4oy5_aUhAifJWHxUCDbt2Xd0pBOSKDpZxTuH6l6TzrY_xWbtwywet9IPJpXjyRc75fkJOGn_Xca_xrmRj4-CwI8qAAWGBu8uBx6Ub_Uwz4Q8MWzXK9q4j_OpPm9opmTFtu41BaJQcU_tK73AZDGeB7ib4vwsNnpGeVZGRZ4C3BKJzagAmNf_Q9gw/s16000/2.png" /></p><p><img alt="" width="784" height="362" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3hYcebG3T9LuXJmaX1gE8sb_AHW7HY1Hdl4nHPOhGb7Pdi0YnSI7s6DiJRjcyhEO1tYfRQUS1NihCMdMpo9nblYzbvCKznATcSdcBmFYt0gAzjiLVd8d7SzePFumIAsz-k3yfvocBGUPtAiVtdGmYCbMns_LP7guVkqukLbtbE8MFUwWfAT0yiPQkdQ/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="838" height="479" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivOZNy11wgeD6qw6Nmtg0b1X5PD71awLCgRh4xDjDsEzKxCIHQjuidG3CceQ9Z0oipri7qbkjAXVelFrd3uJG0gclkJB4X3lE96w6AA2mAlRkPmYFqvsJ1kTRlxMkJdWEcFkVD-YD4A3kaX5WZh1wvJyIr9_UMKkYnXFJ6hDsm2fWtk03B7MAJDHE8Ug/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjKFGAJuunyM_etSx5vamUgxwN7edB6LBvzOgGXvmyeBkps_6rRl1O8jcffI3Oa_GWDTwia0YS1YtsKSMrXb4u1AVy_DOt8g8T_1jqDcYc0xIdpc7fKnfDaHHYnKQN9_G-GMO5huYjDyJCNLMJaUw9czVAB2s4ztxQYQwVo0Si2TPkusvlHVSiJyMRNMw/s16000/6.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjCup9mDqD8bEWwg3b4ibQtmhEhm80gnlMv0JrjGsnGDi1f-flT6iRUEmUhyXSfWD7DM_o15__onVRA3XNlSfuPVnKxJ7E73NfixOvigXrIkOEki9_m_teITLkJjKuTvs8ZxrQ3CcVKY5YNv3fKU7CJv27xYYyso5ekyKq-mL-WbiwJ3jmo6zKRXQS-iw/s16000/7.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjkfYZm09JDbSgolb4hso-7dpqYYVryh3YfEISJRCQ0XDKZX9qhzvnssw098_V7N4eoBRod5kRAjg2fYOXPN4wapVNHDDQBowX7LtmMhF8TQHiz3RFoGsxnfrEV7vdYBwXXO2uK7ewsa9kousgIoQ3Rjg1-unsLdw_APAka-Bm6gKQnTv-1baNExKfJQw/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi4yxQv7GC204cmUXr326wG1IFJ-2tVKUT4Spd5j2-_I4XUM7unt1sU1YQkwBa67wAVZJFCx8US9U0iQzWRTBtQvj5yBGaaFJF4e7hgzzuyf5WDzL-Cyx_mLCOUQju_B4kvcaWmRZ4-YA2qh5Zq_cw8QH7DUwsRxRUE0qmPtWSFZFdJoFvgeja9Lru1tw/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEggaUEXHQJlj56DVChe1z-RwGViISVl__4Df9j7fEDv7nCv7KdSnyKj5GXxzx0w-MCXk2lcrqsC8W6At0E_a9ATq5eBZqEG3IiD_p0NGLLvDeadHTQ-1WuQaT6g3lA-dYKzIO1oCp1-poYRmnAuodxfcaXt4NYrfSwSk-ZmNzC_RTD2XgKlH9CzkCtDOw/s16000/10.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhpkkX8jQ6188Zr_FMFsv_MfiTx4H90umYvyxjN_NPXij230UIIRSeZhVSXlXjnibXaNaa5scctt2U0hoc8g_J2B6MTvpoTibkkEOG5nmQZzH4z9C9bPdnI1QXeTLn6BZ5aSbVZUM8PoTAWWmNTYqEmDjclOotyW9ANIXXW9BpaYuweakFEu0-ENzCIZQ/s16000/11.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgslScvh0hF5Phfn3xuf677TEmATOdApfo2qJZNVftz4cTiy3jZTXum5BmjmjzJzr34Bo2jImiakeVJVXyqpSthFoaLP0URraon_u15PC5f18vAfSBoYg93-l7B8tynX2ev1aEY8_ePeBe1xyPjU_nIaH0jIQM5NsLKAJqvmiyCqrwxMEMASV37Aca5Mw/s16000/12.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>임베디드 타입</strong></p><p><br />&bull; 새로운 값 타입을 직접 정의할 수 있음<br />&bull; JPA는 임베디드 타입(embedded type)이라 함<br />&bull; 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함<br />&bull; int, String과 같은 값 타입</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>임베디드 타입 사용법</strong><br />&bull; @Embeddable: 값 타입을 정의하는 곳에 표시<br />&bull; @Embedded: 값 타입을 사용하는 곳에 표시<br />&bull; 기본 생성자 필수<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>임베디드 타입의 장점</strong></p><p><br />&bull; 재사용<br />&bull; 높은 응집도<br />&bull; Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음<br />&bull; 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티 티에 생명주기를 의존함</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong>값 타입 컬렉션</strong><br />&bull; 값 타입을 하나 이상 저장할 때 사용<br />&bull; @ElementCollection, @CollectionTable 사용<br />&bull; 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.<br />&bull; 컬렉션을 저장하기 위한 별도의 테이블이 필요함</p><p>&nbsp;</p><p><br /><strong>값 타입 컬렉션 사용</strong><br />&bull; 값 타입 저장 예제<br />&bull; 값 타입 조회 예제<br />&bull; 값 타입 컬렉션도 지연 로딩 전략 사용<br />&bull; 값 타입 수정 예제<br />&bull; 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>값 타입 컬렉션의 제약사항</strong><br />&bull; 값 타입은 엔티티와 다르게 식별자 개념이 없다.<br />&bull; 값은 변경하면 추적이 어렵다.<br />&bull; 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.<br />&bull; 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>값 타입 컬렉션 대안</strong><br />&bull; 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려<br />&bull; 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용<br />&bull; 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬 렉션 처럼 사용<br />&bull; EX) AddressEntity</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong>정리</strong><br />&bull; 엔티티 타입의 특징<br />&bull; 식별자O<br />&bull; 생명 주기 관리<br />&bull; 공유<br />&bull; 값 타입의 특징<br />&bull; 식별자X<br />&bull; 생명 주기를 엔티티에 의존<br />&bull; 공유하지 않는 것이 안전(복사해서 사용)<br /><span style="color:#c0392b"><strong>&bull; 불변 객체로 만드는 것이 안전</strong></span></p><p>항상 new 새롭게 생성하고, 수정시에는 기존것을 제거후 수정치한다.</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:26px"><strong>Role</strong></span></p><pre class="brush:as3;">import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.*; import java.util.HashSet; import java.util.Set; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Role { @GeneratedValue(strategy = GenerationType.AUTO) @Id @Column private Long id; private String name; /** * 다음 코드로 role_perm 테이블이 생성되며 * 1.role_id 컬럼과 2.&quot;perm&quot; 컬럼 이 생성된다. */ @ElementCollection @CollectionTable( name=&quot;role_perm&quot;, // role_perm 테이블 이름 joinColumns = @JoinColumn(name=&quot;role_id&quot;) //1.role_id 컬럼 ) @Column(name=&quot;perm&quot;) //2.&quot;perm&quot; 컬럼 private Set&lt;String&gt; permissions=new HashSet&lt;&gt;(); public Role(String name, Set&lt;String&gt; permissions) { this.name = name; this.permissions=permissions; } } </pre><p>&nbsp;</p><p><strong>RoleRepository</strong></p><pre class="brush:as3;">import org.springframework.data.jpa.repository.JpaRepository; import study.datajpa.entity.Role; public interface RoleRepository extends JpaRepository&lt;Role, Long&gt; { public Role findByName(String name); }</pre><p>&nbsp;</p><p><span style="font-size:18px"><strong>RoleTest</strong></span></p><pre class="brush:as3;">package study.datajpa.entity; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.datajpa.repository.RoleRepository; import javax.persistence.EntityManager; import java.util.Set; @SpringBootTest @Transactional @Rollback(value = false) class RoleTest { @Autowired RoleRepository roleRepository; @Autowired EntityManager em; @Test public void 데이터등록() throws Throwable { Role role1=new Role(&quot;ADMIN&quot; , Set.of(&quot;F1&quot;, &quot;F2&quot;)); roleRepository.save(role1); Role role2=new Role(&quot;GENERAL&quot;, Set.of(&quot;F3&quot;, &quot;F4&quot;)); roleRepository.save(role2); roleRepository.findByName(&quot;ADMIN&quot;); Role admin = roleRepository.findByName(&quot;ADMIN&quot;); System.out.println(&quot;roleName = &quot; + admin.getPermissions()); } }</pre><p>&nbsp;</p><pre class="brush:as3;">/* insert collection row study.datajpa.entity.Role.permissions */ insert into role_perm (role_id, perm) values (?, ?) /* insert collection row study.datajpa.entity.Role.permissions */ insert into role_perm (role_id, perm) values (1, &#39;F1&#39;);</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>9.JPA 기초 08 값 콜렉션 Set 매핑</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/Wq4B5RpIeAY" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="653" height="489" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiQaDM2WYPjEpGFyG8SZc-7kRM9wlBH1N8GnRLveyrrLxxTuH-WqU_qjKBkcDY333V3VEAA_iLpr-oP2CCAzAWA1Yj2LPbGEwxyT3giNm6JpWFvlFrruYicaVNK7ZXiaDLGOdvkAv11E0a454W-rsj_Ht_z6i4fclK_FXKNDc_GH5fYWLSAoC_V6OMbEw/s16000/1.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg74glpAZ3UUzwBouqq8IkFAI03WaaWixBPL66qSnX_WfWXPbo3YJonEc-mzQQyAz3nIczn2AohJk9BRcDjQpjNoCiUPyXGWDsBCn2sukLbjBBTJPmybm8ZA3WJLNZguYSNEiTELDvYkDoqJDPWFdoCcLp-x-vfCGg0WgVAOobPQwRMb_sxOVJcZvSY-g/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxuz3UG2zoE6RSbtXMB23yQUSQD43jMTEJkQr1itNfOASE7YM4rWMVIDWEhmBel5ZAI9xJTPOoM_AZXh4Y84pb7DvK5yGbPnrP3pAi2W6QbCtKuvXdDtXMKbDVr0bpwbx3Mlg92szgkX20h9p1jACLYBs3Df9UL6yAPQl91jRD7AcI5T8HpUj7sOqKZQ/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjebTft-qEEaNjD_D4TRp4Dp5_IPLCj5vpeAVvbCHkFtUAaLiuUtuYQqz0LbSsxgb-aT5iGdY0gJYD_hHHOLsD3QeiQ48Sa1njqosfBbjRUUDwZmro23kqo1vuqR4kpYYcVe9-wtQHGJGzmJHvVpcJzOmxBnyO9l_6lY1HE-gyRie5wusiLst1P0Jku_g/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgo2wq7AC1M2uQZu278KX-pd_629Jayx_3fTbMWhh_M0YDaWTmm-mEZB3nJwl5bNh2nOWmTvUI4wQOSJHrM5QtxCHXqzAZpaO1RtgXPLHs-NR1OhO1tbPH6FDUBKYNj5aUx--x6t8pnQa2MR4fbKoO-aSWdULmSlIQo_jg9_BcktSLAgbe4vAgsc6-eVQ/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJbgQvbR6aY43GzLqk84_I4FI9K1YXbOFrkpNhsXYcuubF7fVt9Ke_TwmrTcXSMgKq0uzM56IVTA_Ch8xhjANPVyjcoMVFNa9E883Qcn3B-_jfumt1Y7J9eajrSKB5MEwJEaLvDPhnDdomNYMIMXkyZ9vu2ttERwteH3TlNq5abzUlrBK-PpximMbm7Q/s16000/6.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkDhhadRTh9KV3DVukOsAtWgPkl5YeT8aYmi0n4VboZKlMGsWqSeb2r3BvxPy4ccbvtjRSIIqIdXw3soTWhuV29Bcteuii6zCGeeGTVuNriNRC9H8V9T8GkREXkqcu38NfB0pJuV1I-x4ZHPBncSJQVmoTIdoEQRPRYVRRJiVHwVROHAac7r0gaUaK5g/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>Choice 객체 생성 후&nbsp; @Embeddable</strong></span></span></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHt8Rbw-v61WWvgtVz-5MESEE24rUGdrxEKh3RiiGH9itGG9u2KCIxy6DUYDEfTFaJA7w2C3AXBjSJznM2XxJmnoAbB_eT7bIgSnpGhjYc-MFI5N8OQ4e5lynv0-X6r5B6_o8F_xuYbBaahOINdJjiwbCNdHLmmswZHTRWVA0F7RUGAmyLmzfRv-DeHg/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRf6n8v8cceW8Gyd5gNuXQhIVXRYmcIqcBbUjl4i0VRSjjOsBlghwv13KGINtMvhwhX-2qrW_pRJwR_ql5qtEqFN02cFtgA7MnvgccsC55wuwiP-ID0h6JkeJmLpzFgOzKr_gViUjXONwp3ICAeq9eO1VHQze4Xx-WiZHuwsaPbhUX_bqAILVMUp6EgQ/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>10.JPA 기초 08 값 콜렉션 Set 매핑</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/CPIgicoqLnM" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="696" height="512" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEibmQ_W-R7-l44QHtobPuGe9grYqAUFvPydzu7YiM1VD6HSZ2lPEMgRznj8FbBPH5-ht7yd_51kvDxIcw5gS7wTSy24n1WmzHQeziIEYCOjQH6SDisNzQuQGhfdctZr07ChbIP2XNXJ1RH1-gJ85MpG6v2iPfXBVCWuvvjiGoDOM6vWmQQFoze2btjHvw/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivgLrDY6PI_CXNeaSnsNKdZ7BTfCcjlBdI4eF62hi7uBv7PtwpEWAE1oy2MMmvqOq1ToL85z-GFOOeJOi8oeqiAorxy4PEx-gZ7E8b-8fAPX1PsWnT83IxPy7FU_zfVYNI7DAt_trWkPlYTbaRgwfAf-Liq2Y9j8l_vMiGo65XBJKeQN8mq5Lc4_yKSg/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDkEHQwM5gE53IJJFAedHnaAm0YgtAhvjCyO06ovO8VeMvbB9GOR_9JSVaBKHfkUocghmh5APbWicz-D9kosl2-RzfhvGciDYULyTkZYKkld0jDFwZh7q7QiE5PUoukIC-LnytrQoz1bgDITW8p6zOfweS4skXsm5t8irTHeUQdE9mfkPz2-HsuH4VLw/s16000/3.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhdN10FE6m5DHIzQRwNNUGRb3YUoxwFQF_woNJq27hluEroBTpxkW4KvlJc9mzT4aYWQQ3pyG0bGVLt7r2PyqtDDuC2s8wl0RXRcrdHh1aOlgpZzQk0V6WMIDwAqXiAuwP9kEbYJ-P9WRMQ2XxGGkKH7dNUW4GzpJ0jytVJYjjiWn0i_BTLsHmFfx0SEQ/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgScjbLT-OGa9QSazkDHjr7-2sr11-P1tOrsvNtiETXr3COcV27IitLO7UMvFfXOKpC041qILJqIU_KkmViEXCgGaqqgigZ1drk6y_78dFwMn3aWSwLZAfkDZV7LZA8LyQ5Dj92-zLzJTgRncT2bLIdqmae9kdTB4EXLu2BZ_Sw3eM9vqRlaqyYLUMG1A/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPM4SIgsYyECuemHHHjSqsxTdOIyuoyGrDBLsPHikDIVSaMdHL0hl2qLTRNsF_GwHbnXrvARWSCUQQ1Q_lDPxSV2I8TS0h-OY35vKK5QANjiSgu8SJosqxyjcjACcREE3fcFOcNnIqWYSQV9KpLbst_8urKuU9kcvmd1ELuCXbfcYx-9-dIxpaGJ6MVg/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjH8IFaviTDUuZrORbL78xxdf0BzzNrOhs98sgRBIe0s5olIRyS0GkWcPn4TCAoFrHdCU4O8Y9qLO2Dced-59EO79zR9IUDvD30BpXEmIw5a8IDmsNY8d9rr3GxYIEHG77jkqMfFy7sp1vXz3vdf25k23UQ1m8nM3Bljt_jETZIFfZ0b08ettebdgXpLw/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>11.JPA 기초 11 값 콜렉션 주의사항</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/yK4Avtxqz-k" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="1012" height="488" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEju00ol109R0RAEoKY73dmjgMo17r_hMb0Qn6q-AdcEn6GfotgOZkOMoB4fHRrOmScGATdPX_HnOZhBtPq7KuUugf0f19qp6v9UKbZr8whH9VG1Na3WvDMj26NFfaMVdv0asGVTBdSpSkXr1RlpZUfUTb7Sx5gLx0Cp-z2cYa5BxSrUnhFj3HkedfZaQA/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFNls73hJRkEWN6bHzqIdLmA1tkMZjFIdB7YhEnZbYKMjV3Rbyj3oEtWJpeO9v0aiaycqpuOdnwLLpJc8MHRO-aU98l9u30UjeWOUCBbw0i1UVZZQLyObmvAjH6b9I4VrMtbp8Rqoq4LiPwKTm1g9oJCZ4rn8y6TZdfIYtO8tAC98AAJ43p1Pn7ArTLg/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5mCYOrdRPD4QFg-C7_Q-OZ_BWs9Lspi4EYiOyowEDzbPGgcjCuprumY9uRwiREev4aYh7m3HHmalf4Uo-kiRMMYcJdiJuye_SQLnAgsuXZTqE-E7M8ZmCjVXDHH0gNYk4OG4MdmURe9eU-p6iPXQ7yY__iKe11KvqtFlP4b5nB3wdyYybJSA0YvA3uw/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="783" height="566" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHZXAcG-ILr3jgGMxN4V1C20eVt_x5jxiRyWI3ehyGgXxcyOmkesUJc0Qc307ScYmbYBSXmb4nJ0i9u87qvFXXhQkbAdAJCw4yqeOWHzCSaect89Q7H4mJ7dwy4UlP_ds4V_6TInALdRjb-NdRHM-dZj3n1-FH5DFE--yAxekMSF1PNvQDYUHhW12esw/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgL4Ve1D05YRgqNb2JyNReb3985Cy-w3TEm7ws4QURZ5xLiM3rhLl6MFXm2bfwmTv7a_e1BtYtX34rOk9sOH80hPd2bbhwFvpxMwNM0NkAI862hMQQ7liVuuXgutg-rXQWrlSFurEn-2Hgq9ptPV1kpuHgUK3nbSkDsdf8PU6VT8bLWurgE1Z3Dp3ncoQ/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#c0392b"><strong>참고</strong>:&nbsp;<strong>값 타입은 변경 불가능하게 설계해야 한다.</strong></span></span></p><p>&nbsp;</p><p>&gt; @Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티티나 임베디드 타입( @Embeddable )은 자바 기본 생성자(default constructor)를 public 또는 protected 로 설정해야 한다. public 으로 두는 것 보다는 protected 로 설정하는 것이 그나마 더&nbsp; 안전하다.</p><p><br />&gt; JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.</p><p>&nbsp;</p><p>참조 :&nbsp;&nbsp;<a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2055">https://macaronics.net/index.php/m01/spring/view/2055</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><span style="color:#c0392b"><strong>다대일 엔티에서&nbsp; 상관이 없으나&nbsp; &nbsp;일대 다 엔티인&nbsp;&nbsp;컬렉션에&nbsp; 문제가 발생한다.</strong></span></span></p><p><br />1.지연 전략을 사용한다.<br />2. DISTINCT는 중복된 결과를 제거하는 명령을 사용한다.<br />3. 페치 조인을 사용해서 데이터를 가져오면 되지만. 페이징 처리에서 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.</p><p>해결 방법:&nbsp;<br />페치 조인을 풀고, _batch_fetch_siz 전략을 통해 데이트를 가져온다.</p><p><br />#spring.jpa.properties.hibernate.default_batch_fetch_size=100</p><p>&nbsp;</p><p><br />참조:&nbsp;<br /><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2054">https://macaronics.net/index.php/m01/spring/view/2054</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>12.JPA 기초 12 영속 컨텍스트 &amp; 라이프사이클</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/tUwg78VkWJ0" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="307" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhvstfCXV4xSUeWD9P6aNwdm48vmK1EPoDkOaYNAqfHl31n3PGJhSEC-KPmPMqJpNJtg5sCbjHAgiQqjyOm5rMRHTCKCnjQw0qZBRMdyiyGhkLfiYYhX4gmL-z2vsmfvugg2tHJYVxsEq8Is-1jGE_gHjVrOAXSPRwbQTItQTGH7P5oRGZkVO6epDVSw/s16000/1.png" /></p><p>&nbsp;</p><p><img alt="" width="988" height="376" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgeIvoo4jG2_516eZK00W2QA13S71_Y8i9t6E53Gcep_ks0Kl9bsgPTcBd9XKz1yrenKZi9-r6toUyLBgLH6aLvruFkozEgbin5iBf9CIgn-sBXTSv2a5HeGzL5HLWpevR9oxd_IuBxCYl0vQeYyI3f8B5D_EElJZoa5eXW2O98wYX2MR4qSTEO7l3FA/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik2cilm8r2Y6pLu2Bm_a1O-BiQ6zib1Dbjz9DByLPF_DAG0HbAEuvMZm5aodhA0d1vQOp8ErUrzF3gAr_le3oFPPvuiTQhDZQp64inmUsR3c3VguFyL4r-UegBD-AXmpn5kAzkID-iTc1WsqpvE7Bf30RQDmKMiGjJEMQVqboDLGA5XTrjA6Kv8b1sug/s16000/3.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjscoXBVzHjT3UjJ4cZ-tZR7CzTBtSPgYUpPVboUn4DAcvp6fi73TeiSoY_XnnCfE9yoEVx-sR-Xzl2Y8S2B6ziscC_zTbpOo2wr9vOOriv-l9tm7n8G6hJxOlZfmBSNx_4GupMpsfodj-8I8gBDRa1fHJHY6faK3GFsWVny3YVQI-FmF2iOnMWS_Na8A/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3dc7EGk5UVVKtb474U9DAAqZ9J6sSXjutipl9o8sM-s55QMaSXCmucfGxLHns3zoyZgFqtkp16imoHtdXI2nLWUPc0PGCd5zWWqLhvk9a_XOUfTitcIseoK7cCxWNHDkFRPAtVp1NgcyMS1K3KGlzDWpW2_rM91L9WtRawnw0mWvai7_uxfYnGvL1Ag/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBcW4qy3gc3DVtnAJfzxbWBnkR_Tv19rQpL8ZdKjw-8swBRQ4ZYLdfOFoe6Roztm3l01Ns7lEkvkWpMChb7cLjZ3FES_Z-rJ3-Euo5GVrzIwxgwFKZhts202irF3AfNA7ReY2Fk1sVC7kczzYpu1fS6PbaM6inPYTFSDHYYdzIhHdupIv_-F40EsOPcQ/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>13.JPA 기초 13 엔티티 연관 매핑 시작에 앞서</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/rZZSYG__8Jc" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="780" height="436" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEipg9-6z74_AJ-g02jC83pxSQhQsZEY-cZ0HJbEaWwPThiWoUxGs4j7fQjz7ORsGTy3pCh8fBy4DOKayKh-g3hTEv5SAVlo-1qbEs6paNj3lgOqgwC1Ofa4Ut3mGMdrUyvtzA8yqmxmVlbA7QayqDWFeD7SzLcjK2gRrU-0XSzJwbu0MA819QUPc9T7cA/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgRX_u-4E4w7H-HIv1zhkK99UEJjRl3EjbsXuhO1FPUy5FdqANpr3DjAkRQXp3fmnqxoJxp3OFnossspyYq9CG6zd3xKdJaMicF46cAnJu7xjTR2XUhynPlXBvprmlL-ZvbbY8mSIYiBkY7RioFF6XyFu4QQDGtvdGqlsEFRbJpPve7WpjA0EY4q1E-NQ/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWfqAMDq7oYNX0hXwfFB-GVE946EH0jh8r43T6vNPxx5IR1sAYGzPwYMS65Od3oY9Zwhg0UZHfg2OmXGuS_CwV0p4iVU32h-RxhG6KxGjZARNjPORiZ6O7M457ouxnRWfMK1pYtjrmYRXxbOfkWfcHP9xB3BqWwJlXnq6EJXQgCiiHjcSeulsMSeiCIA/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>14.JPA 기초 14 엔티티 간 1-1 단방향 연관 매핑</strong></span></span></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">항상&nbsp; 지연로딩&nbsp;&nbsp;&nbsp;fetch = FetchType.LAZY 을 사용해라</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/BhVzS90Ep78" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="828" height="268" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSFfqh37mNvkcKXYrmMXJ665qtoW3MHjJgnHGIGlgsqQwRDossOjK0katjO4tkRS6nvEv0amlcXf5VtjBtpQwx2tlg4xBwqmeZToWHIXiIcykWpv9WcxrOtrRoK7TCK7eGObw21tkemJO_MdqKpiQ6R1xUCF-0f_zXgna-YBFMGFSvOhWlbaznUORAJQ/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhHj_J8yOhA8Suf4H7kYuOIMPescG2a8Wxl_mw_K-1IjXaMLMtIuCJTFHF_41WE4dtnRAQ_RUjeM_ROprj4V1SzzFV83rEjkvdN6EAGxoloIOsM4KKVUhAOyoOLOb5lTx4hlaK3TX34lAjhLJNkbeZEVeJvkqJh67rP9dbs7YpC2kkOyFxyTeOWs-qPhA/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>참조키 방식 1-1 단방향 연관 매핑</strong></span></p><p>&nbsp;</p><p><img alt="" width="802" height="448" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvKfbPnYkovHcgljP2iQeHOwkNjP3AIZqpdRQW8HSi1AK47xRN43pYzPbeCn5HEHbZHkKQF9nN0oAsod4UsGavb4LYaYiDgzUB6wfcjf1QoQFH87hxwhsdBHjIcYh3wkN1I21DZQ5yE8rzqh7j-rvBPjndCdOiGLWNjKgEeyKU6RflkKYjAVvEmSXX_g/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjU2VU6Lgw9BKJ2SsfzAvCWDUZ49MO0ctWMfncjJIg41ElafjpOSGoUy6BcGmaa85oGC2t_chq60C_bC0X2PMIFib_XUbHSLogHIzXAMIA6ogIHPWpgo3mouthp1OsHCe601IqSgbQ7tt--GbZL4vOjNPQX-QhtnldO5lmHexvRwOg3CSX4JSkJnrHwxg/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgAFn2PlwpxNrCXWyFppbXqHUjuLHMHl2VB4ufq1C0hPAE55mnZDisP5wuuIC9zJRtBSLpc-RMqseVyOgkaDOHjC6UEzNUZ1MMhKjz4j3pm0yPoQS8ZPIF0r-BZ3rJKezYYCIt1q67XDYH5s4V1kPhir1v4-dzIXjmQsNASjLoSwU8pa8AjS1-g3YGbZA/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#c0392b">식별자 공유&nbsp; 방식 1-1 단방향 연관 매핑</span></strong></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgY1xQC264ofusMH3O3gityTZpvEIXHo1dBAHOZQlj_ATgPGqZbP95KCi80BKkgWrbR8ezeybD_6PlVllxxdJUBZneEVoXTBfXWeYFnSYqOUnl3SbJmHkf19woDL7HI8mtxjmixcE5BWDhWaj3bOWHTBh8yf4hFQT80bg_Re6FSsasA1yhmBXg-YXt_oQ/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEinq0nZllpegCpj5aFeQS7eP46OWUpQOIqvuGkvCWdws1OZqrWUoMs9Uld9zMsMS9zXQAn4NK6KVaOtVUz2ff9fXMlX8oDlenGkd0ep35SlDVTxRKNjIFrK9CGgStu8BBxp8D97limkSLTba5rAe9d1ds5I5NCVygU0JvZEWkwNl-YB7LFHt8Y4ggKiKw/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiypdKAYOwZksPWj3KjrGeu6qskLFjGDVABzRH7G6i9WStGoW07W0BrH8uhljp2WVQNhZeskb-UlpXk8Y6Y59fK37E6_MnvT8A74S-zQr6sprpsRyE_7oc0kgD0Xkr6lp6y664b2yK6qiHJa5bhG6J8eiSovLha6znLGZuOn-ccuPaiFfZesNH_MAkYrw/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>15.JPA 기초 15 엔티티 간 N-1 단방향 연관 매핑</strong></span></span></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad">항상&nbsp; 지연로딩&nbsp;&nbsp;&nbsp;fetch = FetchType.LAZY 을 사용해라</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/i8XAqCGcLqw" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="614" height="248" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhAkDucCdpsj846o93_3CKZ5FT2pOIodCprGY5hlDQnd4BQPV60k8PFIuxDgQb1FyLekRq50L1mLuzUhYquMJO52zLV-NDBZMr9fTIMcsSBAfHR_esvPYkED7sixs-c5zKL6dpJ7xqWad6xHYXrh6m0H5iCBUJExbEeZshMwLTk7mZ_7r4yDPZvkXNyFg/s16000/1.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh3ZoFaC0ljoeXPEbaOYkWUUVCCTUAeATl5UKbMNOgArueIguptjvMN_IcLVrxZtRlpjm5pIQn-gs3gAQHJJ7Kp0nhr98uwYGXs1wcLpTfL7EoUwGX3SfWJcvXUr-pANlzAaU5lGMQIp18ci3XVO-5MQDLjyENmez1imW7LdtzDnmQ5EjunhKS1N9E4zA/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbE4BNv2R2Yvp1n9nhwvzTuwGQ6SQMuucXL72ohV-h6QuD0Fq2mlwUHvzi3YvKWs3j9fCKCMBGFepzcshcFOLI0mXsF9QA5vFIOUexUgdsfBwF5-s5hNNSYIsciidqSQ7kY7Xqkin4bcNZsk3t-OJ0pCJxMc3OesS1IybU1TZRIHaPc0eLltDrK10R7A/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXyAGKRF19Lrw5aUhuKMeyF46YVEcQR33FVrt1Y6haWH4MtAtfj4xWqeiqH9ycFCAdQXLWr5tSDkWRr26mCpCTnbIPtjnU08atrs24vVQM_Obe6VUdkc3nzZTqSuTIftUlAslBNqixODftFAKj4ySnKw8p2EP2sZjVTDVLumDquWPFL2wogaAm0OYOng/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUO2Oddf0FBakOjmzVyOwTzemrNhBwY20DrNpPyRNvV6QWmiLT6a8aun2uUmlmuY5nKiS1XbiVQ4C17wwVjy0mZ8WVcpiY_7HXsXAf6ZaNmVPumuMEu_iws41sDLJAjxBPJ_lnH0tjNax5VGFpjjenLSEX7Ju1feufAf-kGndRMDDJQvT_iNPi29MeNw/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZ_w8HhrCPNKA_k5SHxthnnTlBOOmsUijLPfwh6fcg9GbXg2Bj1ZLEhmK4tvtwp5GxXxbh5cxZL78Pf5Jaw6mSAf7this35BuiuPg0UdFj4WLgxzbDn3Onm0gXvVB4DvtbZWrcr7-hcAycd-RXOPwWyL3l0JHts-j6hMhReq2ZUMT1dyYznDFksmbaZg/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>16.JPA 기초 16 엔티티 간 1-N 단방향 연관 매핑</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/LAA8ICFS8bs" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="194" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjDhlzrofK4VIllmomrV3lZTekuCWUZD7XF5ROE8mLrMbXedPj4MlpmE0eHA3ONS4t2ZHHNTMA2U8Lp3ozCO4tzdsrk9j_RACPxq9b3J9HM6UJY6QHuRpRoTkp_egdJ6FfgGEvZpIk2rqCMIEN6X0YfFb43UEKXzPjuJurAp_uU_oUCaiXv9RLwWqQWQ/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="247" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiOPVSmFZwWk65ujVqv9arFQ-AT9RDGixpivKYe-Ku4VDsFg8Q2sbOz8E5tmvF7CHFmFdCtD1qe3dFJx4FdY9s-L1LyglQxoxxZ7dZgSfJPcWVT7D5l3odv5B3ajYZV9GlVJ6_NpFnqnu7tt8RYj0J7btxjKqIrAZkOTgSzDdOmQRWe250CnTZa1MEYg/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" width="600" height="388" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjB-oXr8izyzEGWRrmYYGVZgAeFrfIaQVVqNDpVgLxcrxbOMG4Hnz16k3fMK0m9QxL-w1lDiqiMPxNoUquTPh_Pk39nG9V9NCJUtCzSx19RfFmo8hkU_pE-OAxX77hfX9BP8MP5o1mDDNuYoJSGEsGIdo-bhy-arWhqDBV-YqZCGXtbyY89tXbJwzj5Og/s16000/3.png" /></p><p>&nbsp;</p><p><img alt="" width="600" height="392" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjjFWsIKo0OQKFlxhZ02u7BL3-aCV5odGom8WZkl-qXxqdHsR63d_ES-kiEVkToBUHy3b6t7Fkn0E7zJkmiUH5JIJ7joM4DZqKP99nyOQSEP_-hSJN7rtBQzqwbJhzDKVNS5gJ9G95HYeyT3Tk9d8SOWLNkpO_bp5xVI8r98ClQQ--N6U4BnrlxrSSuQg/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="216" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjG-8Sc54-YNJWeSk8U1VVP0tCRVZp9CUUPJ6e7laHcNgK5lT0qNx6IizN5vC_UERUdlG2-CkhBpxdgUs6FJxFev5VeAp9VBCNRIK7BL7F8EerEacO-KfnvHcZxrVXhwAMlLH8rK0qU3qK_tBr45FSQVMW2IrSx3S0uuoxf-R84oCeCuZqa-Mq5Wld8xQ/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="318" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwz-CnbkATkNrBLafPTdn-mBamsqsTZc5Kthm9kQFZmYk4gZO2h9AX_OfwRDO_ULMW3pnj0X2PfgyJqPY2JL5VxPPHVQq0vwQRoSclKuX1g_ZJwBzrONV_5cUr_bn3lQJh5fPZiAiietMtTCebANbiyAjJW2Ss9KsfE1W1G09_FHXlaipBIre9fWIeJQ/s16000/6.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="471" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJA4_gnMxWqvjVAxKEqj33WjTGqKWgMSiM_OLBGnb2NYEbFtnd5WojvNfWLzcHY3Cp3EMLCb5gFCk-_GEefRTzSR66qqK56f6ud5C5QlMvOh9csnggoDs_1gGA-qDDHsgfoN9V0aFHty_t3JfYekyRqNcZf6jAaXF3HxQvDDHFT6ecEMuxTmlQ2ie_DA/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="406" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgvTT2XfcP9be0zlc0FVzbC5PXLeYrExoZs3TxFkF9Y3N8ZVYNh_rcszNaM3Mx8kHazmb6Yiqnk2EljbW-tD4AXAaL56JLFQPoqi4QbEm1JiY415f3zrEzInf4pEVlYGN_7lodLmJEkkGak3jrB57YQIb8QMZpjTb00QwEChnm-3I2xnLBLlGbNb4ET9Q/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="471" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqurAKUkFAMIT4Z0ysbua-caT5ncgkIh9fzVOeCR9pITRWazI7Xrf6Wlen3PIr2sYGGKJVsPwwwqDpqJwyRzRe4mQtKK7uGbsbFPWYEFNbxiYV6heZ-Bu5sjh_HmL7jHC3uUr9v_3kBI73ydqbmy-OpR0G8F5KYr4pADwCnXpI5_FkYXSn84mRuCei8w/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNsjTXHt5lFeTCMz1B9NVBMXvG1EvFBHNhXdAdAXsohGoMivKZ-tpF32UN4QNfuPQFpzAN-at3V4ajHT7x3GCHiVcL8V8WW7LgChYR9zKEHyMnnQ7JmXEV9CvbaCeIMS3Dv0bci_JHzlUTwGpAM8KYq8OYD_YQYfxTMeZ0ETv24aMFAN99_PgdeOIvVg/s16000/10.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="600" height="381" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWg_OrYHlsoOTx0l-pBz2vo5X84GwIxaIgdJYA1JPp3E9uvjr4o2KZkQLBAEb84voSaZ9kMzQ_qezVVBN7Ra883sCw3TCcGwRU1AWM1jZ_IHGi2kb1zTjKs-cRB96288EKECiQ0fInFxXb_NPUI4UgiHQ-rnT2B9mRtK8cthrEFL4VEhndo6TU4TYCrA/s16000/11.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="877" height="442" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMq7c-VSCGbT_BfEEE2pEsI1GsYoFttxfHEDN2SpWcm_m4amcHK1SQ-j9GJtsLAQuD9CNulMiJL-qZ4EaBbVxynlo0QUsPl1kAPJR4TfN5m7owZBvTBTRJSxumdlfisPUZXoExNaDaYaI4IxbYlfAMWgxwhScEyXaHKKNjQlErRiJBanHGvGbRMAPmKQ/s16000/12.png" /></p><p>&nbsp;</p><p><br /><img alt="" width="900" height="354" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgOdtOh2RkHF-SuVLZH-ifGmfB-fnIofX9gV-1BCyOGAHm0nJeiTHhrW2yiT2xsINkONh5BmDEkriiM7wvbgD1qyhpK0mWBkpCo1ptFYh_Hmz9_c425UPb8XRaU5TBj8OsUT0tCiYRk3C0D1TwvPpm3dBZCY0Frrc13LW8YsmkEvBVUflyTw71w2LM_aw/s16000/13.png" /></p><p>&nbsp;</p><p><img alt="" width="900" height="325" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_Q0qzh46dXzDmE-kKjdWpnzFSWUpjG1h4KPmv2P0UjCa65R3B8DGac-zbu3N_w2KysSEBwww9H64vEAs5f2Olt7eDyy1L-SWl4uXLXSK3CvB0XOo3uOXlXE-_LHRbprJuL5aeKqcovtqH_NSK8X66O-ioemeArn2_mySbTEyguVKd9nRyRtk6CXjwPQ/s16000/14.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>17.JPA 기초 17 영속성 전파 &amp; 연관 고려사항</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/7JAoNNhvsjw" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="918" height="521" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhw15-mWhQFMKbCi_lOEbTa50f48c4-oYYLJwAYZy2qk0KL63hcoEjYRcdRCwJ2vmiBSbonB1FhdH7KV51RI--tJ5tkAhWS9bOzXnZ84msTKvVVcfQkidccg2PcBESFhr4eprv5R-jWO1cFWBg8Ryrxj77tSFpvpcdbU0EnXX1UIp-w2308kZmmn1P5Tw/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim_i2hyKabrvf5A-831XQbFsUr6TBjcaWVkVj6xdd92LgQDNx3Iq8ABSDj2KYfCyC9Ef80SuvkQKpHinlhX-bHAUOlvmFsx-XKWOCH-SQ7tvPGfsXoUhCAZj7WtT1zfHnnfCM0lEJQNn3Iqvj5OBnO4GLJc8BI7dCVJ4xmgA2PtUVZy2yph2LCy1V0Pg/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZxl1brNJDQRZuNds6N4wj04PrhjaGE6SPE6EFmySdFJHQp2dL36exU3TE4re5c1gXesHvgG4Az7co_e57t0daEGZLclNtB-OtWbv5cv5ddzgtFb_P54Lj8e3xjqi7JZwXT_hJ8qlYvw4CGyzIX3AncnVzulZFicQzviiQNeLC8rywWyrGqy8_NZUQ9w/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj_LwmTyAhDBfme-XTxwN4gmt3be9gakxymQjXFD_rWpuWKTXVV9EIXDS5e2tDkYOjeTUBroI3P2xstZQ4EY69_Y8dqu5lzcka6zRDUbnmpyuYL6ahepxET2_GWLkWitEmCWJpuctZp3kVro7-jdD9RoQgSjB8DpG_6nIlvHL_Ob5V-W6SDignUSE8axA/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiSI3ajtLXUrMslqtwR24NZKPc3wl1DwoWaitDiuu2a4NTiKOZUlva74TgQ2XS5C4cz9A-dQPa1wAzPHaMHiWaKREyujTTLQE3gm3S3IhC-mIqJAYrCNPbAY0KaNJZWUIju8gThqPevLHrlS5Y2vdh9V2VWtv3YZB0hgg1hJuXIiwcwGJ15omDa7pwfSg/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-02 20:23:43 Vue pwa 코로도바로 하이브리드 앱 만들기 - 4★ http://macaronics.net/index.php/m04/vue/view/2108 2108 <p>&nbsp;</p><p><strong><span style="font-size:26px">요약</span></strong></p><p><a target="_blank" href="https://github.com/m0dch3n/vue-cli-plugin-cordova">https://github.com/m0dch3n/vue-cli-plugin-cordova</a></p><p>&nbsp;</p><p>&nbsp;</p><p>Create Vue App</p><pre>$ npm install -g @vue/cli $ vue create my-app $ cd my-app $ npm install -g cordova # If cordova is not already installed </pre><p><strong>Add the plugin to your vue app.</strong></p><pre>$ vue add cordova</pre><p>Usage</p><p>Prepare</p><pre>$ npm run cordova-prepare # prepare for build (you can run this command, when you checkouted your project from GIT, it&#39;s like npm install)</pre><p>Android</p><pre>$ npm run cordova-serve-android # Development Android $ npm run cordova-build-android # Build Android $ npm run cordova-build-only-www-android # Build only files to src-cordova</pre><p>IOS</p><pre>$ npm run cordova-serve-ios # Development IOS $ npm run cordova-build-ios # Build IOS $ npm run cordova-build-only-www-ios # Build only files to src-cordova</pre><p>OSX</p><pre>$ npm run cordova-serve-osx # Development OSX $ npm run cordova-build-osx # Build OSX $ npm run cordova-build-only-www-osx # Build only files to src-cordova</pre><p>Browser</p><pre>$ npm run cordova-serve-browser # Development Browser $ npm run cordova-build-browser # Build Browser $ npm run cordova-build-only-www-browser # Build only files to src-cordova</pre><p>Electron</p><pre>$ npm run cordova-serve-electron # Development Electron $ npm run cordova-build-electron # Build Electron $ npm run cordova-build-only-www-electron # Build only files to src-cordova</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>소스</strong></span></p><p><a target="_blank" href="https://github.com/codedesign-webapp">https://github.com/codedesign-webapp</a></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://github.com/codedesign-webapp/pwa-about">https://github.com/codedesign-webapp/pwa-about</a></p><p>&nbsp;</p><p><a target="_blank" href="https://books.google.co.kr/books?id=7LgDEAAAQBAJ&amp;pg=PA517&amp;lpg=PA517&amp;dq=Do+it!+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C+%EC%9B%B9%EC%95%B1+%EB%A7%8C%EB%93%A4%EA%B8%B0+%EC%BD%94%EB%A5%B4%EB%8F%84%EB%B0%94+%EC%98%B5%EC%85%98+%EC%84%A4%EC%A0%95&amp;source=bl&amp;ots=qe_Ohh4Sv5&amp;sig=ACfU3U1rqUOXPbt81HAs5zAWKejZJBeszA&amp;hl=ko&amp;sa=X&amp;ved=2ahUKEwjDidmYjoj-AhUas1YBHSkSA7MQ6AF6BAglEAM#v=onepage&amp;q=Do%20it!%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C%20%EC%9B%B9%EC%95%B1%20%EB%A7%8C%EB%93%A4%EA%B8%B0%20%EC%BD%94%EB%A5%B4%EB%8F%84%EB%B0%94%20%EC%98%B5%EC%85%98%20%EC%84%A4%EC%A0%95&amp;f=false">https://books.google.co.kr/books?id=7LgDEAAAQBAJ&amp;pg=PA517&amp;lpg=PA517&amp;dq=Do+it!+%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C+%EC%9B%B9%EC%95%B1+%EB%A7%8C%EB%93%A4%EA%B8%B0+%EC%BD%94%EB%A5%B4%EB%8F%84%EB%B0%94+%EC%98%B5%EC%85%98+%EC%84%A4%EC%A0%95&amp;source=bl&amp;ots=qe_Ohh4Sv5&amp;sig=ACfU3U1rqUOXPbt81HAs5zAWKejZJBeszA&amp;hl=ko&amp;sa=X&amp;ved=2ahUKEwjDidmYjoj-AhUas1YBHSkSA7MQ6AF6BAglEAM#v=onepage&amp;q=Do%20it!%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%A0%88%EC%8B%9C%EB%B8%8C%20%EC%9B%B9%EC%95%B1%20%EB%A7%8C%EB%93%A4%EA%B8%B0%20%EC%BD%94%EB%A5%B4%EB%8F%84%EB%B0%94%20%EC%98%B5%EC%85%98%20%EC%84%A4%EC%A0%95&amp;f=false</a></p><p>&nbsp;</p><p>&nbsp;</p><p>이 책으로 배울 수 있는 14가지 핵심 주제</p><p>이 책에 실린 다양한 실습을 따라 하다 보면 다음과 같은 14가지 핵심 주제를 자연스럽게 터득할 수 있습니다. 이러한 기술은 모던 웹과 모바일 앱을 개발할 때 필수이므로 프런트엔드 개발자로 성장하는 데 폭넓은 경험을 제공합니다.</p><pre>1. ES6+ 필수 기능 2. 뷰, 뷰티파이 기초 &amp; 고급 3. 구글 머티리얼 디자인 스펙 2 4. 반응형 웹 프로그래밍 5. 파이어베이스 실시간 DB 6. 워크박스 런타임 캐시 7. 모바일 하드웨어 제어 8. 이메일-구글 인증 9. 푸시 알림 10. 오프라인 동기화 11. 아파치 코르도바로 하이브리드 앱 만들기 12. PWA &rarr; 네이티브 앱 변환 13. 구글 플레이 스토어에 배포 14. 서버리스 프로그래밍 </pre><p>&nbsp;</p><p>책 자세히 살펴보기</p><ul><li><p><a rel="nofollow" href="http://www.yes24.com/Product/Goods/91724510">Yes24 인터넷 서점</a></p><blockquote><p><a rel="nofollow" href="http://www.yes24.com/Product/Goods/91724510">http://www.yes24.com/Product/Goods/91724510</a></p></blockquote></li><li><p><a rel="nofollow" href="https://www.aladin.co.kr/m/mproduct.aspx?start=short&amp;itemid=248639223">알라딘 인터넷 서점</a></p><blockquote><p><a rel="nofollow" href="https://www.aladin.co.kr/m/mproduct.aspx?start=short&amp;itemid=248639223">https://www.aladin.co.kr/m/mproduct.aspx?start=short&amp;itemid=248639223</a></p></blockquote></li></ul><p><br />&nbsp;</p><p>2. 책의 내용</p><p>&#39;Do it! 프로그레시브 웹앱(PWA) 만들기&#39; 책에서 다룰 내용을 간략히 정리해 보았습니다.</p><p>| 첫째마당 |</p><p>전 세계적으로 주목받는 프로그레시브 웹앱의 특징을 자세히 살펴봅니다.</p><ul><li>헬로월드! 프로그레시브 웹앱</li><li>자바스크립트 ES6+</li><li>순수 자바스크립트와 PWA</li></ul><p>| 둘째마당 |</p><p>현대화된 웹앱 작업을 쉽게 수행하려면 뷰 프레임워크를 잘 활용해야 합니다.</p><ul><li>뷰, Vuex, Vue Router, SPA(Single Page Application)</li><li>뷰티파이와 머티리얼디자인(스펙2)</li><li>뷰와 PWA</li></ul><p>| 셋째마당 |</p><p>실전 응용이 가능하도록 수준별로 다양한 상황의 앱을 설계하고 준비했습니다.</p><ul><li>구글 Workbox, 파이어베이스</li><li>실전 상황의 6가지 앱 프로젝트 : To-Do 앱 만들기, 사진 갤러리 앱 만들기, 카메라 갤러리 앱 만들기, 로그인 서비스 만들기, 푸시 알림 서비스 만들기, 오프라인 동기화 기능 만들기</li></ul><p>| 넷째마당 |</p><p>PWA를 구글 플레이에도 배포시키는 방법의 이야기가 시작됩니다.</p><ul><li>하이브리드앱과 코르도바</li><li>PWA와 안드로이드앱</li><li>구글 플레이 스토어에 앱 등록하기</li></ul><p>&nbsp;</p><p>상세한 목차와 예제 파일 다운로드 방법은 아래 링크를 참고하세요.</p><ul><li><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11396">&#39;Do it! 프로그레시브 웹앱 만들기&#39; 책의 전체 목차입니다.</a></p><blockquote><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11396">https://cafe.naver.com/hmaps/11396</a></p></blockquote></li><li><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11391">&#39;Do it! 프로그레시브 웹앱 만들기&#39; 책의 전체 예제 파일 다운로드 방법입니다.</a></p><blockquote><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11391">https://cafe.naver.com/hmaps/11391</a></p></blockquote></li><li><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11392">&#39;Do it! 프로그레시브 웹앱 만들기&#39; 책의 전체 미션 파일 다운로드 방법입니다.</a></p><blockquote><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11392">https://cafe.naver.com/hmaps/11392</a></p></blockquote></li></ul><p><br />&nbsp;</p><p>3. 라이브 데모</p><p>책을 통해서 만들게 될 PWA 앱의 라이브 데모입니다. 실전 상황의 총 6가지 앱을 라이브로 직접 실행할 수 있으며 더 나아가 안드로이드 앱으로 변환된 PWA 앱을 구글 플레이에서 직접 다운로드 받아 테스트할 수 있습니다.</p><ul><li><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11352">&#39;Do it! 프로그레시브 웹앱(PWA) 만들기&#39; 책의 PWA 앱 예제 라이브 데모</a></p><blockquote><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11352">https://cafe.naver.com/hmaps/11352</a></p></blockquote></li><li><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11353">&#39;Do it! 프로그레시브 웹앱(PWA) 만들기&#39; 책의 안드로이드앱(하이브리드앱) 예제의 라이브 데모</a></p><blockquote><p><a rel="nofollow" href="https://cafe.naver.com/hmaps/11353">https://cafe.naver.com/hmaps/11353</a></p></blockquote></li></ul><p>&nbsp;</p><p><a target="_blank" rel="noopener noreferrer" href="https://github.com/codedesign-webapp/pwa-about/blob/master/images/live-view.png"><img width="400px;" alt="" src="https://github.com/codedesign-webapp/pwa-about/raw/master/images/live-view.png" /></a></p><p>&nbsp;</p><p>지금 바로 실행해서 테스트하시려면 아래 링크를 활용해 보세요.</p><blockquote><p><strong>PWA Live Demo</strong></p></blockquote><ul><li><strong>반가워요! PWA by JS</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-hello-js.web.app/">https://pwa-hello-js.web.app/</a></li><li><strong>반가워요! PWA by Vue</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-hello-vue.web.app/">https://pwa-hello-vue.web.app/</a></li><li><strong>사진 갤러리</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-gallery-pic.web.app/">https://pwa-gallery-pic.web.app/</a></li><li><strong>To-Do 리스트</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-to-do.web.app/">https://pwa-to-do.web.app/</a></li><li><strong>카메라 갤러리</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-camera.web.app/">https://pwa-camera.web.app/</a></li><li><strong>구글 로그인 서비스</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-auth-login.web.app/">https://pwa-auth-login.web.app/</a></li><li><strong>푸시 알림 서비스</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-notification-push.web.app/">https://pwa-notification-push.web.app</a></li><li><strong>오프라인 동기화</strong>&nbsp;:&nbsp;<a rel="nofollow" href="https://pwa-offline-sync.web.app/">https://pwa-offline-sync.web.app/</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">학습 목표&nbsp; 6 가지</span></strong></p><p>&nbsp;</p><p><strong>1. PWA 코드를 그대로 적용하여 하이브리드 앱으로 변환하는 과정을 익힌다.</strong></p><p>&nbsp;</p><p><strong>2. 파이어베이스를 활용하여 실시간 DB 등의 서버단 기능도 그대로 활용한다.</strong></p><p>&nbsp;</p><p><strong>3. 뷰와 뷰티파이어를 활용하여 네이티브 앱과 거의 같은 디자인과 사용자 경험을 구현한다.</strong></p><p>&nbsp;</p><p><strong>4.PWA 와 다르게 모바일 기기에 의존하는 기능은 코르도바 플러그인 기능을 활용한다.</strong></p><p>&nbsp;</p><p><strong>5.안드로이드 앱을 기준으로 서명, 정렬 등의 방법을 익힌다.</strong></p><p>&nbsp;</p><p><strong>6. 구글 플레이 스토어에 배포하는 방법을 익힌다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>네이트브 앱을 개발할 때 모바일 기기의 네이티브 기능을 사용하는 것이 가장 어렵습니다. 크로스 플랫폼 프레임워크라 할지라도 플랫폼마다 특성이 다르므로 맞춤형</p><p>작업이 필요한데, 이런 작업을 하다 보면 네이트브 개발과 다름없어집니다.</p><p>하지만 표준 웹 API 를 사용하여 모바일 기기의 네이티브 기능을 처리하면 PWA&nbsp; 와 코르도바 앱을 같은 코드 베이스로 만들 수 있습니다.&nbsp;</p><p>&nbsp;</p><p><a href="https://macaronics.net/m04/vue/view/2064#">Vue Pwa 카메라 사진 갤러리 앱 만들기- 2 ★</a></p><p><a target="_blank" href="https://macaronics.net/m04/vue/view/2064">https://macaronics.net/m04/vue/view/2064</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>1. 개발에 필요한 프로그램 준비하기</strong></span></span></p><p>&nbsp;</p><p>다음 명령을 입력하여 코르도바를 설치합니다.&nbsp; 코르도바는 APK 파일을 만들어 안드로이드 기기에서 테스할 때 필요하며 한 번만 설치하면 됩니다.</p><p>&nbsp;</p><pre class="brush:as3;">npm install -g cordova </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>2.프로그램 실습 준비하기</strong></span></span></p><p>&nbsp;</p><p>vue 프로젝트에서 실해해도 되지만 여기서는&nbsp;</p><p>PWA 로 개발한 프로젝트 내에서&nbsp; cordova 플로그인을 추가 하여 진행하였다.</p><p><br />&nbsp;</p><pre class="brush:as3;">vue&nbsp; add cordova </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#2980b9"><strong>코르도바 옵션 설정</strong></span></span></p><p>&nbsp;</p><p><strong>1. Name of folder where cordova should be installed:</strong></p><p>기본 경로 [src-cordova] 선택 , 코르도바 관련 파일이 생성될 폴더 지정</p><p>&nbsp;</p><p><strong>2.ID off the app:</strong></p><p>기본값 그대로 [com.vue.example.app] 선택&nbsp;<br />앱이 고유한 이름을 가질 수 있도록 도메인 형식으로 작성<br />나중에 설정에서 변경할 수 있으므로 기본값 사용</p><p>&nbsp;</p><p><br /><strong>3.Name of the app:</strong><br />기본값 그대로 [VueExampleAppName] 선택<br />앱 제목 입력, 나중에 설정에서 변경할 수 있으므로 기본값 사용</p><p>&nbsp;</p><p><br /><strong>4.Select Platform:</strong><br />[Android] 선택<br />배포할 플랫폼 선택, 아이폰과 맥에 배포하고 싶다면 ios, osx,선택 가능&nbsp;<br />Browser 는 브라우저에서 디버깅과 테스트를 지원하는 플랫폼<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#16a085"><strong>코르도바로 생성된 프로젝트 구조</strong></span></span></p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7-OVHUwQp1ihkViIJpgiLYEA9FUyXC6Osn-4Y3eKjTORBO0OgYE9TNRgs8GXYWDFcH5tsF_KR4k2IdmGMTAsICEThuiMKASgMBPb4s5uRE0Wxstv0NeRtfMOoruDrLskSs_9MSjX2GbwdjUQYgCD2moO7GcAyoLj2TakS5ovOlLXTIPuk025g92BG5g/s16000/2023-04-01%2016%2022%2045.png">이미지 확대</a></p><p>&nbsp;</p><p><img alt="" width="900" height="587" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7-OVHUwQp1ihkViIJpgiLYEA9FUyXC6Osn-4Y3eKjTORBO0OgYE9TNRgs8GXYWDFcH5tsF_KR4k2IdmGMTAsICEThuiMKASgMBPb4s5uRE0Wxstv0NeRtfMOoruDrLskSs_9MSjX2GbwdjUQYgCD2moO7GcAyoLj2TakS5ovOlLXTIPuk025g92BG5g/s16000/2023-04-01%2016%2022%2045.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>3.안드로이드 SDK 설치하기</strong></span></span></p><p>&nbsp;</p><p>Android 스튜디오 설치</p><p>bookmark_border</p><p>클릭 몇 번으로 Android 스튜디오를 설정하세요. 먼저&nbsp;<a href="https://developer.android.com/studio?hl=ko">최신 버전의 Android 스튜디오를 다운로드</a>합니다.</p><p>Windows</p><p>Windows에 Android 스튜디오를 설치하려면 다음 단계를 따르세요.</p><ul><li><p>.exe&nbsp;파일을 다운로드한 경우(권장) 파일을 더블클릭하여 실행합니다.</p></li><li><p>.zip&nbsp;파일을 다운로드한 경우:</p><ol><li>.zip의 압축을 풉니다.</li><li><strong>android-studio</strong>&nbsp;폴더를&nbsp;<strong>Program Files</strong>&nbsp;폴더에 복사합니다.</li><li><strong>android-studio &gt; bin</strong>&nbsp;폴더를 엽니다.</li><li>studio64.exe(64비트 컴퓨터) 또는&nbsp;studio.exe(32비트 컴퓨터)를 실행합니다.</li><li>Android 스튜디오의&nbsp;<strong>설정 마법사</strong>에 따라 권장 SDK 패키지를 설치합니다.</li></ol></li></ul><p>다음 동영상은 권장&nbsp;.exe&nbsp;다운로드를 위한 설정 절차의 각 단계를 보여줍니다.</p><p><video controls="">&nbsp;</video></p><p>새로운 도구와 기타 API를 사용할 수 있게 되면 Android 스튜디오에서 팝업으로 알려줍니다. 업데이트를 수동으로 확인하려면&nbsp;<strong>Help &gt; Check for Update</strong>를 클릭합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#c0392b">다음 링크 주소를 참조해서 실행&nbsp; =&gt;&nbsp; &nbsp;&nbsp;</span></strong></span><a target="_blank" href="https://velog.io/@completed1991/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%85%8B%ED%8C%85">안드로이드 스튜디오 설치 및 환경변수 셋팅</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#16a085"><span style="font-size:16px">*코르도바로 화면 UI를 디자인할 때는 hello-hybrid.png 파일의 위치가 중요합니다. 이미지 복사 위치에 따른 사용법을 정리하면 다음과 같습니다.</span></span></strong></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>1) 이미지 복사 위치</strong></span></p><p><strong>./assets 폴더에 이미지를 넣는다. 현재 폴더를 기준으로 하므로 점(.) 을 사용한다. 예제에서는 간단히 이 방법을 사용한다.</strong></p><p><br />&nbsp;</p><pre class="brush:as3;">&lt;img src=&quot;./assets/hello-hybrid.png&quot;/&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>2) 이미지 복사 위치</strong></span></p><p>public/img 폴더에 이미지를 넣는다.&nbsp; 코르드보 빌드 후에 는&nbsp; file://android_assets/www/&nbsp; 기준으로 바뀌므로 점(.) 과 슬래시(/)를 모두 빼야 한다.</p><p><br />&nbsp;</p><pre class="brush:as3;">&lt;img src=&quot;img/hello-hybrid.png&quot; /&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>샘플&nbsp; src/App.vue 예</strong></span></p><pre class="brush:as3;">&lt;template&gt; &lt;v-app dark&gt; &lt;v-main&gt; &lt;!-- fill-height는 브라우저 높이를 100%, 수직으로 가운데 정렬시킴 --&gt; &lt;v-container fluid fill-height&gt; &lt;v-row&gt; &lt;!-- text-center는 수평 가운데 정렬 --&gt; &lt;v-col cols=&quot;12&quot; class=&quot;text-center&quot;&gt; &lt;!-- 타이포 스타일은 title, 글자색은 흰색으로 설정 --&gt; &lt;h1 class=&quot;title white--text&quot;&gt;반가워요!&lt;/h1&gt; &lt;p class=&quot;caption mb-0&quot;&gt;by Cordova&lt;/p&gt; &lt;img src=&quot;./assets/hello-hybrid.png&quot; alt=&quot;&quot; /&gt; &lt;/v-col&gt; &lt;/v-row&gt; &lt;/v-container&gt; &lt;/v-main&gt; &lt;/v-app&gt; &lt;/template&gt; &lt;script&gt; export default { name: &#39;App&#39;, created () { // 배경색을 다크모드로 함 this.$vuetify.theme.dark = true; } } &lt;/script&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>4. 스플래시 화면과 아이콘 이미지 준비하기</strong></span></span></p><p>&nbsp;</p><p><strong><span style="font-size:16px"><span style="color:#2980b9">다음 사이트에서&nbsp; 아이콘을 생성 한다.</span></span></strong></p><p><a target="_blank" href="https://apetools.webprofusion.com/#/tools/imagegorilla"><strong>https://apetools.webprofusion.com/#/tools/imagegorilla</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>스플래시 설정&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>스플래시 화면이란 하이브리드 앱을 로딩하는 동안 텍스트,&nbsp; 이미지, 로고 등의 그래픽 요소를 이용해서 어떤 앱인지 사용자에게 빠르게 소개하는 페이지 입니다.</p><p>일반적으로 는 전체 화면을 가득히 채워서 표시합니다.</p><p>&nbsp;</p><p>앱이 처음 실행 될 때 스플래시 화면을 띄우려면 cordova-plugin-splashcreen 이라는 플러그인을 설치해야 합니다. 코르도바 명령어를 입력할 때 plugin add 다음에</p><p>플러그인이 이름을 지정하면 됩니다.</p><p>&nbsp;</p><pre class="brush:as3;">$ cd src-cordova/ $ cordova plugin add cordova-plugin-splashscreen</pre><p>&nbsp;</p><p>만약에 개별적으로 스플래시 스크린 플러그인넣을 이미지가 존재하면 이 단계는 건너띄어도 됩니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>안드로이드 플랫폼의 스플래시 스크린 이미지는 앞서 <strong><span style="color:#c0392b">vue add cordova </span></strong>명령어로 코드로바 플러그인을 설치할 때 해상도에 따라</p><p>다음의 폴더에 자동으로 생성됩니다.</p><pre class="brush:as3;">프로젝트/src-cordova/platforms/android/app/arc/main/res </pre><p>&nbsp;</p><p><img alt="" width="308" height="914" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgm5sgZ_B2TlnSXd-1CCD7TRka4o_5cTgfXTj9PXaHzoIqD-1bU9CL0XLyX_31jjv-EwEmQsGeXsoroLFYApdthOkJfAKz4EOyQYO5QYdMc21TFADgbmtYt1DdifUl-FJevOR3K6Z_xlaitoyqfPvv85fhspIcRbKxP4ZPoaHdVkftNOzJTeu5du7vi9w/s16000/2023-04-01%2017%2044%2023.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>5. config.xml 설정하기</strong></span></span></p><p>&nbsp;</p><p>참조 :</p><p><a target="_blank" href="https://bundw.tistory.com/66">Cordova(코르도바) 실행 로딩화면 Splashscreen(스플래시 스크린) 적용</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>프로젝트를 빌드하면 코르도바 루트 폴더의 config.xml 에 설정된 내용이 각 플랫폼의 매니페스트 파일에 자동으로 반영됩니다. 예를 들어 안드로이드 플랫폼이라면</p><p>AndroidMainfest.xml 파일에 자동으로 반영됩니다.&nbsp; 따라서 플랫폼별로 일일이 설정하지 않아도 config.xml 파일만 통합해서 관리하면 됩니다.</p><p>&nbsp;</p><p>config.xml 파일을 열면 여러 엘리먼트와 어트리뷰트가 있지만 중요한 것만 다음과 같이 입력값을 수정합니다.</p><p>&nbsp;</p><p><strong>config.xml</strong></p><p>기존</p><pre class="brush:as3;">&lt;?xml version=&#39;1.0&#39; encoding=&#39;utf-8&#39;?&gt; &lt;widget id=&quot;com.vue.example.app&quot; version=&quot;1.0.0&quot; xmlns=&quot;http://www.w3.org/ns/widgets&quot; xmlns:cdv=&quot;http://cordova.apache.org/ns/1.0&quot;&gt; &lt;name&gt;VueExampleAppName&lt;/name&gt; &lt;description&gt;Sample Apache Cordova App&lt;/description&gt; &lt;author email=&quot;dev@cordova.apache.org&quot; href=&quot;https://cordova.apache.org&quot;&gt; Apache Cordova Team &lt;/author&gt; &lt;!-- this hook will point your config.xml to the DevServer on Serve --&gt; &lt;hook type=&quot;after_prepare&quot; src=&quot;../node_modules/vue-cli-plugin-cordova/serve-config-hook.js&quot; /&gt; &lt;content src=&quot;index.html&quot; /&gt; &lt;allow-intent href=&quot;http://*/*&quot; /&gt; &lt;allow-intent href=&quot;https://*/*&quot; /&gt; &lt;/widget&gt; </pre><p>&nbsp;</p><p><strong>변경=&gt;</strong></p><pre class="brush:as3;">&lt;?xml version=&#39;1.0&#39; encoding=&#39;utf-8&#39;?&gt; &lt;widget id=&quot;com.vue.example.app&quot; version=&quot;1.0.0&quot; xmlns=&quot;http://www.w3.org/ns/widgets&quot; xmlns:cdv=&quot;http://cordova.apache.org/ns/1.0&quot;&gt; &lt;name&gt;반가워요! Hybrid by Cordova&lt;/name&gt; &lt;description&gt;Sample Apache Cordova App&lt;/description&gt; &lt;author email=&quot;dev@cordova.apache.org&quot; href=&quot;https://cordova.apache.org&quot;&gt; Apache Cordova Team &lt;/author&gt; &lt;!-- this hook will point your config.xml to the DevServer on Serve --&gt; &lt;hook type=&quot;after_prepare&quot; src=&quot;../node_modules/vue-cli-plugin-cordova/serve-config-hook.js&quot; /&gt; &lt;content src=&quot;index.html&quot; /&gt; &lt;allow-intent href=&quot;http://*/*&quot; /&gt; &lt;allow-intent href=&quot;https://*/*&quot; /&gt; &lt;platform name=&quot;android&quot;&gt; &lt;preference name=&quot;SplashMaintainAspectRatio&quot; value=&quot;true&quot;/&gt; &lt;preference name=&quot;SplashShowOnlyFirstTime&quot; value=&quot;true&quot;/&gt; &lt;icon density=&quot;ldpi&quot; src=&quot;res/icon/android/ldpi.png&quot;/&gt; &lt;icon density=&quot;mdpi&quot; src=&quot;res/icon/android/mdpi.png&quot;/&gt; &lt;icon density=&quot;hdpi&quot; src=&quot;res/icon/android/hdpi.png&quot;/&gt; &lt;icon density=&quot;xhdpi&quot; src=&quot;res/icon/android/xhdpi.png&quot;/&gt; &lt;icon density=&quot;xxhdpi&quot; src=&quot;res/icon/android/xxhdpi.png&quot;/&gt; &lt;icon density=&quot;xxxhdpi&quot; src=&quot;res/icon/android/xxxhdpi.png&quot;/&gt; &lt;!-- Portrait --&gt; &lt;splash density=&quot;port-ldpi&quot; src=&quot;res/screen/android/splash-port-ldpi.png&quot;/&gt; &lt;splash density=&quot;port-mdpi&quot; src=&quot;res/screen/android/splash-port-mdpi.png&quot;/&gt; &lt;splash density=&quot;port-hdpi&quot; src=&quot;res/screen/android/splash-port-hdpi.png&quot;/&gt; &lt;splash density=&quot;port-xhdpi&quot; src=&quot;res/screen/android/splash-port-xhdpi.png&quot;/&gt; &lt;splash density=&quot;port-xxhdpi&quot; src=&quot;res/screen/android/splash-port-xxhdpi.png&quot;/&gt; &lt;splash density=&quot;port-xxxhdpi&quot; src=&quot;res/screen/android/splash-port-xxxhdpi.png&quot;/&gt; &lt;!-- Landscape --&gt; &lt;!-- &lt;splash density=&quot;land-ldpi&quot; src=&quot;res/screen/android/splash-land-ldpi.png&quot;/&gt; &lt;splash density=&quot;land-mdpi&quot; src=&quot;res/screen/android/splash-land-mdpi.png&quot;/&gt; &lt;splash density=&quot;land-hdpi&quot; src=&quot;res/screen/android/splash-land-hdpi.png&quot;/&gt; &lt;splash density=&quot;land-xhdpi&quot; src=&quot;res/screen/android/splash-land-xhdpi.png&quot;/&gt; &lt;splash density=&quot;land-xxhdpi&quot; src=&quot;res/screen/android/splash-land-xxhdpi.png&quot;/&gt; &lt;splash density=&quot;land-xxxhdpi&quot; src=&quot;res/screen/android/splash-land-xxxhdpi.png&quot;/&gt; --&gt; &lt;/platform&gt; &lt;/widget&gt; </pre><p>&nbsp;</p><p><strong>id=&quot;com.vue.example.app&quot;&nbsp; 앱의&nbsp; 고유한 ID 설정</strong></p><p>앱이 고유한 이름을 가질 수 있도록 도메인 형식으로 지어 줍니다. 구글 플레이 스토어에&nbsp; 업로드할 때 다른 앱과 구분하는 용도로 사용합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>앱의 제목 설정</strong></p><p>홈 화면 아이콘 등에서 앱의 제목으로 사용</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:14px"><span style="color:#c0392b"><strong>AndroidMainfest.xml 직접 설정하기</strong></span></span></p><p>&nbsp;</p><p>AndroidMainfest.xml 파일은 앱의 속성에 관한 내용을 설정할 수 있습니다. 이 파일은 /platforms/android/app/src/main 폴더에 생성됩니다.</p><p>그런데 직접 확인해서 수정해야 할 부분도 있습니다. 예를 들어 안드로이드 앱을 빌들할 때 SDK 버전 번호가 낮으면 구글 플레이 스토어에 업로드할 수 없습니다.</p><p>그래서 SDK 매니저로 최신 파일로 업그레이드한 후 android:targetSdkVersion 값을 요구하는 값으로 변경해야 할 때 직접 수정하면 됩니다.</p><p>&nbsp;</p><p><strong>src-cordova/platforms/android/CordovalLib/src/AndroidManifest.xml</strong></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">&lt;uses-sdk android:minSdkVersion=&quot;integer&quot; android:targetSdkVersion=&quot;integer&quot; android:maxSdkVersion=&quot;integer&quot; /&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>6. 모바일 기기의 USB 디버깅 설정하기</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>참조 =&gt;&nbsp;&nbsp;<a target="_blank" href="https://learnandcreate.tistory.com/796"><strong>삼성 갤럭시에서 개발자 모드 켜기(USB 디버깅 허용)</strong></a></p><p>&nbsp;</p><p>&nbsp;</p><p>디버깅을 설정했다면&nbsp; VScode 의 터미널 창에서 adb devices 명령을 실행하여 잘 연결 되었는지 확인합니다.&nbsp; 여기서는 adb 는 안드로이드 디버깅용 네트워크</p><p>유틸리티 입니다. 다음 그림처럼 나타나 면&nbsp;</p><pre class="brush:as3;">$ adb devices List of devices attached 5c66ac49 device emulator-5554 device</pre><p>&nbsp;</p><p>기기가&nbsp; USB 로 접속되었다는 의미입니다. 물론 기기 번호는 저속한 기기의 고유 번호이므로&nbsp; 다를 수 있습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="718" height="102" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhO_2zd4Mh1DO0jyQ6Ex2Wd5I2xUW-d-_dt_4wmehnHFilY4XHOCT52okUngav28xh-4hU5SD43nAjEhImnhxIC3Niet81ZFkNKsELGEpDSbmKxdDF0-JM3dgRUK9ldJqjZIw1yJQzrpUUb9z40YCrNFt6ziSHArclpB9EpHT_IBS0riFWFRqK7htZT3g/s16000/2023-04-01%2018%2048%2047.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:36px"><span style="color:#c0392b"><strong>7. 개발자 모드로 테스트하기</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>PWA 때와 똑같이&nbsp; 핫 리로드(hot reload) 기능을 사용할 수 있습니다. 코드의 문법이나 로직에 오류가 없는지 가볍게 확인할 때 먼저 이용합니다.</p><p>&nbsp;</p><pre class="brush:as3;">npm run serve </pre><p>&nbsp;</p><p>&nbsp;</p><p>오류가 없는 것이 확인되면 이번에는 모바일 기기에서 테스트하겠습니다.&nbsp; 방법은 웹팩을 이용해서 src 와 public 폴더에서 작업한 내용을 src-cordova 의 www 폴더에 배포용으로</p><p>준비해야 합니다.&nbsp; 이 작업은 npm run cordova-prepare 명령으로 실행합니다. 이 명령은 config.xml 의 설정 내용을 플랫폼별로 매니페스트 파일에 반영하고 www 폴더에 빌드를</p><p>위한 최종 파일 준비합니다.</p><p><br />&nbsp;</p><pre class="brush:as3;">npm run cordova-prepare </pre><p>&nbsp;</p><p>=&gt;빌드 준비 처리</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>USB 로 연결된 안드로이드 기기에서 디버그 모드로 빌드해서 실행 결과를 바로 테스트하고 싶다면 src-cordova 폴더로 이동한&nbsp; 후 다음과 같이&nbsp;</p><p>cordova run android 명령을 실행합니다.</p><p>&nbsp;</p><pre class="brush:as3;">$ npm run cordova-serve-android 또는 $ cd src-cordova $ cordova run android </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>주의 </strong></span></span>:&nbsp; <span style="color:#2980b9">&nbsp;<strong> jDK 17 에서는&nbsp; 현재 오류간 난다.&nbsp; pc 환경 변수를&nbsp;&nbsp;jdk 11 에서 실행할것</strong></span></p><p>오류 원인 찾느라 상당히 애 먹었는데. 주의할것</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>오류</strong> </span></span>: 스플래시 아이콘 설정 잘못하면 오류</p><p>$ cordova run android<br />Source path does not exist: res/icon/android/hdpi.png</p><p>&nbsp;</p><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-04-01 16:21:31 JPA & Spring Data JPA 기초 - 1. JPA 기본 매핑 http://macaronics.net/index.php/m01/spring/view/2107 2107 <p>&nbsp;</p><p><strong>초급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>JPA와 스프링 데이터 JPA의 기본 사용법을 알아봅니다.</p><p>✍️<br />이런 걸<br />배워요!</p><p>JPA 기본 매핑</p><p>스프링 데이터 JPA 기본 사용법</p><p>DB 연동의 열쇠 JPA!&nbsp;<br />실무 중심의 핵심 기본기를 빠르게 ????</p><p>백엔드 실무자를 위한&nbsp;<br /><strong>JPA &amp; 스프링 데이터 JPA</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>인프런 강의<br /><a target="_blank" href="https://www.inflearn.com/course/jpa-spring-data-기초">https://www.inflearn.com/course/jpa-spring-data-기초</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>유튜브<br /><a target="_blank" href="https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX">https://www.youtube.com/playlist?list=PLwouWTPuIjUi9Sih9mEci4Rqhz1VqiQXX</a></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>1.JPA 기초 01 일단 해보기</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/Zwq2McbFOn4" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p></p><p><strong>&bull;ORMObject-Relational Mapping 스펙</strong><br />자바 객체와 관계형 DB 간의 매핑 처리를 위한 API<br />Java Persistence API (2.2 버전)<br />Jakarta Persistence API (3.0 버전)<br />&nbsp;2.2 버전부터 JCP<br />이클립스 재단으로 이관 진행<br />Jakarta EE 9 버전: JPA 3.0<br />보통 JPA만 단독으로 사용하기 보다는 스프링과 함께 사용<br />스프링 6 버전부터 Jakarta EE 9+ 지원</p><p>&nbsp;</p><p></p><p><strong>애노테이션을 이용한 매핑 설정</strong></p><p>&bull;XML 파일을 이용한 매핑 설정도 가능<br />String, int, LocalDate 등 기본적인 타입에 대한 매핑 지원<br />커스텀 타입 변환기 지원<br /><br />내가 만든 Money 타입을 DB 칼럼에 매핑 가능<br />밸류 타입 매핑 지원<br />한 개 이상 칼럼을 한 개 타입으로 매핑 가능<br />클래스 간 연관 지원 : 1-1, 1-N, N-1, N-M<br />상속에 대한 매핑 지원</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>MySQL 준비</strong><br />&nbsp;일단 해보기 : 준비 사항 데이터베이스 생성 자바 프로젝트<br />&nbsp;메이븐<br />&nbsp;사용자 생성 및 권한 부여 테이블 생성<br />&nbsp;pom.xml에 관련 의존 추가 persistence.xml 파일 작성</p><p>&bull; User 클래스 작성 Main 클래스 작성 실행<br /><a target="_blank" href="http:// https://github.com/madvirus/jpa-basic/jpa-01">&nbsp;https://github.com/madvirus/jpa-basic/jpa-01</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>MySQL 설치</strong><br />・직접 다운 받아서 설치</p><p>&bull;https://dev.mysql.com/downloads/</p><p><br /><strong>&bull;도커로 설치</strong><br />docker create --name mysql8 -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 mysql:8.0.27<br />docker start mysql8</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong>데이터베이스, DB 사용자, 권한</strong></p><pre class="brush:as3;"> create database jpabegin CHARACTER SET utf8mb4; CREATE USER &#39;jpauser &#39;@&#39;localhost&#39; IDENTIFIED BY &#39;jpapass&#39;; CREATE USER &#39;jpauser&#39;@&#39;%&#39; IDENTIFIED BY &#39;jpapass&#39;; GRANT ALL PRIVILEGES ON jpabegin.* TO &#39;jpauser &#39;@&#39;localhost&#39;; GRANT ALL PRIVILEGES ON jpabegin.* TO &#39;jpauser&#39;@&#39;%&#39;;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>테이블 생성</strong></p><pre class="brush:as3;">create table jpabegin.user ( email varchar(50) not null primary key, name varchar(50), create_date datetime ) engine innodb character set utf8mb4;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&bull;메이븐 프로젝트<br />&bull; JDK 17<br />자바 프로젝트 생성<br />&bull;<br />프로젝트 이름 : jpa-01</p><p>&nbsp;</p><pre class="brush:as3;">&lt;properties&gt; &lt;maven.compiler.source&gt;17&lt;/maven.compiler.source&gt; &lt;maven.compiler.target&gt;17&lt;/maven.compiler.target&gt; &lt;hiberate.version&gt;6.0.0.Final&lt;/hiberate.version&gt; &lt;/properties&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.hibernate&lt;/groupId&gt; &lt;artifactId&gt;hibernate-core&lt;/artifactId&gt; &lt;version&gt;${hiberate.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.hibernate&lt;/groupId&gt; &lt;artifactId&gt;hibernate-hikaricp&lt;/artifactId&gt; &lt;version&gt;${hiberate.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;mysql&lt;/groupId&gt; &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt; &lt;version&gt;8.0.27&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;ch.qos. logback&lt;/groupId&gt; &lt;artifactId&gt;logback-classic&lt;/artifactId&gt; &lt;version&gt;1.2.6&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>persistence.xml (src/main/resources/META-INF)</strong></p><pre class="brush:as3;">&lt;persistence...&gt; &lt;persistence-unit name=&quot;jpabegin&quot; transaction-type=&quot;RESOURCE_LOCAL&quot;&gt; &lt;class&gt;jpabasic.reserve.domain.User&lt;/class&gt; &lt;exclude-unlisted-classes&gt;true&lt;/exclude-unlisted-classes&gt; &lt;properties&gt; &lt;property name=&quot;jakarta.persistence.jdbc.driver&quot; value=&quot;com.mysql.cj.jdbc.Driver&quot; /&gt; &lt;property name=&quot;jakarta.persistence.jdbc.url&quot; value=&quot;jdbc:mysql://localhost/jpabegin? characterEncoding=utf8&quot; /&gt; &lt;property name=&quot;jakarta.persistence.jdbc.user&quot; value=&quot;jpauser&quot; /&gt; &lt;property name=&quot;jakarta.persistence.jdbc.password&quot; value=&quot;jpapass&quot; /&gt; &lt;property name=&quot;hibernate.dialect&quot; value=&quot;org.hibernate.dialect.MySQLDialect&quot; /&gt; &lt;property name=&quot;hibernate.hikari. poolName&quot; value=&quot;pool&quot; /&gt; &lt;property name=&quot;hibernate.hikari.maximumPoolSize&quot; value=&quot;10&quot; /&gt; &lt;property name=&quot;hibernate.hikari.minimumIdle&quot; value=&quot;10&quot; /&gt; &lt;property name=&quot;hibernate.hikari.connectionTimeout&quot; value=&quot;1000&quot; /&gt; &lt;!-- 1s --&gt; &lt;/properties&gt; &lt;/persistence-unit&gt; &lt;/persistence&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="484" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkxNt4fYbWokIhcVTYnoUDrnxl5ruOVS0Su7PCcreIH16h-5Y5r4yWYT6m-qvCswXBNnkSbhX_PuYcf5K67omDRXXHNUIqnTWqeHwh0v6DLnZ3n1db36VqF-BFDN3avuTIsx0Tu9ES-ezR2xs1ar4PdDJEsXyvGxmdWgsq51EN4Yf2NQXJEiQZ_IF73g/s16000/2023-03-30%2019%2034%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package jpabasic.reserve.domain; import jakarta.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = &quot;user&quot;) public class User { @Id private String email; private String name; @Column(name = &quot;create_date&quot;) private LocalDateTime createDate; protected User() {} public User(String email, String name, LocalDateTime createDate) {} } ...생략 ...get 메서드 생략</pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">클래스와 테이블 매핑 @Entity DB 테이블과 매핑 대상 @Table(name = &quot;user&quot;) user El public class User { @Id 식별자에 대응 private String email; email private String name; name @Column(name = &quot;create_date&quot;) create_date private LocalDateTime createDate;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>UserSaveMain 클래스</strong></p><p><strong>등록</strong></p><pre class="brush:as3;">import jakarta.persistence.*; import jpabasic.reserve.domain.User; import java.time.LocalDateTime; public class UserSaveMain {} public static void main(String[] args) { EntityManagerFactory emf Persistence.createEntityManagerFactory(&quot;jpabegin&quot;); EntityManager entityManager = emf.createEntityManager(); EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); User user = new User(&quot;user@user.com&quot;, &quot;user&quot;, LocalDateTime.now()); entityManager.persist(user); transaction.commit(); } catch (Exception ex) { ex.printStackTrace(); transaction.rollback(); } finally {} entityManager.close(); emf.close();</pre><p>&nbsp;</p><p><strong>값가져오기</strong></p><pre class="brush:as3;">EntityManagerFactory emf Persistence.createEntityManagerFactory(&quot;jpabegin&quot;); EntityManager entityManager = emf.createEntityManager(); EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); User user = entityManager.find(User.class, &quot;user@user.com&quot;); if (user == null) { System.out.println(&quot;User&quot;); } else {} System.out.printf(&quot;User : email=%s, name=%s, createDate=%s\n&quot;, user.getEmail(), user.getName(), user.getCreateDate()); transaction.commit(); } catch (Exception ex) { ex.printStackTrace(); transaction.rollback(); } finally {} entityManager.close(); emf.close();</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>수정</strong></p><pre class="brush:as3;">EntityManagerFactory emf Persistence.createEntityManagerFactory(&quot;jpabegin&quot;); EntityManager entityManager = emf.createEntityManager(); EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); User user = entityManager.find(User.class, &quot;user@user.com&quot;); if (user == null) { System.out.println(&quot;User&quot;); } else {} String newName = &quot;0&quot; + (System.currentTimeMillis() % 100); user.changeName(newName); transaction.commit(); } catch (Exception ex) { ex.printStackTrace(); transaction.rollback(); } finally {} entityManager.close(); emf.close();</pre><p>&nbsp;</p><p>&nbsp;</p><p>01. 일단 해보기 정리<br />&bull;간단한 설정으로 클래스와 테이블 간 매핑 처리<br />・EntityManager를 이용해서 DB 연동 처리<br />&bull;객체 변경만으로 DB 테이블 업데이트<br />&bull;쿼리 작성 X</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>2.JPA 기초 02 코드 구조 &amp; 영속 컨텍스트</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/7ljqL8ThUts" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">EntityManager로 DB 연동 나중에 시... EntityManager Entity Transaction 7 트랜잭션 시작 EntityManager entityManager = emf.createEntityManager(); EntityTransaction transaction entityManager.getTransaction(); try { transaction.begin(); ...entityManager DB 트랜잭션 커밋 transaction.commit(); } catch (Exception ex) { 트랜잭션 롤백 transaction.rollback(); } finally { EntityManager 닫음 entityManager.close(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>저장과 쿼리 실행 시점</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">transaction.begin(); User user = new User(&quot;user@user.com&quot;, &quot;user&quot;, LocalDateTime.now()); entityManager.persist(user); logger.info(&quot;EntityManager.persist &quot;); transaction.commit(); logger.info(&quot;EntityTransaction.commit1&quot;); INFO jpabasic.reserve.domain.UserSaveMain - EntityManager.persist DEBUG org.hibernate.SQL insert into user(create_date, name, email) values( ? , ? , ? ) EntityTransaction.commit INFO jpabasic.reserve.domain.UserSaveMain</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>JPA 기초 02 코드 구조 &amp; 영속 컨트</strong></p><p>&nbsp;</p><pre class="brush:as3;">transaction.begin(); 수정과 쿼리 실행 시점 나중에 시... User user = entityManager.find(User.class, &quot;user@user.com&quot;); if (user == null) { System.out.println(&quot;User 없음&quot;); } else {} String newName = &quot;이름&quot; + (System.currentTimeMillis() % 100); user.changeName(newName); logger.info(&quot;User.changeName 호출함&quot;); transaction.commit(); logger.info(&quot;EntityTransaction.commit 호출함&quot;); </pre><p>&nbsp;</p><pre class="brush:as3;">DEBUG org.hibernate.SQL - select ul_0.email, ul_0.create_date, ul_0.name from user ul_0 where ul_0.email = ? INFOj.reserve.domain.UserUpdateMain - User.changeName 호출함 DEBUG org.hibernate.SQL - update user set create_date = ? , name ? where email = ? INFOj.reserve.domain.UserUpdateMain - EntityTransaction.commit 호출함</pre><p>&nbsp;</p><p><img alt="" width="844" height="476" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLKEhAQmR18zTmqaN_9vZp96fHCbhtHTBxHAqhv_S_aLbXnrnzQ5-1RAJTiBuxqXI_cXGCThUbvj3dKyC3IGmqgZVSaIbzzInQ-9VBksn2T6bGFLwyIh4ah5PtiTySzacfmCj6WwV7X03ldXFPsF7DGd7lEbsBYpn3TJO7fvSogM_KPay8sdqDf8AuUA/s16000/4.png" /></p><p><br /><span style="font-size:20px">정리</span><br /><br /><strong>EntityManagerFactory 초기화</strong></p><p>&nbsp;</p><p><strong>DB 작업이 필요할 때마다 Entity Manager 생성,&nbsp;EntityManager로 DB 조작 , EntityTransaction으로 트랜잭션 관리</strong></p><p><br /><strong>하지만 스프링과 연동할 때는 대부분 스프링이 대신 처리하므로 매핑 설정 중심으로 작업</strong></p><p><br /><strong>영속 컨텍스트 엔티티를 메모리에 보관 변경을 추적해서 트랜잭션 커밋 시점에 DB에 반영</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>3.JPA 기초 03 엔티티 CRUD 처리</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/kmCKAwOie_I" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><span style="font-size:20px">시작전에 보조 클래스 만들기・ </span></p><p><strong><span style="font-size:20px">EntityManager를 쉽게 구하기 위한 클래스</span></strong></p><pre class="brush:as3;">import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Persistence; public class EMF { private static EntityManagerFactory emf; public static void init() {} emf = Persistence.createEntityManagerFactory(&quot;jpabegin&quot;); public static EntityManager createEntityManager() {} return emf.createEntityManager(); public static void close() {} } emf.close();</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px">엔티티 단위 CRUD 처리<br /><strong>EntityManager</strong>가 제공하는 메서드 이용</span></p><pre class="brush:as3;"> persist() find() remove() merge()</pre><p>&nbsp;</p><p><span style="font-size:20px"><strong>저장</strong></span></p><pre class="brush:as3;">EntityManager #persist(Object entity) public class NewUserService { public void saveNewUser(User user) { EntityManager em = EMF.createEntityManager(); EntityTransaction tx em.getTransaction(); try { tx.begin(); em.persist(user); tx.commit(); } catch (Exception ex) { tx.rollback(); throw ex; } finally { em.close(); } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>조회</strong></span></p><pre class="brush:as3;">EntityManager #find(Class &lt;T&gt; entityClass, Object primaryKey) public class GetUserService { public User getUser(String email) { EntityManager em EMF.createEntityManager(); try { User user em.find(User.class, email); if (user == null) {} throw new NoUserException(); return user; } finally {} } } em.close();</pre><p>&nbsp;</p><p>&nbsp;</p><p>조회<br />엔티티 타입, ID 타입이 맞아야 함<br />일치하지 않으면 익셉션</p><p>&nbsp;</p><pre class="brush:as3;">String str = em.find(String.class, &quot;1&quot;); User user = em.find(User.class, 11);</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">수정</span></strong></p><p>트랜잭션 범위 내에서 변경된 값을 자동 반영</p><pre class="brush:as3;">public class Change NameService { public void changeName(String email, String newName) { EntityManager em = EMF.createEntityManager(); EntityTransaction tx = em.getTransaction(); try { tx.begin(); User user = em.find(User.class, email); if (user == null) {} `throw new NoUserException(); user.changeName(newName); tx.commit(); } catch(Exception ex) { tx.rollback(); } throw ex; } finally { em.close(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:20px"><strong>삭제</strong></span></p><p>EntityManager#remove(Object entity)</p><pre class="brush:as3;">public class RemoveUserService { public void removeUser(String email) { EntityManager em = EMF.createEntityManager(); EntityTransaction tx = em.getTransaction(); try { tx.begin(); User user = em.find(User.class, email); if (user = null) {} throw new NoUserException(); em.remove(user); tx.commit(); } catch (Exception ex) { tx.rollback(); throw ex; } finally { em.close(); } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="750" height="449" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZiIY9SHUFgBrSN47njv3BGRYVwFL77rKsUlUBWieZfvgL7G8qtFRc42qsD-3dPUzuyGEA5YuhfDIahvAr7_pHHIOoYh2b-M4Uwe7qZ_H7j_67z6ThpwX9MKUMe9Uz_Rxcbv0d0oYtJ3f9q4U8RbNoUvoh_vZ1bX8mSbylFGI9DCb-N3iaUlcaYLkfvQ/s16000/2023-03-31%2017%2027%2011.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">정리</span></strong></p><p>EntityManager를 사용해서 엔티티 단위로 CRUD 처리 변경은 트랜잭션 범위 안에서 실행<br />1) persist()<br />2) 수정<br />3) remove()</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>4.JPA 기초 04 엔티티 매핑 설정</strong></span></span></p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/SbMJVuv8Iyo" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">기본 애노테이션</span></strong></p><p>엔티티 매핑</p><p><br />@Entity: 엔티티 클래스에 설정, 필수</p><p><br />@Table : 매핑할 테이블 지정</p><p><br />@ld : 식별자 속성에 설정, 필수 @Column : 매핑할 칼럼명 지정<br />지정하지 않으면 필드명/프로퍼티명 사용</p><p><br />@Enumerated: enum 타입 매핑할 때 설정</p><p>@Temporal: java.util.Date, java.util.Calendar 매핑 자바8 시간/날짜 타입 등장 이후로 거의 안 씀</p><p><br />@Basic : 기본 지원 타입 매핑 (거의 안 씀)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">@Table 애노테이션</span></strong><br />애노테이션을 생략하면 클래스 이름과 동일한 이름에 매핑</p><p>&nbsp;속성<br />&nbsp;예<br />&nbsp;name: 테이블 이름 (생략하면 클래스 이름과 동일한 이름)</p><p>catalog : 카탈로그 이름 (예, MySQL DB 이름)<br />schema : 스키마 이름 (예, 오라클 스키마 이름)<br />@Table<br />@Table(name=&quot;hotel_info&quot;)<br />@Table(catalog = &quot;point&quot;, name = &quot;point_history&quot;) @Table(schema = &quot;crm&quot; name=&quot;cust_stat&quot;)</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> &middot; 설정 값 @Enumerated 애노테이션 EnumType.STRING: enum 타입 값 이름을 저장 문자열 타입 칼럼에 매핑 ・EnumType. ORDINAL (기본값): enum 타입의 값의 순서를 저장 &bull;숫자 타입 칼럼에 매핑 public enum Grade { S1, S2, S3, S4, S5, S6, S7 } EnumType.STRING &rarr; &quot;S1&quot; Grade.S1.name() EnumType. ORDINAL 0 Grade.S1.ordinal()</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>매핑 설정 예</strong></span></p><p><img alt="" width="600" height="507" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjRyqc6RkGbYSGtsgz0G4ntRcZq3AyZ3G5-4JFwR1DwZctCcWjeDveOF4Mo4LEKyXWxC5Z1tjTRUtV43jBmCmZwweU1imAXRSxYhILvyXJ6Vq3i1dZrsUt9mK3S866o0jFoPMsht5V0hRcOxbM08RFsvev6a_TjCdU_iWz4-ihOylSLTGKzLYGjAhIR6w/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5c6nf8E4AN5st2AX0aSFcd9BBr4g6OzCpuff203bJMastdVaMwrhcUCxQae_r13dJnv33zb-7Bv1Ua3icIsD4KwkelPAzBGUv2a9Q3BbcUlHCLsI82U9wM4_u3UG2gOxyaY4Y_LQQYZf0OJYelYZ5ffElm63V_DkaOvK7TdhaiVS6ppG0HHRzmQDDtg/s16000/5.png" /></p><p>&nbsp;</p><p><img alt="" width="744" height="500" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi28oj6PKRQPyv0EzkEubdZyJube112jzfRKOyZQzOz1cXtRsYj1S-g5hoXEopicfVPHYMnSTlBopYfxun5Td1E1eXWafRS3amm6V53pRFDzdgWFzcGyizJutF6Y4oK3FpO8O_L8AxOK-JP5dwOB39OCktzkW78yD0_9gBybCPJk1CqXkxTuzo_hmGmUQ/s16000/6.png" /></p><p><br /><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgCHhs9IT4eZiGB6ILrGSEtMmGl-UHy8hq64bffJj9UjG04d0pCR6CwWFNS0AGfW4VmUYowGJxK-jGrOZpGnNToefOV4uvedOvXoK462StFa0HiGXvBNussuxrdvrAlKv4ie9LNd-JwlXT-fjo-SfOjQQGV61SFpAwqBuFYFrB5Oq8_15hJWXsD7ggZPg/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>5.JPA 기초 05 엔티티 식별자 생성 방식</strong></span></span></p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/Xw9uTs72SVo" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="704" height="336" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDl6S8qt-xe8vfnmbsUKhLw07gvzA2BRrF-MxT6SvkFDxCbr1EZ8Sdyl9uOW3vKHFSU_WgkqZxIn7i9HywOF5wEAVVabOt8xp__R5_E2UDA9vqW5DmaoYr9rMoUWQ8rLVFsoSi8HIOWtenfBwrLW_tvV1Y9ohwc-tCgkh0Mlve5hv23Ij7yb9-S-pa7w/s16000/1.png" /></p><p><br /><img alt="" width="933" height="512" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4KtvIND1zGouLBGR7UB1ILN7ISpu7luN8wI2eKiZ8c6CFsW5b7YhgEeusp8T6EMmyrzL0TfTXsAcDOeSiGtze09EIoSI4PqQ4GiGMejrwpVl4QIDjpf9B2vwwbnbMFV6KEyyWExvUlra14K6iXFuy4mQkaGKlF7VrdfYn9sNQe43p_JUvmyXNK61M9w/s16000/2.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvDULsrvV6S0V-23y1pY6D4sdq-XWJDd7vgbi6WhBdXwFFqCvalrAMhjoR8HkpDwXYTt4nZ1Xcg7_cwAJBMaTEjOGRWRcXdi4o91r0Ki6tWso2lQ0nUI91SoQZvWkRhrSPEd70Rfqd6i5w6XPGZUjU4_7WyET5cDstjDh5WI2qjswAM-VfhTDAOAd8kg/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIoZ567FpaocpyGi5-EKdAtdIz7tacQae8OltlvvaSeK00eN4cWpnvEDFN-bZdvUB3kzkyMU5chD_mEHERWxjfwZ_KNGQ0cRs5GaI7lrbYti-eC09R47jabq_TJHJJh1E5qQ58oUYHK0WLAI3NU8fCyrH2RI5DhI9f5kNeCPOhad-HbZ04cJvYT6GmXg/s16000/4.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjK_jL7mCwAgQECE3x-sKzXSt4kYIWgsh3iAeIGclUq6sibMgzfZ11PA3QM0ABcSz2kzzkxX4YeT8dc0vBsEFapUcsnJ2Cw0apkcmhWAMsAerB0gvoX0dPXqBlhtjuiJw2EbJ4ofv4e0dKwvtBi1TC7TlOFwDHBrgsMILUczUIVWfYmhX3oFlFcBGsq9g/s16000/5.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjZb5elJ2UM0i2RDDw_IJlFtmYxChNjALNzcuJol8mXs6c8YMgIZ_tjvElnMkOotV0ieYYSDJXGS5eNHqKTdbAA7RphSUgqZ3HLI-rjcl6hpjbKKQfOv5tmubEdhP3VDfPFmosE90tj8G84-Y7xn_7pSO_ZmV7XQHyB90gtNI4lfcOtcLldn_ydyx2K_Q/s16000/6.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjdRDZbPSDaDrYzDkK_rfdvw1FS5YLNSugualii-1zHK0JOKrgd-rr2yvYW1g4f2K1LI31K4VFYcBMa4GReL1mMk_Jsf88UYqTr4xtZl21Bao54AwE7mlfZw8P-n_9rxgY_2BxTkANtofaptVM5GSNzHQcK7QlUVe5Wr9NrUFj7c29YO063mi_ol4sABw/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgdRPjm66ZuC2SDBhfoXwwAChP-LYUHYmwHIWc-9vKy9neayp8ydtft2gs782DmrxOvP0OvVkci2QjLgqxylrjRhvx9J8RF--EsjHlzdZGhzHT1vPBkzZUCSLJw7kc-W6rL-XYf_UP-Syi0A1_nGUPIfP1At6AlsKTYtlnIt0Cxj33qOhBseVItrmuV-w/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="760" height="316" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijR2ibtOR_awQeKTdqKjmr8tcXYAobCwHpwYVZ9ew79w1oIQTQFwC4TQMPDTgrQHgNEJWG2P1D9bX99PXABVoDEz9-Fv4KcVHQuutODARNdszX_pX1PglIQKDCyU4h7_E5OTxj7yf6uoW-4Kl1xN22SqvA_th52KNYP1caxoWZ1H5URVN0ZYitGyni_g/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>6.JPA 기초 06 @Embeddable</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/WtS5IszIueA" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><img alt="" width="631" height="424" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiqnCpLOa2FIXbk5HmhWT4mO3SehCHcvbVydy-89VabSR9lP1wyy5wkynD3DC-agitepGZA2TynUq-53IlFK_QIZDRciu0P2Mnnnw3mcSvyxpHPLkYJ4m0mCr0aZzKDjMWiZxQCSVWW8Z38AdDYZAS-ULmLQtK_gOtuLNbbPBYXfjFOMGrX3-YjH7JInw/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBhbN6o0c0R_mBhIIOJ-dY_qHlzr0tpQRy3YHMCT9HBqJV26IIn-A_4Qa8CzReShSxB-z47EG6jlgJ5_6CTtE3NtaPwHbGQQFLmZqVxFoH4ECf1ovon_aK7k8MmkzLieDNJU4HMVN3LmSrWsTvkd-sN6q8uF6Ns7lb16mUVbpz-Y6G2lGOGM5p69r2Sg/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiF0r7kAFxNbXqSuuOR7pqAtQwuPUr7xEpN5ic9X2metpwThITXAjFVj6Y5gXXISUXqiS3q9Ci9Hu_J6539Vq17BJBVJYJsPSqt-L6q2rLrsMLIyhnOyVnEdlH809R43fNS_HlgaJ4WQC4ZsZ1gE41Ed0eXNY1kpZUiO1T6EjV9HHW1EG0eX0okEyIhvw/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiMjLL8scA9vmKQPuZeNPoTSAV7t2ocPZUsz2TfdRj38DdBj_-5AS21tusvcAgYePIbpQ-r7P4dradT4gpuDGVewVDSi_6ArGSXQkO8SRZtHFtN9JDXkSLROvihKsKL7kTed3n5n8k4ijS2ZYcgPMysQAvZ06HleDJWhfUyCVuIdh5Ndp0kuKE5LDyEQQ/s16000/4.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7m3FwYs3FHEgeMPn67nwiawoYl-6biR1mrUWMxLG3Aa6bH1ALQ04zozpWc4LfhwMFh_cQvFe5xXXl5E2qJJLQJ0-mPstQDyQGRdZ2DiPsYDoiP33ouDCKVt2fIo3oi5Izq1cOwfOUwbwKmIQNlYEvl4iVgHE0nGCZHsBxaakfIF-FEo2aKuArTwZQ_g/s16000/5.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="788" height="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi7OOvKs7y5Qg1NDnIR_Jhh1MTOoVP9Kv45NJ24iAqIIS4haWYGXRFchVPPk90KvokOWqRIowrRB8dPEy9mqmCg8nDIzlUxwFAOxzTuFxGjVenkG9ExGIvkua5eA8vne2WZAh6lhMB8L71op_MHSYElRjKwpDj2jXZgdfnjjriV95lsVOqAS19PFLx9DQ/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjD8dIGW3jfr71RpPBL3Q1HV-IvBrFvnKEZYumIsIE4gH-mUycI1yKtZTeSJCUyMka-_AYrk8N2Dsg-bVzEH51G3zbuMltBIZW_rfJdsjyaM2ng8LThxFggCk2s2_pJUgYFRDL2SCbCsMAAR6d3eYNGwtNFHs182kNmHUcePSm5p63w6YIKjYvJektlYg/s16000/8.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="737" height="210" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7D95OrwxNMAcMcr-4a72RSz5elcvSYizQBu195eviu3bDj4nA1_C6Kl8UvJ4FDbyat9kKZ74IFBQ_b5xWiNBajZWhxqzcCRUYF03Oi5sr_bYp7HmjikG_5qIZ608GI9rfQeA9_cxkDcFMU3thauiiPj73TFXNiWarg4_0cwsj9Pjuvgj3r6c1hHyI3w/s16000/9.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#e67e22"><span style="font-size:24px"><strong>@Embeddable 은 다음을 참조</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://macaronics.net/index.php/user/search/lists/s/%40Embeddable/page/1">https://macaronics.net/index.php/user/search/lists/s/%40Embeddable/page/1</a></p><p>&nbsp;</p><p><strong><span style="color:#c0392b">연관된&nbsp; 엔티티들이 전부 변경 될 수 있으므로,</span></strong></p><p><strong><span style="color:#c0392b">항상 불변 객체로 만들어야 한다.&nbsp; 추가 삭제를 항상 새로운 객체로 생성해서 처리해야 한다.</span></strong></p><p>&nbsp;</p><p><span style="color:#8e44ad"><strong>값 타입은 변경 불가능하게 설계해야 한다.</strong></span></p><p>&nbsp;</p><p>&gt; @Setter 를 제거하고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만들자. JPA 스펙상 엔티티나 임베디드 타입( @Embeddable )은 자바 기본 생성자(default constructor)를 public 또는 protected 로 설정해야 한다. public 으로 두는 것 보다는 protected 로 설정하는 것이 그나마 더&nbsp; 안전하다.</p><p><br />&gt; JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.</p><p>&nbsp;</p><pre class="brush:as3;">/** * 엔티티의 값이 공유되면 전 a, b,c.. 모두 변경 처리 될수 있기때문에 * @Embeddable클래스를 불변 절대 공유되서는 안된다. * 따라서, setter 은 사용하지 않는다. */ @Embeddable @Getter public class Address { private String city; private String street; private String zipcode; //함부로 new 로 생성처리 하면 안된다. protected Address() { } public Address(String city, String street, String zipcode) { this.city = city; this.street = street; this.zipcode = zipcode; } }</pre><p>&nbsp;</p><p><img alt="" width="1020" height="759" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjN-m9VVVw4LRF4y_Y6OrGDKzm1LXmLOVNImsHHLt4mhYlxk9xf9hvzoR0T-5weYH-iX9RYIpGvMjFj7DE0eGe1yDX5SNlapKWPZCbdtNpepGPiNt7SFGKEJfv6cJZhRtmfnceA2w3pHhPj6kqZy6l-i7XzPlclalrFLS5Cnv4Unj5U4I41gHPYWV9SZQ/s16000/2023-03-03%2014%2049%2051.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>7.JPA 기초 07 @Embeddable 다른 테이블에 매핑하기</strong></span></span></p><p>&nbsp;</p><p><iframe width="340" height="250" src="https://www.youtube.com/embed/3_sdQGfL2Lg" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSKOxOJHSF7Kn0CLGMZBiSXPaox-n5D6fS19IDpKYV3J3dqVhsJNHwtbeiyuObfmKlguTCw_7PaJ0CTrNYd8WjjFPpIwZSzZdK6snivp5fPoSC0KUN7hU8C6nj_Nien8SXYqOBzdBedfz82228Ud6xSwtPpJPUeyygUPss91L2_P28K90Ui7ZJ0ksNkg/s16000/1.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiuQLsO_qtorGJYLYvYFq4djiU1CMr0vvBfvtgTuhhNfoskpRMeszcrDGMWGcCZtUj4Rxfs9PRDSnDnwSnwxcv48Ug4GcRGQ7Ve_jRQX0HANI0Sf8xc7cOebKHMxQcQ22zYeRO8gYsq6M-dOFnp2BzTDxsZHffiNNidzkiATMzH54DF01waaEoAJMPBww/s16000/2.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi5uE2eIbadlxQYTuMdTLv4hxREOcVo6HndIFzednOUtVowYekAz6y96xnGln_tjLO2I62f_e42eXuEpPFJD1YrwQJ2EBzoR0ztQDu1GQlW2mVxLonXACF-xXRaIVKU1gzV2EKs_da2p3MbQOsj_dbdJiMg4CPQ1myED0B7Ush3OftSu0rbmQQnTZmNQw/s16000/3.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmWM2UPinLLcRPEk8e_lyO1RTc2I8gPmh5ik9E_OEHnLBml2SVjQejbGlrTHlPMml_umBryb6DMq0WyTTNBnf1bZdLbkQiJwz-82rmuRYqLJ5fBkuhJYsOCBGq_HGYXxkmugRmD5J2qYsAN1_0RvEmQd2q4wcbk_YW0Pki0jScbc7G2sFrM5EIv9qYwA/s16000/4.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhvsGiCixPnBwx62IvkQ1bCe6RiCpfPwZMjsvTb-_p9HBKqkG9xRJ4KsKfU_vKRmTRs6tGkBh-Vn_xODc5NgW3gcfGLIhPcJhwagjM7IzuIzI0tFxEBDNrEU4vK9bfKE4FYTFRR603p_gp2QLDzo28PV-CiFmn_M3xhnoDZBSeAB2ccM6_o-x5izzuzRQ/s16000/5.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqkOVPkxwU-ZYRl0MLu29yjnHmKhZ6XePRKczGOLeKMOZUSRcE0PrkPpDKOL6mSnH2tSOPACsn47BQrudvQM1xjDTmHyNYMqtw2ghKMq36RAYtJ2ko-x1CYr4oPgupIO2h_IXVc5tw9fBA12agEdO3rT713PJJJVWTE5a0XoQblMGNbcSftiOIsovhFw/s16000/6.png" /></p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEigk9Drjc8O2Agys-d0Lu_MCOeCFRisfstFtjKnNSUWH2BPbk2xT_HIgkgsyukHeLbcM7aigDONbjDZUcKK-iqNLIJggygJs8NERbIdXUv5iC2V3ebIKpMlZzieu3XOT_XrIKLI1mLRu7XdB3Wq91mKMgBGreiptlDxbMIVkAMTm5XPoWKIqVpXFRALrA/s16000/7.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-30 19:58:20 스프링 부트 3.0 으로 업그레이드 방법 http://macaronics.net/index.php/m01/spring/view/2106 2106 <p>&nbsp;</p><p><span style="font-size:22px"><span style="color:#2980b9"><strong>스프링부트 2.7.6&nbsp; &nbsp;+ mybatis + jsp +maven&nbsp; 기준으로&nbsp; 3.0.5 업그레이드 방법&nbsp;</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>1단계 :</strong></span></span></p><p><span style="font-size:16px"><strong>pc에 설치 된 자바 버전을 17이상으로 &nbsp;올려준다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>2단계 :</strong></span></span></p><p><span style="font-size:16px"><strong>&nbsp;pom.xml 이나 , build.gradle&nbsp;에서 &nbsp;단순히 &nbsp;버전만 올리면 에러 뿐만 아니라 프로젝트 구조가 바뀌어서 힘들다.<br />따라서,&nbsp; <a target="_blank" href="https://start.spring.io/">https://start.spring.io/</a> 사이트에서 기본적인&nbsp;&nbsp;</strong></span><span style="font-size:16px"><strong>샘플 프로젝트를 다운 받은 후 &nbsp;임포트 해서 &nbsp; 이 프로젝트를 기준으로 &nbsp;업데이트할 프로젝트</strong></span></p><p><span style="font-size:16px"><strong>파일들을&nbsp; 옮기는 방법을 추천한다.&nbsp;</strong></span><br />&nbsp;</p><p><img alt="" width="900" height="491" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgDtG-YZmdvMYF3jfKf9NdnAl5jrGroxCwL9PIJl5_90jJrEZxOnz-h3GLoDsiMrNA0HG4k8sXknfDYhD3oZR8kPcmZuOjzvatjyYlf5j9sUJ8tPFwHsLQPy9Y-oafAZnQMGSeIExIRJ4A2X9VL_FI_eY2xuHg0BUUqcNfT2mrJMxV4bfZT2T35yJTqRg/s16000/2023-03-30%2013%2050%2018.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>샘플&nbsp;</strong></span><br /><a target="_blank" href="https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.0.5&amp;packaging=war&amp;jvmVersion=17&amp;groupId=com.example&amp;artifactId=macaronics&amp;name=sample&amp;description=3.0%20%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C%20%EC%83%98%ED%94%8C&amp;packageName=com.example.macaronics&amp;dependencies=data-jdbc,mail,mustache,oauth2-client,security,validation,web,devtools,mybatis,mariadb,lombok,data-jpa,mysql">https://start.spring.io/#!type=maven-project&amp;language=java&amp;platformVersion=3.0.5&amp;packaging=war&amp;jvmVersion=17&amp;groupId=com.example&amp;artifactId=macaronics&amp;name=sample&amp;description=3.0%20%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C%20%EC%83%98%ED%94%8C&amp;packageName=com.example.macaronics&amp;dependencies=data-jdbc,mail,mustache,oauth2-client,security,validation,web,devtools,mybatis,mariadb,lombok,data-jpa,mysql</a></p><p>&nbsp;</p><p>&nbsp;</p><p>위와 같이 프로젝트에 사용한 <strong><span style="color:#c0392b">기본&nbsp;&nbsp;라이브러리를 설정</span> </strong>한 후 다운 받는다.</p><p>&nbsp;</p><p>pom.xml 에서 다음 부분만 기존 프로젝트 있는 내용으로 변경 처리한다.&nbsp;</p><pre class="brush:as3;"> &lt;groupId&gt;com.example&lt;/groupId&gt; &lt;artifactId&gt;macaronics&lt;/artifactId&gt; &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt; &lt;packaging&gt;war&lt;/packaging&gt; &lt;name&gt;sample&lt;/name&gt; &lt;description&gt;3.0 업그레이드 샘플&lt;/description&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>3단계 :</strong></span></span><br />&nbsp;</p><p><span style="font-size:16px"><strong>&nbsp;기존 프로젝트에서&nbsp; &nbsp;파일들을 덮어씌운다.</strong></span></p><p><br />1) src/main/java/이하파일들 덮어 씌우기</p><p>2)src/main/resources/이하파일들 덮어 씌우기</p><p>3)src/main/webapp/이하파일들 덮어 씌우기</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>4단계 :</strong></span></span></p><p><strong><span style="font-size:16px">아직 추가하지 않은 라이브러리 및&nbsp; 기타 3.0 에서 특화된&nbsp; 많은&nbsp;&nbsp;오류사항이 보일텐데,</span></strong></p><p><strong><span style="font-size:16px">먼저,&nbsp; javax.&nbsp; &nbsp;를&nbsp; &nbsp;이클립스 crtl +shfit +h , shift+shift 로 전체 파일을&nbsp;jakarta. 포 변경한다.</span></strong></p><p>&nbsp;</p><p><strong>javax.sql.<span style="color:#c0392b">DataSource&nbsp; </span>,&nbsp; &nbsp;javax.imageio.<span style="color:#c0392b">ImageIO&nbsp; &nbsp;,&nbsp;</span>javax.crypto.SecretKey</strong><strong><span style="color:#c0392b">&nbsp;</span>등은&nbsp; &nbsp;</strong>&nbsp;jakarta 가 아니라 기존 그대로&nbsp; javax 이다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>5단계 :</strong></span></span></p><p><span style="font-size:16px"><strong>시큐리티 변경</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">Spring Security 변경 SecurityConfig에서 제거된 다음의 메서드를 변경해주어야 한다. authorizeRequests() ➔ authorizeHttpRequests() antMatchers() ➔ requestMatchers() regexMatchers() ➔ RegexRequestMatchers()</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>@Configurable -&gt; 스프링부트 3.0 이상 작동 안됨</strong></p><p>&nbsp;</p><p><strong><span style="color:#c0392b">@Configurable&nbsp; 사용하고 있다면&nbsp;&nbsp;@Component </span>변경 처리한다.</strong></p><p>&nbsp;</p><p><strong>다음과 같은&nbsp;&nbsp;@Component 만 사용해도 작동 되는 것 같으나 세부적으로 살펴보지 않은관계로</strong></p><p><strong>&nbsp;@EnableWebSecurity 과&nbsp;&nbsp;@EnableMethodSecurity은 사용처리해 준다.</strong></p><pre class="brush:as3;">//@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) // secured 어노테이션 활성화 //@Configurable -&gt; 스프링부트 3.0 이상 작동 안됨 //@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록이 됩니다. @Component public class SecurityConfig {</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>샘플 예&nbsp;</strong></span></p><pre class="brush:as3;"> @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable().cors().disable() .authorizeHttpRequests(request -&gt; request .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() .requestMatchers(HttpMethod.GET, &quot;/board/**&quot;).permitAll() .requestMatchers(HttpMethod.POST, &quot;/board/**&quot;,&quot;/comment/**&quot;).hasAnyAuthority(&quot;ROLE_USER&quot;, &quot;ROLE_ADMIN&quot;, &quot;ROLE_MANAGER&quot;) .requestMatchers(HttpMethod.DELETE, &quot;/board/**&quot; ,&quot;/comment/**&quot;).hasAnyAuthority(&quot;ROLE_USER&quot;, &quot;ROLE_ADMIN&quot;, &quot;ROLE_MANAGER&quot;) .requestMatchers(HttpMethod.PUT, &quot;/board/**&quot; ,&quot;/comment/**&quot;).hasAnyAuthority(&quot;ROLE_ADMIN&quot;, &quot;ROLE_MANAGER&quot; ,&quot;ROLE_USER&quot;) .requestMatchers(&quot;/&quot;, &quot;/resources/**&quot;, &quot;/loginForm/**&quot;, &quot;/login/**&quot;, &quot;/join/**&quot;, &quot;/displayFile&quot;,&quot;/phoneCertification&quot;, &quot;/emailAuthentication/send&quot;, &quot;/findIdPassword/**&quot;,&quot;/like/list&quot;).permitAll() ) .formLogin(login -&gt; login .loginPage(&quot;/loginForm&quot;) .loginProcessingUrl(&quot;/login&quot;) .usernameParameter(&quot;userId&quot;) .passwordParameter(&quot;password&quot;) .defaultSuccessUrl(&quot;/&quot;, true) .failureHandler(customFailureHandler) // 로그인 오류 실패 체크 핸들러 .permitAll() ) .logout(); return http.build(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>6단계 :</strong></span></span></p><p><strong><span style="font-size:16px">6. <span style="color:#c0392b">com.navercorp.lucy&nbsp;</span> &nbsp;라이브러리를 사용하고 있다면 오류가 나올텐데.</span></strong></p><p><strong><span style="font-size:16px">이것이 아직 업데이트 되지 않고 기존&nbsp;javax 를 사용하고 있어서 이다. </span></strong></p><p><strong><span style="font-size:16px">다음과 같이 파일을 만들어 준다.</span></strong></p><p>&nbsp;</p><p><strong>XssEscapeServletFilterUpdate</strong></p><pre class="brush:as3;">package net.macaronics.util.filter.navercorp; import java.io.IOException; import com.navercorp.lucy.security.xss.servletfilter.XssEscapeFilter; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; @Component public class XssEscapeServletFilterUpdate implements Filter { private XssEscapeFilter xssEscapeFilter = XssEscapeFilter.getInstance(); @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssEscapeServletFilterWrapperUpdate(request, xssEscapeFilter), response); } @Override public void destroy() { } } </pre><p>&nbsp;</p><p><strong>XssEscapeServletFilterWrapperUpdate</strong></p><pre class="brush:as3;">package net.macaronics.util.filter.navercorp; import java.util.HashMap; import java.util.Map; import java.util.Set; import com.navercorp.lucy.security.xss.servletfilter.XssEscapeFilter; import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; public class XssEscapeServletFilterWrapperUpdate extends HttpServletRequestWrapper { private XssEscapeFilter xssEscapeFilter; private String path = null; public XssEscapeServletFilterWrapperUpdate(ServletRequest request, XssEscapeFilter xssEscapeFilter) { super((HttpServletRequest)request); this.xssEscapeFilter = xssEscapeFilter; String contextPath = ((HttpServletRequest) request).getContextPath(); this.path = ((HttpServletRequest) request).getRequestURI().substring(contextPath.length()); } @Override public String getParameter(String paramName) { String value = super.getParameter(paramName); return doFilter(paramName, value); } @Override public String[] getParameterValues(String paramName) { String values[] = super.getParameterValues(paramName); if (values == null) { return values; } for (int index = 0; index &lt; values.length; index++) { values[index] = doFilter(paramName, values[index]); } return values; } @Override public Map&lt;String, String[]&gt; getParameterMap() { Map&lt;String, String[]&gt; paramMap = super.getParameterMap(); Map&lt;String, String[]&gt; newFilteredParamMap = new HashMap&lt;&gt;(); Set&lt;Map.Entry&lt;String, String[]&gt;&gt; entries = paramMap.entrySet(); for (Map.Entry&lt;String, String[]&gt; entry : entries) { String paramName = entry.getKey(); Object[] valueObj = (Object[])entry.getValue(); String[] filteredValue = new String[valueObj.length]; for (int index = 0; index &lt; valueObj.length; index++) { filteredValue[index] = doFilter(paramName, String.valueOf(valueObj[index])); } newFilteredParamMap.put(entry.getKey(), filteredValue); } return newFilteredParamMap; } /** * @param paramName String * @param value String * @return String */ private String doFilter(String paramName, String value) { return xssEscapeFilter.doFilter(path, paramName, value); } } </pre><p>&nbsp;</p><p><strong>사용</strong></p><pre class="brush:as3;"> @Bean FilterRegistrationBean&lt;?&gt; getFilterRegistrationBean() { FilterRegistrationBean&lt;XssEscapeServletFilterUpdate&gt; filterRegistration = new FilterRegistrationBean&lt;XssEscapeServletFilterUpdate&gt;(); filterRegistration.setFilter(new XssEscapeServletFilterUpdate()); filterRegistration.setOrder(1); // registrationBean.addUrlPatterns(&quot;*.do&quot;, &quot;*.go&quot;); //filter&cedil;&brvbar; // registrationBean.addUrlPatterns(&quot;*.do&quot;, &quot;*.go&quot; ,&quot;/member/*&quot;); // url patterns filterRegistration.addUrlPatterns(&quot;/comment/*&quot;, &quot;/customerSupport/*&quot;); // filter&cedil;&brvbar; return filterRegistration; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>7단계 :</strong></span></span></p><p><span style="font-size:20px"><strong>&nbsp;jsp 오류 잡기</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>- Servlet</strong></p><p>우선 Servlet의 패키지가 변경되었습니다.</p><p>&nbsp;</p><p><strong>javax.servlet</strong>&nbsp; &nbsp; &nbsp;=====&gt;&nbsp; &nbsp;&nbsp;<strong>jakarta.servlet</strong></p><p>&nbsp;</p><p>SpringBoot 버전업을 원하시는 분들은 해당 사항 주의하시길 바랍니다.</p><p>&nbsp;</p><p><strong>- JSP, JSTL</strong></p><p>물론, 추세에 따르면 JSP보다는 Vue.js, react 등 별도의 프론트 프레임워크 혹은 라이브러리를</p><p>&nbsp;</p><p>이용하고 있지만, 혹시나 모를 JSP 이용자분들을 위해 세팅법을 알려드립니다.</p><p>&nbsp;</p><p>기존 SpringBoot 2.x 와는 또 다른 부분들이 있더군요.</p><p>&nbsp;</p><p>기존의 제공되는 내장형 tomcat의 의존성을 주석처리 및 제거해주도록 합니다.</p><p>[<strong>pom.xml</strong>]</p><pre>&lt;!--&lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-tomcat&lt;/artifactId&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; --&gt;</pre><p>&nbsp;</p><p>이후, JSP로 View를 리턴해줄 수 있는 tomcat의 의존성을 주입해주도록 합니다.</p><p>[<strong>pom.xml</strong>]</p><pre>&lt;dependency&gt; &lt;groupId&gt;org.apache.tomcat.embed&lt;/groupId&gt; &lt;artifactId&gt;tomcat-embed-jasper&lt;/artifactId&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>이제 JSP 에 대한 resolver를 세팅해줄건데요, bean이 아닌 properties 를 통해 세팅해주도록 하겠습니다.</p><p>&nbsp;</p><p>[<strong>application.properties</strong>]</p><pre>spring.mvc.view.prefix=/WEB-INF/view/ spring.mvc.view.suffix=.jsp</pre><p>&nbsp;</p><p>위와같이 본인의 환경에 맞게 세팅을 해주시면 간단하게 완료입니다.</p><p>&nbsp;</p><p>이제 JSTL 사용에 대해 알아볼건데요.</p><p>&nbsp;</p><p>기존 2.x 에서 사용하듯 의존성을 주입하고 jstl을 사용하려고 하면</p><p>&nbsp;</p><pre>&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&gt;</pre><p>&nbsp;</p><p>JSP내에서 해당 taglib을 못받아오는 에러를 경험해보실 수 있습니다.</p><p>&nbsp;</p><p>글의 상단에서 말씀 드렸듯, servlet의 패키지가 변경되었듯 jstl 또한 변경이 있었더군요.</p><p>&nbsp;</p><p>그럴땐 아래의 의존성을 주입해주도록 합니다.</p><p>&nbsp;</p><pre>&lt;!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api --&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.servlet&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet-api&lt;/artifactId&gt; &lt;version&gt;6.0.0&lt;/version&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; &lt;!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api --&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.servlet.jsp.jstl&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet.jsp.jstl-api&lt;/artifactId&gt; &lt;version&gt;3.0.0&lt;/version&gt; &lt;/dependency&gt; &lt;!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl --&gt; &lt;dependency&gt; &lt;groupId&gt;org.glassfish.web&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet.jsp.jstl&lt;/artifactId&gt; &lt;version&gt;3.0.1&lt;/version&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>출처 : <a target="_blank" href="https://islet4you.tistory.com/entry/SpringBoot-SpringBoot-30-%EC%97%90%EC%84%9C-Jsp-Jstl-%EC%84%B8%ED%8C%85-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Servlet-%EB%B3%80%EA%B2%BD%EC%A0%90">&nbsp;[SpringBoot] SpringBoot 3.0 에서 Jsp, Jstl 세팅 그리고 Servlet 변경점</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:22px"><strong>8 단계 :</strong></span></span></p><p><span style="font-size:20px"><strong>&nbsp;기존 프로젝트에서&nbsp; 추가 안한 라이브러리들을&nbsp; </strong></span><span style="font-size:18px"><strong><span style="color:#000000">하나씩 추가해보면서 오류를 잡아 본다.</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>9. 만약 JPA&nbsp; 프로젝트라만&nbsp;&nbsp;Query DSL 설정 참조.</strong></span></p><p>&nbsp;</p><p>기존</p><pre class="brush:as3;">&lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-apt&lt;/artifactId&gt; &lt;version&gt;5.0.0&lt;/version&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-jpa&lt;/artifactId&gt; &lt;version&gt;5.0.0&lt;/version&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>= &gt; 업데이트</p><pre class="brush:as3;">&lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-apt&lt;/artifactId&gt; &lt;version&gt;5.0.0&lt;/version&gt; &lt;classifier&gt;jakarta&lt;/classifier&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.querydsl&lt;/groupId&gt; &lt;artifactId&gt;querydsl-jpa&lt;/artifactId&gt; &lt;version&gt;5.0.0&lt;/version&gt; &lt;classifier&gt;jakarta&lt;/classifier&gt; &lt;/dependency&gt;</pre><p>&nbsp;</p><p>오류 참조</p><p><a target="_blank" href="https://github.com/querydsl/querydsl/issues/3431#issuecomment-1328623180">https://github.com/querydsl/querydsl/issues/3431#issuecomment-1328623180</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>10. 기타&nbsp; &nbsp;pom.xml 참조</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt; &lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&gt; &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt; &lt;parent&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt; &lt;version&gt;3.0.5&lt;/version&gt; &lt;relativePath /&gt; &lt;!-- lookup parent from repository --&gt; &lt;/parent&gt; &lt;groupId&gt;net.macaronics&lt;/groupId&gt; &lt;artifactId&gt;macaronics&lt;/artifactId&gt; &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt; &lt;packaging&gt;war&lt;/packaging&gt; &lt;name&gt;system&lt;/name&gt; &lt;description&gt;Macaronics system&lt;/description&gt; &lt;properties&gt; &lt;java.version&gt;17&lt;/java.version&gt; &lt;/properties&gt; &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-mail&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-mustache&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-oauth2-client&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt; &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;3.0.0&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.mariadb.jdbc&lt;/groupId&gt; &lt;artifactId&gt;mariadb-java-client&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;!-- https://islet4you.tistory.com/entry/SpringBoot-SpringBoot-30-에서-Jsp-Jstl-세팅-그리고-Servlet-변경점 [SpringBoot] SpringBoot 3.0 에서 Jsp, Jstl 세팅 그리고 Servlet 변경점 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-tomcat&lt;/artifactId&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt;--&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.tomcat.embed&lt;/groupId&gt; &lt;artifactId&gt;tomcat-embed-jasper&lt;/artifactId&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; &lt;!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api --&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.servlet&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet-api&lt;/artifactId&gt; &lt;version&gt;6.0.0&lt;/version&gt; &lt;scope&gt;provided&lt;/scope&gt; &lt;/dependency&gt; &lt;!-- https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api --&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.servlet.jsp.jstl&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet.jsp.jstl-api&lt;/artifactId&gt; &lt;version&gt;3.0.0&lt;/version&gt; &lt;/dependency&gt; &lt;!-- https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl --&gt; &lt;dependency&gt; &lt;groupId&gt;org.glassfish.web&lt;/groupId&gt; &lt;artifactId&gt;jakarta.servlet.jsp.jstl&lt;/artifactId&gt; &lt;version&gt;3.0.1&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.security&lt;/groupId&gt; &lt;artifactId&gt;spring-security-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.mysql&lt;/groupId&gt; &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; &lt;!--여기부터 커스텀 라이브러리 추가--&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.tika&lt;/groupId&gt; &lt;artifactId&gt;tika-core&lt;/artifactId&gt; &lt;version&gt;2.7.0&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.imgscalr&lt;/groupId&gt; &lt;artifactId&gt;imgscalr-lib&lt;/artifactId&gt; &lt;version&gt;4.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.poi&lt;/groupId&gt; &lt;artifactId&gt;poi&lt;/artifactId&gt; &lt;version&gt;5.2.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.poi&lt;/groupId&gt; &lt;artifactId&gt;poi-ooxml&lt;/artifactId&gt; &lt;version&gt;5.2.2&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.commons&lt;/groupId&gt; &lt;artifactId&gt;commons-text&lt;/artifactId&gt; &lt;version&gt;1.10.0&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.navercorp.lucy&lt;/groupId&gt; &lt;artifactId&gt;lucy-xss-servlet&lt;/artifactId&gt; &lt;version&gt;2.0.1&lt;/version&gt; &lt;/dependency&gt; &lt;!--// After Spring Boot 3.0--&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.xml.bind&lt;/groupId&gt; &lt;artifactId&gt;jakarta.xml.bind-api&lt;/artifactId&gt; &lt;version&gt;4.0.0&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;jakarta.mail&lt;/groupId&gt; &lt;artifactId&gt;jakarta.mail-api&lt;/artifactId&gt; &lt;version&gt;2.1.0&lt;/version&gt; &lt;/dependency&gt; &lt;!--3.0 업데이트 설명서--&gt; &lt;!-- &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-properties-migrator&lt;/artifactId&gt; &lt;scope&gt;runtime&lt;/scope&gt; &lt;/dependency&gt; --&gt; &lt;!-- log4jdbc-remix , mybatis 쿼리 실행 보기 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.lazyluke&lt;/groupId&gt; &lt;artifactId&gt;log4jdbc-remix&lt;/artifactId&gt; &lt;version&gt;0.2.7&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; &lt;!-- &lt;build&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt; &lt;configuration&gt; &lt;excludes&gt; &lt;exclude&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/exclude&gt; &lt;/excludes&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;/build&gt;--&gt; &lt;build&gt; &lt;finalName&gt;${project.artifactId}&lt;/finalName&gt; &lt;plugins&gt; &lt;plugin&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt; &lt;configuration&gt; &lt;excludes&gt; &lt;exclude&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/exclude&gt; &lt;/excludes&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;plugin&gt; &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt; &lt;artifactId&gt;maven-surefire-plugin&lt;/artifactId&gt; &lt;version&gt;2.22.2&lt;/version&gt; &lt;configuration&gt; &lt;skipTests&gt;true&lt;/skipTests&gt; &lt;/configuration&gt; &lt;/plugin&gt; &lt;/plugins&gt; &lt;defaultGoal&gt;install&lt;/defaultGoal&gt; &lt;/build&gt; &lt;/project&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-30 14:49:43 실전! Querydsl -8.스프링 데이터 JPA가 제공하는 Querydsl 기능, ★4.3 스프링 부트 2.6 이상, Querydsl 5.0 지원 방법 , Querydsl4RepositorySupport 사용 코드 http://macaronics.net/index.php/m01/spring/view/2105 2105 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[8] 스프링 데이터 JPA가 제공하는 Querydsl 기능(</strong></span></span><strong><span style="font-size:16px"><span style="color:#8e44ad">실무 환경에서 사용하기에는 많이 부족)</span></span></strong></p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#000000"><strong>참고삼아 보고,&nbsp;&nbsp;</strong></span></span><span style="font-size:18px"><span style="color:#c0392b"><strong>42. Querydsl 지원 클래스 직접 만들기 를 볼것</strong></span></span></p><p>&nbsp;</p><p><strong><span style="font-size:16px"><span style="color:#8e44ad">여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다. </span></span></strong></p><p><span style="color:#8e44ad"><strong><span style="font-size:16px">그래도 스프링 데이터에서 제공하는 기능이므로 간단히 소개하고, 왜 부족한지 설명하겠다.</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>39.인터페이스 지원 - QuerydslPredicateExecutor</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30155&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30155&amp;tab=curriculum</a><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>인터페이스 지원 - QuerydslPredicateExecutor 공식 URL: <a target="_blank" href="https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/ #core.extensions.querydsl">https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/ #core.extensions.querydsl</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; , MemberRepositoryCustom, QuerydslPredicateExecutor&lt;Member&gt; { List&lt;Member&gt; findByUsername(String username); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>QuerydslPredicateExecutor 인터페이스</strong></p><p>&nbsp;</p><pre class="brush:as3;">public interface QuerydslPredicateExecutor &lt;T&gt; { Optional &lt;T&gt; findById(Predicate predicate); Iterable &lt;T&gt; findAll(Predicate predicate); long count(Predicate predicate); boolean exists(Predicate predicate); // &hellip; more functionality omitted. }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>리포지토리에 적용</strong></p><pre class="brush:as3;">interface MemberRepository extends JpaRepository&lt;User, Long&gt;, QuerydslPredicateExecutor&lt;User&gt; { }</pre><p>&nbsp;</p><pre class="brush:as3;">Iterable result = memberRepository.findAll( member.age.between(10, 40) .and(member.username.eq(&quot;member1&quot;)) );</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>한계점</strong></span></p><p>조인X (묵시적 조인은 가능하지만 left join이 불가능하다.)<br />클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.<br />복잡한 실무환경에서 사용하기에는 한계가 명확하다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>&gt; 참고: QuerydslPredicateExecutor 는 Pagable, Sort를 모두 지원하고 정상 동작한다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>40. Querydsl Web 지원</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30156&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30156&amp;tab=curriculum</a><br />&nbsp;</p><p>&nbsp;</p><p>공식 URL: <a target="_blank" href="https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/">https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/</a><br />#core.web.type-safe</p><p><br />한계점<br />단순한 조건만 가능<br />조건을 커스텀하는 기능이 복잡하고 명시적이지 않음<br />컨트롤러가 Querydsl에 의존<br />복잡한 실무환경에서 사용하기에는 한계가 명확</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>41. 리포지토리 지원 - QuerydslRepositorySupport</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30157&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30157&amp;tab=curriculum</a><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>장점</strong></span></p><p><br />getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환&nbsp; 가능(단! Sort는 오류발생)</p><p><br />from() 으로 시작 가능(최근에는 QueryFactory를 사용해서 select() 로 시작하는 것이 더 명시적) EntityManager 제공</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>한계</strong></span></p><p><br />Querydsl 3.x 버전을 대상으로 만듬</p><p><br />Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음 select로 시작할 수 없음 (from으로 시작해야함)</p><p><br />QueryFactory 를 제공하지 않음 스프링 데이터 Sort 기능이 정상 동작하지 않음</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>42. Querydsl 지원 클래스 직접 만들기</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30158&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30158&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>장점</strong></span></p><p><br /><strong>스프링 데이터가 제공하는 페이징을 편리하게 변환</strong></p><p><br /><strong>페이징과 카운트 쿼리 분리 가능</strong></p><p><br /><strong>스프링 데이터 Sort 지원</strong></p><p><br /><strong>select() , selectFrom() 으로 시작 가능</strong></p><p><br /><strong>EntityManager , QueryFactory 제공</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>Querydsl4RepositorySupport</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.repository.support; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Expression; import com.querydsl.core.types.dsl.PathBuilder; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; import org.springframework.data.jpa.repository.support.Querydsl; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import org.springframework.util.Assert; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import java.util.List; import java.util.function.Function; /** * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리 * * @author Younghan Kim * @see org.springframework.data.jpa.repository.support.QuerydslRepositorySupport */ @Repository public abstract class Querydsl4RepositorySupport { private final Class domainClass; private Querydsl querydsl; private EntityManager entityManager; private JPAQueryFactory queryFactory; public Querydsl4RepositorySupport(Class&lt;?&gt; domainClass) { Assert.notNull(domainClass, &quot;Domain class must not be null!&quot;); this.domainClass = domainClass; } @Autowired public void setEntityManager(EntityManager entityManager) { Assert.notNull(entityManager, &quot;EntityManager must not be null!&quot;); JpaEntityInformation entityInformation = JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager); SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE; EntityPath path = resolver.createPath(entityInformation.getJavaType()); this.entityManager = entityManager; this.querydsl = new Querydsl(entityManager, new PathBuilder&lt;&gt;(path.getType(), path.getMetadata())); this.queryFactory = new JPAQueryFactory(entityManager); } @PostConstruct public void validate() { Assert.notNull(entityManager, &quot;EntityManager must not be null!&quot;); Assert.notNull(querydsl, &quot;Querydsl must not be null!&quot;); Assert.notNull(queryFactory, &quot;QueryFactory must not be null!&quot;); } protected JPAQueryFactory getQueryFactory() { return queryFactory; } protected Querydsl getQuerydsl() { return querydsl; } protected EntityManager getEntityManager() { return entityManager; } protected &lt;T&gt; JPAQuery&lt;T&gt; select(Expression&lt;T&gt; expr) { return getQueryFactory().select(expr); } protected &lt;T&gt; JPAQuery&lt;T&gt; selectFrom(EntityPath&lt;T&gt; from) { return getQueryFactory().selectFrom(from); } protected &lt;T&gt; Page&lt;T&gt; applyPagination(Pageable pageable, Function&lt;JPAQueryFactory, JPAQuery&gt; contentQuery) { JPAQuery jpaQuery = contentQuery.apply(getQueryFactory()); List&lt;T&gt; content = getQuerydsl().applyPagination(pageable, jpaQuery).fetch(); return PageableExecutionUtils.getPage(content, pageable, jpaQuery::fetchCount); } protected &lt;T&gt; Page&lt;T&gt; applyPagination(Pageable pageable, Function&lt;JPAQueryFactory, JPAQuery&gt; contentQuery, Function&lt;JPAQueryFactory, JPAQuery&gt; countQuery) { JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory()); List&lt;T&gt; content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch(); JPAQuery countResult = countQuery.apply(getQueryFactory()); return PageableExecutionUtils.getPage(content, pageable, countResult::fetchCount); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#16a085"><strong>Querydsl4RepositorySupport 사용 코드</strong></span></span></p><pre class="brush:as3;">package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.QMember; import study.querydsl.repository.support.Querydsl4RepositorySupport; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; @Repository public class MemberTestRepository extends Querydsl4RepositorySupport { public MemberTestRepository(Class&lt;?&gt; domainClass) { super(domainClass); } public List&lt;Member&gt; basicSelect(){ return select(member) .from(member) .fetch(); } public List&lt;Member&gt; basicSelectFrom(){ return selectFrom(member).fetch(); } public Page&lt;Member&gt; searchPageByApplyPage(MemberSearchCondition condition, Pageable pageable){ JPAQuery&lt;Member&gt; query = selectFrom(member) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ); List&lt;Member&gt; content = getQuerydsl().applyPagination(pageable, query).fetch(); return PageableExecutionUtils.getPage(content, pageable, query::fetchCount); } /** searchPageByApplyPage 메서드와 applyPagination 는 반환 값은 동일하다 */ public Page&lt;Member&gt; applyPagination(MemberSearchCondition condition, Pageable pageable){ return applyPagination(pageable, query-&gt;query .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) )); } /** applyPagination2 메서드와 MemberRepositoryImpl 클래스의 searchPageComplex 는 반환 값은 동일하다 */ public Page&lt;Member&gt; applyPagination2(MemberSearchCondition condition, Pageable pageable){ return applyPagination(pageable, contentQuery-&gt;contentQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ), countQuery -&gt; countQuery .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())) ); } /** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ /** @Override public Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List&lt;MemberTeamDto&gt; content = queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery&lt;Member&gt; countQuery = queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ); // .fetchCount(); //첫페이지가 페이지 사이즈 content 사이즈 보다 작을 경우 countQuery.fetchCount() 작동하지 않고 해당 content 사이즈로 처리 //마지막 페이지일경우 countQuery 실행되지 않고, offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함 // return PageableExecutionUtils.getPage(content,pageable , ()-&gt;countQuery.fetchCount() ); return PageableExecutionUtils.getPage(content,pageable , countQuery::fetchCount ); //return new PageImpl&lt;&gt;(content, pageable, total); } /** * count 쿼리가 생략 가능한 경우 생략해서 처리 * 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 * 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함) */ private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoe(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoe(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:28px"><strong>★4.3 스프링 부트 2.6 이상, Querydsl 5.0 지원 방법</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>참고: 해당 내용은 강의 이후에 추가된 내용입니다.</strong></p><p><br />최신 스프링 부트 2.6부터는 Querydsl 5.0을 사용한다.</p><p><br />스프링 부트 2.6 이상 사용시 다음과 같은 부분을 확인해야 한다.</p><p><br />1. build.gradle 설정 변경</p><p><br /><strong>2. PageableExecutionUtils Deprecated(<span style="color:#c0392b">향후 미지원</span>) 패키지 변경</strong></p><p><br /><strong>3. Querydsl fetchResults() , fetchCount() Deprecated(<span style="color:#c0392b">향후 미지원</span>)</strong><br />&nbsp;</p><p>&nbsp;</p><p><strong>build.gradle 설정 방법</strong></p><pre class="brush:as3;">//querydsl 추가 buildscript { ext { queryDslVersion = &quot;5.0.0&quot; } } plugins { id &#39;java&#39; id &#39;org.springframework.boot&#39; version &#39;2.7.9&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.0.15.RELEASE&#39; //querydsl 추가 id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot; } group = &#39;study&#39; version = &#39;0.0.1-SNAPSHOT&#39; sourceCompatibility = &#39;17&#39; configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8&#39; //querydsl 추가 implementation &quot;com.querydsl:querydsl-jpa:${queryDslVersion}&quot; implementation &quot;com.querydsl:querydsl-apt:${queryDslVersion}&quot; compileOnly &#39;org.projectlombok:lombok&#39; developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39; runtimeOnly &#39;com.h2database:h2&#39; runtimeOnly &#39;com.mysql:mysql-connector-j&#39; annotationProcessor &#39;org.projectlombok:lombok&#39; testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39; } tasks.named(&#39;test&#39;) { useJUnitPlatform() } //querydsl 추가 시작 def querydslDir = &quot;$buildDir/generated/querydsl&quot; querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } //querydsl 추가 끝 </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>querydsl-jpa , querydsl-apt 를 추가하고 버전을 명시해야 한다.</strong></span></p><p><br /><strong><span style="font-size:20px">참고로 위 설정은 JUnit은 4를 사용한다.</span></strong></p><p><br /><strong><span style="font-size:20px">JUnit 5를 사용하려면 다음 설정을 제거하면 된다.</span></strong></p><p><br /><span style="color:#c0392b"><strong>exclude group: &#39;org.junit.vintage&#39;, module: &#39;junit-vintage-engine&#39;</strong></span><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:22px"><strong>PageableExecutionUtils Deprecated(<span style="color:#c0392b">향후 미지원</span>) 패키지 변경</strong></span></p><p>&nbsp;</p><p><strong><span style="color:#2980b9"><span style="font-size:16px">PageableExecutionUtils 클래스 사용 패키지 변경</span></span></strong></p><p><strong>기능이 Deprecated 된 것은 아니고, 사용 패키지 위치가 변경되었습니다. 기존 위치를 신규 위치로 변경해주시면 문제 없이 사용할 수 있습니다.</strong></p><p><br /><strong>기존: org.springframework.data.repository.support.PageableExecutionUtils<br />신규: org.springframework.data.support.PageableExecutionUtils</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">★ ★ <span style="color:#16a085">Querydsl fetchResults() , fetchCount() Deprecated</span>(<span style="color:#c0392b">향후 미지원</span>)</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>Querydsl의 fetchCount() , fetchResult() 는 개발자가 작성한 select 쿼리를 기반으로 count용&nbsp; 쿼리를 내부에서 만들어서 실행합니다.</p><p><br />그런데 이 기능은 강의에서 설명드린 것 처럼 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도입니다.</p><p>&nbsp;</p><p>따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않습니다.&nbsp;</p><p>&nbsp;</p><p>Querydsl은 향후 fetchCount() , fetchResult() 를 지원하지 않기로 결정했습니다.&nbsp;</p><p>&nbsp;</p><p>참고로 Querydsl의 변화가 빠르지는 않기 때문에 당장 해당 기능을 제거하지는 않을 것입니다. 따라서 count 쿼리가 필요하면 다음과 같이 별도로 작성해야 합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">★</span>&nbsp;count 쿼리는 예제</strong></p><p>&nbsp;</p><pre class="brush:as3;">@Test public void count() { Long totalCount = queryFactory //.select(Wildcard.count) //select count(*) .select(member.count()) //select count(member.id) .from(member) .fetchOne(); System.out.println(&quot;totalCount = &quot; + totalCount); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>count(*) 을 사용하고 싶으면 예제의 주석처럼 Wildcard.count 를 사용하시면 됩니다.</strong></p><p><strong>member.count() 를 사용하면 count(member.id) 로 처리됩니다.</strong></p><p><strong>응답 결과는 숫자 하나이므로 fetchOne() 을 사용합니다.</strong></p><p><strong>MemberRepositoryImpl.searchPageComplex() 예제에서 보여드린 것 처럼 select 쿼리와는 별도로 count 쿼리를 작성하고 fetch() 를 사용해야 합니다.&nbsp;</strong></p><p>&nbsp;</p><p><strong>다음은 최신 버전에 맞추어 수정된 예제입니다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">★</span>수정된 searchPageComplex 예제<span style="color:#c0392b">★</span></strong></span></p><p>&nbsp;</p><pre class="brush:as3;">import org.springframework.data.support.PageableExecutionUtils; //패키지 변경 public Page &lt; MemberTeamDto &gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List &lt;MemberTeamDto&gt; content = queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;))) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery &lt;Long&gt; countQuery = queryFactory .select(member.count()) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-30 11:41:45 실전! Querydsl - 7. 스프링 데이터 JPA와 Querydsl,스프링 데이터 JPA 리포지토리로 변경,사용자 정의 리포지토리, Querydsl 페이징 연동,CountQuery 최적화 ,컨트롤러 개발 http://macaronics.net/index.php/m01/spring/view/2104 2104 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[7] 실무 활용 - 스프링 데이터 JPA와 Querydsl</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>34.스프링 데이터 JPA 리포지토리로 변경</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30149&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30149&amp;tab=curriculum</a><br />&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 JPA - MemberRepository 생성</strong></p><pre class="brush:as3;">package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; { List&lt;Member&gt; findByUsername(String username); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>스프링 데이터 JPA 테스트</strong></span></p><pre class="brush:as3;">package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import javax.persistence.EntityManager; import java.util.List; @SpringBootTest @Transactional class MemberRepositoryTest { @Autowired EntityManager em; @Autowired MemberRepository memberRepository; @Test public void basicTest(){ Member member=new Member(&quot;member1&quot;, 10); memberRepository.save(member); Member findMember =memberRepository.findById(member.getId()).get(); Assertions.assertThat(findMember).isEqualTo(member); List&lt;Member&gt; result1 = memberRepository.findAll(); Assertions.assertThat(result1).containsExactly(member); List&lt;Member&gt; result2=memberRepository.findByUsername(&quot;member1&quot;); Assertions.assertThat(result2).containsExactly(member); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>35.사용자 정의 리포지토리</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30149&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30149&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="655" height="357" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiC2nLyI71f3wvfKy7IvNcK410XYWMhVumrmVkew2hborVDM3c5tjCPCsDW1MAdMPJ8i0AvjidoAc7f7p_oMcLfBMECfRGrfO663bOlAYKl2Ni9eYYpKGg_a1S4l-lWQUkJYlcDgDdk3HsoMPaonGhz0RDdGXQUJ3ipiD8ngN_vQh4fgSyf1uZUH9WVw/s16000/2023-03-28%2010%2043%2009.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong><span style="font-size:20px">사용자 정의 리포지토리 사용법</span></strong></p><p>&nbsp;</p><p><strong>1. 사용자 정의 인터페이스 작성</strong></p><p><br /><strong>2. 사용자 정의 인터페이스&nbsp; impl 구현</strong></p><p><br /><strong>3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속</strong><br />&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#2980b9">4. 굳이&nbsp;사용자 정의 인터페이스 구현하지 않고 @Repository 해서 개별 구현해도 된다.</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgiC2nLyI71f3wvfKy7IvNcK410XYWMhVumrmVkew2hborVDM3c5tjCPCsDW1MAdMPJ8i0AvjidoAc7f7p_oMcLfBMECfRGrfO663bOlAYKl2Ni9eYYpKGg_a1S4l-lWQUkJYlcDgDdk3HsoMPaonGhz0RDdGXQUJ3ipiD8ngN_vQh4fgSyf1uZUH9WVw/s16000/2023-03-28%2010%2043%2009.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><span style="font-size:20px"><strong>1) 사용자 정의 인터페이스 작성</strong></span></span></p><pre class="brush:as3;">package study.querydsl.repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import java.util.List; public interface MemberRepositoryCustom { List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#16a085"><span style="font-size:20px">2. 사용자 정의 인터페이스&nbsp; Impl 구현</span></span></strong></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import javax.persistence.EntityManager; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; public class MemberRepositoryImpl implements MemberRepositoryCustom{ private final JPAQueryFactory queryFactory; public MemberRepositoryImpl(EntityManager em){ this.queryFactory=new JPAQueryFactory(em); } @Override public List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition){ return queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><span style="font-size:20px"><strong>3)&nbsp;&nbsp;스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; , MemberRepositoryCustom{ List&lt;Member&gt; findByUsername(String username); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>커스텀 리포지토리 동작 테스트 추가</strong></span></p><pre class="brush:as3;">package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional class MemberRepositoryImplTest { @Autowired EntityManager em; @Autowired MemberRepository memberRepository; @Test public void search(){ Team teamA = new Team(&quot;teamA&quot;); Team teamB=new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1=new Member(&quot;member1&quot;, 10, teamA); Member member2=new Member(&quot;member2&quot;, 20, teamA); Member member3=new Member(&quot;member3&quot;, 30, teamB); Member member4=new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName(&quot;teamB&quot;); List&lt;MemberTeamDto&gt; result = memberRepository.search(condition); Assertions.assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;member4&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#16a085"><strong>4) &nbsp;굳이&nbsp;사용자 정의 인터페이스 구현하지 않고 @Repository 해서 개별 구현해도 된다.</strong></span></span></p><pre class="brush:as3;">package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; @Repository @RequiredArgsConstructor public class MemberQueryRepository { private final JPAQueryFactory queryFactory; public List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition){ return queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } } </pre><p>&nbsp;</p><p><strong>MemberQueryRepositoryTest</strong></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import java.util.List; @SpringBootTest @Transactional class MemberQueryRepositoryTest { @Autowired EntityManager em; @Autowired MemberQueryRepository memberQueryRepository; @Test public void search(){ Team teamA = new Team(&quot;teamA&quot;); Team teamB=new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1=new Member(&quot;member1&quot;, 10, teamA); Member member2=new Member(&quot;member2&quot;, 20, teamA); Member member3=new Member(&quot;member3&quot;, 30, teamB); Member member4=new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName(&quot;teamB&quot;); List&lt;MemberTeamDto&gt; result = memberQueryRepository.search(condition); Assertions.assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;member4&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>36.스프링 데이터 페이징 활용1 - Querydsl 페이징 연동</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30151&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30151&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터의 Page, Pageable을 활용해보자.</strong></p><p><br /><strong>전체 카운트를 한번에 조회하는 단순한 방법</strong></p><p><br /><strong>데이터 내용과 전체 카운트를 별도로 조회하는 방법</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>사용자 정의 인터페이스에 페이징 2가지 추가</strong></span></p><pre class="brush:as3;">package study.querydsl.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import java.util.List; public interface MemberRepositoryCustom { List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition); Page&lt;MemberTeamDto&gt; searchPageSimple(MemberSearchCondition condition, Pageable pageable); Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><strong><span style="font-size:20px">1) 전체 카운트를 한번에 조회하는 단순한 방법</span></strong></span></p><p><br /><strong><span style="font-size:20px">searchPageSimple(), fetchResults() 사용</span></strong></p><p>&nbsp;</p><pre class="brush:as3;"> /** * 단순한 페이징, fetchResults() 사용 */ @Override public Page&lt;MemberTeamDto&gt; searchPageSimple(MemberSearchCondition condition, Pageable pageable) { QueryResults&lt;MemberTeamDto&gt; results = queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetchResults(); List&lt;MemberTeamDto&gt; content = results.getResults(); long total=results.getTotal(); return new PageImpl&lt;&gt;(content,pageable,total); }</pre><p>&nbsp;</p><p><span style="color:#c0392b"><strong>Querydsl이 제공하는 fetchResults() 를 사용하면 내용과 전체 카운트를 한번에 조회할 수 있다.(실제&nbsp; 쿼리는 2번 호출)</strong></span></p><p><br /><span style="color:#c0392b"><strong>fetchResult() 는 카운트 쿼리 실행시 필요없는 order by 는 제거한다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#27ae60"><span style="font-size:20px">2) 데이터 내용과 전체 카운트를 별도로 조회하는 방법</span></span></strong></p><p><br /><strong>searchPageComplex()</strong><br />&nbsp;</p><pre class="brush:as3;"> /** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ @Override public Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List&lt;MemberTeamDto&gt; content = queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); long total =queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetchCount(); return new PageImpl&lt;&gt;(content, pageable, total); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } </pre><p>&nbsp;</p><p><span style="color:#c0392b"><strong>전체 카운트를 조회 하는 방법을 최적화 할 수 있으면 이렇게 분리하면 된다. </strong></span></p><p><span style="color:#c0392b"><strong>(예를 들어서 전체 카운트를 조회할 때 조인 쿼리를 줄일 수 있다면 상당한 효과가 있다.)</strong></span></p><p><br /><span style="color:#c0392b"><strong>코드를 리펙토링해서 내용 쿼리과 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>테스트&nbsp;</p><pre class="brush:as3;"> @Test public void searchPageSimpleAndComplexTest(){ Team teamA = new Team(&quot;teamA&quot;); Team teamB=new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1=new Member(&quot;member1&quot;, 10, teamA); Member member2=new Member(&quot;member2&quot;, 20, teamA); Member member3=new Member(&quot;member3&quot;, 30, teamB); Member member4=new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); PageRequest pageRequest =PageRequest.of(0,3); // Page&lt;MemberTeamDto&gt; result=memberRepository.searchPageSimple(condition, pageRequest); Page&lt;MemberTeamDto&gt; result=memberRepository.searchPageComplex(condition, pageRequest); Assertions.assertThat(result.getSize()).isEqualTo(3); Assertions.assertThat(result.getContent()).extracting(&quot;username&quot;).containsExactly(&quot;member1&quot;, &quot;member2&quot;, &quot;member3&quot;); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★★★37.스프링 데이터 페이징 활용2 - CountQuery 최적화</strong></span></span></p><p><strong><span style="color:#8e44ad"><span style="font-size:20px">필수적용할것&nbsp; 페이징 최적화&nbsp;</span></span></strong></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30152&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30152&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> /** * 복잡한 페이징 * 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리 */ @Override public Page&lt;MemberTeamDto&gt; searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List&lt;MemberTeamDto&gt; content = queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery&lt;Member&gt; countQuery = queryFactory .selectFrom(member) // .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ); // .fetchCount(); //첫페이지가 페이지 사이즈 content 사이즈 보다 작을 경우 countQuery.fetchCount() 작동하지 않고 해당 content 사이즈로 처리 //마지막 페이지일경우 countQuery 실행되지 않고, offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함 /** * count 쿼리가 생략 가능한 경우 생략해서 처리 * 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 * 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함) */ // return PageableExecutionUtils.getPage(content,pageable , ()-&gt;countQuery.fetchCount() ); return PageableExecutionUtils.getPage(content,pageable , countQuery::fetchCount ); //return new PageImpl&lt;&gt;(content, pageable, total); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>스프링 데이터 라이브러리가 제공</strong></p><p><br /><strong>count 쿼리가 생략 가능한 경우 생략해서 처리</strong></p><p><br /><strong>페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때</strong></p><p><br /><strong>마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>38.스프링 데이터 페이징 활용3 - 컨트롤러 개발</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p>&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30153&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30153&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>실제 컨트롤러</strong></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.controller; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.repository.MemberJpaRepository; import study.querydsl.repository.MemberRepository; import java.util.List; @RestController @RequiredArgsConstructor public class MemberController { private final MemberJpaRepository memberJpaRepository; private final MemberRepository memberRepository; @GetMapping(&quot;/v1/members&quot;) public List&lt;MemberTeamDto&gt; searchMemberV1(MemberSearchCondition condition){ return memberJpaRepository.search(condition); } //http://localhost:8080/v2/members?page=0&amp;size=20 @GetMapping(&quot;/v2/members&quot;) public Page&lt;MemberTeamDto&gt; searchMemberV2(MemberSearchCondition condition, Pageable pageable){ return memberRepository.searchPageSimple(condition, pageable); } //http://localhost:8080/v3/members?size=5&amp;page=2 //http://localhost:8080/v3/members?page=0&amp;size=20 @GetMapping(&quot;/v3/members&quot;) public Page&lt;MemberTeamDto&gt; searchMemberV3(MemberSearchCondition condition, Pageable pageable){ return memberRepository.searchPageComplex(condition, pageable); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>스프링 데이터 JPA는 자신의 정렬(Sort)을 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는<br />기능을 제공한다. 이 부분은 뒤에 스프링 데이터 JPA가 제공하는 Querydsl 기능에서 살펴보겠다.</p><p><br />스프링 데이터의 정렬을 Querydsl의 정렬로 직접 전환하는 방법은 다음 코드를 참고하자.</p><p><br /><span style="font-size:20px"><strong>스프링 데이터 Sort를 Querydsl의 OrderSpecifier로 변환</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">JPAQuery&lt;Member&gt; query = queryFactory.selectFrom(member); for (Sort.Order o: pageable.getSort()) { PathBuilder pathBuilder = new PathBuilder(member.getType(), member.getMetadata()); query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, pathBuilder.get(o.getProperty()))); } List&lt;Member&gt; result = query.fetch();</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>&gt; 참고:</strong></span> <strong>정렬( Sort )은 조건이 조금만 복잡해져도 Pageable 의 Sort 기능을 사용하기 어렵다. 루트 엔티티</strong></span></p><p><br /><span style="color:#c0392b"><strong>범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 Sort 를 사용하기 보다는 </strong></span></p><p>&nbsp;</p><p><strong><span style="color:#c0392b">파라미터를 받아서 직접 처리하는 것을 권장한다.</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-28 10:18:40 실전! Querydsl - 6. 실무 활용 - 순수 JPA와 Querydsl- 순수 JPA 리포지토리와 Querydsl,동적 쿼리와 성능 최적화 조회 - Builder 사용,★동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용,조회 API 컨트롤러 개발 http://macaronics.net/index.php/m01/spring/view/2103 2103 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[6] 실무 활용 - 순수 JPA와 Querydsl</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>30.순수 JPA 리포지토리와 Querydsl</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30144&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30144&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>1) 순수 JPA 리포지토리와 Querydsl</strong></p><p><br /><strong>2) 동적쿼리 Builder 적용</strong></p><p><br /><strong>3) 동적쿼리 Where 적용</strong></p><p><br /><strong>4) 조회 API 컨트롤러 개발</strong><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>순수 JPA 리포지토리</strong></span></p><pre class="brush:as3;">package study.querydsl.repository; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.stereotype.Repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import study.querydsl.entity.Member; import javax.persistence.EntityManager; import java.util.List; import java.util.Optional; import static org.springframework.util.StringUtils.hasText; import static org.springframework.util.StringUtils.isEmpty; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; @Repository public class MemberJpaRepository { private final EntityManager em; private final JPAQueryFactory queryFactory; public MemberJpaRepository(EntityManager em) { this.em = em; this.queryFactory = new JPAQueryFactory(em); } public void save(Member member) { em.persist(member); } public Optional&lt;Member&gt; findById(Long id) { Member findMember = em.find(Member.class, id); return Optional.ofNullable(findMember); } public List&lt;Member&gt; findAll() { return em.createQuery(&quot;select m from Member m&quot;, Member.class) .getResultList(); } public List&lt;Member&gt; findByUsername(String username) { return em.createQuery(&quot;select m from Member m where m.username = :username&quot;, Member.class) .setParameter(&quot;username&quot;, username) .getResultList(); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>순수 JPA 리포지토리 테스트</strong></p><pre class="brush:as3;">ingframework.transaction.annotation.Transactional; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @Transactional class MemberJpaRepositoryTest { @Autowired EntityManager em; @Autowired MemberJpaRepository memberJpaRepository; @Test public void basicTest() { Member member = new Member(&quot;member1&quot;, 10); memberJpaRepository.save(member); Member findMember = memberJpaRepository.findById(member.getId()).get(); assertThat(findMember).isEqualTo(member); List &lt;Member&gt; result1 = memberJpaRepository.findAll(); assertThat(result1).containsExactly(member); List &lt;Member&gt; result2 = memberJpaRepository.findByUsername(&quot;member1&quot;); assertThat(result2).containsExactly(member); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Querydsl 사용</strong></p><pre class="brush:as3;">public List &lt;Member&gt; findAll_Querydsl() { return queryFactory .selectFrom(member).fetch(); } public List &lt;Member&gt; findByUsername_Querydsl(String username) { return queryFactory .selectFrom(member) .where(member.username.eq(username)) .fetch(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>JPAQueryFactory 스프링 빈 등록 다음과 같이 </strong></span></p><p><span style="color:#c0392b"><strong>JPAQueryFactory 를 스프링 빈으로 등록해서 주입받아 사용해도 된다</strong></span>.</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @Bean public JPAQueryFactory queryFactory(EntityManager em) { return new JPAQueryFactory(em); } </pre><p>&nbsp;</p><p>또는</p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.persistence.EntityManager; @Configuration @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory() { return new JPAQueryFactory(em); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>참고: </strong></span>동시성 문제는 걱정하지 않아도 된다. 왜냐하면 여기서 스프링이 주입해주는 엔티티 매니저는 실제</p><p><br />동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 이 가짜 엔티티 매니저는</p><p><br />실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당해준다.</p><p><br />&gt; 더 자세한 내용은 자바 ORM 표준 JPA 책 13.1 트랜잭션 범위의 영속성 컨텍스트를 참고하자.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>31.동적 쿼리와 성능 최적화 조회 - Builder 사용</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30145&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30145&amp;tab=curriculum</a></p><p>&nbsp;</p><p><br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Builder를 사용한 예제</strong></p><p>&nbsp;</p><p><strong>MemberTeamDto</strong></p><pre class="brush:as3;">package study.querydsl.dto; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; @Data public class MemberTeamDto { private Long memberId; private String username; private int age; private Long teamId; private String teamName; @QueryProjection public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) { this.memberId = memberId; this.username = username; this.age = age; this.teamId = teamId; this.teamName = teamName; } } </pre><p>&nbsp;</p><p><strong>MemberSearchCondition</strong></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.dto; import lombok.Data; @Data public class MemberSearchCondition { //회원명, 팀명, 나이(ageGoe, ageLoe) private String username; private String teamName; private Integer ageGoe; private Integer ageLoe; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#8e44ad">1) Where절에 파라미터를 사용한 예제</span></strong></span></p><pre class="brush:as3;"> public List&lt;MemberTeamDto&gt; searchByBuilder(MemberSearchCondition condition){ BooleanBuilder builder =new BooleanBuilder(); if (hasText(condition.getUsername())) { builder.and(member.username.eq(condition.getUsername())); } if (hasText(condition.getTeamName())) { builder.and(team.name.eq(condition.getTeamName())); } if(condition.getAgeGoe() !=null){ builder.and(member.age.goe(condition.getAgeGoe())); } if(condition.getAgeLoe() !=null){ builder.and(member.age.loe(condition.getAgeLoe())); } return queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where(builder) .fetch(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong><span style="color:#c0392b">오류 정정</span></strong></span></p><p><br />&gt; 강의 영상에서는 member.id.as(&quot;memberId&quot;) 라고 적었는데, QMemberTeamDto 는 생성자를 사용하기</p><p><br /><strong>때문에 필드 이름을 맞추지 않아도 된다. 따라서 member.id 만 적으면 된다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>조회 예제 테스트</strong></span></p><pre class="brush:as3;"> @Test public void searchTest(){ Team teamA = new Team(&quot;teamA&quot;); Team teamB=new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1=new Member(&quot;member1&quot;, 10, teamA); Member member2=new Member(&quot;member2&quot;, 20, teamA); Member member3=new Member(&quot;member3&quot;, 30, teamB); Member member4=new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member3); em.persist(member4); MemberSearchCondition condition=new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName(&quot;teamB&quot;); List&lt;MemberTeamDto&gt; result = memberJpaRepository.searchByBuilder(condition); Assertions.assertThat(result).extracting(&quot;username&quot;).containsExactly(&quot;member4&quot;); } </pre><p>&nbsp;</p><p><strong>출력 쿼리</strong></p><pre class="brush:as3;"> /* select member1.id as memberId, member1.username, member1.age, team.id as teamId, team.name as teamName from Member member1 left join member1.team as team where team.name = ?1 and member1.age &gt;= ?2 and member1.age &lt;= ?3 */ select member0_.member_id as col_0_0_, member0_.username as col_1_0_, member0_.age as col_2_0_, team1_.team_id as col_3_0_, team1_.name as col_4_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id where team1_.name=? and member0_.age&gt;=? and member0_.age&lt;=?</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#e67e22">백기선&nbsp; 강좌&nbsp; 반드시&nbsp; &nbsp; 참조</span></span></strong> :&nbsp; &nbsp; &nbsp;&nbsp;<a target="_blank" href="https://macaronics.net/m01/spring/view/2120"><strong>★★★</strong><strong> 38.스프링 데이터 JPA: Projection&nbsp; </strong><strong>★★★</strong></a></p><pre class="brush:as3;">package com.example.demojap3.comment; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; /** * 클래스방식 커스텀 */ @Data @AllArgsConstructor(access = AccessLevel.PROTECTED) public class CommentSummary2 { private String comment; private int up; private int down; public String getVotes(){ return getUp() + &quot; : &quot; +getDown(); } }</pre><p>&nbsp;</p><pre class="brush:as3;">package com.example.demojap3.comment; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; public interface CommentRepository extends JpaRepository&lt;Comment, Long&gt; { /** * ==&gt; 다음과 같이 제네릭으로 변경 */ &lt;T&gt;List&lt;T&gt; findByPost_Id(Long id, Class&lt;T&gt; type); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★32.동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30146&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30146&amp;tab=curriculum</a></p><p>&nbsp;</p><pre class="brush:as3;"> public List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition){ return queryFactory .select(new QMemberTeamDto( member.id.as(&quot;memberId&quot;), member.username, member.age, team.id.as(&quot;teamId&quot;), team.name.as(&quot;teamName&quot;) )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoeEq(condition.getAgeGoe()), ageLoeEq(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) :null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) :null; } private BooleanExpression ageGoeEq(Integer ageGoe) { return ageGoe!=null? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoeEq(Integer ageLoe) { return ageLoe!=null? member.age.loe(ageLoe) : null; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>참고: where 절에 파라미터 방식을 사용하면 조건 재사용 가능</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">//where 파라미터 방식은 이런식으로 재사용이 가능하다. public List &lt; Member &gt; findMember(MemberSearchCondition condition) { return queryFactory .selectFrom(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe())) .fetch(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>33.조회 API 컨트롤러 개발</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30147&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30147&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>편리한 데이터 확인을 위해 샘플 데이터를 추가하자.</strong></p><p><br /><strong>샘플 데이터 추가가 테스트 케이스 실행에 영향을 주지 않도록 다음과 같이 프로파일을 설정하자</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>src/main/resources/application.yml</strong></p><p>&nbsp;</p><pre class="brush:as3;">spring: profiles: active: local</pre><p>&nbsp;</p><pre class="brush:as3;">spring.profiles.active=local </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>테스트는 기존 application.yml을 복사해서 다음 경로로 복사하고, 프로파일을 test로 수정하자<br />src/test/resources/application.yml</p><pre class="brush:as3;">spring: profiles: active: test</pre><p>&nbsp;</p><p>&nbsp;</p><p>이렇게 분리하면 main 소스코드와 테스트 소스 코드 실행시 프로파일을 분리할 수 있다.</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>샘플 데이터 추가</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Profile(&quot;local&quot;) @Component @RequiredArgsConstructor public class InitMember { private final InitMemberService initMemberService; @PostConstruct public void init() { initMemberService.init(); } @Component static class InitMemberService { @PersistenceContext EntityManager em; @Transactional public void init() { Team teamA = new Team(&quot;teamA&quot;); Team teamB = new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); for (int i = 0; i &lt; 100; i++) { Team selectedTeam = i % 2 == 0 ? teamA : teamB; em.persist(new Member(&quot;member&quot; + i, i, selectedTeam)); } } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>조회 컨트롤러</strong></span></p><pre class="brush:as3;">package study.querydsl; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Profile(&quot;local&quot;) @Component @RequiredArgsConstructor public class InitMember { private final InitMemberService initMemberService; @PostConstruct public void init() { initMemberService.init(); } @Component static class InitMemberService { @PersistenceContext EntityManager em; @Transactional public void init() { Team teamA = new Team(&quot;teamA&quot;); Team teamB = new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); for (int i = 0; i &lt; 100; i++) { Team selectedTeam = i % 2 == 0 ? teamA : teamB; em.persist(new Member(&quot;member&quot; + i, i, selectedTeam)); } } } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>예제 실행(postman)</strong></span></p><p><br /><span style="font-size:18px"><strong>http://localhost:8080/v1/members?teamName=teamB&amp;ageGoe=31&amp;ageLoe=35</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-27 19:01:14 실전! Querydsl - 5. 중급문법 프로젝션과- 결과 반환, 프로젝션과 결과 DTO , @QueryProjection, 동적 쿼리 , BooleanBuilder 사용, 동적 쿼리 Where 다중 파라미터 사용, 수정, 삭제 벌크 연산,SQL function 호출하기 http://macaronics.net/index.php/m01/spring/view/2102 2102 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[5] 중급 문법</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>22.프로젝션과 결과 반환 - 기본</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30136&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30136&amp;tab=curriculum</a></p><p>&nbsp;</p><p>프로젝션:</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>select 대상 지정 프로젝션 대상이 하나</strong></span></p><pre class="brush:as3;"> @Test public void simpleProjection(){ List&lt;String&gt; result = queryFactory .select(member.username) .from(member) .fetch(); for (String s : result) { System.out.println(&quot;s = &quot; + s); } } </pre><p>&nbsp;</p><p>쿼리출력</p><pre class="brush:as3;">/* select member1.username from Member member1 */ select member0_.username as col_0_0_ from member member0_</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음</strong></p><p><br /><strong>프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>튜플 조회</strong></span></p><p><span style="color:#2980b9"><strong>프로젝션 대상이 둘 이상일 때 사용</strong></span></p><p>&nbsp;</p><p>com.querydsl.core.Tuple</p><pre class="brush:as3;"> @Test public void tupleProjection(){ List&lt;Tuple&gt; result = queryFactory. select(member.username, member.age) .from(member) .fetch(); for (Tuple tuple : result) { String username = tuple.get(member.username); Integer age = tuple.get(member.age); System.out.println(&quot;username = &quot; + username); System.out.println(&quot;age = &quot; + age); } }</pre><p>&nbsp;</p><p>쿼리출력</p><pre class="brush:as3;"> /* select member1.username, member1.age from Member member1 */ select member0_.username as col_0_0_, member0_.age as col_1_0_ from member member0_</pre><p>&nbsp;</p><p>=&gt;</p><pre class="brush:as3;">username = member1 age = 10 username = member2 age = 20 username = member3 age = 30 username = member4 age = 40</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★23.프로젝션과 결과 반환 - DTO 조회★</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30137&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30137&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>1) 순수 JPA에서 DTO 조회</strong></span></span></p><p>&nbsp;</p><p><strong>MemberDto</strong></p><pre class="brush:as3;">package study.querydsl.dto; import lombok.Data; @Data @NoArgsConstructor public class MemberDto { private String username; private int age; public MemberDto(String username, int age){ this.username =username; this.age=age; } } </pre><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void findDtoByJPQL(){ List&lt;MemberDto&gt; resultList = em.createQuery(&quot;select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m&quot;, MemberDto.class) .getResultList(); for (MemberDto memberDto : resultList) { System.out.println(&quot;memberDto = &quot; + memberDto); } } </pre><p>&nbsp;</p><p>쿼리 출력</p><pre class="brush:as3;">/* select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m */ select member0_.username as col_0_0_, member0_.age as col_1_0_ from member member0_</pre><p>&nbsp;</p><p><strong>순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함</strong></p><p><br /><strong>DTO의 package이름을 다 적어줘야해서 지저분함</strong></p><p><br /><strong>생성자 방식만 지원함</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#2980b9"><strong>2) Querydsl 빈 생성(Bean population)</strong></span></span></p><p>결과를 DTO 반환할 때 사용<br />다음 4가지 방법 지원</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>프로퍼티 접근</strong></span></p><p><br /><span style="color:#c0392b"><strong>필드 직접 접근</strong></span></p><p><br /><span style="color:#c0392b"><strong>생성자 사용</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#9b59b6"><span style="font-size:16px">1. 프로퍼티 접근 - Setter</span></span></strong></p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void findDtoBySetter(){ List&lt;MemberDto&gt; result = queryFactory .select(Projections.bean(MemberDto.class, member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println(&quot;memberDto = &quot; + memberDto); } }</pre><p>&nbsp;</p><pre class="brush:as3;">memberDto = MemberDto(username=member1, age=10) memberDto = MemberDto(username=member2, age=20) memberDto = MemberDto(username=member3, age=30) memberDto = MemberDto(username=member4, age=40)</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#9b59b6"><span style="font-size:16px">2. 필드 직접 접근</span></span></strong></p><pre class="brush:as3;"> @Test public void findDtoByField(){ List&lt;MemberDto&gt; result = queryFactory .select(Projections.fields(MemberDto.class, member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println(&quot;memberDto = &quot; + memberDto); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#9b59b6"><span style="font-size:16px">3. 생성자 방식</span></span></strong></p><pre class="brush:as3;"> /** * 생성자 방식 * **/ @Test public void findDtoByConstructor(){ List&lt;MemberDto&gt; result = queryFactory .select(Projections.constructor(MemberDto.class, member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println(&quot;memberDto = &quot; + memberDto); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="color:#9b59b6"><span style="font-size:16px">4. 별칭이 다를 때</span></span></strong></p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void findUserDto(){ QMember memberSub =new QMember(&quot;memberSub&quot;); List&lt;UserDto&gt; fetch = queryFactory .select(Projections.fields(UserDto.class, member.username.as(&quot;name&quot;), ExpressionUtils.as( JPAExpressions .select(memberSub.age.max()) .from(memberSub), &quot;age&quot;) ) ).from(member) .fetch(); for (UserDto userDto : fetch) { System.out.println(&quot;userDto = &quot; + userDto); } } </pre><p>&nbsp;</p><p><strong>프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때 해결 방안</strong></p><p><br /><span style="color:#c0392b"><strong>ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용</strong></span></p><p><br /><strong>username.as(&quot;memberName&quot;) : 필드에 별칭 적용</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★★★24.프로젝션과 결과 반환 - @QueryProjection&nbsp;</strong></span></span></p><p><span style="font-size:22px"><span style="color:#c0392b"><strong>(</strong></span><span style="color:#8e44ad"><strong>대부분 이 방식을 사용한다 </strong></span><span style="color:#c0392b"><strong>)</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30138&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30138&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong><span style="color:#2980b9">1) 생성자 + @QueryProjection</span></strong></span></p><pre class="brush:as3;">package study.querydsl.dto; import com.querydsl.core.annotations.QueryProjection; import lombok.AccessLevel; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class MemberDto { private String username; private int age; @QueryProjection public MemberDto(String username, int age){ this.username =username; this.age=age; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>./gradlew compileQuerydsl</strong></p><p><br /><strong>QMemberDto 생성 확인</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:16px"><strong>2) @QueryProjection 활용</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void findDtoByQueryProjection(){ List&lt;MemberDto&gt; fetch = queryFactory .select(new QMemberDto(member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : fetch) { System.out.println(&quot;memberDto = &quot; + memberDto); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 QueryDSL</strong></p><p><br /><strong>어노테이션을 유지해야 하는 점과 DTO까지 Q 파일을 생성해야 하는 단점이 있다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:18px"><strong>distinct</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">List&lt;String&gt; result = queryFactory .select(member.username).distinct() .from(member) .fetch();</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>참고: distinct는 JPQL의 distinct와 같다.</strong></p><p>&nbsp;</p><p><strong>distinct 에 대해서는 다음 페이지를 참조</strong></p><p>&nbsp;</p><p><a href="https://macaronics.net/index.php/m01/spring/view/2054#">자바 ORM 표준 JPA 프로그래밍 - 10. 객체지향 쿼리 언어2 - 중급 문법 ★ 페치 조인(Fetch Join)</a></p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2054">https://macaronics.net/index.php/m01/spring/view/2054</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★★★25.동적 쿼리 - BooleanBuilder 사용</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30139&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30139&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong><span style="font-size:20px">동적 쿼리를 해결하는 두가지 방식</span></strong></p><p><br /><span style="font-size:18px"><strong>1) BooleanBuilder</strong></span></p><p><br /><span style="font-size:18px"><strong>2 ) Where 다중 파라미터 사용</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>1) BooleanBuilder</strong></span></span></p><pre class="brush:as3;"> @Test public void dynamicQuery_BooleanBuilder(){ String usernameParam =&quot;member1&quot;; Integer ageParam =null; List&lt;Member&gt; result=searchMember1(usernameParam, ageParam); Assertions.assertThat(result.size()).isEqualTo(1); } private List&lt;Member&gt; searchMember1(String usernameCond, Integer ageCond) { /** username 이 필수값이아여야 할경우 다음과 같이 BooleanBuilder 생성자 설정 BooleanBuilder builder =new BooleanBuilder(member.username.eq(usernameCond)); */ BooleanBuilder builder =new BooleanBuilder(member.username.eq(usernameCond)); if(usernameCond!=null){ builder.and(member.username.eq(usernameCond)); } if(ageCond!=null){ builder.and(member.age.eq(ageCond)); } return queryFactory .selectFrom(member) .where(builder) .fetch(); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★★★★★</strong></span></span></p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>26.동적 쿼리 - Where 다중 파라미터 사용</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30140&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30140&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>코드가 깔끔하게 이해하기 가 쉽다.</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void dynamicQuery_WhereParam(){ String usernameParam =&quot;member1&quot;; Integer ageParam =null; List&lt;Member&gt; result=searchMember2(usernameParam, ageParam); Assertions.assertThat(result.size()).isEqualTo(1); } private List&lt;Member&gt; searchMember2(String usernameCond, Integer ageCond){ //** where 조건에서 null 이면 무시한다. return queryFactory .selectFrom(member) // .where(usernameEq(usernameCond), ageEq(ageCond)) .where(allEq(usernameCond, ageCond)) .fetch(); } private BooleanExpression usernameEq(String usernameCond) { return usernameCond!=null? member.username.eq(usernameCond) : null ; } private BooleanExpression ageEq(Integer ageCond) { return ageCond!=null ? member.age.eq(ageCond) : null; } /** * 다음과 같이 조합할 수 있다. */ private BooleanExpression allEq(String usernameCond, Integer ageCond){ return usernameEq(usernameCond).and(ageEq(ageCond)); }</pre><p>&nbsp;</p><p><span style="font-size:16px"><strong>where 조건에 null 값은 무시된다.</strong></span></p><p><br /><span style="font-size:16px"><strong>메서드를 다른 쿼리에서도 재활용 할 수 있다.</strong></span></p><p><br /><span style="font-size:16px"><strong>쿼리 자체의 가독성이 높아진다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>조합 가능</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">private BooleanExpression allEq(String usernameCond, Integer ageCond) { return usernameEq(usernameCond).and(ageEq(ageCond)); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">null 체크는 주의해서 처리해야함</span></strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>27.수정, 삭제 벌크 연산</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30141&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30141&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>쿼리 한번으로 대량 데이터 수정</strong></span></p><pre class="brush:as3;"> @Test @Commit public void bulkUpdate(){ //member1 =10 -&gt; DB member1 //member2 =20 -&gt; DB member2 //member3 =30 -&gt; DB member3 //member4 =40 -&gt; DB member4 long count = queryFactory .update(member) .set(member.username, &quot;비회원&quot;) .where(member.age.lt(28)) .execute(); em.flush(); em.clear(); //member1 =10 -&gt; DB 비회원 //member2 =20 -&gt; DB 비회원 //member3 =30 -&gt; DB member3 //member4 =40 -&gt; DB member4 List&lt;Member&gt; result = queryFactory.selectFrom(member).fetch(); for (Member member1 : result) { System.out.println(&quot;member1 = &quot; + member1); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> /** * 벌크연산 빼기 - 로 */ @Test public void bulkMinus(){ long count =queryFactory .update(member) .set(member.age, member.age.add(-1)) .execute(); } /** * 벌크 연산 곱하기 */ @Test public void bulkMultiple(){ long count =queryFactory .update(member) .set(member.age, member.age.multiply(2)) .execute(); } /** * 벌크 연산 삭제 */ @Test public void bulkDelete(){ queryFactory .delete(member) .where(member.age.gt(18)) .execute(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>주의:</strong></span></p><p><strong>JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를</strong></p><p><br /><strong>실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>29.SQL function 호출하기</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30142&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30142&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>member M으로 변경하는 replace 함수 사용</strong></span></span></p><pre class="brush:as3;"> @Test public void sqlFunction(){ List&lt;String&gt; result = queryFactory .select(Expressions.stringTemplate(&quot;function(&#39;replace&#39;, {0}, {1}, {2})&quot;, member.username, &quot;member&quot;, &quot;M&quot;)) .from(member) .fetch(); for (String s : result) { System.out.println(&quot;s = &quot; + s); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>소문자로 변경해서 비교해라</strong></span></span></p><pre class="brush:as3;"> @Test public void sqlFunction2(){ List&lt;String&gt; result = queryFactory .select(member.username) .from(member) .where(member.username.eq(Expressions.stringTemplate(&quot;function(&#39;lower&#39;, {0})&quot;, member.username))) .fetch(); for (String s : result) { System.out.println(&quot;s = &quot; + s); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><span style="color:#ffffff"><strong><span style="background-color:#c0392b">lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.</span></strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> .where(member.username.eq(member.username.lower())) </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-26 17:05:36 우리나라 통합교육의 실태와 문제점과 해결방안 http://macaronics.net/index.php/tv/report/view/2101 2101 <p>&nbsp;</p><p><span style="font-size:20px"><strong>우리나라 통합교육의 실태와 문제점과 해결방안</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px">목차</span></p><p><span style="font-size:16px">서론</span></p><p><span style="font-size:16px">1.우리나라 통합교육의 필요성과 배경</span></p><p><span style="font-size:16px">2.통합교육의 개념과 역사</span></p><p><span style="font-size:16px">3.통합교육의 목표와 기대효과</span></p><p><span style="font-size:16px">본론</span></p><p><span style="font-size:16px">4.통합교육의 현황과 성과 평가</span></p><p><span style="font-size:16px">5.통합교육의 실패 사례와 원인 분석</span></p><p><span style="font-size:16px">6.학생, 교사, 학부모 관점에서 본 통합교육의 문제점</span></p><p><span style="font-size:16px">7.교육과정과 교육환경 개선 방안</span></p><p><span style="font-size:16px">결론</span></p><p><span style="font-size:16px">8.우리나라 통합교육의 성과와 문제점</span></p><p><span style="font-size:16px">9.통합교육 개선을 위한 정책적 대응 방안</span></p><p><span style="font-size:16px">10.다양성을 존중하는 교육시스템으로 나아가는 방안</span></p><p>&nbsp;</p><p><span style="font-size:16px">참고서적</span></p><p>&nbsp;</p><p><strong><span style="font-size:20px">서론</span></strong></p><p><strong><span style="font-size:20px">1.&nbsp;&nbsp;&nbsp; 우리나라 통합교육의 필요성과 배경</span></strong></p><p><span style="font-size:16px">우리나라는 다양한 문제를 안고 있는 교육 현장에서 운영되고 있습니다. 이러한 문제 중 하나는 학생들 간의 인종, 출신지, 가족구성 등 다양한 차이점 때문에 일어나는 차별입니다. 이러한 차별로 인해 악순환 현상이 발생하게 되는데, 이는 교육과정에 참여하는 모든 학생들의 학습 효과를 떨어뜨릴 수 있습니다.</span></p><p><span style="font-size:16px">이에 대한 대안으로 우리나라에서는 통합교육이 도입되었습니다. 통합교육이란, 다양한 출신, 인종, 성별 등 다양한 인적 사항을 가진 학생들을 통합하여 교육을 제공하는 것입니다. 이를 통해 학생들이 서로 이해하고 존중할 수 있는 사고방식과 태도를 형성하게 됩니다</span></p><p><span style="font-size:16px">이러한 통합교육의 필요성은 교육의 본질과 연결되어 있습니다. 교육은 학생들의 성장과 발달을 지원하는 것이 주요 목적입니다. 그러나 기존의 교육은 학생들의 개인적인 차이점을 고려하지 않고 일괄적인 교육 방식으로 운영되어 왔습니다. 이는 학생들의 다양한 발달 단계와 요구사항에 부합하지 않을 수 있습니다.</span></p><p><span style="font-size:16px">또한, 국제화 시대에 접어들면서 국제사회와의 교류가 활발하게 이루어지고 있습니다. 이에 따라 다양한 문화적 차이점을 가진 학생들끼리의 교류와 상호 이해가 중요해지고 있습니다. 따라서 통합교육은 이러한 문화적 차이점을 이해하고 받아들이는 데에도 중요한 역할을 합니다.</span></p><p><span style="font-size:16px">또한, 통합교육은 학생들의 사회성과 커뮤니케이션 능력을 발달시키는 데에도 큰 도움이 됩니다. 학생들이 서로 다른 배경을 가진 친구들과 함께 공부하면서 자연스럽게 타인의 의견을 수용하고 배려하는 태도를 기를 수 있습니다. 이러한 태도는 학생들이 사회 생활에서도 중요한 역할을 합니다.</span></p><p><span style="font-size:16px">또한, 통합교육의 배경에는 국제적인 동향과 국내적인 사회적 요구가 있습니다. 국제적으로는 장애인 차별금지와 인권보호를 위한 국제조약이 제정되었으며, 이를 수용하여 다양성과 평등성을 존중하는 사회가 필요해졌습니다. 또한, 다양한 문제를 가진 학생들을 통합하는 방안으로 통합교육이 제시되었습니다. 국내적으로는 교육기회의 평등과 차별금지 등을 위한 법률이 제정되면서, 학교에서는 학생들의 다양성을 존중하고 포용하는 교육이 필요하다는 인식이 높아졌습니다.</span></p><p><span style="font-size:16px">그러나, 통합교육을 실현하는 데에는 여전히 많은 문제가 존재합니다. 특히, 교육자들의 인식 부족과 교육환경 등이 큰 문제로 대두되고 있습니다. 또한, 특수학교와 통합교육의 연계도 중요한 문제 중 하나입니다. 이에 대한 해결책으로는 교육자들의 교육과 역량 강화, 학교 및 교육기관의 통합교육 환경 개선, 지역사회와의 협력 등이 필요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">결론적으로, 우리나라 통합교육은 다양성과 평등성을 존중하는 사회 구성원으로서의 인식 변화와 국제적인 동향, 국내적인 사회적 요구에 따라 필요성이 대두되었습니다. 하지만, 이를 실현하기 위해서는 교육자들의 역량강화와 교육환경 개선 등 다양한 문제를 해결해 나가야 합니다. 향후에는 지속적인 노력과 협력으로 통합교육의 질적인 향상을 이루어 나가야 할 것입니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">2.통합교육의 개념과 역사</span></strong></p><p><span style="font-size:16px">통합교육은 장애를 가진 학생들과 일반 학생들을 함께 교육하는 방식을 의미한다. 이러한 교육 방식은 장애를 가진 학생들에게 사회통합의 기회를 제공하고, 일반 학생들에게는 다양성과 차이에 대한 이해와 존중을 촉진하며, 더 나아가 모두가 함께 자연스럽게 공존하는 사회를 만들어 가는 것을 목표로 한다.</span></p><p><span style="font-size:16px">통합교육의 역사는 근대 교육의 발전과 함께 시작되었다. 18세기에는 장애를 가진 아이들을 별도의 시설에 모아서 교육하는 방식이 일반적이었다. 그러나 19세기에 들어서면서 장애를 가진 아이들도 일반 아이들과 함께 교육을 받을 수 있도록 시도하는 흐름이 나타나기 시작했다. 이러한 노력은 20세기에 들어서면서 특수 교육이라는 개념으로 정착되었으며, 이는 1954년 미국 법률인 &#39;브라운 vs. 교육위원회&#39; 판결을 기점으로 대대적인 변화를 이끌어냈다. 이 판결은 인종 차별을 금지하는 것을 선언하였으며, 이로 인해 흑인 아이들도 백인 아이들과 함께 교육을 받을 수 있게 되었다. 이후 1975년 미국 교육개정법 IDEA를 통해 장애 아이들도 일반 아이들과 함께 교육을 받을 수 있는 통합교육의 개념이 제시되었다.</span></p><p><span style="font-size:16px">한국에서는 1987년 6월 국가인권위원회 보도자료에서 &quot;장애인도 국민이다&quot;는 문구를 발표하면서 장애인의 권리를 인식하게 되었으며, 이후 2001년에는 국가 인권위원회와 교육부, 보건복지부 등이 함께 &quot;장애인통합교육추진계획&quot;을 발표하였다. 이를 계기로 통합교육이 대두되면서, 현재는 다양한 교육기관에서 통합교육이 이루어지고 있다.</span></p><p><span style="font-size:16px">19세기 말부터 20세기 초까지 많은 나라에서는 특수 교육 기관을 만들어서 장애 아동들을 격리시켜 교육을 시켰습니다. 그러나 이런 격리 교육은 기존 교육과 분리되어 있어서 일반 학생들과 장애 아동들 간의 교류가 거의 없었고, 장애 아동들의 사회성과 자립성을 키울 수 없었습니다. 이에 따라 20세기 중반부터는 일부 나라에서 통합교육을 시행하기 시작했습니다. 1961년에는 미국에서 장애 아동 교육법이 제정되어 장애 아동들도 일반 학생들과 함께 교육을 받을 수 있게 되었고, 이후 다른 나라들에서도 이러한 움직임이 이어졌습니다. 1970년대부터는 유럽을 중심으로 통합교육을 확대하면서 이론적인 발전과 함께 실제 교육 현장에서도 다양한 실험이 이루어졌습니다.</span></p><p><span style="font-size:16px">통합교육의 개념은 다양하지만, 일반적으로는 장애 아동뿐 아니라 다양한 문제를 가진 학생들도 일반 교육과정에 참여하며, 교육 기회와 교육환경에서의 평등성과 다양성을 추구하는 교육 방식을 의미합니다. 이를 위해서는 일반 교육 현장에서 다양한 지원체계와 전문가들의 지원이 필요합니다.</span></p><p><span style="font-size:16px">통합교육의 역사와 개념을 살펴보면, 우리나라에서도 1980년대부터 특수 교육 대신 통합교육을 추진하였으며, 2007년에는 특수교육법이 개정되어 모든 장애 아동들에게 교육이 보장되도록 되었습니다. 그러나 아직까지도 현실적인 문제와 한계점이 존재하고 있어서, 보다 발전된 통합교육 체제를 구축하기 위한 노력이 필요합니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">3.통합교육의 목표와 기대효과</span></strong></p><p><span style="font-size:16px">통합교육의 목표와 기대효과는 다양하게 제시되고 있으나, 대체로 장애학생을 비롯한 모든 학생들이 함께 배우고 성장할 수 있는 포용적인 교육환경을 조성하는 것이라고 할 수 있다. 이를 위해서는 일반학생과 장애학생 간의 이해와 존중이 필요하며, 학교 내에서는 장애학생들의 교육적 필요에 맞는 교육자원을 제공하고, 학생들의 다양한 차이를 인정하고 지원하는 시스템을 구축해야 한다.</span></p><p><span style="font-size:16px">통합교육이 실현되면 여러 가지 기대효과를 가져올 수 있다. 먼저, 일반학생들은 장애학생과 함께 배우며 서로의 차이를 인식하고 존중하는 태도를 배울 수 있다. 이는 불특정 다수와 상호작용하는 사회생활에서 중요한 역할을 하게 된다. 또한, 장애학생들은 다양한 교육기회를 제공받으며, 자신감과 자립심을 키울 수 있다. 특히, 일반학생과 함께하는 교육이 가능해지면, 장애학생들의 사회통합도 증진될 것으로 기대된다.</span></p><p><span style="font-size:16px">또한, 통합교육은 사회적 포용성 증진과 다문화 교육의 필요성에서도 중요한 역할을 한다. 다문화 교육에서는 다양한 문화와 언어, 국적, 종교를 가진 학생들이 함께 배우고, 이해하며, 존중하는 문화가 필요하다. 이를 위해서는 통합교육의 사고방식과 방법론이 적용될 수 있다.</span></p><p><span style="font-size:16px">하지만, 통합교육이 모든 학생들에게 이로운 것인가에 대한 논란도 있다. 일부 학부모와 교사들은 통합교육이 학생들의 학습능력 저하와 행동문제 등을 야기할 수 있다고 우려하고 있다.</span></p><p><span style="font-size:16px">또한, 통합교육은 차별화된 교육을 받는 학생들이 교실 안에서 서로 다양한 경험을 나누며 상호작용하는 과정에서 서로의 문화를 이해하고 존중할 수 있는 태도를 기를 수 있는 기회를 제공합니다. 이렇게 상호작용과 커뮤니케이션을 통해 학생들이 타인의 차이를 이해하고 존중하는 태도를 기를 수 있다면, 그들은 다양성과 상호존중을 바탕으로 한 인격적 성장과 사회적 능력 향상을 이룰 수 있습니다. 이러한 능력은 현대 사회에서 요구되는 인재상이며, 이러한 인재상을 기를 수 있는 교육시스템 구축이 국가적으로 중요한 사안이 될 것입니다.</span></p><p><span style="font-size:16px">또한, 통합교육은 학생들의 학습 성취도를 높일 수 있는 효과를 가지고 있습니다. 통합교육은 차별화된 교육과 달리 학생들이 다양한 배경과 수준을 가진 친구들과 함께 학습하게 됨으로써, 학생들이 서로 도우며 협력적으로 학습할 수 있는 환경을 제공합니다. 또한, 학생들이 서로 경쟁하는 것이 아니라 서로 도우며 성장한다는 믿음을 바탕으로 학습에 참여함으로써, 자신감과 동기부여를 갖추게 됩니다. 이는 학생들의 학습태도와 성취도를 향상시키는 효과를 가지며, 그 결과로 학생들의 미래 발전 가능성을 높일 수 있습니다.</span></p><p><span style="font-size:16px">따라서, 통합교육은 다양한 배경과 수준을 가진 학생들이 상호작용하며 함께 성장할 수 있는 교육적인 환경을 제공함으로써, 차별화된 교육과는 달리 다양성을 인식하고 존중하는 태도를 기를 수 있으며, 미래 발전 가능성을 높일 수 있는 교육적 가치가 있습니다. 이러한 목표와 기대효과를 가진 통합교육은 국가 교육정책의 중요한 과제 중 하나로, 지속적인 노력과 투자가 필요한 과제임을 인식해야 합니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">4.통합교육의 현황과 성과 평가</span></strong></p><p><span style="font-size:16px">통합교육은 다양한 성향, 장애, 문제를 가진 학생들에게 동등한 교육 기회를 제공하기 위한 교육 방식이다. 우리나라에서는 2007년 특수교육법의 개정을 통해 통합교육이 정식으로 시행되기 시작하였다. 그러나, 통합교육이 시행된 이후에도 아직까지도 다양한 문제점들이 존재한다.</span></p><p><span style="font-size:16px">통합교육의 현황을 살펴보면, 국내에는 교육 공동체의 인식이 미흡하다는 문제점이 있다. 많은 교사들은 통합교육에 대한 이해가 부족하기 때문에, 특수교육에 대한 전문성과 경험이 부족한 경우가 많다. 또한, 일부 교사들은 통합교육이 학생들의 학습 성취도에 부정적인 영향을 미친다는 인식을 가지고 있어서, 통합교육에 대한 거부감을 가질 수 있다.</span></p><p><span style="font-size:16px">또 다른 문제점은, 통합교육에 대한 교육환경이 부족하다는 것이다. 통합교육을 위해서는 특수교육에 필요한 인적, 물적 자원이 필요하지만, 현재 국내의 학교들은 이러한 자원이 충분하지 않다. 이러한 이유로, 특수교육이 필요한 학생들은 통합교육에서 완전한 교육을 받지 못할 가능성이 있다.</span></p><p><span style="font-size:16px">하지만, 통합교육이 국내 교육체제에서 일정한 시간이 지나면서 성과가 발생하고 있다는 평가도 존재한다. 통합교육을 실시하는 학교들은 학생들의 인권 존중과 다양성 존중, 성공적인 협력과 의사소통을 위한 능력, 자기주도적 학습 능력 등의 발전을 촉진할 수 있는 교육환경을 제공할 수 있다.</span></p><p><span style="font-size:16px">그러나 통합교육 시행에는 아직도 여러 가지 문제점이 존재한다. 예를 들어, 학교 내부에서는 교사들이 다양한 학생들에 대한 대처 방법을 배우는 교육을 받지 못하는 경우가 많다. 또한, 통합교육을 실시하는 학교들에서는 학생들의 다양성에 대한 이해 부족으로 인한 혐오 발언, 차별 등의 문제가 발생하기도 한다. 이러한 문제들은 교사들의 교육과 학교 내부 문화 개선, 학생들의 다양성에 대한 이해 증진 등을 통해 해결될 수 있다.</span></p><p><span style="font-size:16px">또한, 통합교육의 성과 평가에 대한 문제도 있다. 현재까지 대부분의 연구는 통합교육에 참여한 학생들의 학업성취도와 사회성 등의 측면에서 긍정적인 결과를 보고하고 있지만, 이러한 평가는 대체로 짧은 기간 동안 수행된 것이며, 효과가 지속적으로 나타나는지에 대한 평가는 아직 미흡하다. 또한, 학교마다 통합교육을 실시하는 방법이 다르기 때문에 성과 평가도 학교마다 다르게 이루어져 문제점이 발생할 수 있다.</span></p><p><span style="font-size:16px">이러한 문제들은 통합교육의 성공적인 시행을 위한 노력이 필요하다는 것을 보여준다. 이를 위해서는 교사들과 학교 임원들의 다양성에 대한 교육과 인식 개선, 교육 방법 및 교재 개발 등이 필요하다. 또한, 통합교육을 실시하는 학교들 간의 정보 공유 및 협력도 필요하다. 이러한 노력들을 통해 통합교육의 효과와 성과를 극대화하고, 더 많은 학생들이 통합교육을 받을 수 있도록 지속적으로 노력해야 할 것이다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">5.통합교육의 실패 사례와 원인 분석</span></strong></p><p><span style="font-size:16px">통합교육은 다양한 학생들이 함께 배우는 것을 추구하는 교육 체계이다. 그러나 통합교육이 실패한 사례들도 존재한다. 통합교육에서의 실패는 교육체계나 학교 자체의 문제뿐만 아니라 학생, 교사, 학부모 등의 인적 요소에서도 발생할 수 있다.</span></p><p><span style="font-size:16px">첫째, 교육체계적인 문제가 통합교육의 실패를 초래할 수 있다. 예를 들어, 교육제도가 학생들의 다양성을 인정하지 않는 경우, 통합교육은 제대로 이루어질 수 없다. 특수교육 학생들에 대한 교육 자금 부족, 교육환경 개선 부족, 교사 자격증 취득 시 필요한 교육 과정 부족 등이 그 예시이다.</span></p><p><span style="font-size:16px">둘째, 학생과 교사의 인적 요소 역시 통합교육의 실패 원인 중 하나이다. 학생들의 성격이나 특성, 학업 능력 차이 등이 크게 작용할 수 있다. 또한, 교사의 특성, 능력 차이, 교육태도 등이 통합교육의 성패를 좌우할 수 있다.</span></p><p><span style="font-size:16px">셋째, 학부모들의 인식과 태도도 통합교육의 성패에 영향을 미친다. 일부 학부모들은 자녀들의 학업 성취도에 대한 걱정으로 통합교육에 대한 반대 입장을 취하기도 한다. 그러나 통합교육을 성공적으로 이끌어 나가려면 학부모들의 지지와 협조가 필수적이다.</span></p><p><span style="font-size:16px">따라서, 통합교육의 실패 원인을 분석하여 대안을 모색하는 것이 중요하다. 교육체계적으로는 다양성을 수용하고 지원하는 제도와 교육환경을 조성하고, 학생과 교사들은 상호존중, 공감 능력을 기르며 유연하게 대처하는 방법을 연구하고, 학부모들은 통합교육의 중요성과 성과를 인식하며 교육과 협력적으로 대해야 한다.</span></p><p><span style="font-size:16px">그러나 통합교육이 실패하는 경우도 있습니다. 일부 학생들은 통합교육 환경에서 제대로 된 교육을 받지 못하거나, 혼자서 따라가기 어려운 상황이 발생할 수 있습니다. 이는 특히 장애 학생들에게 부정적인 영향을 미칠 수 있습니다. 장애 학생들은 교육 환경에 대한 접근성 문제나 학습 능력의 제한 등으로 인해 통합교육에서 불리한 지위에 있을 수 있습니다.</span></p><p><span style="font-size:16px">또한, 교사들의 능력과 태도도 통합교육의 성공에 매우 중요합니다. 일부 교사들은 다양성에 대한 이해 부족이나 편견 때문에 통합교육에서 학생들을 제대로 지원하지 못할 수 있습니다. 또한, 통합교육에서는 일반 교사들 뿐만 아니라 특수교육을 전문적으로 담당하는 교사들도 필요합니다. 그러나 이러한 교사들의 부족과 교육자를 양성하는 교육체계의 미비로 인해 통합교육에서는 적절한 교육을 받지 못하는 학생들이 존재할 수 있습니다.</span></p><p><span style="font-size:16px">따라서 통합교육의 성공을 위해서는 교육 환경의 접근성을 높이고, 학생들의 다양한 학습 능력을 고려한 맞춤형 교육 방법을 도입하며, 교사들의 다양성에 대한 이해와 전문성을 강화하는 등의 노력이 필요합니다. 이를 통해 모든 학생들이 참여하고 성장할 수 있는 포용적인 교육 환경을 조성할 수 있을 것입니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>6.학생, 교사, 학부모 관점에서 본 통합교육의 문제점</strong></span></p><p><span style="font-size:16px">통합교육은 학생, 교사, 학부모 등 다양한 이해관계자들의 참여와 협력이 필수적인 교육 방법이다. 그러나, 통합교육에서는 이해관계자들의 다양한 요구와 필요에 대한 대응이 제대로 이루어지지 않아 문제가 발생할 수 있다.</span></p><p><span style="font-size:16px">먼저, 학생들의 관점에서는 통합교육에서 개별적인 교육에 비해 차별과 혐오를 경험할 가능성이 높아진다는 우려가 있다. 통합교육은 학생들이 서로 다른 특성을 가지고 있어도 함께 배우고 성장할 수 있는 환경을 제공하는 것이 목표이지만, 현실적으로는 특별한 교육이 필요한 학생들이 부족한 지원을 받아 불공평한 상황이 발생하기도 한다. 또한, 학생들은 서로 다른 배경과 경험을 가지고 있기 때문에, 교사들은 학생들의 다양한 수준과 요구에 대응하기 어렵다는 문제도 있다.</span></p><p><span style="font-size:16px">둘째, 교사들의 관점에서는 통합교육에서는 개별적인 교육에 비해 교육 프로그램을 구성하는 것이 어렵다는 문제가 있다. 교사들은 다양한 학생들의 특성과 요구에 대응하면서 효과적인 교육 방법을 찾아야 하기 때문에, 교육 프로그램을 구성하는 것이 어렵다. 또한, 특별한 교육이 필요한 학생들에 대한 지원도 제대로 이루어지지 않으면서, 교사들은 과도한 업무 부담을 느낄 수 있다.</span></p><p><span style="font-size:16px">셋째, 학부모들의 관점에서는 통합교육에서는 자녀의 교육에 대한 우려가 많이 나타난다. 학부모들은 자녀의 교육에 대한 권리와 책임을 가지고 있기 때문에, 자녀들이 부적절한 교육을 받지 않도록 주의를 기울이고 있다. 또한, 자녀들의 다양한 요구와 특성에 대한 대응이 이루어지지 않으면서, 자녀들이 부적절한 교육을 받을 가능성도 있다.</span></p><p><span style="font-size:16px">또한, 학생과 교사의 적극적인 참여와 학부모의 협력이 필수적이며, 이를 위해 학생과 교사의 교육과 지원이 필요하다. 또한, 통합교육에 대한 인식 부족 문제도 존재한다. 일부 학부모들은 통합교육이 자신의 자녀에게 불리한 영향을 미친다는 인식을 가지고 있으며, 이러한 인식을 극복하기 위해 교육적인 노력이 필요하다.</span></p><p><span style="font-size:16px">마지막으로, 학생, 교사, 학부모가 모두 참여하여 통합교육의 문제점을 해결해 나가는 것이 중요하다. 학생은 자신의 다양성을 인정하고 긍정적인 자아 개발을 위한 노력을 해야하며, 교사는 다양한 학생들의 특성과 학습 수준을 고려한 맞춤형 교육 방법을 모색해야 한다. 또한, 학부모는 자녀의 다양성을 인정하고 이를 존중하는 태도를 가져야 한다. 이를 위해 학부모와의 소통을 강화하고 상호 협력적인 관계를 유지하는 것이 중요하다.</span></p><p><span style="font-size:16px">결론적으로, 통합교육은 사회적 다양성과 평등을 추구하며 모든 학생들에게 교육 기회를 보장하는 중요한 교육 체제이다. 하지만, 다양한 문제점과 한계점이 존재하며 이를 해결하기 위해서는 학생, 교사, 학부모의 적극적인 참여와 교육적인 노력이 필요하다. 특히, 학생 중심의 맞춤형 교육 방법과 상호 협력적인 학교 문화를 구축하는 것이 통합교육의 성공에 중요한 역할을 할 것이다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>7. 교육과정과 교육환경 개선 방안</strong></span></p><p><span style="font-size:16px">교육과정과 교육환경 개선은 통합교육의 성공을 위해 매우 중요하다. 이를 위해 교육과정에 대한 개선이 필요하다. 교육과정은 통합교육의 원칙과 목표를 반영해야 하며, 모든 학생이 교육과정을 수용할 수 있도록 만들어져야 한다. 이를 위해 학생들의 다양성을 인식하고, 이를 교육과정에 반영하는 것이 필요하다.</span></p><p><span style="font-size:16px">또한, 통합교육 환경 개선을 위해 교육시설과 교육환경을 개선해야 한다. 특수교육을 위한 시설과 교사를 늘리고, 보육교실을 늘리는 등 학생들의 다양한 학습환경을 제공해야 한다. 또한, 교육자들의 교육 역량을 높여서 학생들이 다양한 상황에서 교육을 받을 수 있도록 지원해야 한다.</span></p><p><span style="font-size:16px">교육과정 개선을 위해서는 교육기관에서 특수교육 전문가를 고용하거나 특수교육 전문가들과의 협업을 강화하는 것이 필요하다. 또한, 다양한 학생들을 위한 교육자료나 교육프로그램 개발이 필요하다. 이를 위해 교육기관에서 다양한 교육자료나 교육 프로그램을 개발하고, 교사들에게 이를 활용하는 방법을 교육하는 것이 필요하다.</span></p><p><span style="font-size:16px">또한, 교육환경 개선을 위해서는 보편적 설계 원칙을 적용하여 교실 및 학교 시설물을 설계하고, 접근성을 높이는 등의 방안이 필요하다. 이를 위해 교육기관에서 다양한 보편적 설계 원칙을 적용하여 교실 및 학교 시설물을 설계하고, 접근성을 높이는 등의 방안을 시행하는 것이 필요하다.</span></p><p><span style="font-size:16px">교육과정과 교육환경 개선 방안을 고민할 때는 다양한 측면에서 접근할 필요가 있습니다. 먼저, 교육과정에서는 특수교육 분야에 대한 교육과정 개편이 필요합니다. 통합교육에서는 일반교육과정에 특수교육을 포함시켜 운영하고 있지만, 이는 양질의 특수교육을 제공하지 못할 수 있습니다. 따라서, 특수교육 분야에서 필요한 교육과정을 별도로 마련해야 합니다. 이를 위해서는 특수교육 분야의 전문가와 협력하여 특수교육 분야의 교육과정을 개편하는 것이 필요합니다.</span></p><p><span style="font-size:16px">또한, 교육환경에서는 교사 교육 및 지원을 강화하는 것이 필요합니다. 교사들이 통합교육을 성공적으로 운영하기 위해서는 다양한 교육 전문성과 기술이 요구됩니다. 따라서, 교육기관에서는 교사들에 대한 교육을 강화하고, 통합교육을 운영하면서 발생할 수 있는 문제들에 대한 교사 지원을 확대해야 합니다.</span></p><p><span style="font-size:16px">또한, 통합교육에서는 교실 내에서 다양한 학생들이 함께 공부하므로, 교육환경도 이에 맞게 조성되어야 합니다. 예를 들어, 장애인을 위한 시설물과 교육용 장비, 보조교사, 번역기 등을 제공하는 것이 필요합니다. 또한, 다양한 학생들이 함께 공부할 수 있는 적절한 교육환경을 조성하기 위해서는 교육시설의 재원 확보와 교육환경 조성에 대한 지속적인 노력이 필요합니다.</span></p><p><span style="font-size:16px">마지막으로, 통합교육은 다양한 교육전문가들의 협업이 필요합니다. 특수교육 전문가, 보건의료 전문가, 상담 전문가 등 다양한 전문가들이 협업하여 학생들의 교육, 상담 및 치료를 지원해야 합니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">결론</span></strong></p><p><strong><span style="font-size:20px">8. 우리나라 통합교육의 성과와 문제점</span></strong></p><p><span style="font-size:16px">우리나라 통합교육은 장애를 가진 학생들과 일반 학생들을 함께 교육하는 시스템으로, 국가 교육정책의 중요한 일환으로 자리 잡았다. 이러한 통합교육 시스템은 대체로 긍정적인 성과를 보이고 있으며, 일반 학생들은 인간성과 공감 능력, 차이점에 대한 인식력, 협동심 등의 측면에서 많은 성장을 이루고 있다. 특히, 장애 학생들도 사회적 자립능력 및 적응능력 향상, 교육 기회 확대 등의 긍정적인 효과가 나타나고 있다.</span></p><p><span style="font-size:16px">그러나, 통합교육 시스템도 여전히 문제점이 존재한다. 첫째, 장애를 가진 학생들의 교육 환경이 부적절하여 교육 목표를 달성하기 어려운 경우가 많다. 둘째, 일반 학생들의 수업 진행이 장애 학생들에 의해 방해되는 경우가 있어 학습 효과가 떨어지는 경우가 발생한다. 셋째, 교사들은 특별한 교육 지식과 능력이 필요하며, 이러한 인력 부족으로 인해 교육 품질 저하 문제가 발생할 수 있다.</span></p><p><span style="font-size:16px">이러한 문제점들은 교육과정과 교육환경 개선을 통해 극복할 수 있다. 첫째, 교육과정 개선을 통해 교사들의 교육 지식과 능력을 강화하고, 장애 학생들의 교육 환경을 개선하여 학생들의 교육 목표를 달성할 수 있도록 한다. 둘째, 교육환경 개선을 통해 일반 학생들과 장애 학생들이 함께 수업을 이수할 수 있는 환경을 조성하고, 일반 학생들이 장애 학생들과 친구가 될 수 있는 기회를 제공한다. 셋째, 교육자금의 지원을 강화하여 교사와 학생들에게 필요한 교육 장비 및 교재를 제공하여 교육 품질을 향상시킨다.</span></p><p><span style="font-size:16px">하지만, 통합교육 시행 후에도 몇몇 문제점들은 여전히 존재한다. 첫째, 특수학교 폐지로 인한 특수교사 인력 부족 문제가 있다. 대부분의 일반학교에서는 특수교사의 인력이 부족하여 특수학생들에게 적절한 교육을 제공할 수 없는 경우가 종종 발생한다. 이는 특수교육대상학생 중 일부가 별도의 특수학교나 교실에서 교육받아야 할 필요성을 보여주는 문제점이기도 하다.</span></p><p><span style="font-size:16px">둘째, 학생들의 차이에 대한 대처 능력 부족 문제가 있다. 일반학교에서는 학생들의 차이를 이해하고 적절하게 대처하기 위한 노력이 필요하다. 그러나 현재 많은 교사들은 이러한 노력이 부족한 상황이다. 이는 통합교육에 있어서 가장 중요한 문제점 중 하나이다.</span></p><p><span style="font-size:16px">마지막으로, 통합교육에서는 특수교육대상학생들의 인권과 권리에 대한 이해와 존중이 필요하다. 하지만, 실제 현장에서는 이러한 인권과 권리에 대한 이해와 존중이 부족한 경우가 종종 있다. 이는 교사들의 인식 부족이나 학생들의 차이에 대한 인식 부족 등으로 인한 문제점이다.</span></p><p><span style="font-size:16px">따라서, 통합교육의 성과를 높이고 문제점을 해결하기 위해서는 특수교사 인력 확보와 학생들의 차이에 대한 대처 능력 향상, 그리고 특수교육대상학생들의 인권과 권리에 대한 이해와 존중 등이 필요하다. 이러한 노력들이 통합교육의 발전과 성과를 높이는데 큰 역할을 할 것이다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">9.통합교육 개선을 위한 정책적 대응 방안</span></strong></p><p><span style="font-size:16px">통합교육을 보다 효과적으로 발전시키기 위해서는 정책적인 대응이 필요하다. 먼저, 교사 교육과정 개선이 필요하다. 교사들은 통합교육의 중요성과 이론을 숙지하고, 학생들에게 다양한 학습 전략을 적용할 수 있도록 교육을 받아야 한다. 또한, 학생들의 교육과정을 개선하기 위해서는 학교 내 다양한 교육 프로그램을 도입하고, 학생들의 특성과 능력에 맞는 맞춤형 교육 방식을 제공해야 한다.</span></p><p><span style="font-size:16px">또한, 학생들의 특수교육 및 장애인 교육에 대한 지원을 강화해야 한다. 특수교육 및 장애인 교육에 필요한 시설과 장비, 인력 등을 충분히 지원함으로써, 통합교육의 질을 높일 수 있다. 이를 위해서는 예산 증액과 재원 모색 방안을 마련해야 한다.</span></p><p><span style="font-size:16px">또한, 학생들이 교육을 받는 환경을 개선할 필요가 있다. 학생들이 안전하고 편안하게 교육을 받을 수 있는 시설과 환경을 제공함으로써, 학생들의 교육 열정을 높이고 교육 성과를 개선할 수 있다. 이를 위해 학교 시설 개선과 재정지원, 안전시설 강화 등의 방안이 필요하다.</span></p><p><span style="font-size:16px">마지막으로, 교육 관련 기관들 간의 협력이 필요하다. 교육부, 지자체, 학교, 학부모 등 모든 이해관계자들이 소통하고 협력하여 통합교육의 질을 높이는 데 노력해야 한다. 이를 위해 교육 관련 기관들 간의 역할과 책임 분담을 명확히 하고, 협력체제를 구축하는 등의 노력이 필요하다.</span></p><p><span style="font-size:16px">또한, 통합교육 개선을 위해 정책적 대응이 필요하다. 먼저, 교육과정 개편이 필요하다. 다양성과 개인 차이에 대한 이해를 바탕으로 한 교육과정이 개발되어야 하며, 이에 따른 교재와 교육자 양성이 이루어져야 한다. 또한, 교육환경 개선을 위한 물리적 인프라와 장비 등이 제공되어야 하며, 특수교육 선생님의 적극적인 지원과 교사들의 통합교육에 대한 이해와 노력이 필요하다.</span></p><p><span style="font-size:16px">정부 차원에서는 특수교육에 대한 예산을 늘리고, 이를 통합교육의 인프라 개선에 활용하는 것이 중요하다. 또한, 통합교육에 대한 정책적인 역량을 강화하고, 다양한 협력체계를 구축하여 지속적인 발전이 이루어질 수 있도록 해야 한다. 또한, 학부모와 학생들에게 통합교육의 가치와 중요성에 대한 인식 제고와 함께, 특수교육에 대한 부정적인 인식 극복이 필요하다.</span></p><p><span style="font-size:16px">마지막으로, 통합교육의 성공을 위해서는 국민 전체적으로 적극적인 참여와 지원이 필요하다. 특수교육이 필요한 학생들이 통합교육에서 정상적인 교육을 받을 수 있도록 지속적인 관심과 노력이 필요하며, 이를 위한 다양한 지원 시스템과 참여 방안이 마련되어야 한다.</span></p><p><span style="font-size:16px">통합교육은 우리 사회에서 다양성과 평등을 추구하는데 있어 중요한 역할을 하고 있다. 하지만, 아직도 많은 문제점과 과제가 남아있으며, 이를 해결하기 위해서는 국가와 교육계의 적극적인 대응과 노력이 필요하다. 따라서, 앞으로도 지속적인 관심과 노력이 필요하며, 통합교육이 보다 나은 방향으로 발전할 수 있도록 지속적인 노력이 이루어져야 할 것이다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>10. 다양성을 존중하는 교육시스템으로 나아가는 방안</strong></span></p><p><span style="font-size:16px">다양성을 존중하는 교육시스템을 구현하기 위해서는 다음과 같은 방안이 필요합니다.</span></p><p><span style="font-size:16px">첫째, 학교 내부에서의 다양성 존중을 강화해야 합니다. 학교 내부에서는 학생들의 인종, 성별, 종교, 문화 등의 차이를 인정하고 이를 존중하는 교육환경을 조성해야 합니다. 이를 위해 학교 내부에서 교사들에 대한 교육이 필요하며, 교육과정에서 다양성 존중과 관련된 내용을 반영해야 합니다.</span></p><p><span style="font-size:16px">둘째, 학생들의 다양성을 존중하는 교육과정이 필요합니다. 교육과정에서는 다양한 인종, 문화, 역사, 종교 등에 대한 내용을 반영해 학생들이 다양성을 이해하고 존중할 수 있도록 해야 합니다. 또한, 학생들이 자신의 능력과 특성에 맞춰 교육을 받을 수 있는 다양한 교육과정이 필요합니다.</span></p><p><span style="font-size:16px">셋째, 학교 외부와의 연계를 강화해야 합니다. 학교는 학생들의 인생을 총체적으로 책임지는 곳이 아닙니다. 따라서 학교 외부와의 연계를 강화하여 학생들이 다양한 경험과 활동을 할 수 있도록 지원해야 합니다. 이를 위해서는 지역사회와의 협력체제를 구축하고, 학부모와의 소통을 강화해야 합니다.</span></p><p><span style="font-size:16px">넷째, 교사들의 다양성 존중 교육을 강화해야 합니다. 교사들은 학생들의 역할모델이며, 학생들이 다양성을 존중하고 이해할 수 있도록 가이드 역할을 수행해야 합니다. 따라서 교사들에 대한 교육이 필요하며, 이를 통해 다양성 존중 교육 능력을 강화해야 합니다.</span></p><p><span style="font-size:16px">마지막으로, 교육체제와 교육시스템 전반에 대한 변화가 필요합니다. 이를 위해서는 교육정책의 방향성을 다시 세워야 하며, 다양성 존중을 중심으로 교육시스템을 재구성해야 합니다.</span></p><p><span style="font-size:16px">또한, 다양성을 존중하는 교육시스템을 구현하기 위해서는 교육과정, 교육자, 교육환경 등 다양한 측면에서 대응해야 한다. 우선, 교육과정에서는 다양성을 인식하고 존중하는 교육내용이 반영되어야 한다. 이를 위해 교육과정의 다양성을 보장하고 평등하게 교육이 이루어지도록, 학생들의 다양한 경험과 문화를 수용하고 포용하는 교육과정을 개발해야 한다. 또한, 교육자들은 학생들의 다양한 배경을 이해하고 이를 존중하며, 그들이 갖고 있는 다양한 능력과 잠재력을 발견하고 키울 수 있도록 지원해야 한다. 이를 위해 교사들의 교육을 강화하고, 다양성에 대한 이해와 인식을 높이는 교육이 필요하다.</span></p><p><span style="font-size:16px">또한, 교육환경에서는 다양한 학생들이 안전하게 교육을 받을 수 있는 환경을 조성해야 한다. 이를 위해 학교 내 괴롭힘, 혐오 발언, 차별 등을 근절하고, 학생들이 다양한 문화와 경험을 나눌 수 있는 공간을 마련해야 한다. 또한, 교육 기술과 시스템의 혁신을 통해 학생들의 다양한 학습 방법에 대응할 수 있는 교육환경을 조성해야 한다.</span></p><p><span style="font-size:16px">마지막으로, 정책적 대응이 필요하다. 다양성을 존중하고 포용하는 교육시스템을 구현하기 위해서는 이를 지원하는 국가 및 지자체의 정책적 대응이 필요하다. 이를 위해 교육과정과 교육환경의 개선을 위한 예산 확보와 지원, 교육자들의 다양성 인식을 강화하기 위한 교육과정 개편 및 교육자 자격 증명제도의 도입 등의 정책적 대응이 필요하다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">참고서적</span></strong></p><p>&nbsp;</p><p><span style="font-size:16px">&quot;우리 아이 통합 교육 지도서&quot; (이영미, 최은영 지음, 이레미디어)</span></p><p><span style="font-size:16px">&quot;쉬운 통합 교육 이야기&quot; (이현진, 이유경 지음, 어크로스)</span></p><p><span style="font-size:16px">&quot;통합교육의 이해와 실천&quot; (김혜연, 김현진, 이한결 지음, 법문사)</span></p><p><span style="font-size:16px">&quot;통합교육 현장의 이해와 적용&quot; (홍혜정, 박재은, 최석우 지음, 학지사)</span></p><p><span style="font-size:16px">&quot;통합교육 실천지침서&quot; (한국교원단체총연합회 지음, 교육과학기술부 출판)</span></p> 2023-03-26 14:49:17 우리나라의 만연된 빈곤은 두 빈곤 중 어떤 빈곤인지 다수파보고서와 소수파 보고서 http://macaronics.net/index.php/tv/report/view/2100 2100 <p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">서론</span></strong></p><p><span style="font-size:18px">에서는 우리나라의 빈곤 문제에 대해 간략히 설명하고, 이에 대한 다수파와 소수파의 시각을 소개합니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>본론</strong></span>에서는</p><p>다수파와 소수파 보고서의 내용을 비교하고 분석합니다. 다수파 보고서에서는 대다수의 가난한 사람들의 실상을 다루는 반면,</p><p>소수파 보고서에서는 극심한 빈곤에 시달리는 소수의 취약 계층을 중심으로 다룹니다. 이러한 시각 차이가 빈곤 문제 해결에 어떤 영향을 미칠 수 있는지 등을 다룹니다.</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>결론</strong></span>에서는</p><p>두 보고서의 시각을 종합하여 우리나라의 빈곤 문제에 대한 전반적인 시각을 제시하고, 이를 바탕으로 정책적인 개선 방안을 제안합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px">다수파보고서는 우리나라의 만연된 빈곤을 경제적 측면에서 접근합니다. </span></p><p><span style="font-size:16px">이 보고서는 우리나라의 GDP가 지속적으로 증가하면서도 빈곤 문제가 여전히 해결되지 않는 이유를 분석합니다. </span></p><p><span style="font-size:16px">이 보고서는 우리나라의 빈곤 문제를 &#39;분배적 빈곤&#39;으로 분류합니다. 즉, 소득과 부의 분배가 불균형하게 이루어져서 일부 인구층이 빈곤한 상태에 처해 있는 것입니다. 이 보고서는 또한 빈곤 문제의 해결책으로 소득재분배와 노동시장 개선을 제안합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">반면에, 소수파보고서는 우리나라의 빈곤 문제를 사회적 측면에서 접근합니다. 이 보고서는 &#39;사회적 빈곤&#39;이라는 개념을 도입하여 우리나라의 빈곤 문제를 분석합니다. 사회적 빈곤은 개인이나 집단이 사회적으로 인정받지 못하고 차별받거나 제외되는 상태를 말합니다. 이 보고서는 빈곤 문제를 해결하기 위해서는 정치적 참여를 촉진하고 교육, 건강 등의 사회서비스를 개선하는 것이 중요하다고 제안합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이렇게 보면 다수파와 소수파보고서는 빈곤 문제를 접근하는 방식이 다르다는 것을 알 수 있습니다. 다수파보고서는 경제적 측면에서 접근하고, 빈곤 문제를 분배적 빈곤으로 분류합니다. 반면에 소수파보고서는 사회적 측면에서 접근하고, 빈곤 문제를 사회적 빈곤으로 분류합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이러한 차이점을 바탕으로 이제 더 자세히 살펴보겠습니다. 우선 다수파보고서에서는 빈곤 문제를 해결하기 위해 소득재분배와 노동시장 개선을 제안합니다. 이는 경제적인 해결책으로서 중요합니다. 그러나 이것만으로는 빈곤 문제를 완전히 해결할 수 수는 없습니다. 이와 별개로, 다수파보고서에서는 경제적 해결책뿐만 아니라 사회적 해결책도 제안합니다. 예를 들어, 교육의 질을 높이고, 사회서비스를 개선함으로써 빈곤층의 사회적 문제를 해결하려는 노력을 제안합니다. 이는 소수파보고서에서 강조하는 사회적 측면에서 빈곤 문제를 해결하는 것과 유사한 접근 방식입니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">반면에 소수파보고서에서는 사회적 참여를 촉진하고 교육, 건강 등의 사회서비스를 개선하는 것이 중요하다고 강조합니다. 이는 경제적 해결책이나 정치적 제도개선과는 다른 사회적인 해결책입니다. 이 보고서는 사회적 빈곤을 해결하기 위해 개인이나 집단이 사회적으로 인정받을 수 있도록 하고, 차별과 제외를 해소하는 것이 필요하다고 제안합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이러한 차이점을 요약하자면, 다수파보고서는 경제적 측면에서 빈곤 문제를 분석하고, 경제적 해결책을 제안합니다. 반면에 소수파보고서는 사회적 측면에서 빈곤 문제를 분석하고, 사회적 해결책을 제안합니다. 그러나 이들은 상호 보완적인 관계에 있으며, 빈곤 문제를 해결하기 위해서는 경제적 해결책과 사회적 해결책이 모두 필요하다고 할 수 있습니다.</span></p><p><span style="font-size:16px">최종적으로, 빈곤 문제는 다양한 요인들이 복합적으로 작용하는 복잡한 문제입니다. 경제적인 측면뿐만 아니라, 사회적인 측면도 고려하여 종합적인 대책이 필요합니다. 다수파보고서와 소수파보고서는 서로 다른 접근 방식을 취하고 있지만, 이를 종합하여 유의미한 결과를 도출할 수 있다면, 우리나라의 빈곤 문제를 해결하는 데 큰 도움이 될 것입니다.</span></p><p><br /><span style="font-size:16px">또한, 이 두 보고서에서는 빈곤 문제의 원인으로 사회적 구조적 요인과 개인적 요인을 모두 고려합니다. 다수파보고서에서는 대외적인 경제적 변화나 산업화로 인한 구조적 문제가 빈곤 문제를 악화시키는 요인으로 언급됩니다. 반면에 소수파보고서에서는 국가나 사회체제에서 제공하는 기회나 리소스가 불균형적으로 분배되어 있는 것이 빈곤 문제의 원인이라는 주장을 제시합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이러한 분석을 바탕으로, 다수파보고서와 소수파보고서는 각각의 방식으로 빈곤 문제를 해결하기 위한 정책적 대안을 제시합니다. 다수파보고서에서는 산업화를 촉진하여 경제적 발전을 도모하고, 사회복지 정책을 강화하는 방안을 제시합니다. 반면에 소수파보고서에서는 국가적 차원에서의 불균형을 해소하고, 사회적 자본을 확대하고, 차별과 제외를 해소하기 위한 정책을 제시합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이러한 정책들은 다양한 분야에서 제안되어 있으며, 예를 들어, 교육, 건강, 일자리, 주거 등 다양한 분야에서 개별적인 정책이 제안되고 있습니다. 이러한 정책들은 빈곤 문제의 다양한 측면을 고려하여 제안되었으며, 다수파보고서와 소수파보고서에서 제시한 대안들도 이러한 정책들과 유사한 방향으로 나아가고 있습니다.</span></p><p><span style="font-size:16px">최종적으로, 우리나라의 만연한 빈곤 문제는 다양한 원인과 측면이 복합적으로 작용하는 문제이며, 이를 해결하기 위해서는 경제적, 사회적 해결책을 종합하여 제시해야 합니다. 이러한 대안들은 다수파보고서와 소수파보고서에서 제시된 것처럼, 경제적인 측면과 사회적인 측면에서의 해결책을 함께 고려하는 것이 중요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">빈곤 문제는 단기적인 해결책뿐만 아니라 장기적인 관점에서도 고려되어야 합니다. 예를 들어, 교육 분야에서의 대안은 단기적으로는 교육 지원 정책 등을 통해 빈곤층의 교육 기회를 높이는 것이 중요합니다. 그러나 장기적으로는 교육 수준을 높이고, 인적 자원을 키워 경제적인 발전을 이루어 나가는 것이 더욱 중요합니다. 마찬가지로 일자리 창출 등의 정책도 단기적인 문제 해결과 함께 장기적인 발전을 위한 정책으로 다양한 차원에서 고려되어야 합니다.</span></p><p><span style="font-size:16px">또한, 빈곤 문제는 단순한 경제적 문제가 아니며, 문화적, 심리적, 사회적 측면에서도 매우 복잡한 문제입니다. 따라서 이러한 다양한 측면을 고려하여 제시된 대안들은 상호보완적으로 적용되어야 합니다. 예를 들어, 교육 정책과 동시에 직업 훈련과 일자리 창출 정책이 함께 적용되어야 하며, 건강 보험 제도와 함께 건강 캠페인이 같이 진행되어야 합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">마지막으로, 대안들을 제시하는 것뿐만 아니라 이를 실행하기 위한 정책의 효과적인 시행이 필요합니다. 빈곤 문제는 해결하기 어려운 문제이기 때문에 단순히 대안을 제시하는 것만으로는 충분하지 않습니다. 이를 위해서는 정책의 실행을 위한 체계적이고 효율적인 제도적 지원이 필요하며, 이를 위해서는 국가적 차원에서의 적극적인 지원과 지속적인 관심이 필요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">종합적으로, 우리나라의 만연한 빈곤 문제는 다양한 측면에서 복잡한 문제이며, 이를 해결하기 위해서는 경제적, 사회적 해결책을 종합하여 제시하고 이를 실행하기 위한 효율적인 정책적 지원이 필요합니다.&nbsp;</span></p><p>&nbsp;</p><p><br /><span style="font-size:16px">빈곤층 자체의 목소리를 청취하는 것은 매우 중요합니다. 빈곤층은 현재 많은 어려움과 불평등을 겪고 있으며, 이를 해결하기 위해서는 그들이 직접 자신의 문제를 제기하고 그 해결책을 찾아내는 것이 중요합니다. 따라서 빈곤층의 참여와 지원을 유도하는 프로그램이 필요합니다. 예를 들어, 빈곤층의 대표자를 선발하여 의견을 수렴하는 정책, 빈곤층의 참여를 유도하는 교육 프로그램, 빈곤층을 대상으로한 자금 지원 프로그램 등이 필요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">또한, 빈곤 문제 해결을 위해서는 일반 대중들의 관심과 참여가 필요합니다. 대중들은 빈곤 문제를 다양한 측면에서 경험할 수 있으며, 이를 통해 그들이 직접적으로 빈곤 문제와 관련된 지식을 습득하고, 그들의 의식을 바꾸는 것이 중요합니다. 이를 위해 매스 커뮤니케이션, 봉사활동, 자원봉사, 기부 등의 활동이 필요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">마지막으로, 빈곤 문제는 다른 문제들과 연결되어 있으며, 이를 함께 해결해 나가는 것이 중요합니다. 예를 들어, 빈곤 문제와 환경문제, 성별 문제, 노동 문제, 교육 문제, 의료 문제 등은 서로 연결되어 있으며, 이를 함께 해결해 나가는 것이 중요합니다. 따라서 빈곤 문제 해결을 위해서는 다양한 분야의 전문가들이 함께 협력하여 해결책을 찾아내는 것이 필요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">종합적으로, 우리나라의 만연한 빈곤 문제는 매우 복잡하며, 이를 해결하기 위해서는 다양한 분야의 전문가들과 빈곤층 자체의 참여가 필요합니다. 또한, 일반 대중들의 관심과 참여도 중요하며, 이를 위해서는 다양한 활동이 필요합니다.&nbsp;</span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>참고 서적&nbsp;</strong></span></p><p>1. &quot;빈곤국가 한국의 빈곤문제&quot; (김종규 지음, 한울 아카데미)</p><p><br />2. &quot;빈곤의 현실과 대책&quot; (조선일보 사회팀 지음, 조선일보사)</p><p><br />3. &quot;한국의 빈곤분석과 대책&quot; (김종우, 김규리 지음, 역사비평사)</p><p><br />4. &quot;사회복지 이론과 실제&quot; (박윤식, 안정섭 지음, 법문사)</p><p><br />5. &quot;한국사회복지론&quot; (이영준, 이광재 지음, 법문사)</p><p><br />6. &quot;빈곤과 사회복지&quot; (이영준, 한희경 지음, 학지사)</p><p><br />7. &quot;빈곤과 사회정책&quot; (한명숙, 이형근 지음, 박영사)</p><p><br />8. &quot;빈곤의 실체와 대책&quot; (김영관, 조현주 지음, 자유아카데미)</p><p><br />9. &quot;빈곤문제론&quot; (박세영, 최우영 지음, 박영사)</p><p><br />10. &quot;사회복지학의 이론과 실제&quot; (이인환, 김태균 지음, 법문사)</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-26 12:38:27 더 이상 노예가 되지 않겠다! 폭발적인 액션과 함께 뜨거운 감동을 전하는 작품노바디(Nobody) http://macaronics.net/index.php/movie/foreignfilm/view/2098 2098 <p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="900" height="600" src="https://blog.kakaocdn.net/dn/829Rp/btr5O86P7xp/spJHkU70hdoS6puhUaQlRK/img.jpg" /></p><p>&nbsp;</p><p>&nbsp;</p><p>2022년에&nbsp;개봉한&nbsp;미국의&nbsp;범죄&nbsp;액션&nbsp;영화&nbsp;&quot;노바디(Nobody)&quot;는&nbsp;밥&nbsp;오덴커크(Bob&nbsp;Odenkirk)가&nbsp;감독하였으며,&nbsp;다양한&nbsp;배우들이&nbsp;출연하였습니다.&nbsp;영화의&nbsp;주인공인&nbsp;헉터(바바커&nbsp;플링크먼)는&nbsp;평범한&nbsp;가정의&nbsp;아버지로&nbsp;보이지만,&nbsp;과거에는&nbsp;위험한&nbsp;조직의&nbsp;전문&nbsp;암살자였던&nbsp;과거를&nbsp;가지고&nbsp;있습니다.&nbsp;그러나&nbsp;현재&nbsp;그는&nbsp;조용한&nbsp;삶을&nbsp;살고&nbsp;있습니다.<br /><br />하지만&nbsp;어느&nbsp;날,&nbsp;그는&nbsp;집에&nbsp;침입한&nbsp;도둑들을&nbsp;맞아&nbsp;싸우다가&nbsp;이들을&nbsp;처리하는&nbsp;데에&nbsp;능숙함을&nbsp;보이게&nbsp;됩니다.&nbsp;그리고&nbsp;이&nbsp;사건이&nbsp;시작되면서,&nbsp;그의&nbsp;과거가&nbsp;새롭게&nbsp;드러나며&nbsp;다시&nbsp;한&nbsp;번&nbsp;위험한&nbsp;상황에&nbsp;빠지게&nbsp;됩니다.&nbsp;헉터는&nbsp;자신이&nbsp;과거의&nbsp;자신으로&nbsp;돌아가야만&nbsp;현재의&nbsp;문제를&nbsp;해결할&nbsp;수&nbsp;있다고&nbsp;판단하고,&nbsp;전문&nbsp;암살자로서의&nbsp;기술을&nbsp;다시&nbsp;부활시키게&nbsp;됩니다.<br /><br />이&nbsp;영화에서&nbsp;바바커&nbsp;플링크먼은&nbsp;전문&nbsp;암살자로서의&nbsp;역할을&nbsp;매우&nbsp;잘&nbsp;소화하였습니다.&nbsp;그는&nbsp;평소에는&nbsp;조용하고&nbsp;차분한&nbsp;헉터의&nbsp;모습과는&nbsp;달리,&nbsp;적들을&nbsp;상대할&nbsp;때는&nbsp;무자비한&nbsp;액션&nbsp;실력을&nbsp;발휘합니다.&nbsp;또한,&nbsp;그의&nbsp;가족과&nbsp;함께&nbsp;행복한&nbsp;삶을&nbsp;살고&nbsp;있는&nbsp;모습과&nbsp;암살자로서의&nbsp;두&nbsp;얼굴을&nbsp;극적으로&nbsp;표현하였습니다.<br /><br />이&nbsp;영화에서는&nbsp;헉터를&nbsp;비롯하여&nbsp;여러&nbsp;캐릭터들의&nbsp;신비한&nbsp;과거와&nbsp;복잡한&nbsp;인간관계가&nbsp;그려져&nbsp;있습니다.&nbsp;또한,&nbsp;전투씬에서는&nbsp;매우&nbsp;치열하고&nbsp;힘든&nbsp;상황에서&nbsp;헉터가&nbsp;자신의&nbsp;기술을&nbsp;발휘하는&nbsp;모습을&nbsp;볼&nbsp;수&nbsp;있습니다.&nbsp;이러한&nbsp;씬들은&nbsp;관객들에게&nbsp;긴장감을&nbsp;높이며,&nbsp;영화의&nbsp;전반적인&nbsp;분위기를&nbsp;고조시킵니다.<br /><br />또한, 이 영화에서는 배경음악도 매우 중요한 역할을 합니다. 빠르고 강렬한 비트와 함께 전투씬을 강조하는 음악은 관객들에게 더욱 강한 인상을 줍니다. 이 영화의 전반적인 분위기를 고조시킵니다. 또한, 영화의 스토리텔링은 매우 흥미진진하게 펼쳐지며, 플롯의 전개와 캐릭터들의 관계성은 관객들의 이목을 끌고 끊임없이 궁금증을 유발합니다.<br /><br />영화의&nbsp;캐스팅도&nbsp;매우&nbsp;인상적입니다.&nbsp;헉터를&nbsp;맡은&nbsp;바바커&nbsp;플링크먼&nbsp;이외에도,&nbsp;콜린&nbsp;사몬,&nbsp;크리스&nbsp;크리스토프슨,&nbsp;리아&nbsp;서도&nbsp;등의&nbsp;명품&nbsp;배우들이&nbsp;출연하였습니다.&nbsp;이들의&nbsp;연기는&nbsp;영화의&nbsp;전반적인&nbsp;퀄리티를&nbsp;높이는&nbsp;데에&nbsp;큰&nbsp;역할을&nbsp;하였습니다.<br /><br />또한,&nbsp;이&nbsp;영화에서는&nbsp;CG나&nbsp;특수효과보다는&nbsp;현실적인&nbsp;액션&nbsp;연출에&nbsp;더&nbsp;집중하였습니다.&nbsp;이는&nbsp;더욱&nbsp;영화의&nbsp;몰입도를&nbsp;높이는&nbsp;데에&nbsp;기여하였습니다.&nbsp;또한,&nbsp;영화&nbsp;전반에서&nbsp;사용된&nbsp;촬영&nbsp;기법과&nbsp;편집도&nbsp;매우&nbsp;인상적입니다.&nbsp;각각의&nbsp;씬이&nbsp;순차적으로&nbsp;연결되어&nbsp;전체적으로&nbsp;매우&nbsp;일관성&nbsp;있게&nbsp;구성되어&nbsp;있습니다.<br /><br />이&nbsp;영화는&nbsp;범죄&nbsp;액션&nbsp;영화의&nbsp;전형적인&nbsp;형태를&nbsp;보여줍니다.&nbsp;하지만&nbsp;이&nbsp;영화에서는&nbsp;단순한&nbsp;액션물이&nbsp;아니라,&nbsp;캐릭터들의&nbsp;심리적인&nbsp;내면과&nbsp;그들의&nbsp;과거가&nbsp;영화의&nbsp;주요&nbsp;요소로&nbsp;높은&nbsp;완성도를&nbsp;보여주고&nbsp;있습니다.&nbsp;이는&nbsp;이&nbsp;영화를&nbsp;더욱&nbsp;재미있게&nbsp;보게&nbsp;만들며,&nbsp;시간&nbsp;가는&nbsp;줄&nbsp;모르고&nbsp;끊임없이&nbsp;관객의&nbsp;이목을&nbsp;끌어당깁니다.<br /><br />마지막으로,&nbsp;&quot;노바디(Nobody)&quot;는&nbsp;매우&nbsp;스릴&nbsp;넘치는&nbsp;영화로,&nbsp;액션과&nbsp;재미를&nbsp;원하는&nbsp;관객에게&nbsp;강력히&nbsp;추천할&nbsp;만한&nbsp;작품입니다.&nbsp;특히,&nbsp;강렬한&nbsp;전투&nbsp;씬과&nbsp;인물들의&nbsp;감정적인&nbsp;내면이&nbsp;어우러져,&nbsp;시간&nbsp;가는&nbsp;줄&nbsp;모르게&nbsp;관객들을&nbsp;열광시키는&nbsp;작품입니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><iframe src="https://www.youtube.com/embed/ca5hz2BOfbU" width="860" height="484" frameborder="0" allowfullscreen="true"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&quot;노바디(Nobody)&quot;는&nbsp;범죄&nbsp;액션&nbsp;영화&nbsp;중에서도&nbsp;특별한&nbsp;작품입니다.&nbsp;영화는&nbsp;한&nbsp;남자의&nbsp;가정을&nbsp;침입한&nbsp;범죄자들로&nbsp;인해&nbsp;일어나는&nbsp;이야기를&nbsp;그리고&nbsp;있습니다.&nbsp;이&nbsp;남자는&nbsp;이전에&nbsp;범죄자로&nbsp;활동했지만,&nbsp;지금은&nbsp;일반&nbsp;가정인으로&nbsp;살고&nbsp;있습니다.&nbsp;그러나&nbsp;침입&nbsp;사건으로&nbsp;인해&nbsp;그는&nbsp;자신의&nbsp;과거로&nbsp;돌아가게&nbsp;되며,&nbsp;자신의&nbsp;전투&nbsp;능력을&nbsp;발휘하여&nbsp;자신의&nbsp;가족을&nbsp;지키려고&nbsp;합니다.<br /><br />이&nbsp;영화는&nbsp;액션과&nbsp;스릴만&nbsp;있는&nbsp;영화가&nbsp;아닙니다.&nbsp;영화는&nbsp;그들의&nbsp;삶과&nbsp;가족,&nbsp;그리고&nbsp;가족을&nbsp;지키기&nbsp;위해&nbsp;헉터가&nbsp;취해야&nbsp;할&nbsp;선택과&nbsp;그에&nbsp;따른&nbsp;결과를&nbsp;그리고&nbsp;있습니다.&nbsp;이는&nbsp;우리가&nbsp;가족을&nbsp;지키기&nbsp;위해&nbsp;얼마나&nbsp;멀리&nbsp;갈&nbsp;수&nbsp;있는지를&nbsp;보여주는&nbsp;중요한&nbsp;메시지입니다.<br /><br />또한,&nbsp;이&nbsp;영화에서는&nbsp;범죄자와&nbsp;일반인의&nbsp;경계가&nbsp;모호해지는&nbsp;측면도&nbsp;보입니다.&nbsp;헉터는&nbsp;범죄자이지만,&nbsp;그는&nbsp;자신의&nbsp;가족을&nbsp;지키기&nbsp;위해&nbsp;범죄자들과&nbsp;맞서&nbsp;싸우는&nbsp;것입니다.&nbsp;이는&nbsp;이&nbsp;영화가&nbsp;단순한&nbsp;액션&nbsp;영화가&nbsp;아니라,&nbsp;좀&nbsp;더&nbsp;깊은&nbsp;의미와&nbsp;메시지를&nbsp;담고&nbsp;있음을&nbsp;보여줍니다.<br /><br />이&nbsp;영화의&nbsp;결말은&nbsp;감동적입니다.&nbsp;헉터는&nbsp;과거의&nbsp;자신을&nbsp;마주하면서도&nbsp;자신의&nbsp;가족을&nbsp;지키기&nbsp;위해&nbsp;최선을&nbsp;다하였습니다.&nbsp;이는&nbsp;우리가&nbsp;살아가면서&nbsp;어떤&nbsp;상황에서도&nbsp;가족의&nbsp;중요성과&nbsp;가치를&nbsp;상기시켜주는&nbsp;좋은&nbsp;메시지입니다.<br /><br />이&nbsp;영화는&nbsp;바바커&nbsp;플링크먼의&nbsp;연기와&nbsp;연출,&nbsp;그리고&nbsp;뛰어난&nbsp;캐스팅으로&nbsp;인해&nbsp;좋은&nbsp;평가를&nbsp;받고&nbsp;있습니다.&nbsp;특히,&nbsp;바바커&nbsp;플링크먼은&nbsp;이전의&nbsp;작품에서는&nbsp;보지&nbsp;못했던&nbsp;강렬한&nbsp;연기를&nbsp;선보이며,&nbsp;이&nbsp;영화를&nbsp;좀&nbsp;더&nbsp;매력적으로&nbsp;만들어주고&nbsp;있습니다.<br /><br />&nbsp;</p><p>영화&nbsp;&quot;노바디(Nobody)&quot;는&nbsp;폭발적인&nbsp;액션과&nbsp;함께&nbsp;뜨거운&nbsp;감동을&nbsp;전하는&nbsp;작품입니다.&nbsp;이&nbsp;작품은&nbsp;일반적인&nbsp;범죄&nbsp;액션&nbsp;영화와는&nbsp;다른&nbsp;독특한&nbsp;매력을&nbsp;가지고&nbsp;있습니다.<br /><br />영화의&nbsp;주인공인&nbsp;헉터는&nbsp;이전에&nbsp;범죄자로&nbsp;활동하였지만,&nbsp;지금은&nbsp;일반&nbsp;가정인으로&nbsp;살고&nbsp;있습니다.&nbsp;그러나&nbsp;어느&nbsp;날,&nbsp;그의&nbsp;가정에&nbsp;범죄자들이&nbsp;침입하여&nbsp;가족을&nbsp;위협합니다.&nbsp;이에&nbsp;대처하기&nbsp;위해&nbsp;헉터는&nbsp;과거의&nbsp;자신의&nbsp;능력을&nbsp;되찾아,&nbsp;범죄자들과&nbsp;맞서&nbsp;싸우는데,&nbsp;이&nbsp;때문에&nbsp;그의&nbsp;가족과&nbsp;친구들은&nbsp;큰&nbsp;위험에&nbsp;빠지게&nbsp;됩니다.<br /><br />이&nbsp;영화에서&nbsp;가장&nbsp;놀라운&nbsp;것&nbsp;중&nbsp;하나는&nbsp;주인공인&nbsp;헉터가&nbsp;이전에는&nbsp;범죄자로&nbsp;활동했다는&nbsp;것입니다.&nbsp;그는&nbsp;그리스도교도인&nbsp;가정에서&nbsp;자랐지만,&nbsp;가정&nbsp;내부에서는&nbsp;항상&nbsp;부모님의&nbsp;싸움을&nbsp;목격하였습니다.&nbsp;그래서&nbsp;그는&nbsp;자신을&nbsp;구할&nbsp;수&nbsp;있는&nbsp;방법으로&nbsp;범죄자로&nbsp;활동하게&nbsp;되었고,&nbsp;이를&nbsp;계기로&nbsp;범죄자들의&nbsp;세계에&nbsp;발을&nbsp;들이게&nbsp;됩니다.<br /><br />하지만&nbsp;지금은&nbsp;그가&nbsp;그렇게&nbsp;했던&nbsp;과거와는&nbsp;다른&nbsp;삶을&nbsp;살고&nbsp;있습니다.&nbsp;그의&nbsp;아내와&nbsp;두&nbsp;아이들과&nbsp;함께&nbsp;평화롭게&nbsp;살고&nbsp;있으며,&nbsp;이를&nbsp;지키기&nbsp;위해&nbsp;그는&nbsp;과거의&nbsp;자신과&nbsp;달리&nbsp;법을&nbsp;지키고&nbsp;일반인으로서의&nbsp;삶을&nbsp;유지하고&nbsp;있습니다.<br /><br />그러나&nbsp;그의&nbsp;가정에&nbsp;침입한&nbsp;범죄자들로&nbsp;인해&nbsp;그는&nbsp;과거의&nbsp;자신으로&nbsp;돌아가야&nbsp;합니다.&nbsp;그의&nbsp;경험과&nbsp;능력을&nbsp;바탕으로&nbsp;그는&nbsp;범죄자들과&nbsp;맞서&nbsp;싸웁니다.&nbsp;이&nbsp;때문에&nbsp;그의&nbsp;가족과&nbsp;친구들은&nbsp;큰&nbsp;위험에&nbsp;빠지게&nbsp;되며,&nbsp;이들을&nbsp;구하기&nbsp;위해&nbsp;헉터는&nbsp;모든&nbsp;것을&nbsp;걸고&nbsp;싸웁니다.<br /><br />영화의&nbsp;결말은&nbsp;헉터가&nbsp;그동안의&nbsp;모든&nbsp;일에&nbsp;대해&nbsp;회고하는&nbsp;장면으로&nbsp;시작됩니다.&nbsp;그는&nbsp;자신이&nbsp;범죄자로&nbsp;활동했던&nbsp;과거에&nbsp;대한&nbsp;후회와,&nbsp;그가&nbsp;이제&nbsp;가족을&nbsp;위해&nbsp;행동한&nbsp;것에&nbsp;대한&nbsp;스스로의&nbsp;존경심을&nbsp;느끼고&nbsp;있습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><iframe src="https://www.youtube.com/embed/DfVEZ4y3Scw" width="860" height="484" frameborder="0" allowfullscreen="true"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>헉터는&nbsp;결국&nbsp;범죄자들을&nbsp;제압하고&nbsp;가족과&nbsp;친구들을&nbsp;구할&nbsp;수&nbsp;있었습니다.&nbsp;하지만&nbsp;그가&nbsp;행한&nbsp;행동들은&nbsp;모두&nbsp;불법적이었으며,&nbsp;이를&nbsp;알고&nbsp;있던&nbsp;FBI는&nbsp;그를&nbsp;체포하고자&nbsp;합니다.&nbsp;그러나&nbsp;그들은&nbsp;그가&nbsp;마지막에&nbsp;범죄자들과의&nbsp;싸움에서&nbsp;수많은&nbsp;인질을&nbsp;구한&nbsp;영웅으로&nbsp;칭하며,&nbsp;그를&nbsp;직접&nbsp;체포하지&nbsp;않고&nbsp;그의&nbsp;가족과&nbsp;함께&nbsp;살&nbsp;수&nbsp;있도록&nbsp;체포&nbsp;이후&nbsp;수개월이&nbsp;지난&nbsp;후&nbsp;그에게&nbsp;연락을&nbsp;합니다.<br /><br />이를&nbsp;통해&nbsp;영화는&nbsp;범죄자로&nbsp;살았던&nbsp;과거와&nbsp;이제&nbsp;일반인으로서의&nbsp;삶,&nbsp;그리고&nbsp;그것이&nbsp;어떻게&nbsp;그의&nbsp;가족과&nbsp;친구들에게&nbsp;영향을&nbsp;미쳤는지를&nbsp;보여주며,&nbsp;결말은&nbsp;그의&nbsp;행동이&nbsp;그동안의&nbsp;모든&nbsp;것에&nbsp;대한&nbsp;자기&nbsp;수용과&nbsp;존경심을&nbsp;느끼게&nbsp;합니다.<br /><br />이&nbsp;작품은&nbsp;흥미진진한&nbsp;액션과&nbsp;함께,&nbsp;가족과의&nbsp;사랑과&nbsp;희생,&nbsp;그리고&nbsp;과거와&nbsp;현재의&nbsp;선택의&nbsp;중요성을&nbsp;보여주는&nbsp;멋진&nbsp;작품입니다.&nbsp;헉터를&nbsp;비롯한&nbsp;모든&nbsp;등장인물들은&nbsp;매우&nbsp;흥미롭고&nbsp;볼만한&nbsp;캐릭터들입니다.&nbsp;헉터의&nbsp;집에서&nbsp;벌어지는&nbsp;간단한&nbsp;일상&nbsp;생활의&nbsp;장면들과&nbsp;그와&nbsp;가족들&nbsp;간의&nbsp;대화들은&nbsp;이&nbsp;영화에&nbsp;감동을&nbsp;더합니다.<br /><br />영화는&nbsp;단순히&nbsp;폭력적인&nbsp;액션으로만&nbsp;구성된&nbsp;것이&nbsp;아니라,&nbsp;이야기&nbsp;자체에도&nbsp;깊이가&nbsp;있습니다.&nbsp;과거와&nbsp;현재,&nbsp;법과&nbsp;비법의&nbsp;경계선에서의&nbsp;선택,&nbsp;그리고&nbsp;가족과의&nbsp;연결이라는&nbsp;주제들은&nbsp;이&nbsp;영화를&nbsp;보는&nbsp;이들에게&nbsp;깊은&nbsp;생각을&nbsp;유발시킵니다.<br /><br />총체적으로,&nbsp;&quot;노바디(Nobody)&quot;는&nbsp;다양한&nbsp;감성을&nbsp;자극하는&nbsp;작품입니다.&nbsp;이&nbsp;영화를&nbsp;보면&nbsp;액션뿐만&nbsp;아니라&nbsp;가족과의&nbsp;사랑,&nbsp;희생,&nbsp;과거와&nbsp;현재의&nbsp;선택&nbsp;등&nbsp;다양한&nbsp;면에서&nbsp;생각할&nbsp;거리를&nbsp;얻을&nbsp;수&nbsp;있습니다.&nbsp;헉터의&nbsp;모험은&nbsp;감동적인&nbsp;이야기와&nbsp;함께&nbsp;볼거리를&nbsp;충분히&nbsp;제공합니다.&nbsp;강력하게&nbsp;추천합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p><iframe src="https://www.youtube.com/embed/4KoK3PzzYyY" width="860" height="484" frameborder="0" allowfullscreen="true"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:18px"><strong>영화 링크 사이트&nbsp; <a target="_blank" href="https://now-movies.kro.kr/">나우 무비스</a></strong></span></p><p>&nbsp;</p><p><img alt="" width="500" height="535" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjI-2kDC_vlvTKjkU6af4CVKamVhaE3FyjyBMOcRTtUJLV_jHD1_OGWA3wz6QKxZs-bGfTtkXXNQvkwagJ08E9lkznh4pr12SLFl0RhPb3ChgVVyHcTlNvPuaEgmz7az9d8m3i4oJV9Rz_FQYabl2A7TGdhYgHXoHHq5uIRARzs0SQBOOE5Di8tfVQ6rg/s16000/2023-03-25%2009%2018%2005.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-25 08:29:32 1984 - 조지 오웰 - 조지 오웰이 1949년에 발표한 소설로, 예술적인 작품으로서도 그 당시의 정치적, 사회적 상황을 반영한 문학적인 명작 중 하나 http://macaronics.net/index.php/movie/education/view/2097 2097 <p>&nbsp;</p><p><em><img alt="1984" border="0" src="http://image.yes24.com/goods/372300/XL" /></em></p><p><a href="javascript:void(0)" onfocus="this.blur();" onclick="wiseLog('Pcode','098_001');openPreviewForProduct(372300);"><em>미리보기</em></a>&nbsp;<a href="javascript:void(0);" onclick="wiseLog('Pcode', '098_009');realSizeView();"><em>사이즈비교</em></a>&nbsp;<a href="javascript:void(0);" onclick="wiseLog('Pcode', '098_005'); goGD_cardNews();"><em>카드뉴스</em></a>&nbsp;<a href="http://www.yes24.com/Product/PartnerShop/21" onclick="setPcode('098_007')"><em>파트너샵</em><em>가기</em></a>&nbsp;<a href="javascript:void(0);" onclick="toggleShareGoods(event);"><em>공유하기</em></a></p><p>&nbsp;</p><p>1984</p><p><a target="_blank" href="http://www.yes24.com/Product/Search?domain=ALL&amp;query=%EC%A1%B0%EC%A7%80%20%EC%98%A4%EC%9B%B0&amp;authorNo=738&amp;author=%EC%A1%B0%EC%A7%80%20%EC%98%A4%EC%9B%B0">조지 오웰</a>&nbsp;저/<a target="_blank" href="http://www.yes24.com/Product/Search?domain=ALL&amp;query=%EC%A0%95%ED%9A%8C%EC%84%B1&amp;authorNo=123740&amp;author=%EC%A0%95%ED%9A%8C%EC%84%B1">정회성</a>&nbsp;역&nbsp;<em>|</em>&nbsp;<a href="javascript:void(0);" onclick="openUrl('http://www.yes24.com/Product/Search?domain=ALL&amp;query=%eb%af%bc%ec%9d%8c%ec%82%ac&amp;mkEntrNo=21','Pcode','003_003')">민음사</a>&nbsp;<em>|</em>&nbsp;2003년 06월 16일&nbsp;<em>|</em>&nbsp;</p><p>베스트</p><p>&nbsp;</p><p><a href="javascript:void(0)" onclick="openUrl('/24/category/bestseller?CategoryNumber=001001046&amp;SumGb=02','Pcode','003_008')">소설/시/희곡 33위</a>&nbsp;<em>|</em>&nbsp;소설/시/희곡 top20 4주</p><p>정가<em>9,500원</em></p><p><a target="_blank" href="http://www.yes24.com/Product/Goods/372300">판매가<em>8,550</em>원&nbsp;(10% 할인</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px">1984&#39;는 영국 작가 조지 오웰이 1949년에 발표한 소설로, 예술적인 작품으로서도 그 당시의 정치적, 사회적 상황을 반영한 문학적인 명작 중 하나입니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">소설의 배경은 1984년의 영국에서 시작되며, 이곳은 전 세계가 세력을 행사하는 초강대국인 &#39;오세아니아&#39;의 지배를 받는 국가 중 하나인 &#39;에어스트리아&#39;입니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">이 국가는 &quot;빅 브라더&quot;라는 권력자가 국민의 생각과 감정, 행동을 완전히 통제하는 &#39;최고의 통치 체제&#39;를 구축하여 살아가고 있습니다.</span></p><p><span style="font-size:16px">주인공 윈스턴 스미스는 이러한 지배 체제에서 일하는 일반 시민 중 한 명입니다. 그는 이 체제에 대한 반감과 역겨움을 느끼며 살아가지만, 이러한 생각 자체가</span></p><p><span style="font-size:16px">범죄로 간주되어 집단적인 감시와 통제로 인해 그의 인생은 점점 더 지옥과 같은 곳으로 치닫게 됩니다.</span></p><p><span style="font-size:16px">윈스턴은 결국 반체제운동가인 줄리아와 함께 저항을 시작하며, 이러한 활동으로 인해 그들은 &#39;최고의 통치 체제&#39;에 대항하는 위험한 활동을 하게 됩니다. 이후,</span></p><p><span style="font-size:16px">소설은 반전적인 결말을 향해 진행되며, 독자는 &#39;1984&#39;에서 그려진 초강대국의 끔찍한 현실을 직접 체험하게 됩니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">&#39;1984&#39;는 권력의 무서움과 인간의 자유, 정치와 사회적 불평등의 문제를 논의하며, 인류의 궁극적인 운명에 대한 의문을 제기합니다. </span></p><p>&nbsp;</p><p><span style="font-size:16px">이 소설은 문학적인 가치와 함께 현대사회의 어두운 면을 고스란히 드러내어 우리에게 많은 경각심을 불러일으키는 작품 중 하나입니다.</span></p><p>&nbsp;</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/ON793rPAZ00" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><strong>독후감</strong></span></p><p>&nbsp;</p><p><span style="font-size:16px">&lsquo;1984&rsquo;는 작가 조지 오웰의 소설로, 초강대국 오세아니아에서 최고의 통치 체제를 구축한 지배자 빅 브라더가 국민의 생각과 감정, 행동을 완전히 통제하는 모습을 그린 문학적인 명작입니다. 이 책은 우리에게 지금까지 생각하지 못했던, 그러나 언제나 있을 수 있는 미래를 경고하며, 인간의 자유와 개인의 권리, 그리고 권력과 인간 본성에 대한 논쟁을 제기합니다. 이번 독후감에서는 이러한 내용들을 중심으로 &lsquo;1984&rsquo;에 대해 서술해보도록 하겠습니다.</span></p><p><span style="font-size:16px">&lsquo;1984&rsquo;는 현재의 정치적, 사회적 상황을 비추는 작품이라는 평가를 받고 있습니다. </span></p><p><span style="font-size:16px">작가는 초강대국 오세아니아에서 벌어지는 현실적인 상황을 통해, 미래에 어떤 일이 일어날지를 예측하고 경고하고자 했습니다. </span></p><p><span style="font-size:16px">이러한 접근 방식은 우리가 실제로 겪을 수 있는 상황에 대한 경각심을 불러일으키는 역할을 합니다.</span></p><p><span style="font-size:16px">특히, 작가는 이러한 상황에서 인간의 자유와 권리에 대한 문제를 다룹니다. 초강대국 오세아니아의 국민들은 빅 브라더에게 모든 것을 통제당하며, 자신들의 삶은 완전히 제어당하는 상황에 처해 있습니다. 이러한 상황에서 윈스턴 스미스와 줄리아는 반체제 운동을 시작하며, 빅 브라더의 통제에서 벗어나기 위해 노력합니다. 그러나 그들이 하는 모든 행동은 모두 위험한 범죄 행위로 간주됩니다.</span></p><p><span style="font-size:16px">작가는 이러한 상황에서 인간의 자유와 권리의 중요성을 강조합니다. </span></p><p><span style="font-size:16px">빅 브라더가 국민의 생각과 감정, 행동을 제어하려는 시도는 인간의 본성을 침식시키는 것이며, 이러한 상황에서 인간의 삶은 진정한 의미에서 의미 없는 삶이 됩니다. 이러한 문제를 다루면서 작가는 우리에게 인간 본성의 불변성과 자유, 개인의 권리와 그것이 가지는 의미에 대해 생각해보라고 이야기합니다.</span></p><p><span style="font-size:16px">또한, 이 소설은 권력과 그에 따른 책임에 대한 문제도 다룹니다.</span></p><p><span style="font-size:16px">빅 브라더는 국민들을 완전히 통제하면서도, 그들의 삶을 책임지지 않습니다. 이러한 권력과 책임의 분리는 국민들의 불만과 혼란을 초래하며, 결국 권력의 붕괴를 불러일으킵니다. 이러한 상황은 현재의 정치적, 사회적인 상황에서도 동일하게 적용될 수 있는 문제이며, </span></p><p><span style="font-size:16px">작가는 이를 통해 우리에게 권력과 책임의 관계에 대한 생각을 제안합니다.</span></p><p><span style="font-size:16px">또한, 작가는 이 소설을 통해 언어와 미디어에 대한 문제도 다룹니다. 빅 브라더는 국민들의 언어와 의사소통 방식을 통제하고, 또한 미디어를 통해 국민들의 생각과 감정을 조작합니다. 이러한 통제와 조작은 국민들의 생각과 행동에 큰 영향을 끼치며, 결국 국민들의 인식과 현실 간의 이상한 간극을 만들어 냅니다. 작가는 이러한 문제를 다루며, 우리가 언어와 미디어를 어떻게 사용하는지에 대한 고찰을 요구합니다.</span></p><p><span style="font-size:16px">마지막으로, 이 소설은 인간의 본성과 인간의 삶에 대한 철학적인 문제를 다룹니다.</span></p><p><span style="font-size:16px">초강대국 오세아니아에서는 인간의 삶은 의미 없는 것으로 여겨지며, 인간 본성은 완전히 제어되고 침식되어 버립니다. 이러한 문제는 우리가 인간의 삶과 본성에 대해 생각해볼 때 굉장히 중요한 문제입니다. 이러한 문제를 다루며 작가는 우리에게 인간의 삶과 본성의 의미에 대한 새로운 시각을 제시합니다.</span></p><p><span style="font-size:16px">총적으로, &lsquo;1984&rsquo;는 정치적, 사회적, 철학적인 문제를 다루며, 우리에게 권력과 책임, 자유와 권리, 언어와 미디어, 인간의 삶과 본성에 대한 다양한 문제에 대해 고찰할수 있는 깊이 있는 작품입니다. 또한, 작가의 문체와 문장력도 매우 뛰어나며, 미래의 사회를 상상하는 것에 대한 창의성과 상상력도 높은 편입니다. 이 소설은 고전적인 문학작품 중 하나로 꼽히며, 현재까지도 많은 사람들에게 읽히고 있습니다.</span></p><p><span style="font-size:16px">하지만 이 소설을 읽는 것은 가벼운 일이 아닙니다. 소설에서 다루는 주제와 내용들은 매우 진지하며, 독자들로 하여금 깊은 고민과 생각을 유발시킵니다. 작가가 그려내는 오세아니아의 현실은 매우 혐오스럽고, 독자들은 그 현실에 대한 불안과 공포를 느낄 수 있습니다. </span></p><p><span style="font-size:16px">그러나 이러한 불안과 공포는 우리가 살고 있는 현실에서도 마찬가지입니다. 이 소설은 우리에게 그러한 현실에 대한 경각심을 줄 뿐만 아니라, 이를 극복하기 위한 방법에 대한 힌트도 제공합니다.</span></p><p>&nbsp;</p><p><span style="font-size:16px">최근에는 빅 브라더처럼 권력과 개인 정보 보호, 미디어 조작 등과 관련된 주제들이 매우 중요해졌습니다.</span></p><p><span style="font-size:16px">이러한 문제들은 &lsquo;1984&rsquo;와 같은 작품을 다시금 읽어보는 것이 중요하다는 것을 보여줍니다.</span></p><p><span style="font-size:16px">&lsquo;1984&rsquo;는 우리에게 현재와 미래를 대하는 경각심을 불러일으키는 작품으로, 지금까지도 많은 독자들에게 깊은 감동을 선사하고 있습니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-24 19:36:33 AI 가 추천하는 베스트 셀러  30권 http://macaronics.net/index.php/movie/education/view/2096 2096 <p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:28px">베스트 셀러&nbsp; 30권</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>1984 - 조지 오웰</p><p><br />7년의 밤 - 이영도</p><p><br />겨울왕국 - 한동근</p><p><br />고요할수록 밝아지는 것들 - 혜민 스님</p><p><br />김미영의 시 한 장 - 김미영</p><p><br />나미야 잡화점의 기적 - 히가시노 게이고</p><p><br />달러구트 꿈 백화점 - 이미영</p><p><br />달팽이 게임 - 유제원</p><p><br />대여사건 - 김동식</p><p><br />더 해빙 - 김미경</p><p><br />더 테너 오브 다이어리 - 오감영</p><p><br />떠나지마자, 오래오래 - 미키 타카시</p><p><br />또 다시, 행복하게 - 히가시노 게이고</p><p><br />마음챙김의 시 - 손힘찬</p><p><br />말의 품격 - 조세푸스 브라운</p><p><br />멋진 신세계 - 올더스 헉슬리</p><p><br />미움받을 용기 - 기시미 이치로</p><p><br />반지의 제왕 - 톨킨</p><p><br />사람을 고치는 마음의 상처 치유법 - 김난도</p><p><br />산삼 자는 고양이 - 나태주</p><p><br />세상의 끝과 아이스크림처럼 - 이치조 미사키</p><p><br />소년이 온다 - 한강</p><p><br />신경 끄기의 기술 - 마크 맨슨</p><p><br />아가씨와 밤 - 김용택</p><p><br />언어의 온도 - 이기주</p><p><br />오늘 밤은 친구네 집에서 - 유태오</p><p><br />이방인 - 알베르 카뮈</p><p><br />인간실격 - 다자이 오사무</p><p><br />청소부 밥상 - 신파</p><p><br />해리 포터와 마법사의 돌 - J.K. 롤링</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-24 19:21:25 무료 도메인 업체를 소개합니다. http://macaronics.net/index.php/m05/computer/view/2095 2095 <p>&nbsp;</p><p><span style="font-size:20px"><strong>무료 도메인 업체를 소개합니다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="color:#2980b9"><strong><span style="font-size:20px">1.LCN.com (1년간 무료)</span></strong></span></p><p><a target="_blank" href="https://orders.lcn.com/">https://orders.lcn.com/</a></p><p>&nbsp;</p><p><img alt="" width="900" height="702" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhg4ECAwuLAoi3BQw6NDqT5AE7w_ji921o31jL_e_8oosoybP2r0ymOb4dkOWwIK_sJ-XV8dohNTR5zOSQTvmEqSs-q_1Hx1pBiRw4O6srcOM1Xr5V4rRxUdIsBn0VItO_hgCFqUKuLQZqQEgMUp4cmBuH9h3IC-1MHL5cLQw1dUgWy459bBvepGExzWA/s16000/2023-03-24%2009%2004%2016.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>주의 기타 옵션을 체크해서 결제 하지 말것</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>2. promotions 1년간 무료</strong></span></span><br /><a target="_blank" href="https://promotions.names.co.uk/">https://promotions.names.co.uk/</a></p><p>&nbsp;</p><p><img alt="" width="900" height="686" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgHHcLsPUjKCB7O9hlyMZFCQ1gmH9_GZjPwhE8ogzcL81Ed8OfdeneExDJSWC5cPGfD9wy5ilpIPMKOSBZZFyJtuiJCNa06jlVP9wHd6AE-4B9HaIBeLsY8gevP7A_mSZVrlimgLFQsAHYFkNpbrgx_WfNjQBOYK4SPp4-TqiX6JvMbLJoufLpe4mEIiA/s16000/2023-03-24%2009%2006%2022.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>3.내도메인 한국 (3개월마다 갱신)</strong></span></span><br /><a target="_blank" href="https://내도메인.한국">https://내도메인.한국</a></p><p>&nbsp;</p><p><img alt="" width="1037" height="940" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIEMi7tgCBuNGrt9qQmFsjqxsvTg2WYLzvbNfkjCf2JThp00sXiPW8mO2X9ew4BDqKloTv2BD4FWB7Uw50JBJqJKIK_Z-mrEn3ixQ_WgePaUcPVFJhB9-Wyuh8MZzo3ZojlURq1PbJpSnMhOocyB45wcHl7yON4Laop59wRXPW_-dFof57TEQ-00YgwQ/s16000/2023-03-24%2009%2010%2046.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>4. f.reenom.com 무료 도메인으로&nbsp; 테스트로 많이 이용했는데,<br />사이트가 공격을 받은것 같고, 현재 상태가 않 좋다.</p><p>&nbsp;</p><p><br />1~2번은 1년간 무료 이지만 2년째 부터는 유료라 10년 정도 사용하면&nbsp;<br />15~ 16만원 으로 연간 1만6천원 정도 비용이 든다.&nbsp; &nbsp;따라서,&nbsp; &nbsp;1년간만 사용한다면 추천 합니다.</p><p>&nbsp;</p><p>그렇지 않다면,&nbsp; iwinv 업체를 추천한다.</p><p>연간 pe.kr 도메인은 1만원이고 com 이나 net 도 1만 3천원 이다.</p><p><a target="_blank" href="https://www.iwinv.kr/account/domain.html">https://www.iwinv.kr/account/domain.html</a><br />.pe.kr&nbsp;&nbsp; &nbsp;10,000원 / 년&nbsp;&nbsp; &nbsp;10,000원 / 년&nbsp;&nbsp; &nbsp;제한없음</p><p>&nbsp;</p><p>phps스쿨 에서 도메인&nbsp; 가장 저렴했던것 같은데, 현재는&nbsp;iwinv 이 저렴한듯.</p><p>https://domain.phps.kr/</p><p>&nbsp;</p><p>&nbsp;</p><p>도메인 포함 php 무료 무제한 호스팅 업체를 이용한다면&nbsp; 닷홈</p><p><a target="_blank" href="https://www.dothome.co.kr/">https://www.dothome.co.kr/</a></p><p>&nbsp;</p><p>&nbsp;</p><p><br />해외 업체는 namesilo 를 추천 합니다.<br /><a target="_blank" href="https://www.namesilo.com/login">https://www.namesilo.com/login</a></p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="color:#c0392b"><strong>해외 도메인 &nbsp;주의해야 한다 .&nbsp; 왜냐하면은 대부분 첫 해만 싸고 갱신시에는 엄청나게 요금이<br />올라가는 업체가 대부분이기 때문이다.</strong></span></p><p>&nbsp;</p><p>godaddy 역시 첫해는 1만 1천원이지만... 갱신시에는...</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-24 09:37:52 더글로리 신예은 '꽃선비 열애사' 남신 3대장과 썸타는 달달함 http://macaronics.net/index.php/tv/drama/view/2094 2094 <p>&nbsp;</p><p><span style="font-size:24px">《꽃선비 열애사》는 2023년 3월 20일부터 방송중인 SBS 월화 드라마로</span><br /><strong><span style="font-size:20px">네 명의 청춘이 만들어내는 &#39;상큼 발칙한 미스터리 밀착 로맨스&#39;다.&nbsp;</span></strong></p><p><br />출연진은 신예은(윤단오 역),&nbsp;<br />려운(강산 역),&nbsp;<br />강훈(김시열 역),&nbsp;<br />정건주(정유하 역)<br />&nbsp;<br />첫방송: 2023년 3월 20일 (대한민국)<br />방송사: SBS TV<br />에피소드 수: 18<br />음악: 이지용<br />장르: 로맨스, 코미디, 사극</p><p>&nbsp;</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/JAP-vt5J7V0" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:28px">고몽 자막</span></strong></p><p><br />&nbsp;세 명의 사내가 있었습니다-얘네 아닙니다-꽃이라는 수식어가 잘 어울릴 것 같은-선비 3인방이죠-[음악]-난 오른쪽-치이고 싶어 저 어떡한 콧날에-난 가운데-빠지고 싶어 보석 같은 눈망울에-선망의 대상인 미선미 3인방에게-둘러싸인 그녀-처음 뵙겠습니다-매월이라 하옵니다 우리끼리 주고-싶은데 미안하네-다 같이 치면 그-어떤 여인이 다가와도-시선을 사로잡는 단 한 사람 그녀의-이름은 윤단호 그런데 그녀는이 세-사람과 한 지붕 아래 같이 지내고-있습니다-단어는 조선시 쉐어하우스 객주-이화원의 주인이기 때문입니다-SBS 월화드라마 꽃 선비 열애사-조선에선 많은 의미로 유명한 객주-이화원 이곳에는 각자 연으로 선비-3인방이 머물고 있습니다 이것이-급제생활 제곱에 출연해 명문객-출하던데 그게 잠이요-급재생이야 있지요 그게 누구야 저희-아버지요 또 누가 있어 엄마-산이 좀 부족하죠 그건 넉넉하니-누가 있어-어찌 자꾸 존대를 하십니까 말씀 편히-놓으셔요 저도 편히 오라버니라-부르겠습니다-한 집에 머물게 됐으니 서로 편안하게-웃을 때 올라가던 입꼬리 내키 미라나-말해줄까 없어 그 한 명이 전부란에-뭐 물론 단호 아버지 살아계실-적이에요-종종 유생들이 드나들었지 좀 의하긴-하지요<br />&nbsp;양반 가요식이 객주의를 한다는 것이-뭐 살려면 별 수 있었겠나 부친이-남긴 거라곤 딸랑이었는데-양반집 막내딸 단오가 객주가 되는-선택은 어쩌면 필연적이었을지 모릅니다-13년 전-동일투기 전에 주사를 한다-저에 대한 욕망으로 피로를 들었던-조선 그곳에서 유일하게 살아도망친-폐쇄손 이서린 숨어 들었던 곳이 바로-지금의 객주 이화원이었죠-너 누구야 이름을 묻는 것은요-당연하지-상중에 숨어든 건 너잖아-처음이다 내 이름을 부른이는 말하기-싫다 그거지 아무것도 못지않은 채-폐쇄손했던 단어가 지켜주었었죠-복실아<br />&nbsp;천천히 먹어 여기 많으니까 그때-사나이 하나가 도망 오지 않았느냐-너 워낙 사나워서 먹을 때 건들면-사람도<br />&nbsp;뭅니다<br />&nbsp;[음악]-이렇게 단어의 기지로이 사람 목숨을-부조할 수 있었습니다이가-처음이어서 우리 아버지 보러-와준다<br />&nbsp;보냈어 두-분 모두-[음악]-그날 밤 두 사람은 약속했습니다-다음에 다시 꼭 만나기로 말이죠-다음에 다시 보면 그때 알려주마-헐 내 이름 이렇게 13년이라는-시간이 지난 지금-단호의 생활은 많이 변해 있었습니다-귀신들린 집-문정 경쟁 노래나-공정은 개뿔 귀신이 출몰한다는-소문으로 유명해진-딱 맞춤으로 해드리는 전기 빼먹기-좋겠네<br />&nbsp;양반 찌바시라는 명함도 내려오는지-아버지가 돌아가시는 먹고 살기 위해-발로 뛰고 있었죠 그 순간-[음악]-낭자 이게 무슨 개고생이에요 나한테-씹으면 만사 편할 것인데-어디서 개가 짖나 대체-얼마만 들겠어 말해 보시오 얼마면-날<br />&nbsp;서방으로 봐주겠어 되고 싶어도 참을-수 있습니다 운명의 사내들이 곧-여원으로 이끌리고 있기 때문입니다-낭자는 대체 몇 번을 찍어야 찍지-마셔요 1000번이고 만 번이고-넘어갈 일은 없을테니까-본인도 전에-흠집 내기 싫었는데-신내가 진동합니다는-강한 의지를 보이게 되는데-손 내밀 때 잡으시오 나만한 남자-없다니까 하지만 누군가 단호하게 내민-또 다른 손 이게-돌았나<br />&nbsp;종종 듣는 소리긴 한데-넌 오늘-[음악]-그래<br />&nbsp;끝장을 한번 보자-결판 한번 내주 오늘-이름 모를 사는 당당히-그들의 눈을 따돌리며 단어의 손에 꼭-잡고 그곳에서 빠져나오게 되면서-선배님의 얼굴을 자세히 보니-그리고<br />&nbsp;제가 선배님을 구하는 것까지도요-의원에 안 가보셔도 되겠습니다-[음악]-아무 이유 없이 그녀를 도와준 선배의-정체는<br />&nbsp;이분은 잘-구해주신천입니다이 사내의 이름은-김시열<br />&nbsp;애향이나 아침에도 어여쁘구나 선비님도-참 그럼 또 보자-과거 시험 준비는 핑계일뿐 매일 먹고-마시며 즐기고 싶은 조선의-욜로족이었죠-물론 외상으로 말이죠 아이고-돈 안주지만 오늘 곱게 못 나가십니다-외상으로 하겠네 하지만-계속되는 외상에 희열을 받아주는 곳은-이제 찾기 힘들 정도였죠-이렇게 오늘도 외상으로 도망치던 중-단어와 마주치게 된 것이었죠-난 괜찮았어 오해가 있을까요-그리고 여기 또 다른 꽃 선비가-단어와 만날 준비를 하고 있었으니이-남자 이름은 강산 강산은 무과 시험을-치르기 위해 한양으로 향하고-있었는데요-그런데<br />&nbsp;갑자기 나온 손-도끼와 빡빡이 아저씨-응 내 주지애 같이-고개를 넘어준 조건으로 5냥이나-주었지만-강사는 자신의 실력을 조금 뽐내-보기로 하는데-[음악]-[음악]-꺼지세요-[음악]-덫에 걸리지만 않았어도-나눠주시오 이렇게 무사히 고개를 넘어-도착한 이곳에서-간사는 단어의 눈에 띄게 되는데-왕건이다-잡아주나요 또-역시나 무과 시험 지원자답게 쉽게-피하는 강산에 몸놀림-좀<br />&nbsp;그렇게 단어는 강산을 객주에 머물게-하기 위해-작업을 치기 시작하는데-거머리요-묵을 빵은 정하셨소-아직 못 구하셨으면-우리 집으로 가시지요 오늘-밤 부끄러움을 모르는군요-귀하신 분 같은데 이런 누추한 주막에-먹기엔<br />&nbsp;[박수]-더 이상 손님을 놓칠 수 없던 단어는-결국<br />&nbsp;강산의 짐을 자신의 객주로 가져가게-되고<br />&nbsp;그렇게 먼저 도착한 이화원에는-단어를 박이는 또 다른 꽃 선비가-있었으니-종묘<br />&nbsp;성수초<br />&nbsp;몸에서 흐르는 귀티하며 모나하며-자상한 성품 심지어 총명함까지 이제-오느냐 유아 오라버니 우리 아까-눈 마주치지 않았나 정령 제 눈빛 못-읽으셨어요-뭔데<br />&nbsp;밥 먹는 개와 책 읽는 선미는-건드리지 않는다-제 말에 방점은 책 읽는 선비라구요-뭐 읽고 계셨어요 그냥도 너-단우야<br />&nbsp;난 네가 너무 힘든 일은 하지-않았으면 오라버니 저도-저 아래에 있는 책이 뭔지 되게-궁금한데-정석이라 해도 과언이 아니었죠 전 다-이해합니다 오라버니도 어엿한-성인이신데-뭘 이해해 사내들의-호기심<br />&nbsp;단우야 오해를 좀 풀고 갔으면-좋겠는데-그날밤 광선은 다 녹아 자신의 집을-훔쳐갔다고 생각하게 되면서 이와-운으로 향하게 되고-아무도 안 계세요 다시 터널을-마주하게 되면서-도둑놈<br />&nbsp;단어는 또다시 강산의 품으로 돌진하게-되는데 하지만 주황에서는 달리-단어를 품에 하는 강산의 모습은 마치-신라면 덕감해지던 눈동자 머리카락-한올까지 아름답던-[음악]-한양교인들은 원래 이래도 적극적인가-지금<br />&nbsp;초 달아난다고 짝이 되지는 않아-딱이요 무슨 차 활짝-1호의 대화-선배님이 선녀하고 내가 나무꾼 저는-곳에 올라가셨어요 다 고쳤어 이제-물살일 없을 거야 도둑이라 생각했던-낭자의 정체는-감사합니다-그렇게 악의적인 생각은 없었다는-오해가 풀리게 되자-단어는 더욱 적극적으로 자신의 객주를-홍보하게 되는데-과거 객 전문 보행객주-이화원 그저 손님을 찾았을 뿐인데-나무꾼이 되었습니다-하지만 생각만큼 쉽게 낫기는 필살기를-쓰기로 하는데-[음악]-말이 끝나기로는 통금의 종소리 강산은-이곳에서 먹을 수밖에 없었죠 한-냥 닷전입니다-방금 안녕이라고-묵을 곳은 여기뿐이고 그렇게-이화원으로 들어가는게 가까이 다가오는-아직은 목소리-이리도 보는군요-바로 육봉달 박휘순의 손길에서 구해준-시열이었죠 시열은 선비답게 말합니다-며칠만 좀 묻게 해주시오 지금은 가진-돈이 없네만 추후에 반값을 꼭-마련하도록 하겠어 하지만-외상<br />&nbsp;절대<br />&nbsp;4절<br />&nbsp;아주 독감이 퉁퉁 부은 것까지-딱 하루만입니다 그렇게 각자 연으로-3명의 선비가 이어언에 모이게 되는데-참<br />&nbsp;당부 드릴게 있습니다-별채에는 가지 말아주셔요 그것만-지켜주시면 됩니다 알겠어 선배님도-약속해 주시어요-그래야겠어 항간에 떠도는 여원의-귀신이 출몰한다는 소문 그날밤 강사는-이곳에서 충격적인 정보를 듣게 되는데-내 옆방 묶는 66호라고 하네이-선악으로써 자네에게 알려줄 것이 많을-뜻하여<br />&nbsp;평소엔 정수가 세-명이면<br />&nbsp;엄청 저렴한 것이지 중촌에서는-에 결국 초특가 바가지에다가-다음날 아침-선미님 이번에도 그저 구멍난 문이-신경 쓰여 들어간 강산이 묻고 있던-방<br />&nbsp;목검 하나가 단어의 눈에 들어오게-되면서<br />&nbsp;[음악]-고<br />&nbsp;데기 하지만 또다시 강산의 눈에 들어-궤도의 모습 그런-가장 어리고 예쁠 때 결혼을 하겠지-조건 좋은 남자를 골라서 또다시-가까이서 본 강산의 모습에 나대기-시작하는 타노의 심장 말을 하지-네<br />&nbsp;그래도 탐났으면-탐라면요 숨기려고-걱정 마셔요-탐나서 온 것은 아니니 도둑은-아니다네-제가 어딜 봐서 사기꾼인가 뭐야-방세가 얼만지 똑똑히 들었거든 어떻게-하면 보름이 하루가 되지 후면-보름동안 계속 보시죠 그럼 알게 될-겁니다 이화원을 진가를 뭐-눈썰미도 소변했는데-청소라고 똑부러지게 할까-뭐래셨<br />&nbsp;구나 부문인데 한번 해드릴까요-똑 부러지게-청소요 뭐 하루라도 풀어드릴까 봐요-웃을 때 올라가던 입꼬리 한편-공항에서-형과 동생 모드로 죽이고 얻게 된-지금의 왕자를 이은 아들을 낳기를-원하며 출산 준비가 진행 중이었습니다-하지만<br />&nbsp;[음악]-한끼가 보이진 않습니다-스님의 영정을-억지로 꺼낼 수는 없어 옵니다-그렇게 후궁바뀌는 위험한 계획을-세우게 되는데-지금의 자리를 지키기 위해서 자신을요-후사가 필요했지만-원자는 태어나지 않고 있었습니다 전화-천사의 소리에 귀가 호가가옵니다-그렇게 오직 권력을 탐했던 왕 이창은-주와 세계 미쳐 진정한 폭군이 되었죠-내 다른 것도 강시켜주랴 전화-[음악]-좌상 신호 넣어 싸웁니다-또한 그가 가장 두려워하는 것이-있었으니-석달 전에-폐쇄자 이평의 기일에-묘소를 찾은 젊은이가 있었어-이설이라 생각되어 급히 쫓다가 바로-13년 전 유일하게 죽이지 못한-폐쇄손이 있어요-그렇게 과거 시험으로 많은 선비들이-부리던 틈을 타 14년 만에 한양의-모습을 드러낸 이서를 찾기 위해-좌회전은 한성구를 움직여 이서를-추적하기로 합니다-종촌의 객주와 주막-기반까지 샅샅이 뒤지고 있사옵니다 두-번의 실수는 없어야 할 것이에요 말씀-[음악]-낮추시지요 이렇게 선비들이 머물 수-있는 중촌에 객주와 주막을 직접-찾아가 상선을 통해 확인하기로 하는데-명단은 부영가게 머무는 선비님들의-신상을 적어두었사옵니다 그렇게 궁-밖으로 이서를 차는 한편 아내입니다-여기 스토리상 유력한 이설 용의자로-보이는 두 선비-에헤이 선비님도-앞날이 궁금하시면요 새책방이 아니라-무당집을 가셔야죠-어느 쪽이 궁금하시오 조선의 미래-본인의 미래 조선의 미래에서 조선의-백성의 어찌 자유를 수 있겠어 그-검으로 무가전문 합격할 수 있겠어-되지 않고도이기는 법을 찾는 중이요-그런 법이 있어서 아니 그런 세상이-있어 없어-해서 살아보고 싶소 나라를 걱정하는-선비의 마음인지 또 다른 마음인지-모른 채 돌아가는 길 이거 너무 싫어-무슨 기박이 아기가 있다고 유아는 한-여인을 보고 지나칠 수 없었습니다-그렇게 유아 강사는 부영광으로 향하게-되는데 이번-달 반값에서 제하겠습니다-네 번째 가부 검색-잠깐<br />&nbsp;무슨 적당이에요-그런 거 없는데-장난 결국 두 사람이 이곳에 온-목적을 단어와 시월에게 말하게 되고-내 사람 모두가 터져 그들이 데려갔던-아이를 찾아보기로 하지만 그녀의-말과는 달리 어디에도 보이지 않는-아이 그때 수상한 여인이 나오던 방을-확인해 보니-정말 그곳엔 갓난아기가 있었습니다-단어는 곧장 아이를 데리고 이곳을-빠져나가려 하지만-1초 만에 그들에게 발각되고 마는데-이냐<br />&nbsp;단어의 눈앞에서 순식간을 제압당한-물이 그리고-누가 봐도 누군지 알 것 같은 복면에-사나이는 단어가 탐냈던 목검을 꺼내-무사히 이곳을 빠져나가-지못했다-뒤지기 싫으면 애부터 내놔-그리고 역시나 당연하게 복면의 남자는-모두가 예상했던 남자 강산이었죠-이렇게 이곳에서 아이를 데리고 무사히-빠져나갈 수 있었습니다-찾았구나 가자 가자-정말 고맙습니다-하지만 누군가를 도달하는 기쁨도 잠시-그날 밤 이어원으로 돌아와 보니-집안의 물건들로 딱지를 치는 상황-뒤에 오셨네-무슨 짓이냐 묻지 않소-잠오정과 것들이-빛 받으러 왔지 뭐하러 왔겠습니까-없던 비즈 하늘에서 뚝 떨어지기라도-한단 말이여 지금의-시세로 자그마치 얼마인지 모릅니다-아시는 분 알려주세요 하지만-절대 출입하지 말라는 경고에도 그딴-괴수를 믿어-되는지 안 되는지 그렇게 겁도 없이-단어를 뿌리치게 됐는데-감히 니에게 손을 대느냐-안 된다잖아 꺼져 괜히 후회하지 마-어느새 똘똘 뭉친-자들은<br />&nbsp;어느새 이하원 앞에 도착해 있었습니다-이사를 보신 적이 있사옵니까-딱 한 번-다시는 마마를 못 볼 줄 알았는데-그간 무탈하셨나이까-홀로부터라 했지 어찌 그런 말씀을-하십니까-새 손 마마께서는 조선의 유일한 힘이-있습니다 일단 안으로 드시지요-나를 만나자고요 13년 전부터 줄곧-이소를 죽이기 위해 쫓아왔던 추적자는-그날 밤도 지금과 똑같이 상선을-이용해 이서를 눈앞에서 마주했었습니다-송구합니다 세수한 마마 남은 자라도-살아야 하지 않겠습니까-광명역<br />&nbsp;폐쇄성 이사를-즉시 참아라 부사되는 검으로 세상을-바꾼다들었어-날지도 바뀌는 것이 대체 무엇이오-시대도<br />&nbsp;결국<br />&nbsp;저물 것이요-죽음의 문턱에선 미소를 구하기 위해-나타나 의문의 파수꾼-[음악]-그는 바나나의 칼자루로 이사를 쫓는-추격자들을 전부 몰살시켰고 동시에-장사와의 분노와 복수를 사게-되었습니다-그가 볼살한 추격자들 중에는-장타의 아들이 있었기 때문이었죠-오직 왕명이라는 이유로 그동안 이설을-향했던 칼은-그날 이후 변하게 되었습니다 자신의-아들을 죽인 원수-자신에게-패배를 안겨다준 이설과 파수꾼을 향한-복수의 칼로 말이죠-이렇게 오직 이설과 파수꾼을 찾아-죽이겠다는 일념 하나로 지금의 3명의-선비가 있는 이와 하나에 도착했습니다-한성부에서 나왔어 한양 객주의 역적의-숨어 들어왔다는-고변이<br />&nbsp;있어서는 상선을 마주하게 되는데-어떤가<br />&nbsp;여긴<br />&nbsp;없습니다 확실한가-제가 믿을 수 없는 상선의 진화-갸는 직접 확인해 보기로 합니다-[음악]-호패를 좀 보여주시겠어 그런데 유현은-선뜻 토피를 꺼내지 못하는데 보여주지-못할 연유라도 있나-나주 정수라 혹 부친의 함짜가 세상을-지내셨던-정윤대감이옵니다 그때-인경이 울린지 한참인데-공무원 밤낮이 없습니까-강산에 한마디가 태화의 심리를-건드리게 되면서 모든 시선이 강산에게-쏠리게 되고-호패를 확인해 보니-강사는 사실-[음악]-현감 이름이 감기가 배웁니다 저희-집안에 어르신이지요-그렇구먼-의심을 사기엔 너무나 완벽한 신분-적이 저도 있는데 김시열입니다-김홍익대 가면 14번 15번째-분명 이곳에는 없다던 상선의 말과는-다르게 3명에서 미를 보는 눈빛이-심상치 않은데-전화를 다 뒤졌는데이 설에 머리칼-하나를 못 찾았어 좀 기다려 보시지요-곧<br />&nbsp;잡힐 것이니 상선-난 쓰임이 없는 자를 곁에 두지를-않네 잘라버리지</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:26px">1-2회 요약] 꽃선비들과 동거하면 벌어지는 일&hearts; 신예은에 치이고 선비들 비주얼에 두근두근&nbsp;</span></strong></p><p>&nbsp;</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/CoTHTtE3a44" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><a tabindex="-1" href="https://www.youtube.com/@SBSdrama.official"><img id="img" draggable="false" alt="" width="40" src="https://yt3.ggpht.com/ytc/AL5GRJX8WqyRU_1xr1-aixCc0gCkVwKdxjY8JPmeg--7MA=s48-c-k-c0x00ffffff-no-rj" /></a></p><p><a spellcheck="false" dir="auto" href="https://www.youtube.com/@SBSdrama.official">SBS Drama</a></p><p>&nbsp;</p><p><span style="font-size:28px"><strong>SBS Drama 자막</strong></span></p><p>&nbsp;</p><p>너 누구야-이름을 묻는 것은요-당연하지-상중에 숨어든 건 너잖아-[음악]-못 봤는데요-가까이 오지 마세요 우리 복실이가-워낙 사나워서 먹을 때 건들면 사람도-뭅니다-[음악]-다음에 다시 보면-알려주마-볼<br />&nbsp;내 이름-[음악]-합격해-우리 모두 다 같이 이화원-은<br />&nbsp;합격입니다-손님들 이화원으로 오시지요-[음악]-이 집이-[음악]-대체 얼마만 들겠어 말해 보시오-얼마면-날<br />&nbsp;서방으로 봐주겠어-낭자-낭자-[음악]-입니다-선빈님 해가 벌써 중천입니다-되게 안 가십니까-매형이나 아침에도 예쁘구나-팔 떨기에 작약 같을까-[음악]-손 내밀 때 잡으시오 나만한 남자-없다니까-이게-돌았나 내가 종종 듣는 소리긴 한데-쳐돌진 못했구려이 자식이-어쭈-내주먹을-피했어이 자식이-야<br />&nbsp;넌 오늘 제삿날이야-[음악]-아이고-신고받고 오셨나 보네-일찍 주무시지-벌써 저쪽으로 다 내뺐어 오늘-기류에서-술 드셨죠-그렇습니다-이분은 아닙니다 이분은 절 구해주신-난 괜찮았어 오해가 있을까요-왕건이다-무과생이요-거머리요-묵을 빵은 정하셨소-[음악]-부끄러움을 모르는군-한양 여인들은 다 이런가-귀하신 분 같은데 이런 누추한 주막에-먹기엔 불편하지 않으실지-[박수]-[박수]-[음악]-이제 오느냐-유아 오라버니-근데 뭐 입고 계셨어요-그냥도 넣어-넌 또 아침부터 어딜 다 용기냐 집에-있자니-몸도 찌뿌둥하고 해서-산책 좀 했습니다-[음악]-그럼 객주가 있는데 자기 집으로-오라고 그 짐을 가지고 갔어요-너였구나-도둑놈-[박수]-[음악]-[박수]-과거 객 전문 보행객주 이화원-전원급제 소수정의-숙식 제공-참<br />&nbsp;당부 드릴게 있습니다-어디든 둘러보셔도 좋은데-별채엔 가지 말아주셔요 그것만-지켜주시면 됩니다-그래야겠어-이곳이-[음악]-급제생활 대법에 출연해 명문객-출하던데 그게 잠이요-문 앞에 방해 아주 큼지막하게 붙어-있는 걸 내 봤어-급재생이 아 있지요 그게 누구야 저희-아버지요-역시 아버님도 낭자 같이 아주 훌륭한-분이셨구요-또 누가 있어-아 없어 그 한 명이 전부라네-뭐 물론 단호 아버지 살아계실-적이에요-종종 유생들이 드나들었지-나도 그 유생 중 하나였고-그때야 갭투가 되기 전이었으니-시작하게 된 것입니까 좀 의하긴-하지요-양반 가요식이 객주의를 한다는 것이-뭐 살려면 별 수 있었겠나-부친이 남긴 거라곤 딸랑이었는데-[음악]-아내를 위해서-아직인가-송구하옵니다-좌상도 많이 늙었어-그 어린놈 하나 여태 못 잡고 불미난-소신의 죄이옵니다-엄히 꾸짖어 주시옵소서-이 사람-어찌 되고 있는 중촌의 객주와 주막-기반까지 사서 뒤지고 있사옵니다-두 번의 실수는 없어야 할 것이요-오셨습니까 반가는-조선의 어떠한가-아닙니다-눈매며 입매도-같은 것이 없어요-[음악]-[박수]-내면-시켜준 본가의 몸종이야-그런데-무슨 일이냐 아기를-데려갔습니다-어디에다 쓰려고 오늘은 배가 쫙쫙-돋는구려 그거 미안하게 됐어-[음악]-이번달 반값에서 재야겠습니다-그렇게 안 봤는데 왜요-아 내 말은-자네들이 어찌 이런 곳에-여기 어딘가-믿듯이 끌려 아기가 있어-[음악]-아이가 없어졌어요-[음악]-괜찮아 다친 데는-정말 고맙습니다-은혜를 어찌 갚아야 할지-당분간 몸을 좀 피했거라-몰래 데려왔으니-은자 백야-아이고이를 어쩌나 그-큰 돈을 갚으려면-객주를 팔아야 할 텐데-[웃음]-귀여운이 마지막 집입니다-그렇다면-거기에 폐쇄손이 이설이 있을지도-모르겠구나 반성구에서 나왔어-한양 객주의 역적에 숨어 들어왔다는-고변이 있어서-코펠을 좀 보여주시겠소-나주 정수라-혹 붙이네 함자가 세상을 지내셨던-정윤대감입니다-한양 사람은 아니고-전엔 어디서 왔는가-한 정도의 끌려가야 말할 텐가-제천에서 왔으나-최찬이라-끄긴 진주 강씨 집성촌이 있다 하지-최찬-현감 이름이 어르신이지요-[음악]-그렇구만-저기-저도 있는데-그대는 이름이 김시열입니다-김홍익대 가면 14번 아니다 다섯-번째인가-어쨌든 서지하겠지 예-그렇지요-좀 기다려 보시지요-곧<br />&nbsp;잡힐 것이니-누가 그리 큰 돈을 빌려준 것이오-아셔도 하실 수 있는게 없으실 텐데-말해 보쉬 언론-장태와 판관 날이십니다-지금 뭐라고 하셨어 한성부-장판관 날이라 했습니다-[음악]-물증이 필요하다 예-말은 뱉으며 사라지기 마련인데-어찌 보이지도 않는 것을 믿겠나이까-네 아비가 직접 써준 차용증이다-캘채는 맞으나-어찌 없는 살림에 후악들을 돌본다면-내게 빌려간 것이다-이제 너도 어린아이가 아니니-리아비의 빚을 갚아야 하지 않겠느냐-멈춰라-바뀐 마마님의 모친이시옵니다-산후조양을 위해 놔주셨사옵니다-잠깐-[음악]-전화-바뀌니 무사히 해산하였다 하옵니다-바이는-아이는 무탈하더냐 예 전화-감축드리옵니다-왕자 하기 쉬옵니다-[음악]-은자 백냥이 사주겠어-[음악]-내게 시집만 온다면-[음악]-그만 좀 튕기지 누가 나만조차 그래-뭐야 넌 그만하시오-넌 그때 그놈-여태 손버릇을 못 고쳐-암만 세상이 변해도-혼인은 사람이랑 하는 것은 왜 갑자기-말을 놓지-멍멍이한테 존대하는 사람도 있나-오<br />&nbsp;멍멍이-결국엔 빼앗길 겁니다-이화원보다 더 큰 것을 내어줄 수-있는게 아니라면요-[음악]-큰<br />&nbsp;[음악]-거 큰 거 싫어-잘 보게 여기가 마지막이니-난리도 아니었다니까요-방을 막 헤집고 선비들 호패란 호패도-지지는데 이화원에도 왔었네-헌데 그자는 누군가 한성부 공간으로는-안 보였는데-함께 다니는 것 같더라고-그분은 예전에-임금 모시던 내관이랬나-바보도 아닌 처자를 보쌈하다가 거기-내 충고하는데 건너지 마시오-이 길이 황천길이거든-별 미친놈을 다 보겠네-야 얼른-내가 누군지 아느냐 부활을 어찌-감당하려고-[음악]-내 다시는-낭자를 찾지 않을 테니-둘이 그분께 말씀을 잘해주시고 내-그분이 너무나도 두렵소-그분 오라버니-혹시-오라버니셔요 뭐가-옹생원이요-대체 겁을 얼마나-누군데-정말-오라버니가 그러신 거 아닙니까 내-남의 운사에 관여할 정도로 한가하지-않아서-저자에서 다 보셨군요-혼사 얘기를 하시는 걸 보니-[음악]-전원 급제 아시죠 저희 명문객주-이화원의 위신이 오라버니 어깨에-달렸습니다-웬만하면-붙어여 두 봉고다가 손가락이 느려지게-했더라-이걸-직접 만들었어-[음악]-시각이 다 되었습니다-시작하시길-[음악]-[음악]-[음악]-뭔 일이 있나-어딜 가는 거지-대체 여기가 어디게-[음악]-제가-[음악]-찾아드리면 되겠습니까-[음악]-닿는이가 낸 줄 알고-그분을-그리 부른다지요-옥진-사라진 폐쇄성-이설-[음악]</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-24 07:56:29 실전! Querydsl - 4. 기본문법(Q-Type 활용,검색 조건 쿼리, 결과 조회,정렬,페이징,집합,조인,페치 조인,서브 쿼리,Case 문,상수, 문자 더하기) http://macaronics.net/index.php/m01/spring/view/2093 2093 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[4] 기본 문법</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>9.시작 - JPQL vs Querydsl</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30122&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30122&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>테스트 기본 코드</strong></p><pre class="brush:as3;">package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import study.querydsl.entity.QMember; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import static org.assertj.core.api.Assertions.*; import static study.querydsl.entity.QMember.*; @SpringBootTest @Transactional public class QuerydslBasicTest { @PersistenceContext EntityManager em; @BeforeEach public void before() { Team teamA = new Team(&quot;teamA&quot;); Team teamB = new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1 = new Member(&quot;member1&quot;, 10, teamA); Member member2 = new Member(&quot;member2&quot;, 20, teamA); Member member3 = new Member(&quot;member3&quot;, 30, teamB); Member member4 = new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member4); } }</pre><p>&nbsp;</p><p>지금부터는 이 예제로 실행</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Querydsl vs JPQL</strong></p><pre class="brush:as3;">Querydsl vs JPQL @Test public void startJPQL() { //member1을 찾아라. String qlString = &quot;select m from Member m &quot; + &quot;where m.username = :username&quot;; Member findMember = em.createQuery(qlString, Member.class) .setParameter(&quot;username&quot;, &quot;member1&quot;) .getSingleResult(); assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;); } @Test public void startQuerydsl() { //member1을 찾아라. JPAQueryFactory queryFactory = new JPAQueryFactory(em); QMember m = new QMember(&quot;m&quot;); Member findMember = queryFactory .select(m) .from(m) .where(m.username.eq(&quot;member1&quot;)) //파라미터 바인딩 처리 .fetchOne(); assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>EntityManager 로 JPAQueryFactory 생성</strong></p><p><br />Querydsl은 JPQL 빌더</p><p><br />JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)</p><p><br />JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>JPAQueryFactory를 필드로</strong></span></p><pre class="brush:as3;">package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import study.querydsl.entity.QMember; import study.querydsl.entity.Team; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import static org.assertj.core.api.Assertions.*; import static study.querydsl.entity.QMember.*; @SpringBootTest @Transactional public class QuerydslBasicTest { @PersistenceContext EntityManager em; JPAQueryFactory queryFactory; @BeforeEach public void before() { queryFactory = new JPAQueryFactory(em); //&hellip; } @Test public void startQuerydsl2() { //member1을 찾아라. QMember m = new QMember(&quot;m&quot;); Member findMember = queryFactory .select(m) .from(m) .where(m.username.eq(&quot;member1&quot;)) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를</p><p>생성할 때 제공하는 EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은</p><p>EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는</p><p>걱정하지 않아도 된다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>10.기본 Q-Type 활용</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30123&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30123&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Q클래스 인스턴스를 사용하는 2가지 방법</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">QMember qMember = new QMember(&quot;m&quot;); //별칭 직접 지정 QMember qMember = QMember.member; //기본 인스턴스 사용</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>기본 인스턴스를 static import와 함께 사용</strong></p><p>&nbsp;</p><pre class="brush:as3;">import static study.querydsl.entity.QMember.*; @Test public void startQuerydsl3() { //member1을 찾아라. Member findMember = queryFactory .select(member) .from(member) .where(member.username.eq(&quot;member1&quot;)) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">spring.jpa.properties.hibernate.use_sql_comments: true </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#16a085"><span style="font-size:18px"><strong>참고: 같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용하자</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>11.검색 조건 쿼리</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30124&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30124&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>기본 검색 쿼리</strong></p><pre class="brush:as3;">@Test public void search() { Member findMember = queryFactory .selectFrom(member) .where(member.username.eq(&quot;member1&quot;) .and(member.age.eq(10))) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>검색 조건은 .and() , . or() 를 메서드 체인으로 연결할 수 있다</strong></p><p>&nbsp;</p><p><strong>참고: select , from 을 selectFrom 으로 합칠 수 있음</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>JPQL이 제공하는 모든 검색 조건 제공</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">member.username.eq(&quot;member1&quot;) // username = &#39;member1&#39; member.username.ne(&quot;member1&quot;) //username != &#39;member1&#39; member.username.eq(&quot;member1&quot;).not() // username != &#39;member1&#39; member.username.isNotNull() //이름이 is not null member.age.in(10, 20) // age in (10,20) member.age.notIn(10, 20) // age not in (10, 20) member.age.between(10, 30) //between 10, 30 member.age.goe(30) // age &gt;= 30 member.age.gt(30) // age &gt; 30 member.age.loe(30) // age &lt;= 30 member.age.lt(30) // age &lt; 30 member.username.like(&quot;member%&quot;) //like 검색 member.username.contains(&quot;member&quot;) // like &lsquo;%member%&rsquo; 검색 member.username.startsWith(&quot;member&quot;) //like &lsquo;member%&rsquo; 검색 ...</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>AND 조건을 파라미터로 처리</strong></span></p><pre class="brush:as3;">@Test public void searchAndParam() { List &lt; Member &gt; result1 = queryFactory .selectFrom(member) .where(member.username.eq(&quot;member1&quot;), member.age.eq(10)) .fetch(); assertThat(result1.size()).isEqualTo(1); }</pre><p>&nbsp;</p><p>where() 에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨<br />이 경우 null 값은 무시 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있음 뒤에서 설명</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>12.결과 조회</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30125&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30125&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong>fetch() </strong>: 리스트 조회, 데이터 없으면 빈 리스트 반환</p><p><br /><strong>fetchOne() </strong>: 단 건 조회</p><p>결과가 없으면 : null&nbsp; 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException</p><p><br /><strong>fetchFirst()</strong> : limit(1).fetchOne()</p><p><br /><strong>fetchResults()</strong> : 페이징 정보 포함, total count 쿼리 추가 실행</p><p><br /><strong>fetchCount()</strong> : count 쿼리로 변경해서 count 수 조회<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void resultFetch(){ //List List&lt;Member&gt; fetch = queryFactory .selectFrom(member) .fetch(); //단건 Member findMember1 = queryFactory .selectFrom(member) .fetchOne(); //처음 한 건 조회 Member findMember2=queryFactory .selectFrom(member) .fetchFirst(); //페이징에서 사용 QueryResults&lt;Member&gt; results =queryFactory .selectFrom(member).fetchResults(); results.getTotal(); List&lt;Member&gt; content=results.getResults(); //count 쿼리로 사용 long count=queryFactory .selectFrom(member) .fetchCount(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>13.정렬</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30126&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30126&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> /** * 회원 정렬 순서 * 1. 회원 나이 내림차순(desc) * 2. 회원 이름 올림차순(asc) * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last) */ @Test public void sort(){ em.persist(new Member(null, 100)); em.persist(new Member(&quot;member5&quot;, 100)); em.persist(new Member(&quot;member6&quot;, 100)); List&lt;Member&gt; result = queryFactory.selectFrom(member) .where(member.age.eq(100)) .orderBy(member.age.desc(), member.username.asc().nullsLast()) .fetch(); Member member5 = result.get(0); Member member6 = result.get(1); Member memberNull = result.get(2); Assertions.assertThat(member5.getUsername()).isEqualTo(&quot;member5&quot;); Assertions.assertThat(member6.getUsername()).isEqualTo(&quot;member6&quot;); Assertions.assertThat(memberNull.getUsername()).isNull(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>desc() , asc() : 일반 정렬</strong></span></p><p><br /><span style="font-size:16px"><strong>nullsLast() , nullsFirst() : null 데이터 순서 부여</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>14.페이징</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30127&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30127&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>다음 참조 :</p><p><a href="https://macaronics.net/index.php/m01/spring/view/2081#">실전! 스프링 데이터 - JPA -[5] 확장 기능 ★ (사용자 정의 리포지토리 구현 , 확장 - 도메인 클래스 컨버터, 확장 - 페이징과 정렬)</a></p><p><a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/2081">https://macaronics.net/index.php/m01/spring/view/2081</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>조회 건수 제한</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">@Test public void paging1() { List &lt;Member&gt; result = queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) //0부터 시작(zero index) .limit(2) //최대 2건 조회 .fetch(); assertThat(result.size()).isEqualTo(2); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>전체 조회 수가 필요하면?</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">@Test public void paging2() { QueryResults &lt;Member&gt; queryResults = queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) .limit(2) .fetchResults(); assertThat(queryResults.getTotal()).isEqualTo(4); assertThat(queryResults.getLimit()).isEqualTo(2); assertThat(queryResults.getOffset()).isEqualTo(1); assertThat(queryResults.getResults().size()).isEqualTo(2); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>&gt; 주의: count 쿼리가 실행되니 성능상 주의!</strong></p><p>&nbsp;</p><p><br />&gt; 참고: 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만,</p><p><br />count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두</p><p><br />조인을 해버리기 때문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면,&nbsp; &nbsp; count 전용 쿼리를 별도로 작성해야 한다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>15.집합</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30128&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30128&amp;tab=curriculum</a></p><p>&nbsp;</p><pre class="brush:as3;"> /** * JPQL * select * COUNT(m), //회원수 * SUM(m.age), //나이 합 * AVG(m.age), //평균 나이 * MAX(m.age), //최대 나이 * MIN(m.age) //최소 나이 * from Member m */ @Test public void aggregation(){ List&lt;Tuple&gt; result = queryFactory.select( member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min() ) .from(member).fetch(); Tuple tuple =result.get(0); Assertions.assertThat(tuple.get(member.count())).isEqualTo(4); Assertions.assertThat(tuple.get(member.age.sum())).isEqualTo(100); Assertions.assertThat(tuple.get(member.age.avg())).isEqualTo(25); Assertions.assertThat(tuple.get(member.age.max())).isEqualTo(40); Assertions.assertThat(tuple.get(member.age.min())).isEqualTo(10); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>JPQL이 제공하는 모든 집합 함수를 제공한다</p><p>.<br />tuple은 프로젝션과 결과반환에서 설명한다.</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>GroupBy 사용</strong></span></p><p>&nbsp;</p><pre class="brush:as3;"> /** * 팀의 이름과 각 팀의 평균 연령을 구해라 * @throws Exception */ @Test public void group() throws Exception{ List&lt;Tuple&gt; result = queryFactory.select(team.name, member.age.avg()) .from(member).join(member.team, team) .groupBy(team.name).fetch(); Tuple teamA=result.get(0); Tuple teamB=result.get(1); Assertions.assertThat(teamA.get(team.name)).isEqualTo(&quot;teamA&quot;); Assertions.assertThat(teamA.get(member.age.avg())).isEqualTo(15); //(10+20)/2 Assertions.assertThat(teamA.get(team.name)).isEqualTo(&quot;teamB&quot;); Assertions.assertThat(teamA.get(member.age.avg())).isEqualTo(35); //(30+40)/2 }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>groupBy , 그룹화된 결과를 제한하려면 having</p><p><br />groupBy(), having() 예시</p><p>&nbsp;</p><pre class="brush:as3;"> &hellip; .groupBy(item.price) .having(item.price.gt(1000)) &hellip;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>16.조인 - 기본 조인</strong></span></span></p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30129&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30129&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>기본 조인</strong></span></p><p><br /><span style="font-size:16px"><strong>조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">join(조인 대상, 별칭으로 사용할 Q타입) </pre><p>&nbsp;</p><p><strong>기본 조인</strong></p><pre class="brush:as3;"> /** * 팀 A 에 소속된 모든 회원 */ @Test public void join(){ List&lt;Member&gt; result = queryFactory .selectFrom(member) .join (member.team,team) //.leftJoin(member.team, team) .where(team.name.eq(&quot;teamA&quot;)) .fetch(); Assertions.assertThat(result).extracting(&quot;username&quot;) .containsExactly(&quot;member1&quot;, &quot;member2&quot;); } </pre><p>&nbsp;</p><p><strong>join() , innerJoin() </strong>: 내부 조인(inner join)</p><p><br /><strong>leftJoin() :</strong> left 외부 조인(left outer join)</p><p><br /><strong>rightJoin() </strong>: rigth 외부 조인(rigth outer join)</p><p><br /><span style="font-size:16px"><strong>JPQL의 on 과 성능 최적화를 위한 fetch 조인 제공 다음 on 절에서 설명</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>세타 조인</strong></span></p><p><br /><span style="font-size:20px"><strong>연관관계가 없는 필드로 조인</strong></span></p><pre class="brush:as3;"> /** * 연관 관계가 없는 것도 조인이 가능하다. * 세타 조인 * 회원의 이름이 팀 이름과 같은 회원을 조회 */ @Test public void theta_join(){ em.persist(new Member(&quot;teamA&quot;)); em.persist(new Member(&quot;teamB&quot;)); /** 기존과 다르게 from 절에서 두개 테이블을 묶어줬다. * 모든 팀테이블 조인 세타 조인 =&gt; member member0_ cross * selec member1 from Member member1, Team team where member1.username = team.name * **/ List&lt;Member&gt; result = queryFactory.select(member) .from(member, team) .where(member.username.eq(team.name)).fetch(); Assertions.assertThat(result) .extracting(&quot;username&quot;) .containsExactly(&quot;teamA&quot;, &quot;teamB&quot;); }</pre><p>&nbsp;</p><p><span style="color:#c0392b"><strong>from 절에 여러 엔티티를 선택해서 세타 조인</strong></span></p><p><br /><strong><span style="color:#2980b9">외부 조인 불가능</span></strong> <strong><span style="color:#c0392b">다음에 설명할 조인 on을 사용하면 외부 조인 가능</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>17.조인 - on절</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30130&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30130&amp;tab=curriculum</a></p><p>&nbsp;</p><p><strong><span style="font-size:20px">ON절을 활용한 조인(JPA 2.1부터 지원)</span></strong></p><p><br /><strong>1. 조인 대상 필터링</strong></p><p><br /><strong>2. 연관관계 없는 엔티티 외부 조인</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>1. 조인 대상 필터링</strong></span></p><p>&nbsp;</p><p>예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회</p><pre class="brush:as3;"> /** * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모드 조회 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name */ @Test public void join_on_filtering(){ List&lt;Tuple&gt; result = queryFactory .select(member,team) .from(member) //.leftJoin(member.team, team) .join(member.team, team) //.on(team.name.eq(&quot;teamA&quot;)) //inner join 경우 같다 on 절을 쓰나 where 절을 쓰나 동일하다. .where(team.name.eq(&quot;teamA&quot;)) .fetch(); for(Tuple tuple :result){ System.out.println(&quot;tuple = &quot; + tuple); } /** innerjoin 경우 다음 방법 을 추천 **/ queryFactory .select(member,team) .from(member) .join(member.team, team) .where(team.name.eq(&quot;teamA&quot;)) .fetch(); } </pre><p>&nbsp;</p><p>결과</p><pre class="brush:as3;">t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)] t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)] t=[Member(id=5, username=member3, age=30), null] t=[Member(id=6, username=member4, age=40), null]</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>참고:</strong></span></span> <span style="color:#c0392b">on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 <strong>내부조인(inner join)</strong>을 사용하면,</span></p><p><br /><span style="color:#c0392b"><strong>where 절에서 필터링 하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때,</strong></span></p><p><br /><span style="color:#c0392b"><strong>내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>2. 연관관계 없는 엔티티 외부 조인</strong></span></p><p>&nbsp;</p><p><strong>예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인</strong></p><p>&nbsp;</p><pre class="brush:as3;"> /** * 2. 연관관계 없는 엔티티 외부 조인 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name */ @Test public void join_on_no_relation(){ em.persist(new Member(&quot;teamA&quot;)); em.persist(new Member(&quot;teamB&quot;)); em.persist(new Member(&quot;teamB&quot;)); //주의 .leftJoin(team) 엔티티 하나만 들어갔다. List&lt;Tuple&gt; result = queryFactory .select(member,team) .from(member) .leftJoin(team) .on(member.username.eq(team.name)) .fetch(); for(Tuple tuple :result){ System.out.println(&quot;tuple = &quot; + tuple); } }</pre><p>&nbsp;</p><p>하이버네이트 5.1부터 on 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물론<br />내부 조인도 가능하다.</p><p><br /><span style="font-size:18px"><strong><span style="color:#c0392b">주의! 문법을 잘 봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.</span></strong></span></p><p><br /><span style="font-size:16px"><strong><span style="color:#c0392b">일반조인:&nbsp; </span><span style="color:#2980b9">&nbsp;leftJoin(member.team, team)</span></strong></span></p><p><br /><span style="font-size:16px"><strong><span style="color:#c0392b">on조인:</span>&nbsp; <span style="color:#2980b9">from(member).leftJoin(</span><span style="color:#16a085">team</span><span style="color:#2980b9">).on(xxx)</span></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>출력 쿼리 결과</strong></p><pre class="brush:as3;">/* select member1, team from Member member1 left join Team team with member1.username = team.name */ select member0_.member_id as member_i1_1_0_, team1_.team_id as team_id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ left outer join team team1_ on ( member0_.username=team1_.name )</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>결과</strong></p><pre class="brush:as3;">t=[Member(id=3, username=member1, age=10), null] t=[Member(id=4, username=member2, age=20), null] t=[Member(id=5, username=member3, age=30), null] t=[Member(id=6, username=member4, age=40), null] t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)] t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>18.조인 - 페치 조인</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30131&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30131&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활용해서 연관된 엔티티를 SQL 한번에</strong></p><p><br /><strong>조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>페치 조인 미적용</strong></span></p><p>&nbsp;</p><p><strong>지연로딩으로 Member, Team SQL 쿼리 각각 실행</strong></p><p>&nbsp;</p><pre class="brush:as3;"> @PersistenceUnit EntityManagerFactory emf; @Test public void fetchJoinNo(){ em.flush(); em.clear(); Member finndMember =queryFactory .selectFrom(member) .where(member.username.eq(&quot;member1&quot;)) .fetchOne(); boolean loaded=emf.getPersistenceUnitUtil().isLoaded(finndMember.getTeam()); Assertions.assertThat(loaded).as(&quot;페치 조인 미적용&quot;).isFalse(); } </pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>페치 조인 적용</strong></span></p><p><strong>즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회</strong></p><pre class="brush:as3;"> @Test public void fetchJoinUse(){ em.flush(); em.clear(); Member finndMember =queryFactory .selectFrom(member) .join(member.team, team).fetchJoin() .where(member.username.eq(&quot;member1&quot;)) .fetchOne(); boolean loaded=emf.getPersistenceUnitUtil().isLoaded(finndMember.getTeam()); Assertions.assertThat(loaded).as(&quot;페치 조인 적용&quot;).isTrue(); } </pre><p>&nbsp;</p><p>=&gt;출력</p><pre class="brush:as3;"> /* select member1 from Member member1 inner join fetch member1.team as team where member1.username = ?1 */ select member0_.member_id as member_i1_1_0_, team1_.team_id as team_id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.team_id where member0_.username=?</pre><p>&nbsp;</p><p><span style="font-size:20px"><strong>사용방법</strong></span></p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.</strong></span></p><p>&nbsp;</p><p><strong>&gt; 참고: 페치 조인에 대한 자세한 내용은 JPA 기본편이나, 활용2편을 참고하자</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>★19.서브 쿼리&nbsp;★ </strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30132&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30132&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>서브 쿼리 eq 사용</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> /** * 나이가 가장 많은 회원 조회 */ @Test public void subQuery(){ QMember memberSub =new QMember(&quot;memberSub&quot;); List&lt;Member&gt; reseult = queryFactory. selectFrom(member) .where(member.age.eq( JPAExpressions.select(memberSub.age.max()) .from(memberSub) ) ).fetch(); Assertions.assertThat(reseult).extracting(&quot;age&quot;) .containsExactly(40); }</pre><p>&nbsp;</p><p><strong>출력 쿼리 결과=&gt;</strong></p><pre class="brush:as3;"> /* select member1 from Member member1 where member1.age = ( select max(memberSub.age) from Member memberSub ) */ select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.age=( select max(member1_.age) from member member1_ )</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>서브 쿼리 goe 사용</strong></span></span></p><pre class="brush:as3;"> /** * 나이가 평균 이상인회원 * lt &lt; * * loe &lt;= * * gt &gt; * * goe &gt;= */ @Test public void subQueryGoe(){ QMember memberSub =new QMember(&quot;memberSub&quot;); List&lt;Member&gt; reseult = queryFactory. selectFrom(member) .where(member.age.goe( JPAExpressions.select(memberSub.age.avg()) .from(memberSub) ) ).fetch(); Assertions.assertThat(reseult).extracting(&quot;age&quot;) .containsExactly(30,40); }</pre><p>&nbsp;</p><p><strong>출력 쿼리 결과=&gt;</strong></p><pre class="brush:as3;"> /* select member1 from Member member1 where member1.age &gt;= ( select avg(memberSub.age) from Member memberSub ) */ select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.age&gt;=( select avg(cast(member1_.age as double)) from member member1_ )</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:20px"><strong>서브쿼리 여러 건 처리 in 사용</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;"> @Test public void subQueryIn(){ QMember memberSub =new QMember(&quot;memberSub&quot;); List&lt;Member&gt; reseult = queryFactory. selectFrom(member) .where(member.age.in( JPAExpressions.select(memberSub.age) .from(memberSub) .where(memberSub.age.gt(10)) ) ).fetch(); Assertions.assertThat(reseult).extracting(&quot;age&quot;) .containsExactly(20,30,40); }</pre><p>&nbsp;</p><p><strong>출력 쿼리 결과=&gt;</strong></p><pre class="brush:as3;"> /* select member1 from Member member1 where member1.age in ( select memberSub.age from Member memberSub where memberSub.age &gt; ?1 ) */ select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.age in ( select member1_.age from member member1_ where member1_.age&gt;? )</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><span style="color:#8e44ad"><strong>select 절에 subquery</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">List &lt;Tuple&gt; fetch = queryFactory .select(member.username, JPAExpressions .select(memberSub.age.avg()) .from(memberSub) ).from(member) .fetch(); for (Tuple tuple: fetch) { System.out.println(&quot;username = &quot; + tuple.get(member.username)); System.out.println(&quot;age = &quot; + tuple.get(JPAExpressions.select(memberSub.age.avg()) .from(memberSub))); }</pre><p><span style="color:#2980b9"><strong>static import 활용</strong></span></p><pre class="brush:as3;"> /** * 셀렉트 절에서 서브쿼리 사용 */ @Test public void selectSubQuery(){ QMember memberSub =new QMember(&quot;memberSub&quot;); List&lt;Tuple&gt; result = queryFactory .select(member.username, select(memberSub.age.avg()) .from(memberSub)) .from(member) .fetch(); for(Tuple tuple: result){ System.out.println(&quot;tuple = &quot; + tuple); } }</pre><p><strong>출력 쿼리 결과=&gt;</strong></p><pre class="brush:as3;"> /* select member1.username, (select avg(memberSub.age) from Member memberSub) from Member member1 */ select member0_.username as col_0_0_, (select avg(cast(member1_.age as double)) from member member1_) as col_1_0_ from member member0_</pre><p>&nbsp;</p><pre class="brush:as3;">tuple = [member1, 25.0] tuple = [member2, 25.0] tuple = [member3, 25.0] tuple = [member4, 25.0]</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">★from 절의 서브쿼리 한계</span></strong></span></p><p>&nbsp;</p><p><strong>JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl</strong><br /><strong>도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도<br />하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong><span style="color:#c0392b">★&nbsp;from 절의 서브쿼리 해결방안</span></strong></span></p><p><br /><strong><span style="color:#2980b9"><span style="font-size:16px">1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)</span></span></strong></p><p><br /><strong><span style="color:#2980b9"><span style="font-size:16px">2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.</span></span></strong></p><p><br /><span style="font-size:16px"><span style="color:#2980b9"><strong>3. nativeSQL을 사용한다</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>20.Case 문 </strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30133&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30133&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>select, 조건절(where), order by에서 사용 가능</strong></span></p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>1) 단순한 조건</strong></span></p><pre class="brush:as3;"> @Test public void basicCase(){ List&lt;String&gt; result = queryFactory .select(member.age .when(10).then(&quot;열살&quot;) .when(20).then(&quot;스무살&quot;) .otherwise(&quot;기타&quot;) ) .from(member) .fetch(); for(String s :result){ System.out.println(&quot;s = &quot; + s); } }</pre><p>&nbsp;</p><p>쿼리결과</p><pre class="brush:as3;">select case when member1.age = ? 1 then ? 2 when member1.age = ? 3 then ? 4 else &#39;기타&#39; end from Member member1 * / select case when member0_.age=? then ? when member0_.age=? then ? else &#39;기타&#39; end as col_0_0_ from member member0_ /* select case when member1.age = 101 then &#39;열살&#39;2 when member1.age = 203 then &#39;스무살&#39;4 else &#39;기타&#39; end from Member member1 */ select case when member0_.age = NULL then ? when member0_.age = ? then ? else &#39;기타&#39; end as col_0_0_ from member member0_;</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>2) 복잡한 조건</strong></span></p><pre class="brush:as3;"> @Test public void complexCase(){ List&lt;String&gt; result = queryFactory .select( new CaseBuilder() .when(member.age.between(0, 20)).then(&quot;0~20살&quot;) .when(member.age.between(21, 30)).then(&quot;21-~30살&quot;) .otherwise(&quot;기탕&quot;) ) .from(member) .fetch(); for(String s :result){ System.out.println(&quot;s = &quot; + s); } }</pre><p>&nbsp;</p><p>쿼리 출력 결과</p><pre class="brush:as3;">/* select case when (member1.age between ?1 and ?2) then ?3 when (member1.age between ?4 and ?5) then ?6 else &#39;기탕&#39; end from Member member1 */ select case when member0_.age between ? and ? then ? when member0_.age between ? and ? then ? else &#39;기탕&#39; end as col_0_0_ from member member0_</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong>3) orderBy에서 Case 문 함께 사용하기 예제</strong></span></p><p>&nbsp;</p><p>&gt; 참고: 강의 이후 추가된 내용입니다.</p><p><br />예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?</p><p><br />1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력<br />2. 0 ~ 20살 회원 출력<br />3. 21 ~ 30살 회원 출력</p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;"> NumberExpression&lt;Integer&gt; rankPath = new CaseBuilder() .when(member.age.between(0, 20)).then(2) .when(member.age.between(21, 30)).then(1) .otherwise(3); List&lt;Tuple&gt; result = queryFactory .select(member.username, member.age, rankPath) .from(member) .orderBy(rankPath.desc()) .fetch(); for (Tuple tuple : result) { String username = tuple.get(member.username); Integer age = tuple.get(member.age); Integer rank = tuple.get(rankPath); System.out.println(&quot;username = &quot; + username + &quot; age = &quot; + age + &quot; rank = &quot; + rank); }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">결과 username = member4 age = 40 rank = 3 username = member1 age = 10 rank = 2 username = member2 age = 20 rank = 2 username = member3 age = 30 rank = 1</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>21.상수, 문자 더하기 </strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30134&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30134&amp;tab=curriculum</a></p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:18px"><strong>상수가 필요하면 Expressions.constant(xxx) 사용</strong></span></span></p><pre class="brush:as3;"> @Test public void constant(){ List&lt;Tuple&gt; result = queryFactory .select(member.username, Expressions.constant(&quot;A&quot;)) .from(member) .fetch(); for(Tuple tuple : result) { System.out.println(&quot;tuple = &quot; + tuple); } }</pre><p>&nbsp;</p><p>쿼리 출력</p><pre class="brush:as3;"> /* select member1.username from Member member1 */ select member0_.username as col_0_0_ from member member0_</pre><p>&nbsp;</p><p><strong>참고: 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 상수를 더하는 것 처럼 최적화가<br />어려우면 SQL에 constant 값을 넘긴다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:16px"><strong>문자 더하기 concat</strong></span></span></p><pre class="brush:as3;"> @Test public void concat(){ //{username}_{age} List&lt;String&gt; result = queryFactory.select( member.username.concat(&quot;_&quot;) .concat(member.age.stringValue()) ) .from(member) .fetch(); for (String s :result){ System.out.println(&quot;s = &quot; + s); } }</pre><p>&nbsp;</p><p>쿼리출력=&gt;</p><pre class="brush:as3;"> /* select concat(concat(member1.username, ?1), str(member1.age)) from Member member1 */ select ((member0_.username||?)||cast(member0_.age as character varying)) as col_0_0_ from member member0_</pre><p>&nbsp;</p><p>결과: member1_10</p><p>&nbsp;</p><p><br /><span style="font-size:16px"><strong>&gt; 참고: member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue() 로</strong></span></p><p><br /><span style="font-size:16px"><strong>문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용한다.</strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-23 21:21:41 js 이미지 레이어팝업 확대 , jqeury 이미지 확대 빠르게 구현하기 - magnific-popup http://macaronics.net/index.php/m04/jquery/view/2092 2092 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><a target="_blank" href="https://dimsemenov.com/plugins/magnific-popup/">https://dimsemenov.com/plugins/magnific-popup/</a></span></strong></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">&lt;!DOCTYPE html&gt; &lt;html lang=&quot;ko&quot;&gt; &lt;head&gt; &lt;meta charset=&quot;UTF-8&quot;&gt; &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt; &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt; &lt;title&gt;js 이미지 레이어팝업 확대 (이미지확대)&lt;/title&gt; &lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/magnific-popup.js/1.1.0/magnific-popup.css&quot;&gt; &lt;script src=&quot;https://code.jquery.com/jquery-3.6.4.js&quot; integrity=&quot;sha256-a9jBBRygX1Bh5lt8GZjXDzyOB+bWve9EiO7tROUtj/E=&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt; &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/magnific-popup.js/1.1.0/jquery.magnific-popup.min.js&quot; integrity=&quot;sha512-IsNh5E3eYy3tr/JiX2Yx4vsCujtkhwl7SLqgnwLNgf04Hrt9BT9SXlLlZlWx+OK4ndzAoALhsMNcCmkggjZB1w==&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;div&gt; &lt;a href=&quot;https://image.utoimage.com/preview/cp872722/2022/12/202212008462_206.jpg&quot; class=&quot;image-link&quot;&gt; &lt;img src=&quot;https://image.utoimage.com/preview/cp872722/2022/12/202212008462_206.jpg&quot; onerror=&quot;this.src=&#39;https://via.placeholder.com/147x88&#39;&quot; /&gt; &lt;/a&gt; &lt;br&gt; &lt;br&gt; &lt;a href=&quot;https://cdn.crowdpic.net/list-thumb/thumb_l_CDD94CBD46425E4EDBD18A7A17C199E7.jpg&quot; class=&quot;image-link&quot;&gt; &lt;img src=&quot;https://cdn.crowdpic.net/list-thumb/thumb_l_CDD94CBD46425E4EDBD18A7A17C199E7.jpg&quot; onerror=&quot;this.src=&#39;https://via.placeholder.com/147x88&#39;&quot; /&gt; &lt;/a&gt; &lt;/div&gt; &lt;script&gt; $(function(){ $(&#39;.image-link&#39;).magnificPopup({ type : &#39;image&#39;,}); }); &lt;/script&gt; &lt;/body&gt; &lt;/html&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-23 19:06:02 1cm 다이빙 현실에서 딱 1cm 벗어나는 행복을 찾아! http://macaronics.net/index.php/movie/education/view/2091 2091 <p>&nbsp;</p><p><img alt="" width="817" height="1200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjtHpvSaxQJD2vQvOP-bUL2n-a2xclF4vVOOA3uRbSOE4zr08g6s9LnXkrdDkU2dcYlGVaBiXWrxAYJnkimsKheNy-JnpPwQ3AadVqRg5n75AtT0-OqNESyTCZdt2QRVM2YB1Tpp9DePU79lsgARXGfDS1gb7qUIcrhXaUfvA9zO-kHodsIScK-dwJ5Tw/s16000/XL.jpg" /></p><p>&nbsp;</p><p>&nbsp;</p><p>품목정보</p><p>발행일2020년 01월 21일</p><p>쪽수, 무게, 크기248쪽 | 274g | 128*188*15mm</p><p>ISBN139791190299060</p><p>ISBN101190299062</p><p>&nbsp;</p><p><a target="_blank" href="http://www.yes24.com/Product/Goods/86543588">http://www.yes24.com/Product/Goods/86543588</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&#39;1cm 다이빙&#39;은 김해인 작가가 쓴 자기계발 서적으로, 자신의 삶에서 겪은 어려움과 실패, 그리고 그것을 극복하는 방법 등을 다룹니다. 제목에서 알 수 있듯이, 작가는 인생에서 큰 변화를 이루기 위해 작은 변화부터 시작하는 것이 중요하다는 메시지를 담고 있습니다.</p><p>&nbsp;</p><p>책은 작가의 개인적인 경험과 이야기를 중심으로 구성되어 있으며, 그의 생각과 철학을 토대로 자신에게 필요한 변화를 찾아가는 방법에 대해 알려줍니다. 또한, 작가는 이 책을 통해 우리가 살아가는 현실에서 각종 스트레스와 압박감, 부정적인 감정 등을 극복하는 방법을 제시하고 있습니다.</p><p>&#39;1cm 다이빙&#39;은 많은 독자들에게 자기계발 및 인생 개선에 큰 도움을 주는 책으로, 독서를 통해 자신에게 필요한 변화를 찾고자 하는 분들에게 추천할 만한 책입니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&#39;1cm 다이빙&#39;은 김해인 작가가 쓴 자기계발 서적으로, 작가의 개인적인 경험과 이야기를 중심으로 구성되어 있습니다. 이 책은 자신의 삶에서 겪은 어려움과 실패, 그리고 그것을 극복하는 방법 등을 다루며, 작은 변화부터 시작하여 큰 변화를 이루는 방법에 대해 알려줍니다.</p><p>이 책은 간결하면서도 깊은 생각과 철학을 담고 있으며, 독자들에게 자신의 인생을 바꾸는 계기를 제공합니다. 또한, 작가는 우리가 살아가는 현실에서 마주하는 각종 스트레스와 압박감, 부정적인 감정 등을 극복하는 방법을 제시하고 있습니다.</p><p>작가는 이 책에서 &quot;1cm 다이빙&quot;이라는 표현을 사용하며, 작은 변화부터 시작하여 큰 변화를 이루는 것이 중요하다는 것을 강조합니다. 이는 우리가 인생에서 큰 변화를 이루기 위해서는 작은 변화부터 시작해 나가야 한다는 것을 말하는 것으로, 이 책을 읽은 독자들은 자신이 바꾸고자 하는 것을 1cm씩 단계적으로 바꾸어 나가면서 큰 성과를 이룰 수 있다는 것을 깨닫게 됩니다.</p><p>작가는 또한, 인생에서 실패하는 것은 당연하며, 그것이 성공을 이루는 과정에서 중요한 역할을 한다고 말합니다. 작가의 인생에서의 실패와 그것을 극복하는 방법을 통해, 독자들은 자신의 실패를 긍정적인 경험으로 바꿀 수 있는 방법을 배울 수 있습니다.</p><p>이 책은 또한, 자신과 타인에 대한 이해와 소통의 중요성을 강조합니다. 작가는 자신의 삶에서 인간관계에서의 어려움을 극복하고자 자신에게 더 많은 이해와 배려를 기울이며, 이러한 자세가 자신과 타인 모두에게 큰 변화를 가져올 수 있다는 것을 말합니다.</p><p>&#39;1cm 다이빙&#39;은 작은 변화에서부터 큰 변화를 이루는 것의 중요성과, 자신과 타인에 대한 이해와 소통의 중요성을 강조하는 책입니다. 이 책을 읽은 독자들은 자신의 인생을</p><p>&nbsp;</p><p>&nbsp;</p><p>이 책은 우리가 살아가는 현실 속에서 가끔씩 잊어버리는 작은 것들이 얼마나 중요한지를 알려주는 책입니다. 작가 김해인은 이 책을 통해 우리에게 작은 것들이 가지는 의미와 중요성을 다시 한번 상기시켜줍니다.</p><p>이 책의 주인공은 여러모로 실패를 경험한 대학 졸업생 &#39;예림&#39;입니다. 그녀는 신입 사원으로 취직한 회사에서 열심히 일하면서도 성과가 나오지 않아 우울증에 시달리게 됩니다. 하지만 어느 날 그녀는 일상 속에서 놓치고 있었던 작은 것들을 다시 발견하면서 자신의 삶에 대한 새로운 시선을 얻게 됩니다.</p><p>이 책에서는 예림이 발견한 작은 것들, 예를 들면 운동을 하면서 느꼈던 작은 성취감이나, 일상 생활 속에서 마주하는 작은 감동들이 삶의 의미를 만드는 중요한 요소들이라는 것을 알려줍니다. 작가는 이러한 작은 것들을 인간의 삶에서 가장 소중한 것으로 여기고, 이를 통해 삶을 살아가는 방법을 알려주고자 합니다.</p><p>이 책은 감동적인 이야기와 함께 작은 것들에 대한 사려 깊은 인사이트를 제공하며, 독자들에게 삶을 살아가는 데 있어서 큰 위로가 되어줍니다. 또한, 이 책은 우리가 현대 사회에서 가지고 있는 스트레스와 우울증 같은 문제들에 대한 해결책을 제공합니다. 작은 것들에 주목하면서, 우리는 더 나은 인간이 될 수 있습니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-23 08:28:50 20대, 너는 어떤 모습으로 살아가고 싶니? 면접 답변 http://macaronics.net/index.php/itcommunity/life/view/2090 2090 <p>&nbsp;</p><p><strong><span style="font-size:18px">20대, &nbsp;너는 어떤 모습으로 살아가고 싶니? &nbsp; 면접 답변&nbsp;</span></strong></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>첫째</strong>, 나 자신을 이해하고, 나 자신에게 진실된 모습으로 살아가고 싶습니다. 이는 나 자신의 가치관, 성격, 장단점 등을 잘 파악하고, 그것을 바탕으로 내가 하고 싶은 일, 이루고 싶은 목표 등을 설정하고 그에 맞게 삶을 설계하는 것을 의미합니다. 또한, 이것이 타인의 평가나 영향에 휩쓸리지 않고, 나 자신이 진실된 모습으로 살아가는 것을 의미합니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>둘째</strong>, 배움에 대한 열정을 유지하고, 성장하고 싶습니다. 저는 새로운 것을 배우는 것에 큰 즐거움을 느끼고, 자신의 능력을 끊임없이 발전시키며 성장하는 것을 추구합니다. 이를 위해서는 끊임없이 도전하고, 실패를 두려워하지 않고, 계속해서 도전하며 배워나가는 자세를 가지는 것이 중요합니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>셋째</strong>, 인간관계를 중요시하며, 좋은 사람들과 함께 일하고, 살고 싶습니다. 저는 서로를 존중하고 배려하며, 서로의 성장을 촉진해주는 인간관계를 유지하고 싶습니다. 또한, 다양한 사람들과 소통하며, 서로의 다른 점을 인정하고 존중하는 태도를 가지고, 상호작용하며 인간관계를 발전시키고 싶습니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px"><strong>넷째</strong>, 세상에 긍정적인 영향을 미치고 싶습니다. 나 자신이 갖고 있는 능력을 바탕으로, 타인에게 도움이 되는 일을 하고, 세상을 조금 더 밝고 따뜻하게 만들고 싶습니다. 이를 위해서는 자신의 가치를 실현하는 일을 하며, 타인과의 상호작용을 통해 삶의 질을 높이는 일을 하고 싶습니다.</span></p><p><span style="font-size:18px">이러한 가치를 기반으로 나 자신을 발전시키고, 자신의 인생을 즐겁게 보내며, 타인에게도 긍정적인 영향을 미치도록 합니다.</span></p><p>&nbsp;</p><p><strong><span style="font-size:18px">&nbsp;덧붙여 말씀드리면, 이러한 가치를 실천하기 위해서는 몇 가지 방법이 있을 수 있습니다.</span></strong></p><p><span style="font-size:18px">우선, 자기개발에 힘써야 합니다. 자기개발은 새로운 것을 배우는 것뿐만 아니라, 자신의 강점과 약점을 파악하고, 더 나은 사람으로 성장하기 위해 노력하는 것입니다. 책을 읽거나 온라인 강의를 수강하거나, 스터디를 참여하는 등의 방법으로 자기개발을 할 수 있습니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px">또한, 건강한 생활습관을 유지해야 합니다. 건강한 식습관과 규칙적인 운동, 충분한 수면 등을 통해 건강한 삶을 유지할 수 있습니다. 건강한 상태에서 더 많은 것을 이루어낼 수 있기 때문입니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px">또한, 새로운 경험을 쌓는 것이 중요합니다. 새로운 사람들을 만나거나, 새로운 활동에 참여하거나, 새로운 장소를 방문하는 등의 경험을 통해 자신의 시야를 넓힐 수 있습니다. 이를 통해 새로운 아이디어를 얻거나, 새로운 가능성을 발견할 수 있습니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px">마지막으로, 타인과의 소통과 인간관계를 중요시해야 합니다. 서로를 존중하며 대화를 나누고, 상호적으로 이해하며 지내는 것이 중요합니다. 또한, 타인을 돕는 일을 하면서 자신의 가치를 실현할 수 있습니다.</span></p><p>&nbsp;</p><p><span style="font-size:18px">이러한 방법들을 통해 나 자신을 발전시키고, 성장하며, 즐겁게 살아갈 수 있습니다.</span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-23 08:15:21 특수부대의 신 ! 해킹의 신! 1화부터 시청률 14% 역대급 스타트! 쓰레기들은 감옥까지 따라가서 끝끝내 참교육하는 레전드 사이다 드라마! 모범택시 2 http://macaronics.net/index.php/tv/drama/view/2089 2089 <p><img alt="" src="https://cdn.gynews.kr/news/photo/202302/23010_48156_613.jpg" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">《모범택시 2》는 2023년 2월 17일부터 2023년 4월 15일까지 방송 중인 SBS 금토 드라마다.</span></strong><br />&nbsp;</p><p><strong>출연자</strong>: 이제훈, 김의성, 표예진 외<br /><strong>HD 방송 여부</strong>: UHD 제작 &middot; 방송<br /><strong>방송 국가</strong>: 대한민국<br /><strong>방송 기간</strong>: 2023년 2월 17일 ~ 2023년 4월 15일<br /><strong>방송 분량</strong>: 1시간 10분<br />방송 시간: 매주 금요일, 토요일 밤 10:00 ~ 11:10<br />방송 채널: SBS TV</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>주요 인물</strong></span><br /><strong>이제훈 </strong>: 김도기 역 - &#39;무지개 운수&#39; 택시기사<br />前 육사, 특수부대(육군 특수전사령부 707 특수임무단) 장교. 現 무지개 운수의 택시 기사.<br />타고난 직관력과 냉철한 판단력, 그 어떤 위기 상황에서도 흔들리지 않는 담대함, 다수의 상대와 맞붙어도 결코 밀리지 않는 피지컬.</p><p><strong>이솜</strong> : 강하나 역 - 서울북부검찰청 검사</p><p>검딱지, 검도저, 불검, 똘검 등 일컫는 수식어가 많은 열혈 검사.</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:16px">택시회사 &#39;무지개 운수&#39;</span></strong><br /><strong>김의성 </strong>: 장성철 역 - &#39;무지개 운수&#39;의 대표, 범죄 피해자 지원 센터 &#39;파랑새 재단&#39; 대표<br />택시회사 무지개 운수 대표이자 파랑새 지원센터 회장.</p><p><strong>표예진 </strong>: 안고은 역 - &#39;무지개 운수&#39;의 경리과 직원</p><p><strong>장혁진 </strong>: 최경구 역 - &#39;무지개 운수&#39; 정비실 엔지니어<br />자동차기업 신차개발팀 선임 연구원 출신으로 현재 무지개 운수 정비실을 책임지고 있는 주임<br />.<br /><strong>배유람</strong> : 박진언 역 - &#39;무지개 운수&#39; 정비실 엔지니어<br />유명 항공사 항공기 정비원 출신으로 똥차를 스포츠카로 만들 수 있는 뛰어난 손 기술을 지닌 한국의 맥가이버.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:26px"><a target="_blank" href="https://www.youtube.com/@G-movie/featured"><strong>지무비 : G Movie</strong></a></span></p><p><br /><strong><span style="font-size:18px">≪모범택시2≫1-2화</span></strong><br />미쳤다.. 1화부터 시청률 14%..역대급 스타트..! 쓰레기들은 감옥까지 따라가서 끝끝내 참교육하는 레전드 사이다 드라마가 돌아왔다 ..!<br />https://youtu.be/l9MLHQqz4OY</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/l9MLHQqz4OY" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">≪모범택시2≫ 3-4화</span></strong><br />시골 노인들 상대로 사기 치는 쓰레기들이 순박한 시골 청년인 줄 알고 괴롭혔는데, 알고 보니 복수대행 전직 특수요원 도기좌였다고 한다..! 잘가 ㅠ ㅠ&nbsp;<br />https://youtu.be/IuhJ6qO7Oeg</p><p><iframe width="640" height="360" src="https://www.youtube.com/embed/IuhJ6qO7Oeg" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p><br /><strong><span style="font-size:18px">≪모범택시2≫ 5-6화</span></strong><br />인간쓰레기들을 참교육을 넘어, 그냥 진짜 분리수거장으로 넣어주는ㅋㅋ 역대급 사이다로 단 5화 만에 시청률 19.7% 압도적 1위 찍어버린 레전드 드라마<br /><iframe width="640" height="360" src="https://www.youtube.com/embed/6I6Ryw9dwU4" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><br /><span style="font-size:28px"><a target="_blank" href="https://www.youtube.com/@dartagnan1005"><strong>달타냥씨네마</strong></a></span></p><p><br /><strong><span style="font-size:18px">≪모범택시2≫&nbsp;1~8화 한방에 몰아보기</span></strong><br />2023년 무조건 봐야하는 개꿀잼 1위 드라마 (시청률 16% 돌파) 시청률 1순위&nbsp;<br /><iframe width="640" height="360" src="https://www.youtube.com/embed/Oc4o0RNIHic" frameborder="0" allowfullscreen=""></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-23 07:57:15 실전! Querydsl - 1. Querydsl 소개 , 2.프로젝트 환경설정(프로젝트 생성, ★ Querydsl 설정과 검증,라이브러리 살펴보기,H2 데이터베이스 설치,스프링 부트 설정 - JPA, DB) , 3.예제 도메인 모델 http://macaronics.net/index.php/m01/spring/view/2088 2088 <p>&nbsp;</p><p><strong>중급자</strong>를 위해 준비한<br /><strong>[백엔드, 웹 개발] 강의입니다.</strong></p><p>Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!</p><p>✍️<br />이런 걸<br />배워요!</p><p>Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.</p><p>단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.</p><p>JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.</p><p>복잡한 쿼리, 동적 쿼리는 이제 안녕!&nbsp;<br />Querydsl로 자바 백엔드 기술을 단단하게.</p><p>???? 본 강의는 로드맵 과정입니다.</p><ul><li>본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다.&nbsp;<strong>스프링 부트와 JPA 실무 완전 정복 로드맵</strong>을 우선 확인해주세요.&nbsp;<a target="_blank" rel="noopener" href="https://www.inflearn.com/roadmaps/149">(링크)</a></li></ul><p>&nbsp;</p><p>&nbsp;</p><p>강좌&nbsp;</p><p><a target="_blank" href="https://www.inflearn.com/course/querydsl-실전#">https://www.inflearn.com/course/querydsl-실전#</a></p><p>&nbsp;</p><p>&nbsp;</p><p>강의자료</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-basic-lecture-file2">https://github.com/braverokmc79/jpa-basic-lecture-file2</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>소스 :</p><p><a target="_blank" href="https://github.com/braverokmc79/jpa-querydsl">https://github.com/braverokmc79/jpa-querydsl</a></p><p>&nbsp;</p><p><img alt="" width="778" height="1472" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBQbNH1NXxOd5BwQ1bjjyHBiQfrbzOMaB51bR401LNkie3U9SeIi8Lc8nHoacBELVzkhRUvx0LHQDkPnH7VUCbdPTEYey0XlRmkritbUClfbsJhy1WNx65KvyWoxQ-7b5rLvnodeuEHHUdjOnlIzfUS0MUDS3qT6mI1rYPApfoI5A1Vg1AOnLOfYWhvw/s16000/2023-03-22%2022%2052%2043.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[1] Querydsl 소개</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>1.소개</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=27939&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=27939&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>2.강의자료</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30112&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30112&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[2] 프로젝트 환경설정</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>3.프로젝트 생성</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30114&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30114&amp;tab=curriculum</a></p><p>&nbsp;</p><p>프로젝트 생성<br />스프링 부트 스타터(<a target="_blank" href="https://start.spring.io/">https://start.spring.io/</a>)</p><p>&nbsp;</p><p><img alt="" width="500" height="580" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4HO56lZ2dLz-doreXovGwMRE5WjOYdWHZA3ai-1e8tU1q8X70bJdW5fvxCHPdnyxXK-xXzDzFFg4l4J_XcfDT7uwbhPFNa5Nv3cn2VZR-MCfKATHAyOtRTzvp04OXDmp-KImpPbecxxDwZ6PPIvR1U6RRQJ1oFO1nD7N9FwMHcEG-VhnppABuJTQCaA/s16000/2023-03-22%2017%2014%2002.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Querydsl 스프링 부트 3.0 설정은 다음을 참고해주세요.</strong><br /><a target="_blank" href="https://www.inflearn.com/chats/700670">https://www.inflearn.com/chats/700670</a></p><p>&nbsp;</p><p><br /><strong>스프링 부트 3.0 관련 자세한 내용은 다음 링크를 확인해주세요:</strong></p><p><a target="_blank" href="https://bit.ly/springboot3">https://bit.ly/springboot3</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>강좌 Gradle 전체 설정</strong></p><pre class="brush:as3;">plugins { id &#39;org.springframework.boot&#39; version &#39;2.2.2.RELEASE&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.0.8.RELEASE&#39; id &#39;java&#39; } group = &#39;study&#39; version = &#39;0.0.1-SNAPSHOT&#39; sourceCompatibility = &#39;1.8&#39; configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; compileOnly &#39;org.projectlombok:lombok&#39; runtimeOnly &#39;com.h2database:h2&#39; annotationProcessor &#39;org.projectlombok:lombok&#39; testImplementation(&#39;org.springframework.boot:spring-boot-starter-test&#39;) { exclude group: &#39;org.junit.vintage&#39;, module: &#39;junit-vintage-engine&#39; } } test { useJUnitPlatform() }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>현재 나의</strong>&nbsp;<strong>Gradle 전체 설정</strong></p><pre class="brush:as3;">plugins { id &#39;java&#39; id &#39;org.springframework.boot&#39; version &#39;2.7.9&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.0.15.RELEASE&#39; } group = &#39;study&#39; version = &#39;0.0.1-SNAPSHOT&#39; sourceCompatibility = &#39;17&#39; configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7&#39; compileOnly &#39;org.projectlombok:lombok&#39; developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39; runtimeOnly &#39;com.h2database:h2&#39; runtimeOnly &#39;com.mysql:mysql-connector-j&#39; annotationProcessor &#39;org.projectlombok:lombok&#39; testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39; } tasks.named(&#39;test&#39;) { useJUnitPlatform() } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>동작 확인</strong></p><p><br />기본 테스트 케이스 실행</p><p><br />스프링 부트 메인 실행 후 에러페이지로 간단하게 동작 확인(`http://localhost:8080&#39;)</p><p><br />테스트 컨트롤러를 만들어서 spring web 동작 확인(http://localhost:8080/hello)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>테스트 컨트롤러</strong></span></p><pre class="brush:as3;">package study.querydsl.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping(&quot;/hello&quot;) public String hello() { return &quot;hello!&quot;; } }</pre><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>IntelliJ Gradle 대신에 자바로 바로 실행하기</strong></span></p><p><br />최근 IntelliJ 버전은 Gradle로 실행을 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다. 다음과<br />같이 변경하면 자바로 바로 실행하므로 좀 더 빨라진다.</p><p>&nbsp;</p><p>1. Preferences Build, Execution, Deployment Build Tools Gradle<br />2. Build and run using: Gradle IntelliJ IDEA<br />3. Run tests using: Gradle IntelliJ IDEA</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>롬복 적용</strong></span><br />1. Preferences plugin lombok 검색 실행 (재시작)<br />2. Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)<br />3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px">=&gt;&nbsp;<a target="_blank" href="https://ahn3330.tistory.com/130"><strong>빌드를 인텔리제이로 변경</strong></a></span></p><p>셋팅 -&gt;&nbsp;build 검색 후&nbsp; --&gt; 그래들 build&nbsp; -&gt; 설정&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>=&gt;&nbsp;<a target="_blank" href="https://goddaehee.tistory.com/208">롬복 적용</a></strong></span></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>=&gt;&nbsp;<a target="_blank" href="https://macaronics.net/index.php/m01/spring/view/1809#">인텔리제이 스프링부트 자동빌드 적용</a></strong></span></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>4.Querydsl 설정과 검증</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30115&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30115&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px"><span style="color:#2980b9">1)&nbsp; build.gradle</span></span></strong></p><pre class="brush:as3;">//querydsl 추가 buildscript { ext { queryDslVersion = &quot;5.0.0&quot; } } plugins { id &#39;java&#39; id &#39;org.springframework.boot&#39; version &#39;2.7.9&#39; id &#39;io.spring.dependency-management&#39; version &#39;1.0.15.RELEASE&#39; //querydsl 추가 id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot; } group = &#39;study&#39; version = &#39;0.0.1-SNAPSHOT&#39; sourceCompatibility = &#39;17&#39; configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39; implementation &#39;org.springframework.boot:spring-boot-starter-web&#39; implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7&#39; //querydsl 추가 implementation &quot;com.querydsl:querydsl-jpa:${queryDslVersion}&quot; implementation &quot;com.querydsl:querydsl-apt:${queryDslVersion}&quot; compileOnly &#39;org.projectlombok:lombok&#39; developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39; runtimeOnly &#39;com.h2database:h2&#39; runtimeOnly &#39;com.mysql:mysql-connector-j&#39; annotationProcessor &#39;org.projectlombok:lombok&#39; testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39; } tasks.named(&#39;test&#39;) { useJUnitPlatform() } //querydsl 추가 시작 def querydslDir = &quot;$buildDir/generated/querydsl&quot; querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } //querydsl 추가 끝 </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>2) AppConfig</strong></span></span></p><pre class="brush:as3;">import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.persistence.EntityManager; @Configuration @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory() { return new JPAQueryFactory(em); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>설정후 그래드를 업그레이드 및 빌드처리하면 다음 화면과 같이&nbsp; build 파일에 Q Entity 파일들이 생성된다.</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgsvDW-6r7eFOVWHH7oSchgtgOjIYvRXN718PVvFqsnXRPcblAEiXQlbQDE2qkIlFuw2uO2IDsu8XR1XvX2hF_KX-8tX4L6YRqI9pjf3Qe_GIxFeWQaGe3MpEyUnKbwVC5Zx0U_xQTS5mXcKJT5QfiswtliG4ySweJsdBDktAluk1NDsy2L4SkBQCdhyw/s16000/2023-03-15%2020%2015%2009.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>3) Querydsl 환경설정 검증</strong></span></span></p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgavtxIt_XAaukclveltsEKOK9T76cD3RXZiUzS-p2iSIe00CFZnLxFhL3dajcnpGWG-pCgFTpCeLZ7Q3gXxnwIrcb6CmfzIxtARdDoGVQTMO0-CfV87zmLCoZBvvbGAZaKE3DbxECpShzZDAAhdXloCRtqyh-8Jmlt3ZfR-cm3igR2FKO1FjLHJrIj6Q/s1904/2023-03-22%2020%2003%2025.png">이미지 크게</a></p><p><img alt="" width="900" height="475" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgavtxIt_XAaukclveltsEKOK9T76cD3RXZiUzS-p2iSIe00CFZnLxFhL3dajcnpGWG-pCgFTpCeLZ7Q3gXxnwIrcb6CmfzIxtARdDoGVQTMO0-CfV87zmLCoZBvvbGAZaKE3DbxECpShzZDAAhdXloCRtqyh-8Jmlt3ZfR-cm3igR2FKO1FjLHJrIj6Q/s1904/2023-03-22%2020%2003%2025.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><br /><strong>검증용 엔티티 생성</strong></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Getter @Setter public class Hello { @Id @GeneratedValue private Long id; }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>검증용 Q 타입 생성</strong></span><br />Gradle IntelliJ 사용법<br />Gradle Tasks build clean<br />Gradle Tasks other compileQuerydsl</p><p>&nbsp;</p><p><br /><span style="color:#c0392b"><span style="font-size:20px"><strong>Gradle 콘솔 사용법</strong></span></span><br /><strong><span style="color:#2980b9">./gradlew clean compileQuerydsl</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Q 타입 생성 확인</strong></span></p><p><br />build generated querydsl<br />study.querydsl.entity.QHello.java 파일이 생성되어 있어야 함</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.</strong></p><p><strong>앞서 설정에서&nbsp; 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다. </strong></p><p><strong>(대부분 gradle build 폴더를 git에 포함하지 않는다.)</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>테스트 케이스로 실행 검증</strong></span></p><p>&nbsp;</p><pre class="brush:as3;">package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Hello; import study.querydsl.entity.QHello; import javax.persistence.EntityManager; @SpringBootTest @Transactional @Rollback(value = false) class ApplicationTests { @Autowired EntityManager em; @Test void contextLoads() { Hello hello=new Hello(); em.persist(hello); JPAQueryFactory query=new JPAQueryFactory(em); //QHello qHello=new QHello(&quot;h&quot;); QHello qHello=QHello.hello; //Querydsl Q타입 동작 확인 Hello result =query.selectFrom(qHello) .fetchOne(); Assertions.assertThat(result).isEqualTo(hello); //lombok 동작 확인(hello.getId()); Assertions.assertThat(result.getId()).isEqualTo(hello.getId()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>AppConfig 클래스 파일을 설정하면 다음과 같이 사용가능</p><pre class="brush:as3;">package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Hello; import study.querydsl.entity.QHello; import javax.persistence.EntityManager; @SpringBootTest @Transactional @Rollback(value = false) class ApplicationTests { @Autowired EntityManager em; @Autowired JPAQueryFactory jpaQueryFactory; @Test void contextLoads2() { Hello hello=new Hello(); em.persist(hello); QHello qHello=QHello.hello; Hello result =jpaQueryFactory.selectFrom(qHello).fetchOne(); Assertions.assertThat(result).isEqualTo(hello); //lombok 동작 확인(hello.getId()); Assertions.assertThat(result.getId()).isEqualTo(hello.getId()); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>Querydsl Q타입이 정상 동작하는가?<br />lombok이 정상 동작 하는가?<br />&gt; 참고: 스프링 부트에 아무런 설정도 하지 않으면 h2 DB를 메모리 모드로 JVM안에서 실행한다</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:20px"><strong>4) 사용예</strong></span></span></p><p>&nbsp;</p><pre class="brush:as3;">public List&lt;Order&gt; findAll(OrderSearch orderSearch){ System.out.println(&quot;orderSearch = &quot; + orderSearch); return em.createQuery(&quot;select o from Order o join o.member m &quot; + &quot; where o.status = :status &quot; + &quot; and m.name like :name &quot;, Order.class) .setParameter(&quot;status&quot;, orderSearch.getOrderStatus()) .setParameter(&quot;name&quot;, orderSearch.getMemberName()) //.setFirstResult(10) .setMaxResults(1000) //최대 1000건 .getResultList(); }</pre><p>&nbsp;</p><pre class="brush:as3;">package jpabook.jpashop.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; import java.util.List; import static jpabook.jpashop.domain.QMember.member; import static jpabook.jpashop.domain.QOrder.order; @Repository @RequiredArgsConstructor public class OrderRepository { private final JPAQueryFactory query; public List&lt;Order&gt; findAll(OrderSearch orderSearch){ return query.select(order) .from(order) .join(order.member, member) .where(statusEq(orderSearch.getOrderStatus()), nameLike(orderSearch.getMemberName())) .limit(1000) .fetch(); } //주문자 이름 확인 private BooleanExpression nameLike(String memberName){ if(!StringUtils.hasText(memberName)){ return null; } return member.name.like(memberName); } //주문 상태 private BooleanExpression statusEq(OrderStatus statusCond){ if(statusCond==null){ return null; } return order.status.eq(statusCond); } }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>5.라이브러리 살펴보기</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30116&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30116&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>gradle 의존관계 보기</strong><br />./gradlew dependencies --configuration compileClasspath</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>Querydsl 라이브러리 살펴보기</strong></span><br />querydsl-apt: Querydsl 관련 코드 생성 기능 제공<br />querydsl-jpa: querydsl 라이브러리</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>스프링 부트 라이브러리 살펴보기</strong></span><br />spring-boot-starter-web<br />spring-boot-starter-tomcat: 톰캣 (웹서버)<br />spring-webmvc: 스프링 웹 MVC<br />spring-boot-starter-data-jpa<br />spring-boot-starter-aop<br />spring-boot-starter-jdbc<br />HikariCP 커넥션 풀 (부트 2.0 기본)<br />hibernate + JPA: 하이버네이트 + JPA<br />spring-data-jpa: 스프링 데이터 JPA<br />spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅<br />spring-boot<br />spring-core<br />spring-boot-starter-logging<br />logback, slf4j<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>테스트 라이브러리</strong></span><br />spring-boot-starter-test<br />junit: 테스트 프레임워크, 스프링 부트 2.2부터 <span style="color:#c0392b"><strong>junit5</strong></span>( jupiter ) 사용</p><p>과거 버전은 vintage<br />mockito: 목 라이브러리</p><p><br /><span style="color:#c0392b"><strong>assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리</strong></span><br /><a target="_blank" href="https://joel-costigliola.github.io/assertj/index.html">https://joel-costigliola.github.io/assertj/index.html</a><br />spring-test: 스프링 통합 테스트 지원</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>핵심 라이브러리</strong></span><br />스프링 MVC<br />JPA, 하이버네이트<br />스프링 데이터 JPA<br />Querydsl</p><p><br /><span style="font-size:16px"><strong>기타 라이브러리</strong></span><br />H2 데이터베이스 클라이언트<br />커넥션 풀: 부트 기본은 HikariCP<br />로깅 SLF4J &amp; LogBack<br />테스트</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>6.H2 데이터베이스 설치</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30117&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30117&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" width="693" height="622" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhbtpRhBT0C4fywv_MNyAIvfTE6iqBjVaod1mwKx9HbiXtNmhKnJjT_6ZHwGlAKYeeJffB5R3cdH_fLW0uVTQ6yi4evYqXhwhzJqQhAPknMzBHB95ibwkZOHjYRqEtL9DegA-zrwdmm27KPvT-A3VwC92UgYNjkr8vKWKZ5_uH-3kpYxHhiPLeVMZB4jg/s16000/2023-03-02%2012%2040%2051.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:16px"><strong>개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공</strong></span><br /><a target="_blank" href="https://www.h2database.com">https://www.h2database.com</a></p><p>&nbsp;</p><p><span style="font-size:16px"><strong>다운로드 및 설치</strong></span><br />h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.<br />권한 주기: chmod 755 h2.sh</p><p>&nbsp;</p><p><br /><strong>데이터베이스 파일 생성 방법</strong><br />jdbc:h2:~/querydsl (최소 한번)<br />~/querydsl.mv.db 파일 생성 확인<br />이후 부터는 jdbc:h2:tcp://localhost/~/querydsl 이렇게 접속</p><p>&nbsp;</p><p>&nbsp;</p><p>&gt; 참고: H2 데이터베이스의 MVCC 옵션은 H2 1.4.198 버전부터 제거되었습니다. 이후 부터는 옵션 없이 사용하면 됩니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>주의: 가급적 안정화 버전을 사용하세요. 1.4.200 버전은 몇가지 오류가 있습니다.<br />&gt; 현재 안정화 버전은 1.4.199(2019-03-13) 입니다.<br />&gt; 다운로드 링크: <a target="_blank" href="https://www.h2database.com/html/download.html">https://www.h2database.com/html/download.html</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>7.스프링 부트 설정 - JPA, DB</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30118&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30118&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>application.yml</strong></p><pre class="brush:as3;">spring: datasource: url: jdbc:h2:tcp://localhost/~/querydsl username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: # show_sql: true format_sql: true logging.level: org.hibernate.SQL: debug # org.hibernate.type: trace</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>application.properties</strong></p><pre class="brush:as3;">#yaml 전환사이트 https://mageddo.com/tools/yaml-converter #터미널 코드 컬러 출력 설정 spring.output.ansi.enabled=always #Springboot auto build spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true spring.driver-class-name=org.h2.Driver spring.datasource.url= jdbc:h2:tcp://localhost/~/querydsl spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto= create #spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true #spring.jpa.properties.hibernate.default_batch_fetch_size=100 #페이지크기 spring.data.web.pageable.default-page-size=3 # 최대 페이지 크기 spring.data.web.pageable.max-page-size=2000 logging.level.org.hibernate.SQL=debug logging.level.org.hibernate.type=trace</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>spring.jpa.hibernate.ddl-auto: create<br />이 옵션은 애플리케이션 실행 시점에 테이블을 drop 하고, 다시 생성한다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>&gt; 참고: 모든 로그 출력은 가급적 로거를 통해 남겨야 한다.</strong><br />&gt; <span style="color:#2980b9"><strong>show_sql </strong></span>: 옵션은 System.out 에 하이버네이트 실행 SQL을 남긴다.<br />&gt; <strong><span style="color:#2980b9">org.hibernate.SQL</span></strong> : 옵션은 logger를 통해 하이버네이트 실행 SQL을 남긴다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>쿼리 파라미터 로그 남기기</strong><br />로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.</p><p><br />외부 라이브러리 사용<br /><a target="_blank" href="https://github.com/gavlyukovskiy/spring-boot-data-source-decorator">https://github.com/gavlyukovskiy/spring-boot-data-source-decorator</a></p><p>&nbsp;</p><p>&nbsp;</p><p>스프링 부트를 사용하면 이 라이브러리만 추가하면 된다</p><pre class="brush:as3;"> implementation &#39;com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8&#39; </pre><p>&nbsp;</p><p>&gt; 참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는<br />편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:20px"><strong>★쿼리 파라미터 로그 남기기 - 스프링 부트 3.0</strong></span></span></p><p>&nbsp;</p><p><strong>p6spy-spring-boot-starter 라이브러리는 현재 스프링 부트 3.0을 정상 지원하지 않는다.</strong></p><p><br /><strong>스프링 부트 3.0에서 사용하려면 다음과 같은 추가 설정이 필요하다</strong></p><p>&nbsp;</p><p><strong><span style="color:#8e44ad"><span style="font-size:18px">1) org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 추가</span></span></strong></p><pre class="brush:as3;">src/resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports</pre><p>&nbsp;</p><pre class="brush:as3;">com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfigurati on</pre><p>&nbsp;</p><p><span style="color:#c0392b"><strong>폴더명</strong></span>:&nbsp; &nbsp;<strong>src/resources/META-INF/spring</strong></p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>파일명</strong></span>:&nbsp; &nbsp;<strong>org.springframework.boot.autoconfigure.AutoConfiguration.imports</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#8e44ad"><span style="font-size:16px"><strong>2)&nbsp;&nbsp;spy.properties 파일 추가</strong></span></span></p><p>&nbsp;</p><p><strong>src/resources/spy.properties</strong></p><pre class="brush:as3;">appender=com.p6spy.engine.spy.appender.Slf4JLogger </pre><p>&nbsp;</p><p><strong>이렇게 2개의 파일을 추가하면 정상 동작한다.</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><strong>메이븐</strong></p><pre class="brush:as3;"> &lt;dependency&gt; &lt;groupId&gt;com.github.gavlyukovskiy&lt;/groupId&gt; &lt;artifactId&gt;p6spy-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.9.0&lt;/version&gt; &lt;/dependency&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><span style="font-size:28px"><strong>[3] 예제 도메인 모델</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#c0392b"><span style="font-size:26px"><strong>8.예제 도메인 모델과 동작확인</strong></span></span></p><p>&nbsp;</p><p>강의 :</p><p><a target="_blank" href="https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30120&amp;tab=curriculum">https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&amp;unitId=30120&amp;tab=curriculum</a></p><p>&nbsp;</p><p>&nbsp;</p><p>참고: 스프링 데이터 JPA와 동일한 예제 도메인 모델을 사용합니다. 스프링 데이터 JPA 강의를 들으신 분은<br />엔티티 코드만 작성하고 다음으로 넘어가도 됩니다.<br />예제 도메인 모델과 동작확인</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZXt5gQB0nnYyfkUOp8nyFFGJ04P1mN9BMQKfdNQuqPLfjwxoKvCutVjtxzGZ2ZyyKYOxSeYUzfEQQKzlzBUzJcJh5fytm5Bna4XTA4QLLv3ijogya32-i_EdcMUCLYhlrY5FGWQSN3Mx1Y-SJ1_GyOB-9OphcIBkQrwRqwwaxrCpQaVVRJfUA5jAC5g/s16000/2023-03-16%2017%2013%2035.png" /></p><p>&nbsp;</p><p><strong>Member 엔티티</strong></p><pre class="brush:as3;">package study.querydsl.entity; import lombok.*; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {&quot;id&quot;, &quot;username&quot; , &quot;age&quot;}) public class Member { @Id @GeneratedValue @Column(name=&quot;member_id&quot;) private Long id; private String username; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name=&quot;team_id&quot;) private Team team; public Member(String username) { this(username, 0); } public Member(String username, int age) { this(username, age, null); } public Member(String username, int age, Team team){ this.username=username; this.age=age; if(team!=null){ changeTeam(team); } } private void changeTeam(Team team) { this.team=team; team.getMembers().add(this); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:20px">롬복 설명</span></strong></p><p><br /><span style="color:#c0392b"><strong>@Setter: </strong></span>실무에서 가급적 Setter는 사용하지 않기</p><p><br /><span style="color:#c0392b"><strong>@NoArgsConstructor AccessLevel.PROTECTED</strong></span>: 기본 생성자 막고 싶은데, JPA 스팩상&nbsp; PROTECTED로 열어두어야 함</p><p><br /><span style="color:#c0392b"><strong>@ToString</strong></span>은 가급적 내부 필드만(연관관계 없는 필드만)</p><p><br /><strong><span style="color:#c0392b">changeTeam()</span> </strong>으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드)</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>Team 엔티티</strong></p><pre class="brush:as3;">package study.querydsl.entity; import lombok.*; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {&quot;id&quot;, &quot;name&quot; }) public class Team { @Id @GeneratedValue @Column(name=&quot;team_id&quot;) private Long id; private String name; @OneToMany(mappedBy = &quot;team&quot;) private List&lt;Member&gt; members=new ArrayList&lt;&gt;(); public Team(String name){ this.name=name; } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>Member와 Team은 양방향 연관관계, Member.team 이 연관관계의 주인, Team.members 는 연관관계의<br />주인이 아님, 따라서 Member.team 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>데이터 확인 테스트</strong></p><pre class="brush:as3;">package study.querydsl.entity; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Commit; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional //@Commit class MemberTest { @Autowired EntityManager em; @Test public void testEntity(){ Team teamA =new Team(&quot;teamA&quot;); Team teamB =new Team(&quot;teamB&quot;); em.persist(teamA); em.persist(teamB); Member member1=new Member(&quot;member1&quot;, 10, teamA); Member member2=new Member(&quot;member2&quot;, 20, teamA); Member member3=new Member(&quot;member3&quot;, 30, teamB); Member member4=new Member(&quot;member4&quot;, 40, teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member4); //초기화 em.flush(); em.clear(); List&lt;Member&gt; members = em.createQuery(&quot;select m from Member m &quot;, Member.class).getResultList(); for (Member member :members){ System.out.println(&quot;member = &quot; + member); System.out.println(&quot;-&gt; member.team&quot; +member.getTeam()); } } }</pre><p>&nbsp;</p><p><strong>가급적 순수 JPA로 동작 확인 (뒤에서 변경)</strong></p><p><br /><strong>db 테이블 결과 확인</strong></p><p><br /><strong>지연 로딩 동작 확인</strong></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-22 17:09:53 스프링 fullcalendar 이용한 일정관리 개발하기 http://macaronics.net/index.php/m01/spring/view/2087 2087 <p>&nbsp;</p><p>&nbsp;</p><p><strong>결과 화면</strong></p><p>&nbsp;</p><p>&nbsp;</p><p><a target="_blank" href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZq5xd7b1FZZphCA8e8-SgVierjOhO0W978V1ttsieD-KUL4O0HDlujMIFgCjgAOpHFikS7igsKJVpOApN9RsWfNTHJxc96vfsLZIQvZXZEqMc2lzjsH5R_RWZ1uJXrGKklqaFJJtdt2gilmVAnnboeoTJ43fTr6FBL3EtYFz0ui_fv99xmqRnuTCrXA/s1301/2023-03-22%2011%2019%2014.png">이미지 새창</a></p><p><img alt="" width="900" height="695" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhZq5xd7b1FZZphCA8e8-SgVierjOhO0W978V1ttsieD-KUL4O0HDlujMIFgCjgAOpHFikS7igsKJVpOApN9RsWfNTHJxc96vfsLZIQvZXZEqMc2lzjsH5R_RWZ1uJXrGKklqaFJJtdt2gilmVAnnboeoTJ43fTr6FBL3EtYFz0ui_fv99xmqRnuTCrXA/s16000/2023-03-22%2011%2019%2014.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><img alt="" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy3U3RkULIpZLWdVoZRZ9uhiPz_bymU0J6UZwF5qp1LpHc4450TyliP7AdbFOpukcWRD4Vlqxug_uWQ17bN1Xf_t94wamXjeQOYO9pvdYIiBECOoUT5JhF1pxjBcouyLMDWbhlO4ZZnk4mRNNV2KasNqDhPrLJd5YqVbPckFADTJl3az2NjSjHNwR2wg/s16000/2023-03-22%2011%2014%2037.png" /></p><p>&nbsp;</p><p>&nbsp;</p><p><span style="color:#2980b9"><strong><span style="font-size:18px">개발환경 :&nbsp; &nbsp;</span></strong></span><span style="color:#c0392b"><span style="font-size:18px"><strong>스프링부트 2.7.9 + mybatis&nbsp; +mysql + jsp&nbsp;</strong></span></span></p><p>&nbsp;</p><p>&nbsp;</p><p>fullcalendar 소스는 다음 링크 주소에 있으며 현재 시점 6.15 이다.</p><p><a target="_blank" href="https://github.com/fullcalendar/fullcalendar">https://github.com/fullcalendar/fullcalendar</a></p><p>&nbsp;</p><p>&nbsp;</p><p>여기 프로젝에서 적용한 fullcalendar 버전은&nbsp; 5.9.0&nbsp; 이다.</p><p><a target="_blank" href="https://github.com/braverokmc79/fullcalendar-5.9.0">https://github.com/braverokmc79/fullcalendar-5.9.0</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>MYSQL</strong></p><pre class="brush:as3;">CREATE TABLE `tbl_schedulemanage` ( `scheduleId` int(11) NOT NULL AUTO_INCREMENT COMMENT &#39;schedule번호(PK)&#39;, `scheduleName` varchar(255) DEFAULT NULL COMMENT &#39;스케줄제목&#39;, `url` varchar(100) DEFAULT NULL COMMENT &#39;링크주소&#39;, `textColor` varchar(15) DEFAULT NULL COMMENT &#39;글자색&#39;, `color` varchar(15) DEFAULT NULL COMMENT &#39;테두리색&#39;, `backgroundColor` varchar(15) DEFAULT NULL COMMENT &#39;배경색&#39;, `startDate` varchar(25) DEFAULT NULL COMMENT &#39;시작일&#39;, `endDate` varchar(25) DEFAULT NULL COMMENT &#39;끝일&#39;, PRIMARY KEY (`scheduleId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>Mybatis</strong></p><pre class="brush:as3;">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt; &lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&gt; &lt;mapper namespace=&quot;패키지명.model.dao.mapper.ScheduleManageMapper&quot;&gt; &lt;!-- 관리자 화면일경우 url 생략 --&gt; &lt;select id=&quot;selectAdminEventList&quot; resultType=&quot;패키지명.model.vo.ScheduleManageVO&quot;&gt; SELECT scheduleid AS &quot;id&quot; , schedulename as &quot;title&quot; , if(textColor is null, &#39;#ffffff&#39;, textColor ) as textColor, if(color is null, &#39;#3788d8&#39;, color ) as color, if(backgroundColor is null, &#39;#3788d8&#39;, backgroundColor ) as backgroundColor , startdate as &quot;start&quot; , enddate AS &quot;end&quot; FROM tbl_schedulemanage &lt;/select&gt; &lt;!-- 5년전 데이터까지만 가져오기 --&gt; &lt;select id=&quot;selectEventList&quot; resultType=&quot;lineage.choco.model.vo.ScheduleManageVO&quot;&gt; SELECT scheduleid AS &quot;id&quot; , schedulename as &quot;title&quot; , if(url is null, &#39;#&#39;, url ) as url, if(textColor is null, &#39;#ffffff&#39;, textColor ) as textColor, if(color is null, &#39;#3788d8&#39;, color ) as color, if(backgroundColor is null, &#39;#3788d8&#39;, backgroundColor ) as backgroundColor , startdate as &quot;start&quot; , enddate AS &quot;end&quot; FROM tbl_schedulemanage WHERE startdate &gt;= date_add(now(), interval -5 year) &lt;/select&gt; &lt;insert id=&quot;addSchedule&quot; parameterType=&quot;패키지명.model.vo.ScheduleManageVO&quot;&gt; INSERT INTO tbl_schedulemanage (schedulename, url , textColor, color , backgroundColor , startdate, enddate) VALUES( #{scheduleName}, #{url} , #{textColor} , #{color} , #{backgroundColor} , #{startDate}, #{endDate}) &lt;/insert&gt; &lt;select id=&quot;getEvent&quot; resultType=&quot;패키지명.model.vo.ScheduleManageVO&quot;&gt; SELECT scheduleid AS &quot;id&quot; , schedulename as &quot;title&quot; , if(url is null, &#39;#&#39;, url ) as url, if(textColor is null, &#39;#ffffff&#39;, textColor ) as textColor, if(color is null, &#39;#3788d8&#39;, color ) as color, if(backgroundColor is null, &#39;#3788d8&#39;, backgroundColor ) as backgroundColor , startdate as &quot;start&quot; , enddate AS &quot;end&quot; FROM tbl_schedulemanage WHERE scheduleid =#{id} &lt;/select&gt; &lt;delete id=&quot;deleteSch&quot;&gt; DELETE FROM tbl_schedulemanage WHERE SCHEDULEID =#{id} &lt;/delete&gt; &lt;update id=&quot;updateSch&quot;&gt; UPDATE tbl_schedulemanage SET &lt;if test=&quot;scheduleName!=null and !scheduleName.equals(&#39;&#39;) &quot;&gt; scheduleName= #{scheduleName}, &lt;/if&gt; &lt;if test=&quot;url!=null and !url.equals(&#39;&#39;) &quot;&gt; url=#{url}, &lt;/if&gt; &lt;if test=&quot;textColor!=null and !textColor.equals(&#39;&#39;) &quot;&gt; textColor=#{textColor}, &lt;/if&gt; &lt;if test=&quot;color!=null and !color.equals(&#39;&#39;) &quot;&gt; color=#{color}, &lt;/if&gt; &lt;if test=&quot;backgroundColor!=null and !backgroundColor.equals(&#39;&#39;) &quot;&gt; backgroundColor=#{backgroundColor}, &lt;/if&gt; startdate=#{startDate}, enddate=#{endDate} WHERE scheduleid=#{scheduleId} &lt;/update&gt; &lt;/mapper&gt;</pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>ScheduleManageVO</strong></p><pre class="brush:as3;">import lombok.Data; import lombok.ToString; @Data @ToString public class ScheduleManageVO { private int scheduleId; private String scheduleName; private String startDate; private String endDate; private String id; private String title; //스케줄제목 private String url;// 링크주소 private String textColor; //글자색 private String color; //테두리색 private String backgroundColor;//배경색 private String start; //시작일 private String end; //종료일 } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>ScheduleManageService</strong></p><p>&nbsp;</p><pre class="brush:as3;">import java.util.List; import java.util.Map; import 패키지.model.vo.ScheduleManageVO; public interface ScheduleManageService { public List&lt;ScheduleManageVO&gt; showSchedule() throws Exception; public int addSchedule(ScheduleManageVO calendarVO) throws Exception; public List&lt;ScheduleManageVO&gt; selectEventList(Map&lt;String, Object&gt; map) throws Exception; public ScheduleManageVO getEvent(Map&lt;String, Object&gt; map) throws Exception; public int deleteSch(ScheduleManageVO calendarVO) throws Exception; public int updateSch(ScheduleManageVO calendarVO) throws Exception; } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>ScheduleManageServiceImpl</strong></p><pre class="brush:as3;">import java.util.List; import java.util.Map; import org.springframework.stereotype.Service; import 패키지.model.dao.mapper.ScheduleManageMapper; import 패키지.model.vo.ScheduleManageVO; import 패키지.service.ScheduleManageService; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class ScheduleManageServiceImpl implements ScheduleManageService { private final ScheduleManageMapper scheduleManageMapper; public List&lt;ScheduleManageVO&gt; showSchedule() throws Exception { return scheduleManageMapper.showSchedule(); } public int addSchedule(ScheduleManageVO calendarVO) throws Exception { return scheduleManageMapper.addSchedule(calendarVO); } public List&lt;ScheduleManageVO&gt; selectEventList(Map&lt;String, Object&gt; map) throws Exception { return scheduleManageMapper.selectEventList(map); } public ScheduleManageVO getEvent(Map&lt;String, Object&gt; map) throws Exception { return scheduleManageMapper.getEvent(map); } public int deleteSch(ScheduleManageVO calendarVO) throws Exception { return scheduleManageMapper.deleteSch(calendarVO); } public int updateSch(ScheduleManageVO calendarVO) throws Exception { return scheduleManageMapper.updateSch(calendarVO); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>ScheduleManageMapper</strong></p><pre class="brush:as3;">import java.util.List; import java.util.Map; import org.springframework.stereotype.Repository; import 패키지.model.vo.ScheduleManageVO; @Repository public interface ScheduleManageMapper { public List&lt;ScheduleManageVO&gt; showSchedule() throws Exception; public int addSchedule(ScheduleManageVO calendarVO) throws Exception; public List&lt;ScheduleManageVO&gt; selectEventList(Map&lt;String, Object&gt; map) throws Exception; public ScheduleManageVO getEvent(Map&lt;String, Object&gt; map) throws Exception; public int deleteSch(ScheduleManageVO calendarVO) throws Exception; public int updateSch(ScheduleManageVO calendarVO) throws Exception; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>1) 관리자 화면</strong></span></span></p><p>&nbsp;</p><p><strong>컨트롤</strong></p><pre class="brush:as3;">import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import 패키지.config.auth.PrincipalDetails; import 패키지.model.vo.BoardVO; import 패키지.model.vo.ScheduleManageVO; import 패키지.service.BoardService; import 패키지.service.ScheduleManageService; import lombok.RequiredArgsConstructor; @Controller @RequiredArgsConstructor @RequestMapping(&quot;/admin/scheduleManage/&quot;) public class AdminScheduleManageController { private final ScheduleManageService calendarService; private final BoardService boardService; /** 스캐쥴 화면 */ @GetMapping(&quot;list&quot;) public String scheduleManageList(@AuthenticationPrincipal PrincipalDetails priDetails, Model model) throws Exception { model.addAttribute(&quot;menuName&quot;, &quot;일정관리&quot;); return &quot;admin/scheduleManage/scheduleManage_list&quot;; } @ResponseBody @PostMapping(&quot;addSchedule&quot;) public int addSchedule(ScheduleManageVO scheduleManageVO) throws Exception { return calendarService.addSchedule(scheduleManageVO); } /** 스캐쥴 목록 가져오기 */ @ResponseBody @PostMapping(&quot;selectEventList&quot;) public List&lt;ScheduleManageVO&gt; selectEventList(@RequestParam Map&lt;String, Object&gt; map) throws Exception { return calendarService.selectEventList(map); } @ResponseBody @PostMapping(&quot;getEvent&quot;) public ScheduleManageVO getEvent(@RequestParam Map&lt;String, Object&gt; map) throws Exception { return calendarService.getEvent(map); } @ResponseBody @PostMapping(&quot;deleteSch&quot;) public int deleteSch(ScheduleManageVO scheduleManageVO) throws Exception { return calendarService.deleteSch(scheduleManageVO); } @ResponseBody @PostMapping(&quot;updateSch&quot;) public int updateSch(ScheduleManageVO scheduleManageVO) throws Exception { return calendarService.updateSch(scheduleManageVO); } /** 게시판 링크 URL 가져오기 */ @ResponseBody @PostMapping(&quot;findBoardTypeByTitleList&quot;) public List&lt;BoardVO&gt; findBoardTypeByTitleList(@RequestParam Map&lt;String, Object&gt; map) throws Exception { return boardService.findBoardTypeByTitleList(map); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>scheduleManage_list.jsp</strong></p><pre class="brush:as3;">&lt;%@page import=&quot;java.util.List&quot;%&gt; &lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html lang=&quot;ko&quot;&gt; &lt;head&gt; &lt;!-- 부트스트랩 라이브러리 --&gt; &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC&quot; crossorigin=&quot;anonymous&quot;&gt; &lt;!-- fullcalendar 라이브러리 --&gt; &lt;link href=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/main.css&#39; rel=&#39;stylesheet&#39; /&gt; &lt;!-- daterangepicker 라이브러리 --&gt; &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.css&quot; /&gt; &lt;!-- 커스텀 .css --&gt; &lt;link href=&quot;/resources/lib/fullcalendar/css/scheduleManage_list.css&quot; rel=&quot;stylesheet&quot;&gt; &lt;/head&gt; &lt;body&gt; &lt;div id=&#39;calendar-container&#39;&gt; &lt;div id=&#39;calendar&#39;&gt;&lt;/div&gt; &lt;/div&gt; &lt;%@ include file=&quot;schedule_manage_modal.jsp&quot; %&gt; &lt;!-- jquery 라이브러리 --&gt; &lt;script src=&quot;https://code.jquery.com/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt; &lt;!-- 부트스트랩 라이브러리 --&gt; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js&quot; integrity=&quot;sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js&quot; integrity=&quot;sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt; &lt;!-- 날짜 라이브러리 --&gt; &lt;script type=&quot;text/javascript&quot; src=&quot;//cdn.jsdelivr.net/momentjs/latest/moment.min.js&quot;&gt;&lt;/script&gt; &lt;!-- fullcalendar 라이브러리 --&gt; &lt;script src=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/main.js&#39;&gt;&lt;/script&gt; &lt;script src=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/locales-all.min.js&#39;&gt;&lt;/script&gt; &lt;!-- daterangepicker 라이브러리 --&gt; &lt;script type=&quot;text/javascript&quot; src=&quot;//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.js&quot;&gt;&lt;/script&gt; &lt;!-- 커스텀 js --&gt; &lt;script src=&quot;/resources/lib/fullcalendar/js/scheduleManage_list.js&quot;&gt;&lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>scheduleManage_list.css</strong></p><pre class="brush:as3;">@charset &quot;utf-8&quot;; #calendar{ width:80%; margin:20px auto; margin-bottom:200px; } a { color: #000000; text-decoration: none; } /* 드래그 박스의 스타일 */ #external-events { position: fixed; left: 20px; top: 20px; width: 100px; padding: 0 10px; border: 1px solid #ccc; background: #eee; text-align: left; } #external-events h4 { font-size: 16px; margin-top: 0; padding-top: 1em; } #external-events .fc-event { margin: 3px 0; cursor: move; } #external-events p { margin: 1.5em 0; font-size: 11px; color: #666; } #external-events p input { margin: 0; vertical-align: middle; } #calendar-wrap { margin-left: 0px; } #calendar1 { max-width: 1100px; margin: 0 auto; } .fc-myCustomButton-button { /* background: #b31515 !important; */ } .form-group{ margin-bottom: 30px; } .fc-event-time, .fc-event-title{ font-size: 15px; } .empl_nm{ display: none; } .daterangepicker .table-condensed tbody tr td.available:first-child { color: #0d6efd;} .daterangepicker .table-condensed tbody tr td.available:last-child { color: #f95f53 !important;} .daterangepicker td.active, .daterangepicker td.active:hover {background-color: #c5daed; border-color: transparent; color: #fff !important;} .daterangepicker .table-condensed tbody tr td.off { color: #999 !important; opacity:0.5;} .modal .modal-dialog .modal-content .modal-footer { padding: 15px 31px;display: flex;justify-content: space-around;} .fc-daygrid-day.fc-day.fc-day-sat.fc-day-past .fc-daygrid-day-top a , .fc-daygrid-day.fc-day.fc-day-sat.fc-day-future .fc-daygrid-day-top a { color:#2575fc; } /* 토요일 */ .fc-daygrid-day.fc-day.fc-day-sun.fc-day-past .fc-daygrid-day-top a, .fc-daygrid-day.fc-day.fc-day-sun.fc-day-future .fc-daygrid-day-top a {color:#dd3e0e; }/* 일요일 */ </pre><p>&nbsp;</p><p><strong>scheduleManage_list.js</strong></p><p>&nbsp;</p><p><span style="color:#c0392b"><strong>다음 사이틀 참조해서&nbsp;&nbsp;googleCalendarApiKey&nbsp; 구글 api 키값 적용</strong></span></p><p><a target="_blank" href="https://codingtrip.tistory.com/2">Spring에서 FullCalendar(풀 캘린더)로 Google(구글) 캘린더 DB 연동하기</a></p><p>&nbsp;</p><p>&nbsp;</p><pre class="brush:as3;">let $g_arg; //모달창에서 호출하는 함수에서 참조하기 위함) let $calendar; let $daterangeStartDate = &quot;&quot;; let $daterangeEndDate = &quot;&quot;; document.addEventListener(&#39;DOMContentLoaded&#39;, function() { //FullCalendar 초기 셋팅 및 데이터 불러오기 getFullCalendarEvent(); $(&#39;body&#39;).on(&#39;click&#39;, &#39;button.fc-prev-button&#39;, function(e) { console.log(&quot;prev1&quot;); }); $(&#39;body&#39;).on(&#39;click&#39;, &#39;button.fc-next-button&#39;, function(e) { console.log(&quot;next&quot;); }); }); //FullCalendar 초기 셋팅 function getFullCalendarEvent(currentDatePage) { const calendarEl = document.getElementById(&#39;calendar&#39;); $calendar = new FullCalendar.Calendar(calendarEl, { googleCalendarApiKey: &#39;구글 APIKEY&#39;, //className은 되도록 캘린더랑 맞추길 eventSources: [ { googleCalendarId: &#39;ko.south_korea#holiday@group.v.calendar.google.com&#39;, className: &#39;대한민국의 휴일&#39;, color: &#39;#be5683&#39;, //rgb,#ffffff 등의 형식으로 할 수 있다 //textColor: &#39;black&#39; }, ], customButtons: { myCustomButton: { text: &#39;일정입력&#39;, click: function(event) { onSelectEvent(event); } } }, headerToolbar: { left: &#39;prev,next today&#39;, center: &#39;title&#39;, right: &#39;dayGridMonth,timeGridWeek,timeGridDay,listWeek,myCustomButton&#39; }, initialDate: currentDatePage, // 초기 날짜 설정 (설정하지 않으면 오늘 날짜가 보인다.) locale: &#39;ko&#39;, // 한국어 설정 editable: true, // 수정 가능 droppable: true, // 드래그 가능 drop: function(arg) { // 드래그 엔 드롭 성공시 }, defaultView: &#39;timeGridWeek&#39;, navLinks: false, // can click day/week names to navigate views allDaySlot: false, eventLimit: true, // allow &quot;more&quot; link when too many events //minTime: &#39;10:00:00&#39;, //maxTime: &#39;24:00:00&#39;, //contentHeight: &#39;auto&#39;, dateClick: function(arg) { //해당월 페이지 이동을 위한 날짜가져오기 var getData = this.getCurrentData(); var currentDatePage = moment(getData.currentDate).format(&#39;YYYY-MM-DD&#39;); $(&quot;#currentDatePage&quot;).val(currentDatePage); insertModalOpen(arg); }, eventClick: function(info) { //여기서 info 가 아니라 event 로 처리해야 함 event.preventDefault(); //만약 구글 캘린던라면 링크 이동 중단 처리 if (info.event.url.includes(&#39;https://www.google.com/calendar/&#39;)) { return; } //해당월 페이지 이동을 위한 날짜가져오기 var getData = this.getCurrentData(); var currentDatePage = moment(getData.currentDate).format(&#39;YYYY-MM-DD&#39;); $(&quot;#currentDatePage&quot;).val(currentDatePage); var url = info.event.url; //모달창 호출 updateModalOpen(info.event.id, url); }, eventAdd: function(obj) { // 이벤트가 추가되면 발생하는 이벤트 //console.log(&quot; 이벤트가 추가되면 발생하는 이벤트&quot; ,obj); }, eventChange: function(obj) { // 이벤트가 수정되면 발생하는 이벤트 console.log(&quot;1.벤트가 수정되면 발생하는 이벤트 &quot;, obj); const scheduleId = obj.event._def.publicId; const startDate = moment(obj.event._instance.range.start).format(); const endDate = moment(obj.event._instance.range.end).format(); const param = { scheduleId, startDate, endDate } console.log(&quot;2.벤트가 수정되면 발생하는 이벤트 &quot;, obj.event._def.url); //만약 구글 캘린던라면 링크 중단 처리 if (obj.event._def.url.includes(&#39;https://www.google.com/calendar/&#39;)) { alert(&quot;지정된 공휴일은 업데이트 처리 될수 없습니다.&quot;); getFullCalendarEvent(); return; } $.ajax({ url: &quot;/admin/scheduleManage/updateSch&quot;, type: &quot;POST&quot;, data: param, dataType: &quot;text&quot;, success: function(result) { console.log(&quot; 업데이트 : &quot;, result); }, error: function(result) { console.log(&quot;error:&quot;); console.log(result); } }); }, select: function(arg) { // 캘린더에서 드래그로 이벤트를 생성할 수 있다. /* console.log(&quot; 드래그 &quot;, arg) var title = prompt(&#39;Event Title:&#39;); if (title) { calendar.addEvent({ title: title, start: arg.start, end: arg.end, allDay: arg.allDay }) } calendar.unselect() */ } }); //End ------------ var calendar = new FullCalendar.Calendar(calendarEl, //DB에서 데이터 가져오기 const arr = getCalendarDataInDB(); $.each(arr, function(index, item) { $calendar.addEvent(item); }); $calendar.render(); $calendar.on(&#39;dateClick&#39;, function(info) { // console.log(&#39;clicked on &#39; + info.dateStr); }); return $calendar; } //일정 입력 function onSelectEvent() { insertModalOpen(&quot;onSelectEvent&quot;); } //arr 는 테스트용으로 DB 에서 스케즐 데이터를 가져오지 못했을 경우 테스트용 데이터 function getCalendarDataInDB() { //arr 임의 데이터 let arr = [{ title: &#39;evt111&#39;, start: &#39;2023-03-22T10:30:00&#39;, end: &#39;2023-03-23T10:30:00&#39;, backgroundColor: &quot;#52cdff&quot;, color: &quot;#000&quot;, textColor: &quot;#fff&quot;, url: &quot;https://daum.net&quot; }]; $.ajax({ contentType: &#39;application/json&#39;, dataType: &#39;json&#39;, url: &#39;/admin/scheduleManage/selectEventList&#39;, type: &#39;post&#39;, async: false, success: function(res) { arr = res; }, error: function(res) { console.log(res); } }); return arr; } // 받은 날짜값을 date 형태로 형변환 해주어야 한다. function convertDate(date) { var date = new Date(date); alert(date.yyyymmdd()); } /* ****************************************************************************************************** 여기서 부터 모달 JS ****************************************************************************************************** * */ function customDaterangePicker() { $(&#39;input[name=&quot;daterange&quot;]&#39;).daterangepicker({ &quot;showDropdowns&quot;: true, &quot;showWeekNumbers&quot;: true, &quot;showISOWeekNumbers&quot;: true, &quot;timePicker&quot;: true, &quot;timePicker24Hour&quot;: true, &quot;timePickerSeconds&quot;: true, &quot;locale&quot;: { &quot;format&quot;: &quot;YYYY-MM-DD Ah:mm&quot;, &quot;separator&quot;: &quot; - &quot;, &quot;applyLabel&quot;: &quot;적용&quot;, &quot;cancelLabel&quot;: &quot;취소&quot;, &quot;fromLabel&quot;: &quot;From&quot;, &quot;toLabel&quot;: &quot;To&quot;, &quot;customRangeLabel&quot;: &quot;사용자 지정&quot;, &quot;weekLabel&quot;: &quot;W&quot;, &quot;daysOfWeek&quot;: [ &quot;일&quot;, &quot;월&quot;, &quot;화&quot;, &quot;수&quot;, &quot;목&quot;, &quot;금&quot;, &quot;토&quot; ], &quot;monthNames&quot;: [ &quot;1월&quot;, &quot;2월&quot;, &quot;3월&quot;, &quot;4월&quot;, &quot;5월&quot;, &quot;6월&quot;, &quot;7월&quot;, &quot;8월&quot;, &quot;9월&quot;, &quot;10월&quot;, &quot;11월&quot;, &quot;12월&quot; ], &quot;firstDay&quot;: 1 }, &quot;autoUpdateInput&quot;: false, &quot;alwaysShowCalendars&quot;: true, &quot;startDate&quot;: $daterangeStartDate, &quot;endDate&quot;: $daterangeEndDate, &quot;minDate&quot;: &quot;2000-12-31&quot;, &quot;maxDate&quot;: &quot;2050-12-31&quot; }, function(start, end, label) { $(&quot;#startDate&quot;).val(moment(start).format()); $(&quot;#endDate&quot;).val(moment(end).format()); $(&quot;#daterange&quot;).val(start.format(&#39;YYYY-MM-DD Ah:mm&#39;) + &quot; ~ &quot; + end.format(&#39;YYYY-MM-DD Ah:mm&#39;)); }); } //이벤트 등록 모달 function insertModalOpen(arg) { $(&quot;.scheDeleteBtn&quot;).css(&quot;display&quot;, &quot;none&quot;); $(&quot;.schInsertBtn&quot;).css(&quot;display&quot;, &quot;block&quot;); $(&quot;.schUpdateBtn&quot;).css(&quot;display&quot;, &quot;none&quot;); $(&quot;.modal-title&quot;).text(&quot;일정 등록&quot;); $(&quot;#title&quot;).val(&quot;&quot;); $(&quot;#url&quot;).val(&quot;&quot;); $(&quot;#textColor&quot;).val(&quot;#ffffff&quot;); $(&quot;#color&quot;).val(&quot;#1466b8&quot;); $(&quot;#backgroundColor&quot;).val(&quot;#3788d8&quot;); if (arg == &quot;onSelectEvent&quot;) { const date1 = new Date(); arg = { dateStr: moment(date1).format(&#39;YYYY MM DD&#39;) } } $g_arg = arg; //초기 해당일 날짜 셋팅 $daterangeStartDate = arg.dateStr + &quot; AM12:00&quot;; $daterangeEndDate = arg.dateStr + &quot; PM1:00&quot;; $(&quot;#daterange&quot;).val($daterangeStartDate + &quot; ~ &quot; + $daterangeEndDate); const sd1 = new Date(arg.dateStr + &quot;T12:00:00&quot;); const ed1 = new Date(arg.dateStr + &quot;T13:00:00&quot;); $(&quot;#startDate&quot;).val(moment(sd1).format()); $(&quot;#endDate&quot;).val(moment(ed1).format()); $(&quot;.daterangepicker_input input[name=daterangepicker_start]&quot;).val($daterangeStartDate); $(&quot;.daterangepicker_input input[name=daterangepicker_end]&quot;).val($daterangeEndDate); //end - 초기 해당일 날짜 셋팅 customDaterangePicker(); $(&#39;.insertModal&#39;).modal(&quot;show&quot;); } //스케쥴 등록 function insertSch(modal, arg) { const scheduleName = $(&quot;#title&quot;).val(); const url = $(&quot;#url&quot;).val(); const textColor = $(&quot;#textColor&quot;).val(); const color = $(&quot;#color&quot;).val(); const backgroundColor = $(&quot;#backgroundColor&quot;).val(); const startDate = $(&quot;#startDate&quot;).val(); const endDate = $(&quot;#endDate&quot;).val(); if ($(&#39;.insertModal #end&#39;).val() &lt;= $(&#39;.insertModal #start&#39;).val()) { alert(&#39;종료시간을 시작시간보다 크게 선택해주세요&#39;); $(&#39;.insertModal #end&#39;).focus(); return; } if ($(&#39;.&#39; + modal + &#39; #title&#39;).val() == &#39;&#39;) { alert(&#39;제목을 입력해주세요&#39;); $(&quot;#title&quot;).focus(); return; } if (startDate == &quot;&quot; || startDate == &quot;Invalid date&quot;) { alert(&quot;시작 날짜를 다시 선택해 주세요.&quot;); return; } if (endDate == &quot;&quot; || endDate == &quot;Invalid date&quot;) { alert(&quot;종료 날짜를 다시 선택해 주세요.&quot;); return; } const param = { scheduleName: scheduleName, url: url, textColor: textColor, color: color, backgroundColor: backgroundColor, startDate: startDate, endDate: endDate } //스케줄 작성 $.ajax({ url: &quot;/admin/scheduleManage/addSchedule&quot;, type: &quot;POST&quot;, data: param, dataType: &quot;text&quot;, success: function(result) { //console.log(result); loadEvents(); }, error: function(result) { console.log(&quot;error:&quot;); console.log(result); } }); } //캘린더 Refresh function loadEvents() { $calendar.destroy(); const currentDatePage = $(&quot;#currentDatePage&quot;).val(); //캘린더 데이터 목록 다시 불러오기 getFullCalendarEvent(currentDatePage); $(&#39;.insertModal&#39;).modal(&quot;hide&quot;); } //모달 닫기 function closeModal() { $(&#39;.insertModal&#39;).modal(&quot;hide&quot;); } //이벤트 수정 및 삭제 모달 function updateModalOpen(id, url) { const currentState = &quot;admin&quot;; $(&quot;.scheDeleteBtn&quot;).css(&quot;display&quot;, &quot;block&quot;); $(&quot;.schInsertBtn&quot;).css(&quot;display&quot;, &quot;none&quot;); $(&quot;.schUpdateBtn&quot;).css(&quot;display&quot;, &quot;block&quot;); $(&quot;.modal-title&quot;).text(&quot;일정 수정&quot;); if (currentState == &quot;admin&quot;) { $.ajax({ url: &quot;/admin/scheduleManage/getEvent&quot;, type: &quot;POST&quot;, data: { id: id }, dataType: &quot;json&quot;, success: function(data) { const { id, title, url, textColor, color, backgroundColor, start, end } = data; $(&quot;#scheduleId&quot;).val(id); $(&quot;#title&quot;).val(title); $(&quot;#url&quot;).val(data.url); $(&quot;#textColor&quot;).val(textColor); $(&quot;#color&quot;).val(color); $(&quot;#backgroundColor&quot;).val(backgroundColor); //초기 해당일 날짜 셋팅 $(&quot;#startDate&quot;).val(start); $(&quot;#endDate&quot;).val(end); $daterangeStartDate = moment(start).format(&#39;YYYY-MM-DD Ah:mm&#39;); $daterangeEndDate = moment(end).format(&#39;YYYY-MM-DD Ah:mm&#39;); $(&quot;#daterange&quot;).val($daterangeStartDate + &quot; ~ &quot; + $daterangeEndDate); $(&quot;.daterangepicker_input input[name=daterangepicker_start]&quot;).val($daterangeStartDate); $(&quot;.daterangepicker_input input[name=daterangepicker_end]&quot;).val($daterangeEndDate); //end - 해당일 날짜 셋팅 //date picker 호출 customDaterangePicker(); $(&#39;.insertModal&#39;).modal(&quot;show&quot;); }, error: function(result) { console.log(&quot;error:&quot;); console.log(result); } }); } else { //구글 이동 //window.open(url); } } //삭제 처리 function deleteSch() { const id = $(&quot;#scheduleId&quot;).val(); if (confirm(&quot;정말 삭제 하시겠습니까?&quot;)) { $.ajax({ url: &quot;/admin/scheduleManage/deleteSch&quot;, type: &quot;POST&quot;, data: { id: id }, dataType: &quot;json&quot;, success: function(result) { // console.log(result); if (result == 1) { loadEvents(); } else { console.log(&quot;error&quot;); } }, error: function(result) { console.log(&quot;error:&quot;); console.log(result); } }); } } //스케쥴 수정 function updateSch(modal, arg) { const scheduleId = $(&quot;#scheduleId&quot;).val(); const url = $(&quot;#url&quot;).val(); const textColor = $(&quot;#textColor&quot;).val(); const color = $(&quot;#color&quot;).val(); const backgroundColor = $(&quot;#backgroundColor&quot;).val(); const scheduleName = $(&quot;#title&quot;).val(); const startDate = $(&quot;#startDate&quot;).val(); const endDate = $(&quot;#endDate&quot;).val(); if ($(&#39;.insertModal #end&#39;).val() &lt;= $(&#39;.insertModal #start&#39;).val()) { alert(&#39;종료시간을 시작시간보다 크게 선택해주세요&#39;); $(&#39;.insertModal #end&#39;).focus(); return; } if ($(&#39;.&#39; + modal + &#39; #title&#39;).val() == &#39;&#39;) { alert(&#39;제목을 입력해주세요&#39;); $(&quot;#title&quot;).focus(); return; } if (startDate == &quot;&quot; || startDate == &quot;Invalid date&quot;) { alert(&quot;시작 날짜를 다시 선택해 주세요.&quot;); return; } if (endDate == &quot;&quot; || endDate == &quot;Invalid date&quot;) { alert(&quot;종료 날짜를 다시 선택해 주세요.&quot;); return; } const param = { scheduleId: scheduleId, scheduleName: scheduleName, url: url, textColor: textColor, color: color, backgroundColor: backgroundColor, startDate: startDate, endDate: endDate } $.ajax({ url: &quot;/admin/scheduleManage/updateSch&quot;, type: &quot;POST&quot;, data: param, dataType: &quot;text&quot;, success: function(result) { loadEvents(); }, error: function(result) { console.log(&quot;error:&quot;); console.log(result); } }); } /* ****************************************************************************************************** 여기서 부터는 fullcalendar 과 상관없는 게시판 목록 가져오기 ****************************************************************************************************** * */ function findBoardTypeByTitleList(event) { const boardType = event.value; if (boardType) { $.ajax({ type: &quot;POST&quot;, url: &quot;/admin/scheduleManage/findBoardTypeByTitleList&quot;, data: { boardType }, success: function(res) { console.log(res); const titleList = res.map(board =&gt; { console.log(board.agoTime); let agoTime = &quot;&quot;; if (parseInt(board.agoTime) &gt;= 1) { agoTime = &quot;[&quot; + board.agoTime + &quot;일전]&quot; } else { agoTime = &quot;[오늘]&quot;; } let title = &quot;&quot;; if (board.title.length &gt;= 20) { title = board.title.substring(0, 20) + &quot;...&quot;; } else { title = board.title; } title = title + &quot; &quot; + agoTime; return (&quot;&lt;option value=&#39;/board/&quot; + boardType + &quot;/read/&quot; + board.bno + &quot;&#39; &gt;&quot; + title + &quot;&lt;/option&gt;&quot;) }); $(&quot;#boardTitleList&quot;).html(titleList); setBoardLink(); }, error: function(err) { console.error(&quot;에러 : &quot;, err); } }) } } function setBoardLink() { const url = $(&quot;#boardTitleList&quot;).val(); $(&#39;#url&#39;).val(url); }</pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>schedule_manage_modal.jsp</strong></p><pre class="brush:as3;">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;div class=&quot;modal fade insertModal&quot; id=&quot;myModal&quot;&gt; &lt;div class=&quot;modal-dialog&quot;&gt; &lt;div class=&quot;modal-content&quot;&gt; &lt;!-- Modal Header --&gt; &lt;div class=&quot;modal-header&quot;&gt; &lt;h2 class=&quot;modal-title text-center&quot; style=&quot;width: 100%; font-size: 1.7rem&quot;&gt;&lt;/h2&gt; &lt;button type=&quot;button&quot; class=&quot;close&quot; onclick=&quot;closeModal()&quot;&gt;&amp;times;&lt;/button&gt; &lt;/div&gt; &lt;!-- Modal body --&gt; &lt;div class=&quot;modal-body&quot;&gt; &lt;div class=&quot;form-group empl_nm&quot;&gt; &lt;label for=&quot;empl_nm&quot;&gt;scheduleId:&lt;/label&gt; &lt;input type=&quot;text&quot; class=&quot;form-control&quot; id=&quot;scheduleId&quot; name=&quot;scheduleId&quot; readonly=&quot;readonly&quot;&gt; &lt;/div&gt; &lt;br&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;empl_nm&quot;&gt;날짜:&lt;/label&gt; &lt;input type=&quot;text&quot; name=&quot;daterange&quot; id=&quot;daterange&quot; class=&quot;form-control&quot; /&gt; &lt;/div&gt; &lt;div class=&quot;form-group empl_nm&quot;&gt; &lt;label for=&quot;date&quot;&gt;시작일:&lt;/label&gt; &lt;input type=&quot;text&quot; class=&quot;form-control&quot; id=&quot;startDate&quot; name=&quot;startDate&quot;&gt; &lt;/div&gt; &lt;div class=&quot;form-group empl_nm&quot;&gt; &lt;label for=&quot;date&quot;&gt;종료일:&lt;/label&gt; &lt;input type=&quot;text&quot; class=&quot;form-control&quot; id=&quot;endDate&quot; name=&quot;endDate&quot;&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;title&quot;&gt;제목:&lt;/label&gt; &lt;input type=&quot;text&quot; class=&quot;form-control&quot; placeholder=&quot;&quot; id=&quot;title&quot; maxlength=&quot;30&quot;&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;title&quot;&gt;게시판 URL 선택:&lt;/label&gt; &lt;select onchange=&quot;findBoardTypeByTitleList(this)&quot; class=&quot;form-control&quot;&gt; &lt;option value=&quot;&quot;&gt;게시판을 선택해 주세요.&lt;/option&gt; &lt;option value=&quot;lineage_news&quot;&gt;소식&lt;/option&gt; &lt;option value=&quot;lineage_stats&quot;&gt;서버통계&lt;/option&gt; &lt;option value=&quot;free&quot;&gt;자유게시판&lt;/option&gt; &lt;option value=&quot;broadcast&quot;&gt;방송정보&lt;/option&gt; &lt;option value=&quot;screen_capture&quot;&gt;스크린샷&lt;/option&gt; &lt;option value=&quot;events&quot;&gt;스크린샷&lt;/option&gt; &lt;/select&gt; &lt;select id=&quot;boardTitleList&quot; class=&quot;form-control&quot; onchange=&quot;setBoardLink()&quot;&gt; &lt;/select&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;url&quot;&gt;url : &lt;/label&gt; &lt;input type=&quot;url&quot; class=&quot;form-control&quot; placeholder=&quot;링크주소&quot; name=&quot;url&quot; id=&quot;url&quot; &gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;textColor&quot;&gt;글자색 : &lt;/label&gt; &lt;input type=&quot;color&quot; class=&quot;form-control&quot; name=&quot;textColor&quot; id=&quot;textColor&quot; value=&quot;#ffffff&quot; &gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;color&quot;&gt;테두리색 : &lt;/label&gt; &lt;input type=&quot;color&quot; class=&quot;form-control&quot; name=&quot;color&quot; id=&quot;color&quot; value=&quot;#1466b8&quot;&gt; &lt;/div&gt; &lt;div class=&quot;form-group&quot;&gt; &lt;label for=&quot;backgroundColor&quot;&gt;배경색 : &lt;/label&gt; &lt;input type=&quot;color&quot; class=&quot;form-control&quot; name=&quot;backgroundColor&quot; id=&quot;backgroundColor&quot; value=&quot;#3788d8&quot;&gt; &lt;/div&gt; &lt;!-- Modal footer --&gt; &lt;div class=&quot;modal-footer text-center&quot;&gt; &lt;button type=&quot;button&quot; class=&quot;btn btn-danger scheDeleteBtn&quot; onclick=&quot;deleteSch()&quot; &gt;삭제&lt;/button&gt; &lt;button type=&quot;button&quot; class=&quot;btn btn-warning schInsertBtn&quot; onclick=&quot;insertSch(&#39;insertModal&#39;, $g_arg)&quot;&gt;등록&lt;/button&gt; &lt;button type=&quot;button&quot; class=&quot;btn btn-warning schUpdateBtn&quot; onclick=&quot;updateSch(&#39;insertModal&#39;, $g_arg)&quot; &gt;수정&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; &lt;/div&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px"><span style="color:#c0392b"><strong>2) 유저화면</strong></span></span></p><p>&nbsp;</p><p><strong>컨트롤</strong></p><pre class="brush:as3;">import java.util.List; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import 패키지.model.vo.ScheduleManageVO; import 패키지.service.ScheduleManageService; import lombok.RequiredArgsConstructor; @Controller @RequestMapping(&quot;/schedule/&quot;) @RequiredArgsConstructor public class ScheduleController { private final ScheduleManageService calendarService; /** 목록 */ @GetMapping(&quot;list&quot;) public String scheduleList( Model model) throws Exception { return &quot;web/schedule/schedule_list&quot;; } @ResponseBody @GetMapping(&quot;selectEventList&quot;) public List&lt;ScheduleManageVO&gt; selectEventList(@RequestParam Map&lt;String, Object&gt; map) throws Exception { return calendarService.selectEventList(map); } } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong>schedule_list.jsp</strong></p><pre class="brush:as3;">&lt;%@page import=&quot;java.util.List&quot;%&gt; &lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt; &lt;!DOCTYPE html&gt; &lt;html lang=&quot;ko&quot;&gt; &lt;head&gt; &lt;!-- 부트스트랩 라이브러리 --&gt; &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC&quot; crossorigin=&quot;anonymous&quot;&gt; &lt;!-- fullcalendar 라이브러리 --&gt; &lt;link href=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/main.css&#39; rel=&#39;stylesheet&#39; /&gt; &lt;!-- daterangepicker 라이브러리 --&gt; &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.css&quot; /&gt; &lt;!-- 커스텀 .css --&gt; &lt;link href=&quot;/resources/lib/fullcalendar/css/scheduleManage_list.css&quot; rel=&quot;stylesheet&quot;&gt; &lt;/head&gt; &lt;body&gt; &lt;div id=&#39;calendar-container&#39;&gt; &lt;div id=&#39;calendar&#39;&gt;&lt;/div&gt; &lt;/div&gt; &lt;%@ include file=&quot;schedule_manage_modal.jsp&quot; %&gt; &lt;!-- jquery 라이브러리 --&gt; &lt;script src=&quot;https://code.jquery.com/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt; &lt;!-- 부트스트랩 라이브러리 --&gt; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js&quot; integrity=&quot;sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt; &lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js&quot; integrity=&quot;sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt; &lt;!-- 날짜 라이브러리 --&gt; &lt;script type=&quot;text/javascript&quot; src=&quot;//cdn.jsdelivr.net/momentjs/latest/moment.min.js&quot;&gt;&lt;/script&gt; &lt;!-- fullcalendar 라이브러리 --&gt; &lt;script src=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/main.js&#39;&gt;&lt;/script&gt; &lt;script src=&#39;https://cdn.jsdelivr.net/gh/braverokmc79/fullcalendar-5.9.0@v5.9.0/lib/locales-all.min.js&#39;&gt;&lt;/script&gt; &lt;!-- daterangepicker 라이브러리 --&gt; &lt;script type=&quot;text/javascript&quot; src=&quot;//cdn.jsdelivr.net/bootstrap.daterangepicker/2/daterangepicker.js&quot;&gt;&lt;/script&gt; &lt;!-- 커스텀 js --&gt; &lt;script src=&quot;/resources/lib/fullcalendar/js/schedule_list.js&quot;&gt;&lt;/script&gt; &lt;/body&gt; &lt;/html&gt; </pre><p>&nbsp;</p><p>&nbsp;</p><p><strong>schedule_list.js</strong></p><pre class="brush:as3;">let $calendar; document.addEventListener(&#39;DOMContentLoaded&#39;, function() { //FullCalendar 초기 셋팅 및 데이터 불러오기 getFullCalendarEvent(); }); //FullCalendar 초기 셋팅 function getFullCalendarEvent() { const calendarEl = document.getElementById(&#39;calendar&#39;); $calendar = new FullCalendar.Calendar(calendarEl, { googleCalendarApiKey: &#39;구글 APIKEY&#39;, //className은 되도록 캘린더랑 맞추길 eventSources: [ { googleCalendarId: &#39;ko.south_korea#holiday@group.v.calendar.google.com&#39;, className: &#39;대한민국의 휴일&#39;, color: &#39;#be5683&#39;, //rgb,#ffffff 등의 형식으로 할 수 있다 //textColor: &#39;black&#39; }, ], headerToolbar: { left: &#39;prev,next today&#39;, center: &#39;title&#39;, right: &#39;dayGridMonth,timeGridWeek,timeGridDay,listWeek,myCustomButton&#39; }, //initialDate: currentDatePage, // 초기 날짜 설정 (설정하지 않으면 오늘 날짜가 보인다.) locale: &#39;ko&#39;, // 한국어 설정 editable: false, // 수정 가능 droppable: false, // 드래그 가능 drop: function(arg) { // 드래그 엔 드롭 성공시 }, defaultView: &#39;timeGridWeek&#39;, navLinks: false, // can click day/week names to navigate views allDaySlot: false, eventLimit: true, // allow &quot;more&quot; link when too many events dateClick: function(arg) { }, eventClick: function(info) { //여기서 info 가 아니라 event 로 처리해야 함 event.preventDefault(); //만약 구글 캘린던라면 링크 이동 중단 처리 if (info.event.url.includes(&#39;https://www.google.com/calendar/&#39;)) { return; } const url = info.event.url; if(url!=&quot;&quot; || url!=&quot;#&quot;){ location.href=url; } }, }); //DB에서 데이터 가져오기 const arr = getCalendarDataInDB(); $.each(arr, function(index, item) { $calendar.addEvent(item); }); $calendar.render(); return $calendar; } //arr 는 테스트용으로 DB 에서 스케즐 데이터를 가져오지 못했을 경우 테스트용 데이터 function getCalendarDataInDB() { //arr 임의 데이터 let arr = [{ title: &#39;evt111&#39;, start: &#39;2023-03-22T10:30:00&#39;, end: &#39;2023-03-23T10:30:00&#39;, backgroundColor: &quot;#52cdff&quot;, color: &quot;#000&quot;, textColor: &quot;#fff&quot;, url: &quot;https://daum.net&quot; }]; $.ajax({ contentType: &#39;application/json&#39;, dataType: &#39;json&#39;, url: &#39;/schedule/selectEventList&#39;, type: &#39;GET&#39;, async: false, success: function(res) { arr = res; }, error: function(res) { console.log(res); } }); return arr; } </pre><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-22 11:39:49 이지아’가 초특급 킬러 재벌집 사모님 [판도라: 조작된 낙원 몰아보기] http://macaronics.net/index.php/tv/drama/view/2086 2086 <p>&nbsp;</p><p>&nbsp;</p><p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<img alt="" width="722" height="1024" src="https://img7.yna.co.kr/etc/inner/KR/2023/01/27/AKR20230127114200005_01_i_P4.jpg" /></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>진짜 돌아왔다..! 이번엔 기억을 잃고 다른 사람의 삶을 살게된 &lsquo;이지아&rsquo;가 초특급 킬러 재벌집 사모님으로 각성하면서 벌어지는 tvN 드라마 [판도라 1~2화 몰아보기]</strong></span></p><p>&nbsp;</p><div style="position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden" class="youtube-embed-wrapper"><iframe width="640" height="360" src="https://www.youtube.com/embed/VfuH4xyZMH8" frameborder="0" allowfullscreen="" style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe></div><p>&nbsp;</p><p>&nbsp;</p><p>《판도라: 조작된 낙원》은 2023년 3월 11일부터 2023년 4월 30일까지 방영 중인 tvN 토일드라마다.&nbsp;<br />첫방송: 2023년 3월 11일 (대한민국)<br />방송 국가: 대한민국<br />방송 기간: 2023년 3월 11일 ~ 2023년 4월 30일<br />방송 시간: 매주 토요일, 일요일 밤 9:10 ~ 10:30<br />방송 채널: tvN<br />방송 횟수: 16부작</p><p>&nbsp;</p><p>&nbsp;</p><p><img id="img" draggable="false" alt="" width="128" src="https://yt3.googleusercontent.com/ytc/AL5GRJWDnv-qjjY87y7SHWFTwrvwBk9pSh3xtXUCEZuk=s176-c-k-c0x00ffffff-no-rj" /></p><p>김시선&nbsp;</p><p>@kimsiseon구독자 156만명동영상&nbsp;546개</p><p><a id="endpoint" href="https://www.youtube.com/@kimsiseon/about">영화 드라마 / 게임 / 전자 제품 / 패션 / 예능 / CF / 뮤직비디오 ~ 정주행하</a></p><p>&nbsp;</p><p><a target="_blank" href="https://www.youtube.com/@kimsiseon">https://www.youtube.com/@kimsiseon</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>김시선&nbsp; 자막 보기</strong></span></p><p>&nbsp;</p><p>오랜 기억에서 깨어난 한 여성 그녀의 정체는<br />&nbsp;떠올리면 안 되는 과거 다수의 남자들과 거침없이 맞서는 한 소녀</p><p><br />&nbsp;떠올릴수록 고통스러워하는데 그때 그녀를 에워싸는 괴한들 잘못 들어왔어요 하지만 거침없이 이어지는 공격 그런데<br />&nbsp;평범한 가장 주부가 아무렇지 않게 칼을 피하고 오히려 반격에 나서는데 이건 익혀진 본능입니다 상대 헛점을 파고드는 공격 날카로운 칼을 장난감처럼 뺏어 버리는 스킬<br />&nbsp;상황은 그 순간에도 적의 위치를 파악해 단번에 평범한 가정주부라곤 할 수 없는 킬러본 능 도대체 그녀의 정체는 뭘까요 내가 오형이었어 아무도 열어선 안 될 비밀 김시성과 함께 보는 판도라 조작된 낙원 지금 바로 시작합니다 많은 사람들의 박수를 받으며 등장한 IT 기업 해체 의장 표재현 의료기기는 바로 획기적인 뇌의 연동 스마트패치 클로버입니다 머리</p><p>에 붙이기만 해도 아이큐가 절로 올라가는 신기술입니다 저희 연구팀은 간단한 패치에 부착으로 전혀 학습되지 않은 데이터를</p><p><br />&nbsp;뇌로 이동시키는 스마트패치를 탄생시켰습니다이 기술을 통해 전 세계 최초로 벌어지는 게임 대결 인간과 침팬지의 서바이벌 슈팅 게임 대결입니다 그리고 상대는 무려 그럼 오늘 침팬지와 대결할 선수를 소개하겠습니다 자 김말할 필요 없죠 서바이벌 슈팅 게임의 전설 DK 선수입니다 대한민국 최고의 프로게이머 자 그럼 시작해 볼까요 먼저 배치를 침팬치에게 붙여보겠습니다 저 패치엔 게임 플레이 방법과 전략 최고의 승률 데이터들이 전부 다 입력이 되어 있습니다 참고로 레드는 태어나서 이런 게임을 단 한 번도 해본 적이 없습니다 하지만이 팬츠를 부착하는 것만으로도 최고의 프로게이머 선수가 된다는 건데 부착하자마자 달라지는 침팬치 레드의 눈빛 으로는<br />&nbsp;</p><p>간단합니다 100명의 플레이어들이 전투를 해서 마지막까지 살아남은 단 한 사람이 승리하게 됩니다 여기 계신 dk님을 포함한 99명의 플레이어들은 랭킹 탑 티어들이고 우리 레드는 생애 첫 경기입니다 해치의 떡상 한편 자기 오늘 진짜 최고였어 이따가 야 너 미쳤어 브랜드 런칭을 앞둔 테라의 언니 유라 미<br />&nbsp;치겠네 그때 화만 내고 있을 거야 등장한 테라야 그런데 유라는 이상한 표정을 짓더니 갑자기<br />&nbsp;</p><p>담긴 줄에 의해 무너진 균형 그때 하지만<br />&nbsp;떨어지는 충격에 갑자기 과거의 기억이 떠오른 테라 어딘지 알 수 없는 상황에 처음 보는 어떤 소녀 그리고 자신을 공격하는 다수의 개안들 다시 정신이 들었지만 자꾸만 떠오른 테라의 이상한 찬상 한편 이미 폭등 중인 주식이 증명하고 있다고 생각합니다 장도준 대표님 금족으로 처남에서 해치의 대표로 완전한 자리매김을 하셨는데요 하나만 더 묻겠습니다 상간에 떠도는 금족으로과의 합병설에 대해 한마디 해주시죠 금조제약에서</p><p>&nbsp;</p><p>뇌과학연구소를 신설한다는 얘기가 사실입니까 해수와 금족 그룹은 적당한 거리를 두고 서로 윈윈하며 잘 성장하고 있습니다 합병 생각해본 적도 없구요 이런 행보를 대선 출마를 위한 신호탄으로 봐도 될까요 한민당 쪽에서 입당제의가 있었다는데요 얼마 전 단골집 사장님의 절 스카우트 하고 싶다고나 하시더라구요 고기 예술로 굽는다고 제가 어디서나 탐내는 인재이긴 하죠 왜 말 안했냐이 새끼 진짜 많이 컸다 형한테 한마디 상의도 없이 대선을 나가려고 누구보다 친하지만 재현에게 묘한 경계심을 보이는 도진 한편 병원에서 은밀한 얘기가 왔는데 어떻게 될 것 같네요 점점 기억이 돌아올 겁니다 복용하던 약도 중지시키십시오 기억해보게 도움이 될 겁니다 테라의 숨겨진 기억을 꺼내려는 유라 고생하셨어요 그렇게 건네지는 두둑한 돈다발 테라 역시 조금씩 뭔가 이상함을 느끼고 있었는데<br />&nbsp;</p><p>어떤 여자애가 누구랑 막 싸웠는데 그게 나인 것 같기도 하고 앉을 것 같기도 하고 정확히 보였는데 생각난 거야 모르겠어 정확히는 사실 사고 난지 벌써 15년이나 지났는데 15년 전 잃어버린 기억 아직도 기억이 안 돌아오니 그래도 뭔가 기억이 나르는게 아닐까 그러고 보니까 오늘 제가 봤어 그럼 안 봐 온 세상이 난리였는데 남편 줄까 너무 올라가는 거 아니야</p><p>누가 봐도 부러운 잉꼬부부였던 테라와 재현 저기요 여기에 사람들이 진짜 많은데 많아요 많아<br />&nbsp;대한민국에서 제일 잘 나가신다는데 여기 다 모여 계시네 제가 이런 분들이랑 같이 타운하우스 사는 거예요 성공했나 봐 지우는요<br />&nbsp;내가 할머니 댁 같다고 서운해 하던데 자고 온대요 오늘 와이프 뉴스 끝나고 회의 있다고 많이 났는데요 나 오래간만에 혜수랑 한잔하고 싶었는데 아직 도착 안 한 도진이 아니자 잘나가는 ybc 앵커 괴수 오늘 오전 글로벌 IT 기업 패치가 뇌를 활성화시키는</p><p>&nbsp;</p><p>스마트 패치 클로버를 선보여 전 세계를 놀라게 했습니다 주가의 고향콩 야 남편이 너무 잘 나가신다 감사합니다 근데 저도 충분히 잘 나가서요 오늘 멘트 많아서 집중 좀 할게요 남편은 집안 힘으로 올라온 앵커라는 자리<br />&nbsp;다음 소식입니다 다음 주 예비후보 등록을 시작으로 29대 대통령 선거 레이스에 시동이 걸릴 예정입니다 한편<br />&nbsp;앵커님 앞으로 본건데요 안 죽었네 정체불명의 팬 선물</p><p>&nbsp;곧 퇴선했어요 돌아가신 아버지 장난친 거야 봤어요 갖고 온 사람 퀵으로 왔어요 혼자 상자를 열어보는데 거기 있는 건 웬 처음 보는 소녀의 사진 그리고 뒷면에 적힌 충격적인 내용 아빠를 죽인 범인이 사진 속 소녀 o형이라는 거 우리 아빠를 죽인 범인이라고 사실인지는 알 수 없지만 15년 만에 알게 된 힌트 하나 한때 힘입고 행복한 미래가 있었던 과거를 떠올리는 예수 대통령의 당선된 아빠 하지만 총격당했고 그 순간 모든 꿈이 사라졌지만 여전히 잡히지 않았던</p><p>길로 15년째 고통스러운 기억에 사로잡히는 수를 위로해준 건 오직 테라 뿐이었죠 아무것도 아니야 한편 해치연구소의 몰래 잠입한 누군가 이 괴한의 타깃은 다름 아닌 침팬치 레드<br />&nbsp;어떻게 된거야 인 한편 오영 사진 뒤에 적힌 한울정신병원에 도착한 테라와 그때 옷을 보고 묘한 감정을 느끼는 테라 아이 근데 여긴 어쩐 일이세요 전 사람을 찾는데요 이름은 노영이고요 한쪽 귓바퀴가 잘려나간 것처럼 보여요 환자분인가요 그건 모르겠어요 한편 익숙한 옷을 따라 도착한 곳은 병원에 외친 건물 15년 전이라 그때 기록은 전산에 없어요</p><p>&nbsp;환자분 정보를 함부로 알려드릴 수도 없고요 그리고 낯설지 않은 개동상까지 마치 몇 번이라도 온 것처럼 비밀의 문을 열어버리는데 그렇게 마주하게 된 진실 15년 전 해수의 아버지이자 당선자였던 고태수를 바라보는 한 소녀 나는<br />&nbsp;헌법을 준수하고 국가를 보유하며 국민 앞에 엄숙히 선서합니다</p><p>대통령을 승낙한 그 순간<br />&nbsp;고태수를 저격한 건 다름 아닌 오형 지금의 테라였던 것 비상 소식에 한 걸음에 달려온 한울 정신병원 실장 조규태<br />&nbsp;익숙한 조실장 그때 단련된 요원들이 테라를 잡아보려 하지만 결국 문</p><p>사이를 힘겹게 도망갔는데 성공한 테라<br />&nbsp;난 급한 일이 생겼으면 먼저가 한편 고해수가 오염의 사진을 어떻게 구한 거야 누군가 제보를 했답니다 여기 오면 만날 수 있다고 보안이 뚫린 것에 화가 단단히 난 한올정신병원 원장 김선덕 최여준 고해수하고 같이 온 일행인데 도망쳤습니다 뭔가 초교 문장 왜냐하면 별관은<br />&nbsp;</p><p>등록된 지문 없이는 들어갈 수 없는 곳인가 몰라 혼란스러운 테라 사진들도이 사람 나 맞아 그리고 살았던 프랑스 칩 내가 다닌 프랑스 대학 아르바이트였던 코르슈가 카페 그리고 엄마 아빠 그녀가 믿는 모든 것이 무너져 내린 상황 분식 날 머리 다친 거 확실해 그래 아니잖아 그렇게 부르지 마 이젠 알아버린 진짜 그녀의 이름 호양이잖아 그 말을 듣자마자 미소를 짓는 유라 기억나는 거야 오랫동안 이날만 기다려온 유라 15년이나 걸릴 줄 몰랐네 이런 오형이야 싸늘해진 언니 유라가 아니라 나한테 무슨 일이 있었던 거야 내가 왜 홍타라가 돼 있는 거야 홍태라가 누군데</p><p>&nbsp;황태라는 교통사고로 사망신고를 하기 전에 누군가 내 동생 신분을 필요로 했고 15년 전 프랑스 병원에서<br />&nbsp;처음 봤어 전쟁이라도 치를 것 같은 몰골로 발견된 테라 얼굴은 뭉개서 피떡이 되었었고 살아있는게 기적이랬어 깨어나긴 했지만 아무것도 기억하지 못하는 테라 그렇게만 테라가 됐어 난 꽤 많은 돈을 받았고 누가 시킨 거야 왜 말해줄 수 없어 그게 그 사람과 나의 계약이니까 한 사람 인생을 가짜로 만들어 놓고 그걸 지금 말이라고 해 화를 내지 그동안 네가 단 한 번이라도 불행한 적이 있었어 대한민국에서 제일 잘 나가는 남편의 최고급 타운하우스에서 남들은 꿈도 못 드는 홍화스러운 생활에 더 예쁜 딸까지 얻었어 모든게 완벽했던 테라의 삶 하지만 나 지금 꿈꾸고 있는 거지 제발</p><p>&nbsp;누가 나한테 이런 거지 같은 일을 시킨 건지 말해 누구야 그럼 이제부턴 네가 찾아내야지 널 표지원해 안으로 살게 한 사람이 누군지 모든게 다 조작된 테라의 사람 내가<br />&nbsp;그 사람 계획안에 들어 있던 거야 그럼<br />&nbsp;실제랑 같이 괜찮은 남자가 남편이 된게 우연인 줄 알았어도 괜찮아<br />&nbsp;급하게 가족들이 있는 곳으로 향하는 테라<br />&nbsp;방금 황태라 다녀갔어요 다 돌아온 거 같아요 그럼 이제 우리 거래도 그 친구죠<br />&nbsp;계약해지는 그분이 결정에 넌 명령대로 움직이기만 하면 돼 예쁘잖아요 영화 장도진 대표와의 관계 그분도 알고 있어 너 때문에 장도진이 위험해져도 괜찮아 벗어날 수 없는 족쇄 그 사람은 장 대표가 걱정되면 시키는 대로 하는게 좋을 거야 개자 식들아가 멘붕에 빠진 사이 하늘 정신병원에선 도대체 그년이 누군지 테라가 비밀 공간 안으로 들어올 때 지문을 다시 검색해 보는데 죽은 줄 알았던 오염 어떻게 저년이 살아있어 오용이 죽었다고 하지 않았나 그게 절벽에서 추락했잖아 도저히 사랑할 수 없는 수심이었습니다 우리 애들 중에서도 최고의 에이스였다고 한 번만 더 기회를 주십시오 테라가 기억을 잃은지 모르는 원장은 우리한테<br />&nbsp;전쟁을 선포한 거라고 반드</p><p>&nbsp;시 제가 급하게 짐을 싸는 테라 옷에 이어 돈까지 가득 챙기는데 테라의 객은 네 사모님 지금 엄청 재밌게 놀고 있어요 지우 빨리 집으로 데려와요 딸과 남편을 데리고 해외로 피신한 것 그때<br />&nbsp;오랜만에 더워요 바로 그 사람 이제 오염을 검지와 차례 오염이 없는 단 한 가지 그렇다면<br />&nbsp;</p><p>당신 저 오형이 맞구나 여러분도 바꾸고 사모님 흉내 내면 고사하게 사는 재미가 꽤 쏠쏠해</p><p>&nbsp;</p><p>이젠 더는 도망갈 수 없는 테라 원장님 기다리신다 몸이 먼저 움직이는 놀라운 전투력 절규 빠져나오는데 아니 왜 이래 무슨 일이야 빨리 들어가서 확인해 보세요 집안은 난장판이지만 이미 사라진 그들 집안에는<br />&nbsp;경찰에 신고해주시고 CCTV 하지 마세요 그냥 제가 혼자 넘어진 거예요 아무 일도 아니에요 그만 돌아가세요 무슨 일 있는 거지 나한테 말하기 힘든 일이야 언니 어디가 배수가 보기엔 모든게 다 이상한 상황 아 저거 무슨 일인지 모르겠지만 진정되면 나한테 말해 언니 이름이 뭐 좀 볼게<br />&nbsp;친 자매처럼 서로를 아꼈던 두 사람 하지만 해수의 아버지를 자신이 죽였다고 말할 순 없습니다 한편 내부 고백자를 은밀히 접촉한 해수 하늘정신병원 수간호사예요 쪽지에 감사합니다이 사람 아시나요 20년 전쯤에 하늘에 들어온 아이예요 내가 귀를 치료해 줬고요 도망치다 들깨한테 물려서 끌려왔더라구요 들깨요 왜 저희를 도와주시는 거죠 하늘정심병원 절대 좋은 곳 아니에요 그 안에서 이상한 일들이 너무 많이 일어났어요 그런 곳인 줄 알면서 왜 안 그만두셨어요 마음대로 못 그만둬요 내 마음대로 나갈 수 없는 그곳 기원장이나 하시기 전까지 내일 여기서이 시간 됐죠 오형에 대한 자료를 들고 다시 만나기로 했지만 스마트패치가 조작이라고 언론에서 우리를 의심하기 시작했다고 절망에 빠진 도징과 재현 그리고 누구보다 슬픈 구성찬 소장 네 집이 박혀있는지가 갑자기 달려드는 이런 끔찍한 일을 저지른 사람은 대통령 후보인 한민당의 한 경로 이번 당내 경선 후보의 표제의 의장이 있었던 걸로 아는데요 당신들이 우리 해체한 짓 난 그냥 덮을 겁니다 니네 침팬지 주는게 우선 뭔 상관인데 더는 참지 않은 제안 당신들은 늘 이딴식이야 날 건드린 책임 반드시지게 만들 겁니다 그렇게 고단 하루를 마친 재현 놀랐지</p><p>&nbsp;당신 집에 언제 와 나 당신한테 할 말 있어 그때 안녕하세요 이번 발표회 정말 잘 봤습니다 테라가 손을 쓰기도 전에 먼저 재현에게 접근해버린 원장 선덕 하루 좀 만나면 안 돼 와이프신가봐요 사이가 아주 좋으신 것 같네요 오늘은 제가 좀 정신이 없네요 무슨 일이시죠 급하게 재현이 있는 곳을 향해 질주하던 그때 잠복하고 있었던 조규태 살석고 살았는데네 남편이 아무것도 모른다</p><p>그딴 개소리를 믿을 것 같아 내 손에 죽고 싶지 않으면 인간병기 테라가 각성한다면 이어지는 숲속 대결 누구든<br />&nbsp;방심하면 죽습니다 강하게 치고 들어가는 테라 하지만 뒤태도 만만치 않은 상대 빠진 팔마저 스스로 맞춰버리는 독종 결국 슬슬 마무리에 들어간 조규택 이대로 죽고 마는 걸까요 마지막 반격의 큰 부상을 입고만 조규태 조규태로부터 벗어나 급하게 남편 재현이 있는 곳에 도착하는데</p><p>다행이다<br />&nbsp;</p><p>정말이 시간부로 해치 의장직을 내려놓겠습니다</p><p>이어지는 채연의 충격적인 발표 대선에 출마하려고 합니다 이렇게 되면 도망갈 수 없는 테라<br />&nbsp;게다가 거기서 보게 된 건 자신의 딸을 데리고 있는 하늘정신병원 원장 선덕 동시에 2층 기억 하나가 더 떠오르는 테라<br />&nbsp;다들 동생이 있었어 기존과 다른 김순옥 작가가 크리에이터로 참여한 새로운 도전이 엿보이는 판도라 조작된 나고는 매주 토요일 밤 9시 10분에 보실 수 있습니다</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>재벌집 막내사모님 이지아를 건드렸는데 하필.. 그녀가 &quot;신급 인간병기&quot;라고..?! [펜트하우스] </strong></span></p><p><span style="font-size:20px"><strong>김순옥 사단의 초초기대작! [판도라:조작된낙원]&nbsp;</strong></span><strong><span style="font-size:18px">1-2회</span></strong></p><p>&nbsp;</p><div style="position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden" class="youtube-embed-wrapper"><iframe width="640" height="360" src="https://www.youtube.com/embed/2Q6uqy0I0B0" frameborder="0" allowfullscreen="" style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe></div><p>&nbsp;</p><p><img id="img" draggable="false" alt="" width="128" src="https://yt3.googleusercontent.com/ytc/AL5GRJUeGiYVGskfzi94iVZbrn2fovy464UaD_yWYCGv=s176-c-k-c0x00ffffff-no-rj" /></p><p>고몽&nbsp;</p><p>@GOMONGTUBE구독자 219만명동영상&nbsp;709개</p><p>&nbsp;</p><p><a target="_blank" href="https://www.youtube.com/@GOMONGTUBE">https://www.youtube.com/@GOMONGTUBE</a></p><p>&nbsp;</p><p><span style="font-size:20px"><strong>고몽 : 자막보기&nbsp;</strong></span></p><p>타운하우스에서 초호화 삶의 숨겨진 걸쩍지근하면서 야릇한 관계 그리고 누군가로부터 조작된 기억 때 되면 다 알게 될 텐데 뭐 잃어버린 기억의 판도라 상자를 여는 순간<br />&nbsp;그녀는 알 수 있었습니다 자신의 기억 모두가 주작 아니 조작되어 있음을<br />&nbsp;판도라 조작된다고 세상 위에 상위의 상위 이곳에 모여 사는 사람들이 있습니다 저기요 저기 사람들이 진짜 많은데 많아요 많아 우리 그냥 아주 제대로 보여줄까 바로 it 기어패치의 핵심가무들을 가족이 이곳에 모여 살고 있죠 제가 이런 분들이랑 같이 타운하우스 사는 거예요 오늘 오전 글로벌 IT 기업 패치가 디지털 치료기기 시장에 반도를 바꿨다는 평가입니다 대한민국 의료기기 혁신기업 했지 배치 부착만으로 전혀 학습되지 않는 데이터를 뇌로 이동시키는 VR 전용 스마트패치를 개발하게 되면서 이세돌 알파고의 대결을 맞먹는 역사적인 대결을 선보이는데 그건 바로 인간과 침팬지의 서바이벌 슈팅 게임 대결입니다 [박수] 참고로 레드는 태어나서 이런 게임을 단 한 번도 해본 적이 없습니다 이렇게 레드의 머리에 패치를 부착하자 혼돈의 세계를 지배하는 초월적 존재 누가 최후의 승자가 될까요 이렇게 패치를 통해 게임 데이터를 습득한 레드의 실력은 자 여유있게 마주하게 이렇게 한국을 넘어 세계의 관심을 받고 있는 세 사람 그건 오직 엣지의 기억 가치 때문만은 아버지의 죽음을 목격하고 아직도 정신적인 방해가 될 건 확실하네요 위한 신호탄으로 봐도 될까요 대치의 임원들을 둘러싼 무성한 소문들도 반목하고 있었기 때문입니다 대통령이 된다고 하지만 분명하게 입당 거절 의사 밝혔는데요 설마 무소속 출마를 염두에 두는 건가 친구들이 기다리고 있어서 그만 가봐야 될 것 같네요 그<br />&nbsp;스마트패치라는게 사실은 눈가림이고 침팬지 뇌에다가 직접 집을 막았단 얘기가 있어요 조용히 알아볼 방법을 적이면서 누군가에겐 간절히 원하는 존재가 되어가고 있었습니다 절친의 아내 언니이자 아내 소울메이트의 언니 유라와 이멀전시안 시크릿을 만들고 있던 창도진 같은 시각 일찍 왔네 뭐야이 상황을 알렸던 테러는 해수에게 뜨밤 템을 선물하고 있었죠 언니<br />&nbsp;뜨거운 반 보내 아 올 땐 이런 편 좀 챙겨와라 와인 가지러 갔다 잠들었나 봐 빨리 와 그렇게 집안에 들어와 보니 뜨겁다 못해 타원인듯한 방황과 함께 욕실에서 들려오는 소리 어 왜 금방 나갈게 회의 있어서 늦는다더만 그날 밤 유랑은이 부부 앞에서 위험한 도발을 시작하는데 얼굴이 빨갛네 그만 갈까 당당하게 두 사람 앞에 나타난 것도 오잖아 계획적으로 자신의 귀걸이 하나를 남겨두고간 극한 신데렐라 유라 그렇게 유라는 본격적인 광역 어그로를 끌게 되는데 나 만나는 사람 있어 진짜<br />&nbsp;언제부터 때 되면 다 알게 될 텐데 뭐 그녀의 대답에는 많은 의미가 담겨져 있었습니다 너 몸은 괜찮아 머리 쪽은 쉽게 보면 안 돼 어디 다쳤어 몇 시간 전 너 미쳤어 사고로 부모님을 잃고 후유증으로 기억까지 잃은 테라를 사랑으로 감싸안아주었던 언니 유라 하지만 사고 이후부터 죽을 쓰고 있던 비밀의 가면을 유라는 벗으려 하고 있습니다 바로 잃어버린 테라의 기억을 되살리기 위해서 말이죠 이렇게<br />&nbsp;테라의 머리에 강한 충격을 받게 되자 낯설지 않는 장소가 머릿속에 떠오르는 동시 거울 속에 비친 한 여성의 모습 그리고 그녀를 부르는 목소리와 함께 뭐야<br />&nbsp;갑자기 시작되는 생존 게임 이곳에 있는 모든 사람들이 그녀에게 달려들고 있었죠 이렇게 병원에서도 최대한 기억 회로를 자극해놔서 점점 기억이 돌아올 겁니다 고생하셨어요 유라는 온갖 방법을 동원해서라도 되살리를 하고 있었습니다 정신<br />&nbsp;들어<br />&nbsp;언제나 다정한 언니의 가면은 신체 말이죠<br />&nbsp;괜찮긴 뭐가 괜찮아 진짜 큰일 날 뻔했어 나 이상한 꿈 꿨어 정확히 뭐였는데 모르겠어 정확히는 타고난지 벌써 15년이나 지났는데 아직도 기억이 안 돌아오니 아 근데 왜 말을 안 했어 이렇게 행복하기만 할 줄 알았던 이곳 타운하우스의 아까 누구 집에 왔었어 태풍이 불어오고 있었습니다 넌 또 기자 레이더에 뭐가 잡혔니 더 수상하니까 그만하자 그럼 넌 밖에서 대체 뭐야 바뀌자 덕분에 그 범인 놈 잠깐이라도 뜨끔했을 거 아니야 아직도 범인이 안 잡혔어 어딘가 살아서 날 보고 있다고 내가 말했지 언젠가 그놈 잡혔다는 속보 내가 진행하는 뉴스에서 내 입으로 꼭 말할 거라고 어 그럼 나한테는 안 고맙고 어 그날부터 지금까지 당신 옆을 지킨 사람은 나야 내 치사하게 이런 말까지 해야 되냐 이렇게 진실의 숨겨둔 판도라의 상자가 모습을 드러내게 되면서 유라가 원했던 방향대로 흘러가고 있었습니다 같은 시각 타운하우스에서 기억을 다 잃은 여자 겁나지 않았어 전혀 과거에 내가 어떤 사람이라도 상관없이 네가 어떤 사람이었어도 난 널 사랑했을 거야 눈앞에서 아버지가 죽던 그날의 트라우마로 해서는 의식을 잃어가게 되는데 다행히 아무 일도 일어나지 않았습니다 다음날 아침 뭐야 아침부터 이걸 왜 하고 있는 거야 아침부터 이거 왜 하고 있는 거야<br />&nbsp;당사자도 이유를 모르는 운동이 끝나던 그때<br />&nbsp;믿을 수 없는 전화 한 통을 받게 되는데 어떻게 된 거야 어떤<br />&nbsp;위치가 두뇌해놨어요 칩이 연결되는지 확인한 것 같아요 [박수] 어젯밤<br />&nbsp;혜수에게 배달된 퀵 하나 앵커님 앞으로 본 건데요 이미 돌아가신 아버지의 이름으로 온 창자 안에는 한 여성의 사진과 함께 믿을 수 없는 글이 적혀 있었죠 누군가 나한테이 사진을 보냈어 우리 아빠 죽인 진범이라고 그 사진 속 여성은 바로 테라가 꿈에서 봤던 그 여성 분명 자신의 기억과 관련 있을 거라는 생각을 지울 수 없던 테러도 해수를 따라 사진 속에 적혀있던 하늘정신병원으로 향하게 되는데 전 사람을 찾는데요 이름은 오영이고요 환자분인가요 환자일 수도 있고 직원일 수도 있고 확인해 주실 수 있을까요 죄송해요 무슨 일이십니까 이병헌 실장 조규태입니다 사람을 찾고 있어요</p><p>제 아버지 살해 사건과 관계있는 사람이라고 아버지라면 전 대통령 고태선이요 배수가 그날의 진실에 한 발자국 가까워지던 그 시간 테라도 이곳에서 본능적으로 어디론가 향하게 되는데 그것은<br />&nbsp;분명<br />&nbsp;테라가 꿈속에서 보았던 그 장소와 똑같은 장소 또한 꿈속에서 봤던 그 여성이 이번에도 똑같이 거울 속에 비치면서 테라가 그녀에게 가까워질수록 꿈속의 그녀 거울 속의 그녀도 가까워지는 뭐야 그건 홍채라이 또 다른 이름 꿈속에서 보았던 장면은 전부 자신의 잃어버렸던 기억이자 15년 전 해수의 눈앞에서 아버지를 살해한 그토록 잡고 싶었던 사건의 용의자였습니다 나는 헌법을 준수하고 국민 앞에 고역<br />&nbsp;이었어 테라를 잡기 위해 몰려드는 무리들<br />&nbsp;잘못 들어왔어요 아무런 설명도 없이 테라에게 달려들지만 본능적으로 반응하는 테라의 육체 [박수] 이렇게 하나둘씩 계속해서 떠오르는 기억들 너 뭐야 테란은 일단 이곳에서 빠져나가기로 합니다</p><p><br />&nbsp;한편 헬스는 계속해서 오영의 정부를 얻으려 하지만 조 실장님께서 o형이라는 사람의 자료는 없다고 그만 돌아가시랍니다 그럼 원장님은요 병원에 안 계십니다 모두가 모르쇠로 일관하는 병원 사람들 그때 누군가 해수에게 쪽지 하나를 남기게 되는데 찾고 있는 여자를 알고 있어요 오늘 밤 11시 나 급한 일이 생겼으면 먼저가 저게 무슨 일이야 누군가 제보를 했답니다 이미 o형은 죽었어 그것도 15년 전에 고해수하고 같이 온 일행인데 덩쳤습니다 반면<br />&nbsp;레드를 죽인 범인을 줬던 해체 임원진 이거</p><p><br />&nbsp;회사 사정 잘 아는 거야 회사의 소수만 알고 있던 cctv를 통해 쉽게 용의자를 특정할 수 있었습니다 찾았다</p><p><br />&nbsp;같은 시각 테라는 곧장 유라를 찾아가게 되면서 무슨 일 있었어 자신의 기억에 대해서 묻기 시작하는데 이 사진들 속이 사람 나 맞아 어 그리고 살았던 프랑스 침 내가 다닌 프랑스 대학 아르바이트였던 코르시가 카페 그리고 엄마 아빠 너 분식할 머리 다친 거 확실해 아니잖아 그렇게 부르지 마 그렇게 테라 입에서 꿈속에서 보았던 그녀의 이름을 말하게 되자<br />&nbsp;고양이잖아 15년간 숨겼던 진실을들을 수 있었습니다 희재야 기억난거야 15년 전 프랑스 병원에서 처음 봤어 15년 전 유라에게 접근해 다 죽어가던 그녀를 맡기며 테라의 신분으로 살게 만들었던 누군가 그의 요구대로 유라는 그녀를 테라의 신문과 함께 이렇게 넌 홍테라가 됐어 괜찮아 누구세요 기억을 전부 일은 그녀에게 테라의 기억을 심었던 것이었죠 누가 시킨 거야 왜 말해줄 수 없어 한 사람 인생을 가짜로 만들어 놓고 그걸 지금 말이라고 해 화를 내지 그동안 네가 단 한 번이라도 불행한 적이 있었어 대한민국에서 제일 잘 나가는 남편의 최고급 타운하우스에서 남들은 꿈도 못 드는 호화스러운 생활에 여기다 예쁜 딸까지 얻었어 당연히 쉽게 받아들일 수 없는 진실까지 잘해주고 정신 들어 괜찮긴 뭐가 괜찮아 머리 쪽은 쉽게 보면 안 돼 나는 아끼고 걱정하던 모든게 다 가짜라는 거잖아 지금 그게 없다고 언니 제발 인성을 기억 돌아왔다며 그럼 이제부터 네가 찾아내야지 널 표지원해 안으로 살게 한 사람이 누군지<br />&nbsp;위험해서도 괜찮아 이렇게 언니의 팩트 폭격에 테라가 급히 자리를 떠나자 유라는 누군가에게 전화를 걸게 되는데 다 돌아온 거 같아요 그럼 이제 우리 거래도 계약해지는 그분이 결정해 관계 그런 거 알고 있어 너 때문에 장도진이 위험해져도 괜찮아 이렇게<br />&nbsp;오영에 대한 기억이 돌아오면서 하늘정신병원에서도 외형이 죽지 않고 살아있음을 확인하게 됩니다 북면<br />&nbsp;o형이 죽었다고 하지 않았나 그녀는 보통 얘기가 아니었어 우리한테 전쟁을 선포한 거라고 당장 내 앞에 끌고 와 이렇게 자신의 가족이 위험해질까 테라는 곧장 남편에게 전화를 걸어보지만 엉뚱한 녀석의 백허그를 받게 되고 오랜만에다 오영아 발 빠르게 오염을 잡으러 온 병원 사람들을 다시 보게 되자 수면 위로 선명하게 떠오르는 오영의 기억들 예전 모습은 하나도 없네 그렇게 15년 전 있었던 진실을 알아내기 위해 계란은 필사적으로 저항해 보지만 조용히 가면 좋잖아 때마침 찾아온 해수의 인기척이 가까스로 빠져나올 수 있었습니다 무슨</p><p><br />&nbsp;일 있니 아까 병원에서도 먼저 가버리고 나한테 말하기 힘든 일이야 진정되면 나한테 말해 언니 이름이 뭐 좀 볼게 저 그러니까 테라가 끼고 있던 가짜 귀죠 절대 말할 수 없었습니다 해수가 찾는 사건에 용의자 o형이 바로 자신이라는 사실을 말이죠 두 사람은 가족을 잃은 슬픔을 서로 감싸주며 서로를 의지하며 가족처럼 지냈던 사이였기 때문이죠 그날 밤 헬스는 쪽지를 보낸 직원과 만나게 되면서 감사합니다 오영의 정보를들을 수 있었죠이 사람 아시나요 20년 전쯤에 하늘에 들어온 아이예요 내가 귀를 치료해 줬고요 이름은요 우영이 맞아요 이름은 몰라요 어린 동생하고 같이 도망치다 잡혀와서 확실히 얼굴은 기억해요 이름은 모르지만 확실히 병원에서 보았다는 직원의 증언 그런데 하늘정신병원 절대 좋은 곳 아니에요 그게 무슨 나이트 근무예요 내일 여기서이 시간 얘기하는 동안 불안해 떨던 직원은 자신의 짐도 잊은 채 급히 자리를 뜨게 되는데 간호사<br />&nbsp;한편<br />&nbsp;해치에서네 말이 맞더라 레드 손톱 밑에 범인의 살점이 남아 있더라고 레드를 죽인 범인을 잡게 되면서 사건의 별을 알아내려 하는데 그러니까 누가 시켰냐고 그렇게 지혜는 한 대표의 제의를 거절한 보복이라 생각하게 되면서 이번 당내경선 후보의 표재현 의장이 있었던 걸로 아는데요 루머입니다 당신들이 우리 해체한 짓 난 그냥 덮을 겁니다 실수는 한 대표님이 하신 거죠 레드가 어떻게 죽었는지 여기서 하나씩 짚어볼까요 예 그날 오후 테라는 다시 재현에게 전화를 걸게 되고 놀랐지 당신 집에 언제 와 나 당신한테 할 말 있어 아 나도 할 얘기 있는데 회사로 올래 지금 자리 비우기가 좀 그래서<br />&nbsp;정말 급해 당신이 집으로 좀 와 주면 안 돼<br />&nbsp;안녕하세요 한올 정신병원 원장 수학이 밖에서 들려오는 불안한 대화 중요하게 드릴 말씀이 있어서요 네 남편이 아무것도 모른다 그딴 개소리를 믿을 거 같아 그렇게 이번에도 무사히 빠져나온 테라는<br />&nbsp;혹시나 지연에게도 무슨 일이 생겼을까 곧장 재현이 회사로 풀악셀을 밟게 되고 다행히 너무나 멀쩡한 지연의 얼굴<br />&nbsp;이 시간부로 대선에 출마하려고 합니다 단 한 번도 상상조차 하지 못했던 또 다른 나 오염 엄마 15년간 잃어버렸던 기억이 되살아나는 지금 데라의 모습으로 o형의 기억으로 그녀는 어떤 선택을 하게 될 것인가 9시 10분 조작된 낙원 그리고 여기 해수도 칼을 꺼내고 맙니다 나라는 내가 좀 예민했다 미안해 사과할 필요 없어 티 내지 말고 그냥 당당하게 말하라고 그게 아니고 언제든 당신 나 줄테니까</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><strong><span style="font-size:18px">재벌집 며느리 이지아를 잡아왔는데 하필.. 너무 강함ㅋㅋ 거의 초인임ㄷㄷ [펜트하우스] </span></strong><strong><span style="font-size:18px">김순옥 사단의 화제작! </span></strong></p><p><strong><span style="font-size:18px">[판도라:조작된낙원] 3-4회</span></strong></p><p>&nbsp;</p><p>&nbsp;</p><div style="position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden" class="youtube-embed-wrapper"><iframe width="640" height="360" src="https://www.youtube.com/embed/rfdfhzw8-Ng" frameborder="0" allowfullscreen="" style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe></div><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><img id="img" draggable="false" alt="" width="128" src="https://yt3.googleusercontent.com/ytc/AL5GRJUeGiYVGskfzi94iVZbrn2fovy464UaD_yWYCGv=s176-c-k-c0x00ffffff-no-rj" /></p><p>고몽&nbsp;</p><p>@GOMONGTUBE구독자 219만명동영상&nbsp;709개</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:20px"><strong>고몽 : 자막보기&nbsp;</strong></span></p><p>&nbsp;</p><p>선을 넘을 때로 넘어버린 떳떳한 외도녀 유라의 대범함 나도 자기 보고 싶었어 죽는 줄 알았어&nbsp;</p><p><br />&nbsp;미쳤어 계곡 두 사람의 관계는 거야<br />&nbsp;오래갈 수 없었죠 자기가 좋아하잖아 자신이 엮이지 맙시다 홍유라 씨 내가 그런 거야 한쪽이 끝내면 끝나는 거 세상 전부를 잃은듯한 유라는 그렇게 익숙한 허리에 손을 감으며 말합니다 뭐야 아 이렇게 빨리 환승한다고 공덕역이세요 우리</p><p><br />&nbsp;정말 모든 것이 조작된 이곳 욕망의 타운하우스 가장 가까운 곳에 깊숙히 숨겨져 있던 그들의 진실이 하나둘씩 수면 위로 오르고 있습니다 자신의 딸과 같이 있던 병원장의 모습을 보고 확실히 되살아난 사라진 15년의 기억 정말 뭐든 다 할 수 있겠니 약속할게요 어린 동생과 이곳에서 빠져나가기 위해 앞으로<br />&nbsp;킬러 수업을 받았던 테라 동생과 이곳에서 나가는 조건은 단 하나였습니다 모든 교육을 마치고 살아남아</p><p><br />&nbsp;이곳에서 최고의 에이스 킬러가 되는 것이었죠<br />&nbsp;동생을 위해 더 뛰어 당신은 내 가사인데 내 가족 앞에 다시 한번 나타나면 그 순간 갑작스런 남편 재현의 대선출마 선언으로 지우<br />&nbsp;야 그렇게 테라와 딸 지우의 신상인 세상에 노출돼 버리는데 하지만 테라의 진짜 모습을 알리없는 재현과 친구들은 우리 표재현이 대선주만 축하하고 그저이 소식이 기쁘기만 했죠 약간이 관상은 지금 대통령 느낌이야 반면 기억이 퇴사라는 테라 입장은 달랐죠 제발 여기서 그만두자 당신 과거</p><p><br />&nbsp;당신 과거가 왜 나 때문에 당신 이랑<br />&nbsp;그럴 일은 절대 없어 저자 무슨 일이 있더라도 내가 무슨 짓을 해서라도 당신이랑 지우지킬 거야 여보 이렇게 서로가 서로의 가려진 진실을 모른 채 그동안 지쳤다는 핑계로 아버님이라도 무심했어 미안하다 혜수야 서로를 향한 감정은 더욱 커져만 가고 당신이 생각하는 그런 일 절대 없어 믿어줘 진짜 믿어도 그렇게 테라는 자신의 가족을 지키기 위해 만인의 상황을 위해 다시 사고 현장으로 향하게 되고 그곳에서 해수와 마주치게 되는데 언니</p><p><br />&nbsp;네가 여기를 어떻게 언니가 문자 보냈잖아<br />&nbsp;사고 났다고 도와달라며 내가 많이 다쳤어 내 몸에서는 언니 미안해 언니 요즘 진짜 이상한 거 알아 나 알아야겠어 언니한테 무슨 일이 생긴 건지 깜빡이는 불빛과 함께 순식간에 폭발해버린 테라의 차량 다행히 오영의 기억에 돌아온 테라해 슈퍼에이스 킬러 판사 신경 그리고 극초반 주인공 회식으로 피할 수 있었습니다 어떻게 된 거야 테란은 김선덕 원장이 꿈인지심을 직감하게 되고 혜수는 물론 자신의 가족들에겐 비밀을 유지하기로 하지만 모른 척해줘 걱정할 거야 그래 알겠어 언니 나 오늘 진짜 좋아하는 거 알지 해수의 진심이 담긴 말 한마디에 과거와 현재의 기억에 우선순위가 뒤섞이며</p><p><br />&nbsp;복잡해지는 감정 아시아 미안해 나한테 조금만 시간을 줘 왜 이렇게 만든 놈 찾고 나면 그땐 제 앞에 그렇게<br />&nbsp;테라는 곧장 한울 정신병원으로 향하게 되고</p><p><br />&nbsp;태수의 아버지 사례를 사준 배우를 알아내려 하는데 초실장 처리하는 것 보니까 실력은<br />&nbsp;여전한 것 같은데 내 차에 폭탄을 설치하고 횟수를 불렀다 난 15년 동안 기억을 잃었어 그럼 누가 널 홈트라로 둔갑이라도 시켰다는 거야<br />&nbsp;나도 그게 궁금해 누가 날 홍태라로 만들었는지 당신이 그동안 나한테 한 짓고 동생한테</p><p><br />&nbsp;네가 내 동생 죽였잖아 내가 널 살려주는 이유는 하나요 날 이렇게 만드는 건 찾아내야 될까 그때까지는 내 가족이랑 해서 목숨 걸고 지킬 거야 성근이야 해수 아버지 죽이려고 사주한 인간<br />&nbsp;찾아내면서 해수를 병원으로 직접 불러 해수가라는 오영의 정보를 거짓으로 만들기로 하는데 기다<br />&nbsp;죄송합니다 영광입니다 주실장 말로는 이미 없는 환자라고 확인까지 다 해드렸다던데요 어젯밤 혜수에게 쪽지를 거는 간호사를 직접 해수의 눈으로 확인시키면서 말이죠 우리 병원에 장기 입원 중인 중증 환자입니다 몇 번이나 탈출을 시도해서 잡혀온 이력도 있고요 얼마 전에도 우리 간호사에게 상의를 입히고 도망쳤다 잡혀 왔어요 고행 큰 님한테 무슨 말을 했는지 모르겠지만 믿을 만한 얘기는 아닐 겁니다 그렇게 누군가에게 해수에게 전달되던 오영에 관한 정부는 다시 판도라에 상자 안으로 들어가게 되고 병원장은 간호사에게 정보를 흘린 그 누군가를 알아내려 합니다 그렇게 그녀 입에서 나온 이름은 우린</p><p><br />&nbsp;고태선 대통령을 죽인 진범을 계속 찾고<br />&nbsp;있습니다네 아직 별다른 움직임은 없습니다 대선 나가는 놈 약점은 더 비싸거든요 시아버지 장금호회장인 뒤에 있었습니다 그렇게 전혀 예상하지 못한 남편 지연의 대선 출마로 테라를 향한 국민들의 관심은 더욱 뜨거워지던 급히 갑자기 나사는 오토바이 한 대가서 기자들로 둘러싸인 곳으로 돌진하던 내뿜으면서 테란을 태우고 유유히 사라지는데 그 뒤에는 남편 재연이 있었죠 많이 놀랐지</p><p><br />&nbsp;앞으로<br />&nbsp;당신 경호 담당할 친구야 이제 어딜가든이 친구가 같이 할 거니까 걱정하지 마 그렇게 테라 기사가 각종 언론들의 도배되면서 테라는 자신의 과거가 들통날까 불안해지기만 하고 아니 뉴스 봤지 하루아침에 유명인이 된 소감이 어때 결국 테라이브라함은 현실로 다가오게 되는데 우리 방송국에 언니 친구가 사진도 보냈던데 언니랑 프랑스에서 학교도 같이 다니고 아주 친했었대 언니 사분하기 전에 얼굴이야 몰라보겠어 사고 때 얼굴을 크게 다쳐서 수술했거든 테란은 유라에게들은 대로이 상황을 모면하려 하지만 사실 어젯밤 대수가 테라에게 건넨 사진은 방송국으로 보낸 사진이 아닌 저 모형의 사진을 보내왔던 것처럼 해수에게 보내온 두 번째 상자였습니다 상자 안에는 테라에게 보여준 사진과 함께<br />&nbsp;현재의 테라의 사진이 들어있었고 테라를 가짜라고 알려주는 정보가 들어있었죠 그렇게 수는 곧장 사진 뒷면에 있는 폰으로 연락하게 되고 그렇게 첫 번째 상자와 두 번째 상자의 정보들이 퍼즐처럼 맞춰지면서 배수는 테라가 o형임을 의심하게 드는데 하지만 혜수는이 사실을 쉽게 믿을 수 없었습니다 테라는 해수에게 있어 그녀가 다시 살 수 있게 도와준 생명의 원인과 같았기 때문이었죠 고마워 다들 신경 써줘서 괜찮아 보이지 그런가 그날의 사고로 가족 전부를 잃고 삶의 이유를 찾기 힘들었던 해수 앞에 테라가 나타나게 되면서 변할 수 있었습니다 저기서 벌서고 있는 사람들한테 미안하지도 않아 고맙다는 말은 바라지도 않으니까 제가 좀 그만해 고마워 미쳤니 정말 지옥에 사는 줄 알아 난 사고를 부모님 한꺼번에 읽고 1년 넘게 병원에서 갑자기 톡 쏘는 말을 뱉는 강력한 리얼 탄산 100% 이맛이 해라 그래서 어쩌라고 너도 하나 정도는 있을 거 아니야 니가 꼭 살아야 되는 이유 없으면 찾아 어떻게든 찾아서 살아 그렇게</p><p><br />&nbsp;정신을 차렸던 해수 아니야 언니가 절대 우영일리가 없어 그렇게 자신이 직접 확인하기 전까지는 테라를 믿어보기로 하고 한편 테라도 날 홍태라로 만든 사람이 궁금해 배수와 똑같이 믿을 수 없는 진실을 마주하게 되는 수족 은 해수의 시아버지야 고태선 대통령을 죽여놓고 어떻게 그 딸을 며느리로 삼아 그룹을 위해서 아들의 결혼 쯤이야 얼마든지 장난칠 수 있는 미인이야 널 표정의 부인으로 만들어서 표준이 절대 약점을 만들 수도 있고네 말대로라면 지금 기억이 돌아온 것도 모두 다 계산된 거겠지 로 위험해진 건 고혜수야 이렇게 자신과의</p><p><br />&nbsp;희생양이라는 걸 알게 되고 같은 시각 테라를 뒤따라왔던 해수는 병원장과 같이 있는 모습을 두 눈으로 확인하게 됩니다 천덕을 봤나 정말로 언니야 테라가 사건의 여기자 오영임을 확신하게 되면서 우리 아빠를<br />&nbsp;향한 감정은 한순간에 돌변하기 시작합니다 전국무회장은 기억이 없어진 내가 왜 필요했을까 그지 아시아페 날 두면서 아 미안 내가 돌멩이를 치운다는게 잘못해서 그쪽으로 던져버렸어 많이 놀랐어</p><p><br />&nbsp;거기서 뭐 하는 거야 아니 테라가 만들어준 꽃밭인 거야 갑자기 보기 싫어져서 너무 활짝 피어 있잖아<br />&nbsp;벽에 어울리지도 않게 그렇게 누구나 부러워하던 이곳의 실체가 조금씩 드러나고 있었습니다 해치에서도 말이죠<br />&nbsp;그 날개를 아무도 모르는 거 맞지 이미 내수산 시작됐어요 폭력적인 성향도 심해지고 약물로도 제어가 안 돼요 실험을<br />&nbsp;절대 안 돼 레드는 완벽하게 끝나야 돼 무조건 그렇습니다 사실 해체해서 개발한 스마트 패치는 실패작이었습니다 애들 죽이세요 이렇게 실패를 돕기 위해 지연은 레드를 죽이는 선택을 하게 되면서 부작용 같은 건 없다고 두 사람만의 비밀로 만들려고 하지만 재현이는요 이때까지 단 한 번도 날 속인 적이 없었어요 해치를 위한다 하면서<br />&nbsp;리카 성차니까 속하고 모르겠다 해체 같고 금주로 들어오는 이렇게 재현만 알고 있던 비밀을 폭로하게 되면서 그동안 숨겨져 있던 장부모 회장의 시체가 드러나게 되는데 천하의 표지에 어쩔 수 없는 선택이었습니다 그럼 니는 해치를 놔줘야지 그게 순리라 가는거다 회장님</p><p><br />&nbsp;아니요<br />&nbsp;저 포기 안 합니다 15년 전에 회장님이 하셨던 것처럼 저도 제 거 다 지킬 겁니다 그렇습니다 사실 지현은 전부 알고 있었습니다 건설하청 비리부터 부실 시공에 현장 인부 사망사고 은폐까지 자식이 내 죽마고우란 놈이 실제로 대통령 친구의 뒷배로 금조그룹을 대기업으로 성장시키고 싶었지만 조사에 제대로 인해 친구는 친구의 비리를 넘어갈 수 없었죠 내가 집권하는 동안 부정원 절대 안 돼</p><p><br />&nbsp;분명히 후회할 기다 그리고이 대화를 듣고 있던 또 다른 한 사람이 있어서 바로 해수의 과외 선생님으로 있던 청년 재현이었습니다 저 문제 다 풀었어요</p><p><br />&nbsp;고태선 대통령님 서거했을 때 제일 먼저 배우로 의심받았던 사람 회장님이셨다는 거 알만한 사람들은 다 압니다 그런 회장님이 해수를 며느리로 받아들인 순간 모든 혐의에서 벗어났죠 탁월한 결정이셨어요 나는 증거를 쥐고 있고 너는 말만 씨부리는데 이게 싸움이 될 것 같나 누가 그래요 제가 말 뿐이라고 이렇게 그날의 진실을 알고 있는 재현을 뒤로 테라도 병원장에게들은 정보를 토대로 장금모회장을 조사하던 중 누군가 계속해서 테라의 뒤를 미행하는데 미행이 붙은 것 같습니다 나 좀 도와줄 수 있어요 또 필요한 거 있으십니까 사모님 그런데 사실 필승의 정체는 바로 그 설마가 맞습니다 님들아 같은 건 없습니다 기억을 잃었다는이 자기도 피해자라는 왜 이렇게 많으면 진실을 숨기고 싶은 법이죠 자신을 버렸다는 복수심으로 자란 테라의 동생 필승 언제든 명령만 내려주십시오 저야전 꼭 제 손을 없앨 겁니다 가족부부 친구 자매 형제 남매 누구나 믿을 수 없는 미군 버려</p><p><br />&nbsp;그만 좀 해성 내가 다 설명할게 설명 네가 양박사랑 짜고 한 그 역겨운 짓들 우리 이미 다 안다고 있다 레드 죽음으로네 정치적 욕심 채워놓고 해치 위하는 척 위상 떨지 마 레드한테 급선 뇌전증이 발생했어 부작용 때문이 아니라 발표회장에서 받은 스트레스 때문이었어 우리가 만든 스마트 패치는 완벽했다고 스마트패치가 완벽했다고 야</p><p><br />&nbsp;우린 또 실패한 거야 그러다 사고 나면 뭐 어떻게 사람도 막 죽여 레드처럼 그럼 교진이 형은 교진이 형은 또 누구야 언제까지 저렇게 줄 건데 세 사람이 스마트패치 연구에 매달렸던 건 바로 사고로 깨어나지 못하고 있던 교진 영 때문이었습니다 그리고 그 사고의 중심엔 여기 세 사람이 있었죠 오늘 여기 다들 동의한 거지 무조건 꼴찌가 다 책임지는거다 대신 죽을 때까지 비밀 지켜주고 자 다들 조심하자</p><p><br />&nbsp;이제는<br />&nbsp;안녕<br />&nbsp;그날 서고 우연 아니지 누군가 널 죽이려고 한 거지 도진이니 너 이렇게 만든 사람 어머니 난 걔 안 믿어네 동생도 내 자식도 아니야 그리고이 사과의 별을 배다른 동생 도진이 금조의 준을 차지하기 위해 버린이라 생각하고 있었죠 네 밤 갑작스런 이별에 유라가 도진을 찾아오고</p><p><br />&nbsp;다른 욕심 없어요 그냥 조용히 도진씨 옆에만 있으면 안 될까요 회장님 제가 그동안 잠깐 부회장은 자신의 약점이 될까 유라를 제거하려 합니다 내가 너를 이래 가르쳤나 표치의 혀에 이거 당장 왜곡으로 치워라 그래서 제일 먼 데로 다시는 못 돌아오는 대로 제가 회장님을 위해서 이렇게 장근모에게 희생양의 최후는 비참하게 벌어지게 되는 것이었죠 나</p><p><br />&nbsp;분명 말하는데 해수랑 절대 못해요 그땐 내가 너 죽여버릴지도 이렇게 금족으로의 부자에게 쌍으로 버림받게 된 유라는 아직 한 발도 남았는데 한편 테러도 배수가 자신의 과거를 알고 있음을 알게 되면서 코의 수고 알고 있어 네가 o형이라는 거 데라는 어떤 말부터 꺼내야 할지 막막하기만 하던 그날 밤 너 지금 어디야 지우랑 같이 있니 응 나랑 있어 지우 내가 그쪽으로 데리러 갈게 어디로 갔는데 왜 지민아랑 있으면 안 돼 소름끼치는 해수의 한마디 아니 그게 아니라 언니 왜 이렇게 불안해 설마 내가 언니 딸한테 무슨 짓이라도 할까 봐 그래 너 지금 어딨냐고 어딘지 왜 말 못해 너 서운하네</p><p><br />&nbsp;난 늘 언니를 믿었는데 언니는 아닌가 봐 하지야 제발 어딘지 좀 알려 줘 이렇게 아무런 말 없이 끊어진 통화와 함께 기다렸다는 듯이 쏟아지는 폭은 한편 유라는 무작정 성찬의 집을 뒤지기 시작하고 그것 때문에 나 만났구나 내 연구 파일 훔치려고 너 나 사랑해 나랑 너 사랑하는데 야</p><p><br />&nbsp;이렇게 누군가의 행동을 보이는 두 사람 그런데 빗속을 뚫고 유라를 쫓아오는 누군가<br />&nbsp;한편 테라도 빗속을 들고 회수의 뒤를 쫓기 시작하는데 야 아주 난장판이 되어가는데 매드토익 밤 9시 10분 tvn<br />&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-22 08:17:23 유튜브 영화 리뷰 채널 유튜브 목록 http://macaronics.net/index.php/movie/koreanmovie/view/2085 2085 <p>&nbsp;</p><ul><li><p><a title="지무비" data-v-105e1d38="" href="https://namu.wiki/w/%EC%A7%80%EB%AC%B4%EB%B9%84">지무비</a></p></li><li><p><a title="삐맨" data-v-105e1d38="" href="https://namu.wiki/w/%EC%82%90%EB%A7%A8">삐맨</a></p></li><li><p><a title="고몽" data-v-105e1d38="" href="https://namu.wiki/w/%EA%B3%A0%EB%AA%BD">고몽</a></p></li><li><p><a title="라이너(인터넷 방송인)" data-v-105e1d38="" href="https://namu.wiki/w/%EB%9D%BC%EC%9D%B4%EB%84%88(%EC%9D%B8%ED%84%B0%EB%84%B7%20%EB%B0%A9%EC%86%A1%EC%9D%B8)">라이너</a></p></li><li><p><a title="빨강도깨비" data-v-105e1d38="" href="https://namu.wiki/w/%EB%B9%A8%EA%B0%95%EB%8F%84%EA%B9%A8%EB%B9%84">빨강도깨비</a></p></li><li><p><a title="김채호의 필름찢기" data-v-105e1d38="" href="https://namu.wiki/w/%EA%B9%80%EC%B1%84%ED%98%B8%EC%9D%98%20%ED%95%84%EB%A6%84%EC%B0%A2%EA%B8%B0">김채호의 필름찢기</a></p></li><li><p><a title="필름에빠지다" data-v-105e1d38="" href="https://namu.wiki/w/%ED%95%84%EB%A6%84%EC%97%90%EB%B9%A0%EC%A7%80%EB%8B%A4">필름에빠지다</a></p></li><li><p><a title="엉준" data-v-105e1d38="" href="https://namu.wiki/w/%EC%97%89%EC%A4%80">엉준</a></p></li><li><p><a title="두클립" data-v-105e1d38="" href="https://namu.wiki/w/%EB%91%90%ED%81%B4%EB%A6%BD">두클립</a></p></li></ul><p>&nbsp;</p><p>&nbsp;</p><p><strong>영화의 재미를 두 배로 극대화시켜주는 영화 리뷰 채널 BEST 5</strong></p><p>&ldquo;무슨 영화를 볼까?&rdquo; 이 고민은 누구나 합니다. 수많은 영화가 범람하는 이 시대에, 한국뿐 아니라 외국의 수 많은 영화가 개봉합니다. 모든 영화를 다 본다면 좋겠지만, 사람의 취향에 따라 어떤 영화는 나에게 인생 최고의 작품이며 어떤 이에게는 단순한 지루한 영화 일 수 있습니다. 영화관 혹은 모니터 앞에서 아무것도 모르고 영화를 보고 후회하기보다는, 나의 흥미를 끌 수 있는 영화를 선별적으로 더 많이 보는 게 효율적일 것입니다.</p><p>오늘 다룰 주제는, 영화 리뷰 채널 BEST 5입니다. 영화 리뷰를 통해 새로운 영화에 대한 소개뿐 아니라, 자신만의 새로운 시각을 바탕으로 고전 영화에 대한 재해석은 또 다른 영화를 보는 듯한 느낌을 전달할 것 입니다. 대략 10분의 시간 동안 즐기시기에 좋은 채널들로 선정해보았습니다. (12월 1일 기준, 유튜브 구독자 순 정렬)</p><p><strong>1. 고몽, 96.8만명</strong></p><p>최고 조회수 1641만 명, 제목: 인도의 토르 크리쉬의 신비로운 기원을 다룬 영화</p><p>영화 리뷰 유튜버 중에서도 독보적인 구독자 수를 보유하고 있으며, 유튜브 관련 책을 낼 정도로 많은 이들의 사랑을 받고 있는 유튜버입니다. 자신만의 독특한 시점에서 영화를 풀어내면서 시청자들의 눈을 사로잡습니다. 영화뿐만 아니라 드라마도 함께 다루고 있으며 영화를 뻔하지 않게 해석하는 재미있는 채널입니다.</p><p>&nbsp;</p><p><strong>2. 드림 텔러, 77.1 만명</strong></p><p>최고 조회수 773만 명, 제목: 전직 특수부대 흑형을 잘못건든 편의점 털이들</p><p>네이버 블로그에서 영화 분야 편집자로 일을 시작하며 자신만의 전문성을 가진 유튜버입니다. 그리고 영화 뿐 아니라 뮤직비디오를 해석하는 독보적인 콘셉트를 갖고 있습니다. 처음부터 끝까지 차분한 톤으로 영화 분석을 차분히 이끌어가는 것이 특징과 함께 과한 영상 편집이 아닌 적절한 설명과 영화 분석은 영화에 몰입도를 끌어 올립니다.</p><p>&nbsp;</p><p><strong>3. 달빛 뮤즈, 60.3 만명</strong></p><p>최고 조회수 223만명, 제목: 갑질 하는 재벌 아들을 조져버리는 개쩌는 남자</p><p>여성 유튜버로서, 차분한 목소리와 정확한 발음은 집중도를 높이는 가장 큰 매력일 것입니다. 수 많은 영화 중에서도, 여성들의 취향을 저격하는 영화 추천은 달빛 뮤즈의 장점 중 하나입니다. 장르별로 영화를 추천 및 영화배우별로 추천은 골라보는 재미를 불러일으킵니다.</p><p>&nbsp;</p><p><strong>4. 김시선, 58.8만명</strong></p><p>최고 조회수 813만명, 제목: 누가 먼저 죽을 지 모르는 죽음의 게임</p><p>실험적인 채널 그리고 넷플릭스와 영화를 리뷰하는 채널로, 김시선의 영화 큐레이터, 김시선의 정주행, Discovery of Movie과 같은 자신만의 시선과 같은 카테고리를 통해서 영화를 재미있게 풀어내는 재주를 가진 유튜버입니다. 영화 뿐 아니라 넷플릭스 드라마 &lsquo;종이의 집&rsquo; 리뷰의 경우 많은 이들의 호응을 얻을 정도로 재미있게 드라마를 재각색 하였습니다. MBC 라디오 FM4U &lsquo;정지영의 오늘 아침입니다&rsquo;에서 영화코너 게스트를 맡기도 했으며, 영화 전문 사이트 &lsquo;시선 웹&rsquo;과 영화 잡지 &lsquo;시선일삼&rsquo;을 발행하며 많은 이들의 사랑을 유튜버 전부터 받고 있습니다.</p><p>&nbsp;</p><p><strong>5.&nbsp;백수골방 38.2만명</strong></p><p>애니메이션 최고 조회수 256만명, 제목: 늑대아이에 숨겨진 의미들</p><p>유튜브 설명에 &ldquo;흘러간, 그러나 많은 이들이 기억하고 있는 영화를 영상으로 리뷰합니다.&rdquo; 라고 말할 정도로 옛 영화들에 대한 리뷰를 하는 유튜버입니다. 영화 뿐만 아니라 애니메이션에도 리뷰하며 자극적이지 않게 영화와 애니메이션을 차분히 끌고 나고 가는 것이 특징입니다. 애니메이션 혹은 최신 영화가 아닌 숨어 있던 고전의 영화를 그리워하는 분들에게 추천합니다.</p><p>&nbsp;</p><p>&nbsp;</p><p>출처 :&nbsp;</p><p><a target="_blank" href="http://college.koreadaily.com/무슨-영화-볼까-유튜브-영화-리뷰-채널-best-5/">http://college.koreadaily.com/무슨-영화-볼까-유튜브-영화-리뷰-채널-best-5/</a></p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:48px">해외 유튜브 추천</span></p><p>&nbsp;</p><p>모든 컨텐츠가 그렇겠지만 영어권으로 넘어가면 아무래도 유튭의 컨텐츠 범위가 확 넓어지는데,&nbsp;</p><p>특히 영화 관련 유튭 채널 중에 고퀄 존잼 리뷰어들이 얼마나 많은지 최근에 많이 알게 되서 몇 개 추천하려고 해.&nbsp;</p><p>영어권은 역시 인구가 많아서 그런지 스펙트럼이 훨씬 다양하고 고퀄이 많더라.</p><p>(다 영어주의임...자막있고 그런 건 없엉ㅠ)&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>1. nerdwriter&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/TzLXHViyW7I">https://m.youtu.be/TzLXHViyW7I</a><br /><iframe src="https://www.youtube.com/embed/TzLXHViyW7I?wmode=transparent&amp;jqoemcache=3IxCg" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>영상편집도 고퀄이고 분석의 디테일도 훌륭함. 업로드가 잦지는 않은데 영상 떴다고 하면 꼭 보게 되는 유튜버. 백프로 영화 이야기만 하는 건 아닌데 다 한번쯤은 볼만한 영상들임. 이런 형식의 영상을 보통 video essay라고 하더라. 꼭 이 유튜버가 아니라도 이런 형식의 영상을 보고 싶으면 video essay로 검색해도 좋을 듯. 진짜 에세이를 작성한 것처럼 이런 유튜버들은 영화에 대해 장황하게 주욱 늘어놓는 게 아니라 정갈하게 요점을 보여줌. 이 유튜버는 그 중에서도 특히 영상이 좋다고 생각함. 기생충 분석한 영상도 채널가면 있음(특히 복숭아 몽타쥬 부분)&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>2. CineFix</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/4-JlooqIyFM">https://m.youtu.be/4-JlooqIyFM</a><br /><iframe src="https://www.youtube.com/embed/4-JlooqIyFM?wmode=transparent&amp;jqoemcache=6kM4k" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>주로 Top 10 리스트들이나 영화와 원작간의 비교영상을 많이 내놓는 유튭 채널. Top 10, Top 5 리스트들을 보면 고전부터 요즘 작품까지 정말 스펙트럼 다양하게 고름. 이 채널을 보고 있으면 봐야할 영화 목록이 훅훅 늘어남. 탑텐을 고를 때 딱 열작품만 말하는 게 아니라 장르별로, 소재별로 다른 작품들까지 고르게 소개해주고 취향도 확실함.&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>3. YMS (your movie sucks)</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/zR8sRjBJgQ8">https://m.youtu.be/zR8sRjBJgQ8</a><br /><iframe src="https://www.youtube.com/embed/zR8sRjBJgQ8?wmode=transparent&amp;jqoemcache=RxHRW" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>캐나다의 1인 유튜버. 위 채널들이랑은 다르게 좀 더 가벼운 분위기. 특히 망작을 깔 때 빛을 발함ㅇㅇ 친구들이랑 망작 보면서 낄낄거리는 코멘터리 영상들도 재밌고 원덬은 위의 올드보이 리메이크 까는 영상이랑 샤말란 감독 까는 영상이 특히 재밌었음ㅋㅋㅋㅋ&nbsp;&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>4. Red Letter Media&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/6c-hAbInjt4">https://m.youtu.be/6c-hAbInjt4</a><br /><iframe src="https://www.youtube.com/embed/6c-hAbInjt4?wmode=transparent&amp;jqoemcache=7s8go" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>역시 가벼운 분위기의 영화 채널이고 주요 컨텐츠로는 B급도 안되는 진짜 망작 영화들 까는 컨텐츠랑 Half in the bag이라는 영화 리뷰 컨텐츠 등이 있음. &lt;나홀로집에&gt;의 맥컬리 컬킨도 자주 출연함. 최근 나덬의 최애 채널임. 기생충은 리뷰 안해주려나 하고 포기했었는데 어제 올라와서 가져와봄ㅋㅋㅋ</p><p>&nbsp;</p><p>주요 출연진들이 캐릭터들이 확실하고 아예 시트콤같은? 자체 스토리라인도 존재함. 영화 리뷰할 때는 두 사람이 앉아서 서로 자유롭게 대화를 나누는 형식이고 영상 길이도 길어서 딴 짓할 때 걍 배경으로 틀어놓기 좋음. 아재들이 냉소적이고 모두까기인형같은 리뷰만 하는 것 같지만 나름 다큐멘터리등의 촬영경험이 있는 사람들이라 리뷰 잘 들어보면 예리한 면도 있고 영상들이 길어도 재밌음.&nbsp; 참고로 스타워즈 까는 걸로 유명한 채널이라서 스타워즈 팬에겐 비추함 ㅎㅎ&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>5. Thomas Flight</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/4dn8Fd0TYek">https://m.youtu.be/4dn8Fd0TYek</a><br /><iframe src="https://www.youtube.com/embed/4dn8Fd0TYek?wmode=transparent&amp;jqoemcache=BR1uQ" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>역시 비디오 에세이 계열의 유튭 채널. 위에 말한 채널들은 다 메이저하고 구독자수가 많은데 여기는 퀄에 비해 구독자 수가 적은 듯 왜 그렇지 영상들은 좋은 거 많음! 가지고 온 영상은 보헤미안 랩소디의 편집에 대한 영상인데 왜 작년에 편집상 받을 때 말이 나왔었는지 이거 보니까 이해가 가더라고. 채널 가면 역시 기생충 분석영상도 있음ㅎ&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>여기 말고도 Just Write나 Screened 등의 좋은 채널들도 많고 The take, Wisecrack 도 가볍게 보기 좋은 채널들인데 비슷한 계열 소개가 너무 많아지는 거 같아서 여기에 덧붙임&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>5. Accented Cinema</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/GzxkOpvZoao">https://m.youtu.be/GzxkOpvZoao</a><br /><iframe src="https://www.youtube.com/embed/GzxkOpvZoao?wmode=transparent&amp;jqoemcache=9XrSG" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>&nbsp;</p><p>또 비디오 에세이 계열. 근데 다른 점이 아시아 영화 중심이란 거. 주로 한중일을 중심으로 다루는 거 같음. 목소리 들어보면 알겠지만 네이티브 스피커가 아니라 아시아 계열인 거 같음. 북미인이 보는 시각이 아니라 같은 아시안의 분석이라고 생각하고 보니까 더 공감이 가고 편안한 분석이 많았음. (액센트 때문에 알아듣기 힘들 땐 자막을 틀어놓고 보자)&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>6. The Oscar Expert&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p>&darr;<a target="_blank" href="https://m.youtu.be/z37yb-nYJ-E">https://m.youtu.be/z37yb-nYJ-E</a><br /><iframe src="https://www.youtube.com/embed/z37yb-nYJ-E?wmode=transparent&amp;jqoemcache=uPOBJ" width="425" height="349" allowfullscreen="true" allowscriptaccess="always" scrolling="no" frameborder="0"></iframe></p><p>&nbsp;</p><p>(소리주의ㅋㅋㅋ 오스카 시상식 리액션 영상이라서ㅋㅋ)</p><p>&nbsp;</p><p>이건 넓은 스펙트럼의 예시로서 한번 가져와봤음ㅎㅎ 채널명부터가 심상찮은데 아예 아카데미 예측이 주목적인 유튜버임. 아카데미 레이스가 가시화되는 시점부터 매달 아카데미 예측영상을 올려주더라고ㅋㅋ 형제 두명이 운영하는 채널인데 이번에 봤던 오스카 예측 유튜버 중에서는 정확도가 굉장히 높은 편이었음. 그렇다고 영화 리뷰가 별로인가 하면 그렇지도 않음. 영화 보는 눈은 기본적으로 있는 유튜버라고 생각.</p><p>&nbsp;</p><p>나덬만 그랬는지도 모르겠지만 이번에 기생충덕분에 아카데미 예측을 즐기는 사람들의 풀이 굉장히 넓다는 걸 첨으로 알게 됨. 골드더비도 유명하지만 이렇게 아예 전문으로 하는 유튭채널도 있는게 신기했어서 가지고 와봄&nbsp;</p><p>&nbsp;</p><p>&nbsp;</p><p><span style="font-size:28px">출처 :&nbsp;&nbsp;<a target="_blank" href="https://theqoo.net/square/1320924473">https://theqoo.net/square/1320924473</a></span></p><p><span style="font-size:28px">영어권 유튜브의 방대한 영화 리뷰 채널들의 세계(스압)</span></p><p>&nbsp;</p><p>&nbsp;</p> 2023-03-21 09:08:11