<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>소신의 블로그생활</title>
    <link>https://wolfy.tistory.com/</link>
    <description>개발지식
인테리어</description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 15:31:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>개발자 소신</managingEditor>
    <image>
      <title>소신의 블로그생활</title>
      <url>https://tistory1.daumcdn.net/tistory/3404529/attach/948fc9fffffe4446a7d6715cf4abb01f</url>
      <link>https://wolfy.tistory.com</link>
    </image>
    <item>
      <title>자바 스프링부트 vscode에서 devtools 핫 리로드가 안될때</title>
      <link>https://wolfy.tistory.com/341</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;CTRL + ,&amp;nbsp; -&amp;gt; java 검색하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;java debug settings hot code replace auto 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzeBHT/btsJWAih6fx/ur2ZBw46ZRbDDQRjzY7VX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzeBHT/btsJWAih6fx/ur2ZBw46ZRbDDQRjzY7VX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzeBHT/btsJWAih6fx/ur2ZBw46ZRbDDQRjzY7VX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzeBHT%2FbtsJWAih6fx%2Fur2ZBw46ZRbDDQRjzY7VX1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;828&quot; height=&quot;142&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vscode를 재설치하는게 나을수도있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%USERPROFILE%\AppData\Roaming\Code (캐시 삭제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%USERPROFILE%\.vscode\extensions (확장프로그램 삭제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재설치하고 재부팅하니 잘 작동함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 뭔가 속도가 더 빨라진 느낌..? 이것저것 많이 붙여놨었나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 debug console에서 색깔이 나왔던 것 같은데 안나오고 뭔가 이상한 기호가 붙어있는데..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ansi코드가 원래 디버그콘솔에서 안먹었나 화나네&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCQfBY/btsJVJtQhzI/lCrOWEIv2Y67YhPG3oe5NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCQfBY/btsJVJtQhzI/lCrOWEIv2Y67YhPG3oe5NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCQfBY/btsJVJtQhzI/lCrOWEIv2Y67YhPG3oe5NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCQfBY%2FbtsJVJtQhzI%2FlCrOWEIv2Y67YhPG3oe5NK%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;398&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Backend</category>
      <category>DevTools</category>
      <category>Java</category>
      <category>SpringBoot</category>
      <category>오류</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/341</guid>
      <comments>https://wolfy.tistory.com/341#entry341comment</comments>
      <pubDate>Sun, 6 Oct 2024 20:43:56 +0900</pubDate>
    </item>
    <item>
      <title>Rust Axum 테스팅 자동화 커리큘럼 학습 이야기</title>
      <link>https://wolfy.tistory.com/339</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최종 소스코드 (&lt;a href=&quot;https://github.com/devsosin/test_automation_rust_axum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃허브 링크&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1727158173628&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - devsosin/test_automation_rust_axum&quot; data-og-description=&quot;Contribute to devsosin/test_automation_rust_axum development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/devsosin/test_automation_rust_axum&quot; data-og-url=&quot;https://github.com/devsosin/test_automation_rust_axum&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cQKWxW/hyW6yrKsHw/1gOHdsrZKFpDZrwyLfcHmk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/devsosin/test_automation_rust_axum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/devsosin/test_automation_rust_axum&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cQKWxW/hyW6yrKsHw/1gOHdsrZKFpDZrwyLfcHmk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - devsosin/test_automation_rust_axum&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to devsosin/test_automation_rust_axum development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 지식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 풀스택 개발 경험이 좀 있고 AWS EC2 배포 경험, 도커 활용 Jenkins CI/CD 구축 등 다양하게 해봄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python, javascript, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;java 언어 공부했고&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;django, fastapi, expressjs, reactjs, springboot 사용해봤음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 클론 코딩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적인 백엔드 API 구조는 알고 있었기 때문에 아래 영상을 보며 rust와 axum에 대해서 감을 잡음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtube.com/playlist?list=PLUg0hJGmtzyoh5_3wYzrFbWCN7owoQP-w&amp;amp;si=ESOUBIv1zQV23DH6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rust Axum REST API + MongoDb&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 전체적으로 공부 &amp;amp; 활용한 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust, AXUM, design pattern (handler - usecase - repository)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQLx, Postgres, CTE활용 쿼리 최적화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker, Testing (unit, AAA, TDD), 스크립트 활용 자동화 (커버리지 결과 정리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 활용 image 관리, Auth Middleware&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 테스팅하는 방법에 대해서 관심이 있었고, 마침 관련 아티클을 찾아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라하면서 공부해봐야겠다고 생각해서 아래 내용을 참고해서 개발을 시작함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://medium.com/intelliconnect-engineering/step-by-step-guide-to-test-driven-development-tdd-in-rust-axum-5bef05fd7366&quot;&gt;Step-by-Step Guide to Test Driven Development (TDD) in Rust Axum&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련해서 테스트 데이터베이스를 테스트할때만 만들어서 테스팅하는 방식을 알게 되어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그것도 자동화하면 완전 테스팅 자동화가 될 것 같다는 생각을 하고 shell 스크립트 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 추가)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에 글 정리하면서 알게 된 postgresql 쿼리 성능 모니터링 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_stat_statements -&amp;gt; query 실행 횟수, 소요시간, 평균시간을 확인할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 공부 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요하다고 생각했던 건 보지 않고 로직을 구상해서 내가 직접 구현해보는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 알던 기능을 참고해서 타이핑하고 안보고 해당 내용을 기억해서 타이핑 하는것을 계속해서 연습함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sql 쿼리도 처음엔 gpt가 준대로 따라쳤지만 (정말 안되는 경우 아니면 복붙안함) WITH문 내용을 직접 생각해서 로직대로 타이핑해보는 버릇을 들임. 이렇게 학습하니 나중엔 GPT한테 안물어보고 기계적으로 타이핑하기 시작했음 (필요한 로직들은 단위테스팅에서 미리 설계가 되어있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 생각해서 치는거랑 보고 치는거랑은 어디서 잘못된 코드를 작성했는지, 어느 부분이 중요한지를 학습하는데 생각보다 큰 영향을 미침.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axum 활용해서 API 개발 들어갈 때는 ChatGPT o1-mini 활용하여 지속적으로 개발 방향과 성능 개선 방향에 대한 질문함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변 확인 후 개발 방향을 선택하여 학습 (DI를 최대한 줄이고 CTE를 사용하게 된 계기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Rust&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저는 Rust 언어 자체에 관심이 있었기에 먼저 &lt;a href=&quot;https://rust-exercises.com/100-exercises/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Rust Exercise&lt;/a&gt;를 통해 실습 위주의 언어 공부 (언어 특징, 개념잡기)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dhghomon.github.io/easy_rust/Chapter_1.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Easy Rust&lt;/a&gt;는 학습방식이 안맞아서 중간에 그만둠 (동영상 강의도 있고 언어 기본기를 공부하기엔 좋은 듯)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Axum&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://github.com/tokio-rs/axum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;tokio-rs/Axum&lt;/a&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 레포에 있는 examples를 통해 기본 기술스택을 정함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;mongodb는 한번 해봤었기 때문에, &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;sqlx, postgresql를 사용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Design Pattern&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 클론코딩 영상에서 사용하는 디자인 패턴을 적용하여 개발하기 시작 (handler, usecase, repository)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트에서도 controller / service / repository와 같은 디자인 패턴을 따르기 때문에 무리없이 적용했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 이번에 단위 테스팅하면서 각 레이어의 역할에 대해서 좀 더 깊이 이해할 수 있었음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler에서는 요청하는 데이터에 대한 validation 검증 / response 처리 (statuscode, body 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;usecase는 비즈니스 로직 (하지만 대부분 쿼리를 CTE로 작성하여 repository가 대부분 처리함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler dto를 entity 객체로 변환하는 정도의 역할만 부여하고 나중엔 테스트코드조차 작성하지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repository - 각종 비즈니스 로직을 모두 한 query로 처리 한 쿼리 내에서 검증과 삽입, 수정, 삭제를 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 한 이유가 데이터베이스 네트워크 오버헤드를 줄이기 위해서 적용해봄.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 폴더 구조는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 관련&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YtRKi/btsJKg5HV79/mmTspJC19IBo4J5OldABFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YtRKi/btsJKg5HV79/mmTspJC19IBo4J5OldABFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YtRKi/btsJKg5HV79/mmTspJC19IBo4J5OldABFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYtRKi%2FbtsJKg5HV79%2FmmTspJC19IBo4J5OldABFk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;267&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- erd를 사이트에 자꾸 왔다갔다 하면서 보면 귀찮아서 넣어놓으니까 생각보다 편했음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 도커로 테스트 데이터베이스를 띄우기 위한 설정파일들 init.sql (테이블 초기화, 테스트 데이터 삽입)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- postgresql.conf: 나중에 쿼리 성능 모니터링을 위해 도입한 pg_stat_statements 설정용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app 관련&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;1146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIh9Sm/btsJJNpxfHM/s4y4kwvdkwmQ4vRK0WZFy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIh9Sm/btsJJNpxfHM/s4y4kwvdkwmQ4vRK0WZFy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIh9Sm/btsJJNpxfHM/s4y4kwvdkwmQ4vRK0WZFy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIh9Sm%2FbtsJJNpxfHM%2Fs4y4kwvdkwmQ4vRK0WZFy1%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;845&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;1146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 aws, jwt, database와 관련 설정 파일을 관리하는 config 폴더&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주 기능들을 개발하는 domain폴더, 다양한 도메인에 사용되는 것들은 global 폴더에 모아둠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 인증을 위한 middleware&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tests 폴더는 gpt가 폴더 구조를 저렇게 잡은거고 mocking하는 파일과 그것을 가져다 사용하는 식으로 테스팅 하는 듯 (해당 파일에 단위 테스트 코드를 작성했기 때문에 따로 사용해보진 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;domain의 기능 별&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5OEut/btsJJvirnnH/ifBtv2zPk2R8BMGNEtWuYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5OEut/btsJJvirnnH/ifBtv2zPk2R8BMGNEtWuYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5OEut/btsJJvirnnH/ifBtv2zPk2R8BMGNEtWuYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5OEut%2FbtsJJvirnnH%2FifBtv2zPk2R8BMGNEtWuYk%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;474&quot; height=&quot;1016&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;1380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 CRUD를 기준으로 모든 기능들을 파일로 작성하고 CRUD 내에서도 repository를 별도로 생성함 (SaveBookRepo, GetBookRepo)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 다 하고나서 ChatGPT는 state로 repository, usecase를 관리해서 필요한곳에서 가져다 사용한 방식도 추천함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mod.rs에서 통합하여 repository 하나만 생성하고 그것을 state가 공유하는 방식으로도 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련해서 GPT 선생의 설명은 &lt;a href=&quot;https://chatgpt.com/share/66f25d33-7894-8013-aaa1-4bb50a981db4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;여기&lt;/a&gt;서 확인할 수 있음 (CTE관련 설명도 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트코드를 작성하게 되면 한 파일의 길이가 평균적으로 150줄정도 나오기 때문에 그냥 분리해놓았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 리팩토링 한다면 mod.rs에서 하나의 struct를 만들고 각 파일에 있는 함수를 연결하는 식으로 활용할 듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dto는 데이터 전송단에서 주고받는 것들, entity는 db 객체에 대한 정의, route는 최종적으로 handler에 있는 route들을 모아서 적용해줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.rs부터 타고들어가보면 확인할 수 있는데, pool을 하나 생성한 뒤 그것을 router 별로 repository에 참조자로 전달하고, 최종적으로 repository에서 clone()하여 자원을 공유함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 폴더구조는 저런식으로 잡아놓고 개발을 시작함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. SQLx, PostgreSQL, CTE&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 PostgreSQL을 사용함. 이유는 딱히 없고 그냥 제일 좋다고 해서? 그리고 SpringBoot 쓰면 배포할때 DB는 저걸로 하길래 그냥 씀.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 어떻게 설정을 할지, 어떻게 커넥션 pool을 관리할지 감 잡는거에서 되게 많이 고민한 듯.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 pool을 main.rs에서 clone하고 그걸 계속 넘기는 식으로 repository로 전달했는데 참조자로 넘기고 맨 마지막에 pool.clone()하라고 해서 그렇게 함 근데 결론적으로는 별차이 없을듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB통신 횟수를 줄이기 위해 repository 부분 개발할 때 GPT한테 특정 로직을 설명하고 한 sql로 처리하는 방법이 있는지 물어보니 WITH문으로 쿼리작성하는 방식을 알려주길래 처음엔 그냥 따라서 쳐보고 안되는건 분리했었음. 근데 계속 사용해보니 안되는게 아니었고 문법을 잘 이해 못한거여서 나중엔 결국 한 sql로 처리하도록 다시 작업, 이렇게 하니 usecase의 비즈니스 로직을 repository가 다 먹어버리는 상황이 발생.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 usecase가 비즈니스 로직을 처리해서 적절한 오류 처리를 해주는것이 필요한데, repository가 그 로직들을 모두 담당하게 되면서 비즈니스 로직까지 수행함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과로 최종 CTE의 값만 반환하면 예외 처리를 다양하게 할 수 없어졌고 캐싱된 결과들에 대해서 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;SELECT를 통해 중간중간 예외상황 체크한 결과들을 반환하도록 함&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InsertResult, UpdateResult, DeleteResult를 만들어 그 구조체로 매핑되도록 하였고 그 내용에는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_exist -&amp;gt; 존재 여부 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_authorized -&amp;gt; 권한 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;is_duplicated -&amp;gt; 중복 여부 체크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id -&amp;gt; 생성 시 반환받는 id값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 SQL은 이런 형태로 작성함.&lt;/p&gt;
&lt;pre id=&quot;code_1727162228105&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH BaseExists AS (
    SELECT id, book_id
    FROM tb_base_category
    WHERE id = $2
),
AuthorityCheck AS (
    SELECT be.id AS base_id
    FROM BaseExists AS be
    JOIN tb_book AS b ON be.book_id = b.id
    JOIN tb_user_book_role AS br ON br.book_id = b.id
    WHERE br.user_id = $1 AND br.role != 'viewer'
),
DuplicateCheck AS (
    SELECT EXISTS (
        SELECT 1
        FROM AuthorityCheck AS a
        JOIN tb_sub_category AS s ON a.base_id = s.base_id
        WHERE s.name = $3
    ) AS is_duplicated
),
InsertSubCategory AS (
    INSERT INTO tb_sub_category (base_id, name)
    SELECT base_id, $3
    FROM AuthorityCheck
    WHERE (SELECT is_duplicated FROM DuplicateCheck) = false
    RETURNING id
)
SELECT 
    EXISTS (SELECT 1 FROM BaseExists) AS is_exist,
    EXISTS (SELECT 1 FROM AuthorityCheck) AS is_authorized,
    (SELECT is_duplicated FROM DuplicateCheck),
    (SELECT id FROM InsertSubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 쿼리를 활용한 repository의 한 function&lt;/p&gt;
&lt;pre id=&quot;code_1727162321930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#[derive(Debug, sqlx::FromRow)]
struct InsertResult {
    id: Option&amp;lt;i32&amp;gt;,
    is_exist: bool,
    is_authorized: bool,
    is_duplicated: bool,
}

pub async fn save_sub_category(
    pool: &amp;amp;PgPool,
    user_id: i32,
    sub_category: SubCategory,
) -&amp;gt; Result&amp;lt;i32, Box&amp;lt;CustomError&amp;gt;&amp;gt; {
    let result = sqlx::query_as::&amp;lt;_, InsertResult&amp;gt;(
    	// 위의 쿼리
    )
    .bind(user_id)
    .bind(sub_category.get_base_id())
    .bind(sub_category.get_name())
    .fetch_one(pool)
    .await
    .map_err(|e| {
        let err_msg = format!(&quot;Error(SaveSubCategory): {:?}&quot;, &amp;amp;e);
        tracing::error!(err_msg);

        let err = match e {
            sqlx::Error::Database(_) =&amp;gt; CustomError::DatabaseError(e),
            _ =&amp;gt; CustomError::Unexpected(e.into()),
        };

        Box::new(err)
    })?;
    
    // 결과에 따른 에러처리
    if !result.is_exist {
        return Err(Box::new(CustomError::NotFound(&quot;BaseCategory&quot;.to_string())));
    } else if !result.is_authorized {
        return Err(Box::new(CustomError::Unauthorized(&quot;BookRole&quot;.to_string())));
    } else if result.is_duplicated {
        return Err(Box::new(CustomError::Duplicated(&quot;SubCategory&quot;.to_string())));
    }

    Ok(result.id.unwrap())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 체크같이 중복되는 SQL문이 있긴 했는데 상황마다 조금씩 달랐기 때문에 굳이 통합하지는 않음. 그냥 치는거랑 함수 연결해서 쓰는거랑 큰 차이가 없을 것 같았음 문자열 내에 바꿔끼는것도 귀찮고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 이 방법이 캐싱을 적용하기에는 불리하다고 생각을 하고 있긴 하지만 db가 한 쿼리를 바로 실행하기 때문에 lock에서 안전해지는 것과 실행계획 최적화와 같은 DB자체 기능인 쿼리 최적화를 통한 성능 향상을 노림. 통신 횟수 자체도 비즈니스 로직이 복잡해지면 3번~4번 왔다갔다 할 것을 한 번으로 줄였고 로직의 심플함과 전체 구조나 단일 기능이 크게 바뀌는 상황은 고려하지 않았기 때문에 적용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 테스팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 given-when-then 패턴으로 테스트를 진행하려고 함. 근데 테스트 코드 작성법이 익숙하지 않았고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 참고한 rust axum TDD 문서도 결론적으로 내가 원하는 진짜 unit 단위의 테스팅이 아니었음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 gpt센세한테 가서 repository는 어떻게 테스트를 수행해야되는지 물어보고 AAA패턴이라는 것을 알게 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지금에서야 생각해보면 given-when-then이랑 뭔 차이가 있나 싶지만, 되게 직관적이라고 느꼈음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arange (데이터, 환경 준비) - Act (실행) - Assert (검증)으로 이루어진 테스팅 방법인데, 이걸 도입하고나서 테스트 코드를 어떻게 분리하여 작업해야될지가 좀 감이 잡혔음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;repository에서  가계부에 기록을 삽입하는 로직을 단위테스트한다고 했을 때  테스트해야 할 사항은 크게 다음과 같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 데이터베이스 연결 상태 확인,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 적절한 데이터가 들어와 가계부에 기록이 정상적으로 삽입이 되는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 권한이 없는 유저가 특정 가계부에 기록할 경우 에러 반환하도록 하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 없는 가계부에 기록하려고 할 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. (여기선 없었지만 unique필드가 있다면,) 중복이 발생하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 구현할 비즈니스 로직에 대한 내용들을 먼저 고민해서 작성해놓고 해당 테스트를 통과하도록 기능을 구현하는 방향으로 감.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 어떤 것을 테스트해야 될지 잘 몰랐지만 테스트를 계속 작성하다보니 결국 저 위에서 크게 벗어나지 않았던 것 같음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arange에서 필요한 데이터 준비하고 만약에 권한같은게 필요한 경우에는 init.sql에 미리 테스트용 데이터를 삽입해놓고 수행함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Act에서는 주로 구현할 함수를 Arange에서 준비한 데이터를 넘겨서 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assert에서 기대한 결과가 일치하는지 검증하는 식으로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그걸 구현하면&lt;/p&gt;
&lt;pre id=&quot;code_1727165837909&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async fn check_save_success() {
        // Arrange
        let pool = create_connection_pool().await;

        let user_id = 1;
        let book_id = 1;
        let base_category = BaseCategory::new(
            1,
            book_id,
            true,
            true,
            &quot;서브카테고리용&quot;.to_string(),
            &quot;FF0012&quot;.to_string(),
        );
        let base_id = save_base_category(&amp;amp;pool, user_id, base_category)
            .await
            .unwrap();

        let sub_category = SubCategory::new(base_id, &quot;테스트 서브 카테고리&quot;.to_string());

        // Act
        let result = save_sub_category(&amp;amp;pool, user_id, sub_category).await;
        assert!(result.as_ref().map_err(|e| println!(&quot;{:?}&quot;, e)).is_ok());
        let result = result.unwrap();

        // Assert
        let row = sqlx::query_as::&amp;lt;_, SubCategory&amp;gt;(&quot;SELECT * FROM tb_sub_category WHERE id = $1&quot;)
            .bind(result)
            .fetch_one(&amp;amp;pool)
            .await
            .unwrap();

        assert_eq!(result, row.get_id())
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/devsosin/test_automation_rust_axum/blob/master/src/domain/category/repository/save_sub.rs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위 코드 파일 위치&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스팅 함수 하나는 이런 형태로 작성. 이걸 기능별로 무한반복해서 작성하면되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하다보면 기계적으로 테스트코드를 작성하고 있는 나 자신을 발견하게 됨. 기능 구현하고나서 테스트 통과하면 쾌감이 쩐다. (처음엔)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 usecase로 넘기지만 레포에서 지금 비즈니스 로직을 다 검증하도록 해놓아서 (중복 검사, 권한 검사같은)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;usecase는 테스트할 게 거의 없었고, handler는 테스트 할 것이 응답 코드가 적절하게 오는지, 응답 데이터에는 적절한 데이터가 들어가있는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 repository나 usecase를 실제 구현한 객체로 사용하게 되면 DB에도 반영되고 그렇게 되므로 예상되는 응답에 대해서 mocking한 객체를 별도로 구현하여 어떤 데이터가 들어왔을 때 어떤 식으로 응답을 해야되는지 arange에서 구현해놓고 그 mock 객체를 넘기는 방식으로 테스트를 수행한다. 이렇게 되면 db 통신으로 인한 오버헤드도 줄고 결과에 대해서 빠르게 반환해주기 때문에 테스트 속도에도 이점을 가질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/devsosin/test_automation_rust_axum/blob/master/src/domain/category/handler/delete_base.rs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mocking test 예제&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 테스팅 자동화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gpt랑 참 많은 대화를 하고 issue도 많이 발생했던 파트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 최초에는 데이터베이스를 Docker로 띄웠고, 기존에 사용하던 compose 파일에 db volume이 연결되어 있어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경에서는 부적절했음. 그래서 volume을 연결 해제해서 컨테이너가 내려가면 테이블이나 데이터가 초기화되도록 하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jwt, aws s3 같은 것을 테스트하기 위해 필요한 env들을 별도로 적용하려고 해도 잘 적용이 안돼서 docker-compose 파일을 분리함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트용으로 docker-compose.test.yml을 따로 생성해서 environment를 직접  넣어놓고 테스팅 진행&lt;/p&gt;
&lt;pre id=&quot;code_1727163032409&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: &quot;3&quot;

name: &quot;sosiny-tester&quot;

services:
  db:
    image: postgres:latest
    environment:
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test1234
      - POSTGRES_DB=test_db
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - ./db/init/:/docker-entrypoint-initdb.d/
      - ./db/postgresql.conf:/etc/postgresql/postgresql.conf
    command: [&quot;postgres&quot;, &quot;-c&quot;, &quot;config_file=/etc/postgresql/postgresql.conf&quot;]
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U test&quot;]
      interval: 6s
      retries: 5

  tester:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      db:
        condition: service_healthy
    environment:
      - DATABASE_URL=postgres://test:test1234@db:5432/test_db
      - JWT_ACCESS=test_access
      - JWT_REFRESH=test_refresh
      - AWS_ACCESS_KEY=abcdefgh
      - AWS_SECRET_KEY=dkjaosdicjsoadicj
      - AWS_S3_BUCKET=test-bucket
      - AWS_REGION=ap-northeast-2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 test compose 파일인데 외부에 포트 열어두는건 필요없었지만 추후 서술할 issue들 때문에 넣어둠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 초기 설정이나 테스트 데이터베이스 url, jwt, aws 테스트 정보들을 직접 넣어둠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성해놓고 shell script를 하나 작성해서 테스트 커버리지를 계산, 실패 케이스에 대해서 정리해주도록 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;windows powershell 명령어랑 shell 명령어를 잘 몰랐기 때문에 gpt한테 일임해서 잘 안되는 부분은 다시 설명하면서 작업&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 한 결과&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnR6ic/btsJKibuLzY/p26R7vTo7GbPDxKDZL875K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnR6ic/btsJKibuLzY/p26R7vTo7GbPDxKDZL875K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnR6ic/btsJKibuLzY/p26R7vTo7GbPDxKDZL875K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnR6ic%2FbtsJKibuLzY%2Fp26R7vTo7GbPDxKDZL875K%2Fimg.png&quot; onerror=&quot;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';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;201&quot; data-origin-width=&quot;734&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 전체 테스트중에 몇 개를 성공했는지를 계산, 실패하면 해당 위치를 아래 Failure Section에서 보여주도록 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 db와 tester를 같이 도커컨테이너로 띄우고 장및빛 테스팅 자동화 여정을 마무리하려고 했음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue 1. 도커 캐시 파일이 계속 커지는 상황&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust를 최종 사용할 프로그램 빌드하게되면 Docker의 2 stage를 활용해 나오는 이미지 크기 자체는 100MB정도로 크진 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1-stage에서 주로 빌드를 통해 관련 라이브러리를 가져오는 역할을 수행하는데 캐싱을 지원안함. 근데 코드 한줄만 바뀌어도 컴파일, build를 다시 수행해야하기 때문에 이 캐싱 데이터가 계속 쌓이게 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 200GB의 용량이 하루 이틀 작업하니 사라지는 상황이 발생. 처음에는 도커 이미지 때문인가 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker system prune으로 정리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데도 용량이 크게 개선이 없어서, 도커의 캐시파일이 어디에 들어가는지 확인하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C:\Users\{username}\AppData\Local\Temp 폴더가 담당하는것을 확인. script 안에서 이 폴더를 계속 지워주도록 해서 해결.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(근데 다른 캐시 데이터도 지워지는게 문제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue 2. 컴파일 소요시간 증가, 지금 작업중이 아닌 기능도 테스팅 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust는 참 빠른 언어라고 생각하지만 컴파일속도는 겁나 느림.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소요시간도 계속 늘어나고, 나중엔 테스트 스크립트 실행해놓고 화장실다녀옴.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 테스트 데이터베이스만 띄우고 나머지 테스트는 로컬에서 수행하는 방법으로 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;local_test.sh가 그것이고, 실행할 때 특정 도메인 기능만 테스트할 수 있도록 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sh local_test.sh {테스트할 domain} 방식으로 db만 띄운 뒤 health check하여 사용 가능 여부 확인하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너로 띄우는게 아니라 cargo test domain::{기능} 으로 로컬에 이미 있는 빌드 파일들을 활용해서 테스트를 수행하도록 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고정적으로 소모되던 DB초기 셋팅하는 시간과 약간의 빌드? 시간 외 컴파일 시간은 획기적으로 줄이고 특정 단위기능에 집중해서 테스트 결과를 확인할 수 있게 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 정리하면서 알게된 내용인데 postgresql에 쿼리별 실행 횟수와 소요시간 체크도 가능하게 하는 라이브러리를 추가하여 쿼리 모니터링도 가능하게 수정 pg_stat_statements 이 그것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;7. S3 연동 Image관리, JWT&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 기능 중 하나이고 jwt 미들웨어 적용하는 것도 찾아보면 구현 예제가 많아서 다루진 않겠음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 아직 수행하지 않았지만 수행해보고 싶은 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD, 성능 테스트 (캐싱)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨날 기능구현만 하고 성능 모니터링이나 트래픽 테스트는 환경 구성하기가 쉽지 않은 듯.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 쉽지 않다기보단 이렇게 환경 구성하는게 맞나? 싶은게 많은 듯.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 해야될지 GPT 센세랑 얘기해보고 가닥이 잡히면 수행해볼 예정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저런 테스트 환경은 cloud에 띄우거나 해야될텐데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS, K8S 같은거 공부하면서 서비스 구조 구성해봐야 될 것 같은데..&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>rust</category>
      <category>공부방법</category>
      <category>커리큘럼</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/339</guid>
      <comments>https://wolfy.tistory.com/339#entry339comment</comments>
      <pubDate>Tue, 24 Sep 2024 17:31:47 +0900</pubDate>
    </item>
    <item>
      <title>&amp;amp;Axum SQLx를 활용한 데이터베이스 중급 - 성능 모니터링</title>
      <link>https://wolfy.tistory.com/338</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 성능 모니터링은 애플리케이션의 안정성과 효율성을 유지하는 데 필수적입니다. 데이터베이스의 성능 문제는 전체 시스템의 병목 현상을 초래할 수 있으므로, 적절한 모니터링을 통해 문제를 조기에 발견하고 해결하는 것이 중요합니다. 이번 글에서는 &lt;b&gt;PostgreSQL의 성능 모니터링 도구&lt;/b&gt;를 소개하고, &lt;b&gt;실시간 쿼리 모니터링 및 분석 방법&lt;/b&gt;을 살펴보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5-1. PostgreSQL의 성능 모니터링 도구 소개&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 성능 모니터링의 중요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제 조기 발견&lt;/b&gt;: 성능 저하나 오류를 빠르게 감지하여 대응할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자원 최적화&lt;/b&gt;: 시스템 자원의 효율적인 사용을 통해 비용 절감과 성능 향상을 도모합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 경험 개선&lt;/b&gt;: 빠르고 안정적인 서비스를 제공하여 사용자 만족도를 높입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. PostgreSQL의 내장 성능 모니터링 도구&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1. &lt;code&gt;pg_stat_statements&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: PostgreSQL에서 제공하는 확장 모듈로, 데이터베이스에서 실행된 모든 SQL 문에 대한 통계를 수집합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 쿼리의 실행 횟수, 총 실행 시간, 평균 실행 시간 등을 제공합니다.&lt;/li&gt;
&lt;li&gt;리소스 사용량이 많은 쿼리를 식별하여 성능 최적화에 활용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2. &lt;code&gt;pg_stat_activity&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 현재 데이터베이스에서 실행 중인 모든 세션에 대한 정보를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 ID, 사용자, 클라이언트 주소, 실행 중인 쿼리 등을 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;장시간 실행 중인 쿼리나 잠금 현상을 감지하는 데 유용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.3. &lt;code&gt;EXPLAIN&lt;/code&gt; 및 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 쿼리의 실행 계획을 분석하여 성능 병목 지점을 파악할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리가 데이터를 어떻게 접근하고 처리하는지에 대한 정보를 제공합니다.&lt;/li&gt;
&lt;li&gt;인덱스 사용 여부, 조인 방법, 예상 실행 비용 등을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 외부 성능 모니터링 도구&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1. &lt;code&gt;pgAdmin&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: PostgreSQL을 위한 무료 그래픽 관리 도구로, 성능 모니터링 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 편집기, 서버 모니터링, 통계 정보 조회 등을 지원합니다.&lt;/li&gt;
&lt;li&gt;사용자 친화적인 인터페이스로 쉽게 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2. &lt;code&gt;pgBadger&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: PostgreSQL의 로그 파일을 분석하여 HTML 형식의 성능 리포트를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;슬로우 쿼리 분석, 오류 로그 추적, 리소스 사용량 통계 등을 제공합니다.&lt;/li&gt;
&lt;li&gt;시각화된 리포트를 통해 성능 이슈를 쉽게 파악할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3. &lt;code&gt;pg_top&lt;/code&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 리눅스의 &lt;code&gt;top&lt;/code&gt; 명령어와 유사하게 PostgreSQL의 세션과 프로세스 정보를 실시간으로 모니터링합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션별 CPU, 메모리 사용량, 실행 중인 쿼리 등을 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;시스템 부하를 실시간으로 감지하고 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5-2. 실시간 쿼리 모니터링 및 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;code&gt;pg_stat_statements&lt;/code&gt; 설정 및 사용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.1. 확장 모듈 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt;는 PostgreSQL에 기본적으로 포함되어 있지만, 사용하기 위해서는 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;postgresql.conf&lt;/b&gt; 파일 수정:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;shared_preload_libraries = 'pg_stat_statements'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: PostgreSQL을 재시작할 때 &lt;code&gt;pg_stat_statements&lt;/code&gt;를 로드하도록 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2. 데이터베이스에서 확장 모듈 활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 데이터베이스에서 확장 모듈을 활성화해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE EXTENSION IF NOT EXISTS pg_stat_statements;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.3. 통계 정보 조회&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행된 쿼리의 통계 정보를 조회합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    queryid,
    query,
    calls,
    total_time,
    mean_time,
    rows
FROM
    pg_stat_statements
ORDER BY
    total_time DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 총 실행 시간이 긴 상위 10개의 쿼리를 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;queryid&lt;/code&gt;: 쿼리의 식별자입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query&lt;/code&gt;: 쿼리 텍스트입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calls&lt;/code&gt;: 쿼리 실행 횟수입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;total_time&lt;/code&gt;: 쿼리의 총 실행 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mean_time&lt;/code&gt;: 쿼리의 평균 실행 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rows&lt;/code&gt;: 반환된 행의 수입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.4. 통계 정보 초기화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통계 정보를 초기화하여 새로운 측정을 시작할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT pg_stat_statements_reset();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;code&gt;pg_stat_activity&lt;/code&gt;를 통한 세션 모니터링&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.1. 현재 세션 조회&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 데이터베이스에서 실행 중인 모든 세션을 조회합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    pid,
    usename,
    application_name,
    client_addr,
    state,
    query_start,
    query
FROM
    pg_stat_activity
WHERE
    state != 'idle';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 대기 상태가 아닌(active) 세션의 정보를 조회합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드 설명&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;pid&lt;/code&gt;: 프로세스 ID입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usename&lt;/code&gt;: 데이터베이스 사용자 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application_name&lt;/code&gt;: 애플리케이션 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_addr&lt;/code&gt;: 클라이언트 IP 주소입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;: 세션 상태입니다. (&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;idle&lt;/code&gt; 등)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query_start&lt;/code&gt;: 현재 쿼리가 시작된 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query&lt;/code&gt;: 실행 중인 쿼리 텍스트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2. 장시간 실행 중인 쿼리 감지&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    pid,
    now() - query_start AS duration,
    query
FROM
    pg_stat_activity
WHERE
    state = 'active'
    AND now() - query_start &amp;gt; interval '5 minutes';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 5분 이상 실행 중인 쿼리를 조회합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.3. 세션 종료&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 있는 세션을 강제로 종료할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE pid = [문제가 있는 pid];&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주의&lt;/b&gt;: 세션을 종료하면 해당 세션에서 진행 중인 트랜잭션이 롤백되므로, 신중하게 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 실시간 쿼리 모니터링 도구 활용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1. &lt;code&gt;pg_top&lt;/code&gt; 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설치&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;sudo apt-get install pgtop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;pg_top -U [사용자명] -d [데이터베이스명]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 실시간으로 세션 정보를 모니터링하며, CPU, 메모리 사용량 등을 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2. &lt;code&gt;pgAdmin&lt;/code&gt;의 모니터링 기능 활용&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;실시간 그래프&lt;/b&gt;: 서버의 활동을 그래프로 시각화하여 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 정보 조회&lt;/b&gt;: 현재 연결된 세션과 실행 중인 쿼리를 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통계 정보&lt;/b&gt;: 테이블, 인덱스 등의 통계 정보를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 슬로우 쿼리 로그 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.1. 슬로우 쿼리 로그 활성화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;postgresql.conf&lt;/b&gt; 파일 수정:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;log_min_duration_statement = 1000&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 1초(1000밀리초) 이상 걸리는 쿼리를 로그에 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4.2. 로그 파일 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 파일에서 슬로우 쿼리를 분석하여 성능 개선에 활용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 쿼리 성능 최적화&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.1. 인덱스 적용 여부 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 통해 인덱스 사용 여부를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM tb_book WHERE name = '가계부1';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;결과 해석&lt;/b&gt;: &lt;code&gt;Index Scan&lt;/code&gt;이 나타나면 인덱스를 사용하고 있는 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.2. 실행 계획 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 사용하여 실제 실행 시간을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE SELECT * FROM tb_book WHERE name = '가계부1';&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주의&lt;/b&gt;: 개발 환경에서만 사용하며, 운영 환경에서는 사용을 지양합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 시스템 자원 모니터링&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.1. OS 레벨 모니터링&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;top&lt;/code&gt;&lt;/b&gt;: CPU 및 메모리 사용량을 실시간으로 모니터링합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;vmstat&lt;/code&gt;&lt;/b&gt;: 메모리, 프로세스, I/O 상태를 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;iostat&lt;/code&gt;&lt;/b&gt;: 디스크 I/O 상태를 모니터링합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.2. 모니터링 도구 활용&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Prometheus &amp;amp; Grafana&lt;/b&gt;: 시간에 따른 메트릭 수집과 시각화를 통해 시스템 상태를 모니터링합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pg_prometheus&lt;/b&gt;: PostgreSQL 메트릭을 Prometheus에 통합하여 모니터링할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 성능 모니터링은 시스템의 안정성과 효율성을 유지하는 데 필수적입니다. &lt;b&gt;PostgreSQL&lt;/b&gt;은 다양한 내장 모니터링 도구를 제공하며, 이를 활용하여 실시간으로 시스템 상태를 파악하고 성능 이슈를 조기에 발견할 수 있습니다. 또한, 외부 도구와 통합하여 보다 심층적인 분석과 시각화를 통해 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 모니터링은 단순한 문제 발견을 넘어, 시스템 최적화와 사용자 경험 개선으로 이어집니다. 지속적인 모니터링과 성능 개선 노력을 통해 안정적이고 빠른 서비스를 제공하시기 바랍니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/monitoring.html&quot;&gt;PostgreSQL 공식 문서 - 모니터링&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/pgstatstatements.html&quot;&gt;pg_stat_statements 사용 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.pgadmin.org/&quot;&gt;pgAdmin 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pgbadger.darold.net/&quot;&gt;pgBadger 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wrouesnel/postgres_exporter&quot;&gt;Prometheus PostgreSQL Exporter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이 글은 학습 목적으로 작성되었으며, 실제 환경에서는 보안 및 성능에 대한 추가적인 고려가 필요합니다. 특히, 운영 환경에서의 설정 변경이나 도구 사용 시에는 사전에 충분한 검토와 테스트를 진행하시기 바랍니다.&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>rust</category>
      <category>sqlx</category>
      <category>모니터링</category>
      <category>성능</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/338</guid>
      <comments>https://wolfy.tistory.com/338#entry338comment</comments>
      <pubDate>Tue, 24 Sep 2024 13:23:08 +0900</pubDate>
    </item>
    <item>
      <title>&amp;amp;Axum SQLx를 활용한 데이터베이스 중급 - 고급 쿼리와 최적화 기법</title>
      <link>https://wolfy.tistory.com/337</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 데이터베이스를 효율적으로 사용하기 위한 &lt;b&gt;고급 쿼리 작성 및 최적화 기법&lt;/b&gt;을 다룹니다. 특히 &lt;b&gt;CTE(Common Table Expressions)&lt;/b&gt;, &lt;b&gt;인덱스 적용 방법&lt;/b&gt;, &lt;b&gt;복잡한 조인과 서브쿼리 작성&lt;/b&gt;, &lt;b&gt;쿼리 성능 분석 및 최적화 방법&lt;/b&gt;을 집중적으로 살펴보겠습니다. 이를 통해 복잡한 데이터베이스 작업을 효율적으로 처리하고, 애플리케이션의 성능을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4-1. CTE(Common Table Expressions)의 이해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CTE란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CTE(Common Table Expression)&lt;/b&gt;는 쿼리 내에서 임시 결과셋을 정의하고 이를 다른 쿼리에서 재사용할 수 있도록 하는 SQL 기능입니다. CTE는 &lt;code&gt;WITH&lt;/code&gt; 키워드를 사용하여 정의하며, 복잡한 쿼리를 읽기 쉽고 유지보수하기 쉽게 만들어줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CTE의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가독성 향상&lt;/b&gt;: 복잡한 쿼리를 작은 단위로 나누어 읽기 쉽게 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용성 증가&lt;/b&gt;: 동일한 서브쿼리를 여러 번 사용해야 할 때 중복을 피할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재귀 쿼리 지원&lt;/b&gt;: 자기 자신을 참조하는 재귀적인 쿼리를 작성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. CTE 사용 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 중복 체크 후 데이터 삽입&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 CTE를 사용하여 중복된 데이터 여부를 확인한 후, 중복이 없을 경우에만 데이터를 삽입하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;WITH DuplicateCheck AS (
    SELECT EXISTS (
        SELECT 1
        FROM tb_book AS b
        WHERE b.name = $1
    ) AS is_duplicate
),
InsertBook AS (
    INSERT INTO tb_book (name, type_id)
    SELECT $1, $2
    FROM DuplicateCheck
    WHERE is_duplicate = false
    RETURNING id
)
SELECT 
    (SELECT id FROM InsertBook) AS id,
    (SELECT is_duplicate FROM DuplicateCheck) AS is_duplicated;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설명&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DuplicateCheck&lt;/b&gt;: &lt;code&gt;tb_book&lt;/code&gt; 테이블에서 동일한 이름의 가계부가 존재하는지 확인합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;EXISTS&lt;/code&gt; 서브쿼리를 사용하여 중복 여부를 &lt;code&gt;is_duplicate&lt;/code&gt; 컬럼으로 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;InsertBook&lt;/b&gt;: 중복이 아닌 경우에만 새로운 가계부를 &lt;code&gt;tb_book&lt;/code&gt; 테이블에 삽입합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;DuplicateCheck&lt;/code&gt;의 결과에서 &lt;code&gt;is_duplicate = false&lt;/code&gt;인 경우에만 실행됩니다.&lt;/li&gt;
&lt;li&gt;삽입된 가계부의 &lt;code&gt;id&lt;/code&gt;를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최종 SELECT&lt;/b&gt;: 삽입된 가계부의 &lt;code&gt;id&lt;/code&gt;와 중복 여부를 반환합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;InsertBook&lt;/code&gt;과 &lt;code&gt;DuplicateCheck&lt;/code&gt;의 결과를 각각 추출하여 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Rust 코드와 매핑&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;#[derive(Debug, sqlx::FromRow)]
struct InsertResult {
    id: Option&amp;lt;i32&amp;gt;,
    is_duplicated: bool,
}

impl InsertResult {
    pub fn get_id(&amp;amp;self) -&amp;gt; Option&amp;lt;i32&amp;gt; {
        self.id
    }
    pub fn get_duplicated(&amp;amp;self) -&amp;gt; bool {
        self.is_duplicated
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;InsertResult&lt;/code&gt; 구조체&lt;/b&gt;: 쿼리 결과를 매핑하기 위한 구조체입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: 삽입된 가계부의 ID입니다. 중복으로 인해 삽입되지 않은 경우 &lt;code&gt;None&lt;/code&gt;이 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_duplicated&lt;/code&gt;: 중복 여부를 나타내는 불리언 값입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핸들러 함수 구현&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn create_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;CreateBook&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, AppError&amp;gt; {
    let result = sqlx::query_as::&amp;lt;_, InsertResult&amp;gt;(
        include_str!(&quot;queries/create_book.sql&quot;)
    )
    .bind(&amp;amp;payload.name)
    .bind(payload.type_id)
    .fetch_one(&amp;amp;pool)
    .await?;

    if result.get_duplicated() {
        return Err(AppError::ValidationError(&quot;이미 동일한 이름의 가계부가 존재합니다.&quot;.into()));
    }

    Ok((StatusCode::CREATED, Json(result)))
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 실행&lt;/b&gt;: &lt;code&gt;query_as&lt;/code&gt;를 사용하여 쿼리를 실행하고 &lt;code&gt;InsertResult&lt;/code&gt; 구조체로 매핑합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 처리&lt;/b&gt;: &lt;code&gt;is_duplicated&lt;/code&gt; 값이 &lt;code&gt;true&lt;/code&gt;인 경우 에러를 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성공 응답&lt;/b&gt;: 새로운 가계부의 ID를 포함하여 응답합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. UNIQUE 제약 조건과 &lt;code&gt;ON CONFLICT&lt;/code&gt; 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 처리를 위해 테이블에 UNIQUE 제약 조건을 설정하고, &lt;code&gt;ON CONFLICT&lt;/code&gt; 구문을 사용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테이블에 UNIQUE 제약 조건 추가&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE tb_book
ADD CONSTRAINT unique_book_name UNIQUE (name);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;INSERT 문에서 &lt;code&gt;ON CONFLICT&lt;/code&gt; 사용&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;INSERT INTO tb_book (name, type_id)
VALUES ($1, $2)
ON CONFLICT (name) DO NOTHING
RETURNING id;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: &lt;code&gt;name&lt;/code&gt; 컬럼에 중복이 발생하면 아무 작업도 수행하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RETURNING&lt;/b&gt;: 삽입된 행의 &lt;code&gt;id&lt;/code&gt;를 반환합니다. 중복으로 인해 삽입되지 않은 경우 &lt;code&gt;RETURNING&lt;/code&gt;은 결과를 반환하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Rust 코드에서 처리&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn create_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;CreateBook&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, AppError&amp;gt; {
    let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;INSERT INTO tb_book (name, type_id)
         VALUES ($1, $2)
         ON CONFLICT (name) DO NOTHING
         RETURNING id, name, type_id&quot;
    )
    .bind(&amp;amp;payload.name)
    .bind(payload.type_id)
    .fetch_optional(&amp;amp;pool)
    .await?;

    match rec {
        Some(book) =&amp;gt; Ok((StatusCode::CREATED, Json(book))),
        None =&amp;gt; Err(AppError::ValidationError(&quot;이미 동일한 이름의 가계부가 존재합니다.&quot;.into())),
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;fetch_optional&lt;/code&gt;&lt;/b&gt;: 결과가 없을 수 있으므로 &lt;code&gt;Option&lt;/code&gt;으로 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중복 처리&lt;/b&gt;: 결과가 &lt;code&gt;None&lt;/code&gt;인 경우 중복으로 인해 삽입되지 않은 것이므로 에러를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4-2. 인덱스의 개념과 적용 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 인덱스란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스(Index)&lt;/b&gt;는 데이터베이스에서 테이블의 데이터를 효율적으로 검색하기 위한 데이터 구조입니다. 책의 목차와 유사하게, 특정 컬럼에 대한 인덱스를 생성하면 검색 속도를 크게 향상시킬 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 인덱스의 장점과 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;검색 및 조회 속도 향상&lt;/li&gt;
&lt;li&gt;정렬 및 그룹화 작업 성능 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 삽입, 수정, 삭제 시 추가적인 작업 발생&lt;/li&gt;
&lt;li&gt;디스크 공간 추가 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인덱스 생성 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 인덱스 생성&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE INDEX idx_tb_book_name ON tb_book (name);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;idx_tb_book_name&lt;/code&gt;&lt;/b&gt;: 인덱스의 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;tb_book&lt;/code&gt;&lt;/b&gt;: 인덱스를 적용할 테이블입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;(name)&lt;/code&gt;&lt;/b&gt;: 인덱스를 생성할 컬럼입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인덱스 적용 시 고려사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조회 빈도가 높은 컬럼에 적용&lt;/b&gt;: 자주 조회하거나 조인에 사용되는 컬럼에 인덱스를 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 중복도가 낮은 컬럼 선택&lt;/b&gt;: 중복된 값이 많은 컬럼은 인덱스 효율이 떨어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과도한 인덱스 생성 지양&lt;/b&gt;: 너무 많은 인덱스는 데이터 변경 시 성능 저하를 유발합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4-3. 복잡한 조인과 서브쿼리 작성 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 조인의 종류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;INNER JOIN&lt;/b&gt;: 두 테이블에서 조인 조건을 만족하는 행만 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LEFT JOIN&lt;/b&gt;: 왼쪽 테이블의 모든 행과 오른쪽 테이블의 조인 조건을 만족하는 행을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RIGHT JOIN&lt;/b&gt;: 오른쪽 테이블의 모든 행과 왼쪽 테이블의 조인 조건을 만족하는 행을 반환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;FULL OUTER JOIN&lt;/b&gt;: 두 테이블의 모든 행을 반환하며, 조인 조건을 만족하지 않는 행은 &lt;code&gt;NULL&lt;/code&gt;로 채워집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CROSS JOIN&lt;/b&gt;: 두 테이블의 모든 조합을 반환합니다. (데카르트 곱)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CROSS JOIN 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CROSS JOIN&lt;/b&gt;은 두 테이블의 모든 행을 조합하여 반환합니다. 주의해서 사용해야 하며, 필요한 경우에만 활용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 모든 가계부와 모든 카테고리의 조합&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT b.name AS book_name, c.name AS category_name
FROM tb_book AS b
CROSS JOIN tb_category AS c;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 모든 가계부와 모든 카테고리의 조합을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의&lt;/b&gt;: 결과 행 수가 급격히 증가할 수 있으므로 필요할 때만 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 조인 사용 예시&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 가계부와 사용자 역할 정보 조인&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT b.id AS book_id, b.name AS book_name, ubr.user_id, ubr.role
FROM tb_book AS b
INNER JOIN tb_user_book_role AS ubr ON b.id = ubr.book_id
WHERE ubr.user_id = $1;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 특정 사용자가 소유하거나 참여한 가계부와 그 역할을 조회합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 서브쿼리 사용 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 특정 사용자가 소유한 가계부의 수 계산&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT u.id, u.name, (
    SELECT COUNT(*)
    FROM tb_user_book_role AS ubr
    WHERE ubr.user_id = u.id AND ubr.role = 'owner'
) AS book_count
FROM tb_user AS u;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 각 사용자별로 소유한 가계부의 수를 계산하여 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4-4. 쿼리 성능 분석과 최적화 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 쿼리 성능 분석 도구&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EXPLAIN&lt;/b&gt;: 쿼리의 예상 실행 계획을 제공합니다. 쿼리를 실행하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EXPLAIN ANALYZE&lt;/b&gt;: 쿼리를 실제로 실행하고, 실제 실행 시간과 함께 실행 계획을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. EXPLAIN 및 EXPLAIN ANALYZE 사용 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개발 환경에서만 사용&lt;/b&gt;: 성능 튜닝을 위해 개발자가 직접 실행하여 분석합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL 클라이언트 사용&lt;/b&gt;: &lt;code&gt;psql&lt;/code&gt; 등 데이터베이스 클라이언트를 통해 쿼리를 실행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;EXPLAIN과 EXPLAIN ANALYZE의 차이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EXPLAIN&lt;/b&gt;: 쿼리를 실제로 실행하지 않고, &lt;b&gt;예상되는 실행 계획&lt;/b&gt;을 보여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EXPLAIN ANALYZE&lt;/b&gt;: 쿼리를 &lt;b&gt;실제로 실행&lt;/b&gt;하고, &lt;b&gt;실제 실행 시간&lt;/b&gt;과 함께 실행 계획을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;EXPLAIN ANALYZE 사용 시 주의사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리가 실제로 실행되므로&lt;/b&gt; 데이터에 영향을 줄 수 있습니다. (예: &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; 쿼리)&lt;/li&gt;
&lt;li&gt;실행 시간이 오래 걸리거나 시스템 부하가 큰 쿼리의 경우 &lt;b&gt;성능 저하&lt;/b&gt;를 유발할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 환경에서는 사용을 지양&lt;/b&gt;하고, 필요할 경우 &lt;b&gt;개발 환경에서만&lt;/b&gt; 사용하도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 쿼리 실행 계획 확인&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EXPLAIN
SELECT b.id, b.name
FROM tb_book AS b
WHERE b.name = '가계부1';&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 실제 실행 시간 분석&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT b.id, b.name
FROM tb_book AS b
WHERE b.name = '가계부1';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Rust에서 EXPLAIN 사용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust 애플리케이션에서 &lt;code&gt;EXPLAIN&lt;/code&gt;을 사용하여 쿼리의 실행 계획을 받아올 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: Rust에서 EXPLAIN 결과 가져오기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개발 단계에서&lt;/b&gt; 쿼리의 실행 계획을 분석하기 위해 &lt;code&gt;EXPLAIN&lt;/code&gt; 또는 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 사용할 수 있습니다. 하지만 &lt;b&gt;운영 환경의 코드에서는&lt;/b&gt; &lt;code&gt;EXPLAIN&lt;/code&gt;을 사용하지 않는 것이 일반적입니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn explain_query(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;QueryPayload&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, AppError&amp;gt; {
    let rows = sqlx::query_scalar::&amp;lt;_, String&amp;gt;(
        &quot;EXPLAIN ANALYZE SELECT * FROM tb_book WHERE name = $1&quot;
    )
    .bind(&amp;amp;payload.name)
    .fetch_all(&amp;amp;pool)
    .await?;

    let explain_result = rows.join(&quot;\n&quot;);

    Ok(Json(json!({ &quot;explain&quot;: explain_result })))
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;query_scalar&lt;/code&gt;&lt;/b&gt;: 단일 컬럼의 스칼라 값을 가져올 때 사용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;fetch_all&lt;/code&gt;&lt;/b&gt;: 모든 행을 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 처리&lt;/b&gt;: &lt;code&gt;String&lt;/code&gt; 값의 벡터로 반환되므로 이를 합쳐서 전체 실행 계획을 구성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EXPLAIN 결과 예시&lt;/h4&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;{
  &quot;explain&quot;: &quot;Index Scan using idx_tb_book_name on tb_book b  (cost=0.28..8.30 rows=1 width=72)\n  Index Cond: (name = '가계부1')&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&gt;: 실행 계획을 JSON 형태로 반환하여 애플리케이션에서 활용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 실행 계획 해석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Index Scan&lt;/b&gt;: 인덱스를 사용하여 데이터를 검색합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Seq Scan&lt;/b&gt;: 전체 테이블을 순차적으로 스캔합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cost&lt;/b&gt;: 쿼리 실행 비용을 나타내며, 앞의 숫자는 시작 비용, 뒤의 숫자는 총 비용입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Rows&lt;/b&gt;: 예상 결과 행 수입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Width&lt;/b&gt;: 예상 행의 평균 바이트 크기입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 개발 환경에서 Rust를 통해 EXPLAIN ANALYZE 결과 가져오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 환경에서 쿼리의 성능을 분석하기 위해 Rust 애플리케이션에서 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 실행하고 결과를 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::extract::Extension;
use axum::Json;
use serde_json::json;
use sqlx::PgPool;
use crate::error::AppError;

#[derive(Deserialize)]
struct QueryPayload {
    query: String,
}

async fn explain_analyze_query(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;QueryPayload&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;serde_json::Value&amp;gt;, AppError&amp;gt; {
    // 개발 환경에서만 실행하도록 제한
    if cfg!(not(debug_assertions)) {
        return Err(AppError::ValidationError(
            &quot;EXPLAIN ANALYZE는 개발 환경에서만 사용할 수 있습니다.&quot;.into(),
        ));
    }

    let explain_query = format!(&quot;EXPLAIN ANALYZE {}&quot;, payload.query);

    let rows = sqlx::query_scalar::&amp;lt;_, String&amp;gt;(&amp;amp;explain_query)
        .fetch_all(&amp;amp;pool)
        .await
        .map_err(|e| AppError::DatabaseError(e.into()))?;

    let explain_result = rows.join(&quot;\n&quot;);

    Ok(Json(json!({ &quot;explain_analyze&quot;: explain_result })))
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;cfg!(not(debug_assertions))&lt;/code&gt;&lt;/b&gt;: 코드가 릴리즈 모드인지 확인하여, 개발 환경에서만 실행되도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 실행&lt;/b&gt;: 사용자가 입력한 쿼리에 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 추가하여 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안상 주의&lt;/b&gt;: 실제 애플리케이션에서는 사용자의 입력을 그대로 실행하는 것은 &lt;b&gt;SQL 인젝션&lt;/b&gt;의 위험이 있으므로, 이 예제는 학습 목적으로만 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EXPLAIN ANALYZE 결과 예시&lt;/h4&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;{
  &quot;explain_analyze&quot;: &quot;Seq Scan on tb_book  
  (cost=0.00..1.05 rows=1 width=72) 
  (actual time=0.012..0.013 rows=0 loops=1)
  Filter: (name = '가계부1')
  Rows Removed by Filter: 10
  Planning Time: 0.100 ms
  Execution Time: 0.030 ms&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;actual time&lt;/code&gt;&lt;/b&gt;: 실제 실행 시간입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;rows&lt;/code&gt;&lt;/b&gt;: 실제로 처리된 행의 수입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Execution Time&lt;/code&gt;&lt;/b&gt;: 전체 쿼리 실행 시간입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 쿼리 최적화 기법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.1. 적절한 인덱스 사용&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리에서 자주 사용되는 조건 컬럼에 인덱스를 생성합니다.&lt;/li&gt;
&lt;li&gt;복합 조건에 맞는 복합 인덱스를 활용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.2. 불필요한 데이터 조회 최소화&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 컬럼만 선택적으로 조회합니다.&lt;/li&gt;
&lt;li&gt;서브쿼리 대신 조인을 사용하여 불필요한 중첩을 피합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.3. 쿼리 재작성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 구조를 변경하여 효율적인 실행 계획을 유도합니다.&lt;/li&gt;
&lt;li&gt;CTE나 뷰를 활용하여 복잡한 로직을 단순화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 예제: 쿼리 최적화 적용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원래 쿼리&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM tb_book
WHERE name LIKE '%가계부%'
ORDER BY created_at DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;LIKE '%...%'&lt;/code&gt; 패턴은 인덱스를 사용할 수 없어 전체 테이블 스캔을 유발합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결 방법&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Full-Text Search&lt;/b&gt; 기능을 사용하여 인덱스를 활용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GIN 인덱스&lt;/b&gt;를 생성하여 검색 성능을 향상시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 확장 기능 설치 (최초 1회)
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- GIN 인덱스 생성
CREATE INDEX idx_tb_book_name_gin ON tb_book USING GIN (name gin_trgm_ops);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;개선된 쿼리&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM tb_book
WHERE name ILIKE '%가계부%'
ORDER BY created_at DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ILIKE&lt;/code&gt;와 &lt;code&gt;GIN&lt;/code&gt; 인덱스를 함께 사용하여 대소문자 구분 없는 검색을 효율적으로 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;CTE를 활용한 복잡한 쿼리 작성&lt;/b&gt;, &lt;b&gt;인덱스를 통한 성능 향상&lt;/b&gt;, &lt;b&gt;복잡한 조인과 서브쿼리 작성 방법&lt;/b&gt;, &lt;b&gt;쿼리 성능 분석과 최적화 기법&lt;/b&gt;을 살펴보았습니다. 특히, CTE를 사용하여 중복 체크 및 조건부 삽입을 구현하고, UNIQUE 제약 조건과 &lt;code&gt;ON CONFLICT&lt;/code&gt; 구문을 활용하여 중복 처리를 효율화하는 방법을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, &lt;code&gt;EXPLAIN&lt;/code&gt;과 &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;를 Rust 애플리케이션에서 활용하여 쿼리의 실행 계획을 분석하고 성능을 최적화하는 방법을 알아보았습니다. 이러한 고급 기법들을 잘 활용하면 데이터베이스 작업의 효율성을 크게 높일 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/queries-with.html&quot;&gt;PostgreSQL 공식 문서 - CTE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/indexes.html&quot;&gt;PostgreSQL 공식 문서 - 인덱스&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/sql-explain.html&quot;&gt;PostgreSQL 공식 문서 - EXPLAIN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://use-the-index-luke.com/ko/sql/&quot;&gt;SQL 튜닝 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/sqlx/&quot;&gt;Rust SQLx Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/textsearch-indexes.html&quot;&gt;PostgreSQL GIN Indexes and Full-Text Search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이 글은 학습 목적을 위해 작성되었으며, 실제 환경에서는 데이터베이스 성능 최적화를 위해 더 많은 고려 사항이 필요합니다. 테스트 환경에서 충분히 검증한 후 프로덕션 환경에 적용하시기 바랍니다.&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>CTE</category>
      <category>Index</category>
      <category>rust</category>
      <category>sqlx</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/337</guid>
      <comments>https://wolfy.tistory.com/337#entry337comment</comments>
      <pubDate>Tue, 24 Sep 2024 12:33:59 +0900</pubDate>
    </item>
    <item>
      <title>&amp;amp;Axum SQLx를 활용한 데이터베이스 중급 - 마이그레이션 (Migration)</title>
      <link>https://wolfy.tistory.com/336</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 마이그레이션은 애플리케이션 개발에서 필수적인 부분입니다. 스키마 변경, 새로운 테이블 추가, 기존 테이블 수정 등의 작업을 체계적으로 관리해야 합니다. 이 글에서는 &lt;b&gt;마이그레이션의 필요성과 개념&lt;/b&gt;을 이해하고, &lt;b&gt;&lt;code&gt;sqlx-cli&lt;/code&gt;&lt;/b&gt;를 사용하여 데이터베이스 마이그레이션을 관리하는 방법을 알아보겠습니다. 또한, 실제로 &lt;b&gt;데이터베이스 스키마 버전 관리&lt;/b&gt;를 실습해 보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-1. 마이그레이션의 필요성과 개념 이해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 마이그레이션이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마이그레이션(Migration)&lt;/b&gt;은 데이터베이스의 스키마 변경 사항을 관리하고 적용하는 과정을 말합니다. 마이그레이션을 통해 다음을 수행할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 테이블 생성&lt;/li&gt;
&lt;li&gt;기존 테이블 수정 (컬럼 추가, 삭제, 변경)&lt;/li&gt;
&lt;li&gt;인덱스, 제약 조건 추가 또는 제거&lt;/li&gt;
&lt;li&gt;데이터 변환 또는 초기 데이터 삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 마이그레이션의 필요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;버전 관리&lt;/b&gt;: 데이터베이스 스키마의 변경 이력을 추적하고 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협업 지원&lt;/b&gt;: 여러 개발자가 동시에 작업할 때 스키마 변경 사항을 공유하고 충돌을 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포 자동화&lt;/b&gt;: 개발, 테스트, 프로덕션 환경에서 일관된 스키마를 유지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복구 용이성&lt;/b&gt;: 문제 발생 시 이전 버전으로 롤백할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 수동 스크립트 실행의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 변경 사항을 수동으로 SQL 스크립트를 실행하여 관리하는 것은 다음과 같은 문제가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 이력 관리의 어려움&lt;/li&gt;
&lt;li&gt;스크립트 실행 순서의 혼동 가능성&lt;/li&gt;
&lt;li&gt;환경별로 스크립트 누락 또는 중복 실행 위험&lt;/li&gt;
&lt;li&gt;협업 시 스키마 동기화의 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-2. &lt;code&gt;sqlx-cli&lt;/code&gt;를 이용한 마이그레이션 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;code&gt;sqlx-cli&lt;/code&gt; 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;sqlx-cli&lt;/code&gt;&lt;/b&gt;는 SQLx에서 제공하는 커맨드라인 도구로, 데이터베이스 마이그레이션을 관리하고 쿼리 검증을 수행할 수 있습니다. 주요 기능은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마이그레이션 파일 생성, 적용, 롤백&lt;/li&gt;
&lt;li&gt;컴파일 시점에 쿼리 검증을 위한 데이터베이스 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. &lt;code&gt;sqlx-cli&lt;/code&gt; 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cargo&lt;/code&gt;를 사용하여 &lt;code&gt;sqlx-cli&lt;/code&gt;를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;cargo install sqlx-cli --no-default-features --features postgres&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: &lt;code&gt;--no-default-features&lt;/code&gt; 옵션을 사용하여 필요한 데이터베이스 드라이버만 포함합니다. 여기서는 PostgreSQL을 사용하므로 &lt;code&gt;--features postgres&lt;/code&gt;를 지정합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 마이그레이션 디렉토리 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 &lt;code&gt;migrations&lt;/code&gt; 디렉토리를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;mkdir migrations&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;migrations&lt;/code&gt; 디렉토리에는 마이그레이션 파일들이 위치하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 마이그레이션 파일 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx migrate add&lt;/code&gt; 명령어를 사용하여 새로운 마이그레이션 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sqlx migrate add create_books_table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령을 실행하면 &lt;code&gt;migrations&lt;/code&gt; 디렉토리에 타임스탬프가 포함된 마이그레이션 파일이 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;migrations/
├── 20231001000000_create_books_table.up.sql
├── 20231001000000_create_books_table.down.sql&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;.up.sql&lt;/code&gt;&lt;/b&gt;: 마이그레이션을 적용할 때 실행되는 SQL 스크립트&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;.down.sql&lt;/code&gt;&lt;/b&gt;: 마이그레이션을 롤백할 때 실행되는 SQL 스크립트&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 마이그레이션 파일 작성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;업그레이드 스크립트 (&lt;code&gt;.up.sql&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- migrations/20231001000000_create_books_table.up.sql

CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    author VARCHAR(100) NOT NULL,
    published_date DATE
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다운그레이드 스크립트 (&lt;code&gt;.down.sql&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- migrations/20231001000000_create_books_table.down.sql

DROP TABLE IF EXISTS books;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 마이그레이션 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 URL을 환경 변수로 설정합니다. &lt;code&gt;.env&lt;/code&gt; 파일에 이미 설정되어 있다면 다음 명령어로 마이그레이션을 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sqlx migrate run&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Executing migration 20231001000000_create_books_table&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션이 성공적으로 적용되면 &lt;code&gt;books&lt;/code&gt; 테이블이 생성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 마이그레이션 상태 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 마이그레이션 상태를 확인하려면 다음 명령어를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;sqlx migrate info&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Migration                                 Applied At                  Duration
-------------------------------------------------------------------------------
20231001000000_create_books_table         2023-10-01 12:00:00         50.123ms&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3-3. 데이터베이스 스키마 버전 관리 실습&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 새로운 마이그레이션 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;books&lt;/code&gt; 테이블에 새로운 컬럼을 추가해 보겠습니다. 예를 들어, &lt;code&gt;description&lt;/code&gt; 컬럼을 추가한다고 가정합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sqlx migrate add add_description_to_books&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 마이그레이션 파일:&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;migrations/
├── 20231001000000_create_books_table.up.sql
├── 20231001000000_create_books_table.down.sql
├── 20231002000000_add_description_to_books.up.sql
├── 20231002000000_add_description_to_books.down.sql&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 마이그레이션 파일 작성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;업그레이드 스크립트 (&lt;code&gt;.up.sql&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- migrations/20231002000000_add_description_to_books.up.sql

ALTER TABLE books
ADD COLUMN description TEXT;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다운그레이드 스크립트 (&lt;code&gt;.down.sql&lt;/code&gt;)&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- migrations/20231002000000_add_description_to_books.down.sql

ALTER TABLE books
DROP COLUMN IF EXISTS description;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 마이그레이션 적용&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sqlx migrate run&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Executing migration 20231002000000_add_description_to_books&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;books&lt;/code&gt; 테이블에 &lt;code&gt;description&lt;/code&gt; 컬럼이 추가되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 롤백 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션을 롤백하여 이전 상태로 되돌릴 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;sqlx migrate revert&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Reverting migration 20231002000000_add_description_to_books&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한 번 &lt;code&gt;sqlx migrate revert&lt;/code&gt;를 실행하면 최초 마이그레이션도 롤백됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 특정 버전으로 마이그레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx migrate run&lt;/code&gt;은 모든 마이그레이션을 순서대로 적용합니다. 특정 버전까지 마이그레이션을 적용하거나 롤백하려면 마이그레이션 파일을 조정하거나 수동으로 관리해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 마이그레이션과 코드의 동기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션을 적용한 후, 코드에서도 스키마 변경 사항을 반영해야 합니다. 예를 들어, &lt;code&gt;Book&lt;/code&gt; 구조체에 &lt;code&gt;description&lt;/code&gt; 필드를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;#[derive(Debug, Serialize, Deserialize)]
struct Book {
    id: i32,
    title: String,
    author: String,
    published_date: Option&amp;lt;NaiveDate&amp;gt;,
    description: Option&amp;lt;String&amp;gt;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고&lt;/b&gt;: &lt;code&gt;chrono&lt;/code&gt; 크레이트를 사용하여 날짜 타입을 처리할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/b&gt;에 &lt;code&gt;chrono&lt;/code&gt; 의존성 추가:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
chrono = { version = &quot;0.4&quot;, features = [&quot;serde&quot;] }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 데이터베이스 변경 사항 적용 후 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 다시 빌드하고 실행하여 변경 사항이 정상적으로 적용되었는지 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;cargo run&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 실습: 마이그레이션 충돌 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 마이그레이션 충돌 상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개발자가 동시에 마이그레이션을 생성하고 적용하면 충돌이 발생할 수 있습니다. 이를 방지하기 위해 마이그레이션 파일을 생성할 때 항상 최신 상태를 유지하고, 타임스탬프를 기반으로 마이그레이션을 관리합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 마이그레이션 파일의 버전 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 등의 버전 관리 시스템을 사용하여 마이그레이션 파일을 관리합니다. 마이그레이션 파일을 커밋하고 푸시하여 팀원들과 공유합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 팀 협업 시 주의사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마이그레이션을 생성하기 전에 항상 최신 코드를 풀(Pull) 받습니다.&lt;/li&gt;
&lt;li&gt;마이그레이션 파일의 순서를 유지하기 위해 타임스탬프를 기반으로 파일명을 지정합니다.&lt;/li&gt;
&lt;li&gt;충돌이 발생한 경우 마이그레이션 파일을 병합하고 순서를 조정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;데이터베이스 마이그레이션의 필요성과 개념&lt;/b&gt;을 이해하고, &lt;b&gt;&lt;code&gt;sqlx-cli&lt;/code&gt;&lt;/b&gt;를 사용하여 마이그레이션을 관리하는 방법을 배웠습니다. 마이그레이션을 통해 데이터베이스 스키마 변경 사항을 체계적으로 관리하고, 팀 협업 시 스키마 동기화를 원활하게 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션은 애플리케이션 개발에서 매우 중요한 부분이며, 이를 효율적으로 관리하면 개발 생산성을 높일 수 있습니다. 앞으로의 개발에서는 마이그레이션을 적극적으로 활용하여 데이터베이스 스키마를 관리해 보세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/sqlx/&quot;&gt;SQLx 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/launchbadge/sqlx&quot;&gt;SQLx GitHub 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/&quot;&gt;PostgreSQL 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/chrono/&quot;&gt;Chrono 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rust-lang-nursery.github.io/rust-cookbook/database.html&quot;&gt;Rust 데이터베이스 개발 가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의&lt;/b&gt;: 이 글은 학습 목적으로 작성되었으며, 실제 애플리케이션 개발 시 보안, 성능, 에러 처리 등에 대한 추가 고려가 필요합니다.&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>migration</category>
      <category>rust</category>
      <category>sqlx</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/336</guid>
      <comments>https://wolfy.tistory.com/336#entry336comment</comments>
      <pubDate>Tue, 24 Sep 2024 11:42:47 +0900</pubDate>
    </item>
    <item>
      <title>&amp;amp;Axum SQLx를 활용한 데이터베이스 중급 - 데이터베이스 연동</title>
      <link>https://wolfy.tistory.com/335</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;b&gt;SQLx&lt;/b&gt;를 활용하여 Rust 애플리케이션과 PostgreSQL 데이터베이스를 연동하는 방법을 알아보겠습니다. SQLx는 Rust에서 비동기적으로 SQL 데이터베이스에 접근할 수 있도록 도와주는 강력한 크레이트입니다. 이전 글에서 Docker를 활용하여 PostgreSQL 환경을 설정했으므로, 이제 실제로 데이터베이스에 연결하고 데이터를 조작해 보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-1. SQLx 소개 및 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SQLx란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQLx&lt;/b&gt;는 Rust에서 비동기 SQL 데이터베이스 작업을 수행할 수 있게 해주는 크레이트입니다. 주요 특징은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기 지원&lt;/b&gt;: Tokio와 같은 비동기 런타임과 함께 사용하여 비동기 데이터베이스 작업을 수행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커넥션 풀링&lt;/b&gt;: 효율적인 데이터베이스 연결 관리를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안전한 SQL 쿼리&lt;/b&gt;: 컴파일 시점에 SQL 문법과 타입을 체크하는 매크로를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다중 데이터베이스 지원&lt;/b&gt;: PostgreSQL, MySQL, SQLite 등 여러 데이터베이스를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프로젝트에 SQLx 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/b&gt; 파일에 SQLx와 관련된 의존성을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
sqlx = { version = &quot;0.6&quot;, features = [ &quot;runtime-tokio-native-tls&quot;, &quot;postgres&quot; ] }
dotenv = &quot;0.15&quot;
tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }
serde = { version = &quot;1.0&quot;, features = [&quot;derive&quot;] }
serde_json = &quot;1.0&quot;
axum = &quot;0.6&quot;
thiserror = &quot;1.0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성 설명&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;sqlx&lt;/b&gt;: SQL 데이터베이스에 접근하기 위한 주요 크레이트입니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;runtime-tokio-native-tls&lt;/b&gt;: Tokio 런타임과 native-tls를 사용하기 위한 기능입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;postgres&lt;/b&gt;: PostgreSQL을 사용하기 위한 기능입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dotenv&lt;/b&gt;: &lt;code&gt;.env&lt;/code&gt; 파일에서 환경 변수를 로드하기 위한 크레이트입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tokio&lt;/b&gt;: 비동기 런타임입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;serde&lt;/b&gt;, &lt;b&gt;serde_json&lt;/b&gt;: JSON 직렬화 및 역직렬화를 위한 크레이트입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;axum&lt;/b&gt;: 웹 애플리케이션 프레임워크입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;thiserror&lt;/b&gt;: 에러 핸들링을 위한 크레이트입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Note:&lt;/b&gt; &lt;code&gt;sqlx&lt;/code&gt;의 &lt;code&gt;macros&lt;/code&gt; 피처는 컴파일 시점에 쿼리 검증을 위한 매크로를 사용하기 위해 필요하지만, 매크로를 사용하지 않을 경우 피처를 제거해도 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. sqlx-cli 설치 (선택 사항)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx-cli&lt;/code&gt;는 SQLx에서 제공하는 커맨드라인 도구로, 컴파일 시점에 쿼리를 검증하는 데 사용됩니다. 매크로를 사용하지 않을 경우 필수는 아닙니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;cargo install sqlx-cli --no-default-features --features postgres,runtime-tokio-native-tls&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-2. 데이터베이스 연결 설정 및 환경 변수 관리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 환경 변수 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 데이터베이스 연결 정보를 안전하게 관리하기 위해 환경 변수를 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;code&gt;.env&lt;/code&gt; 파일 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 &lt;code&gt;.env&lt;/code&gt; 파일을 생성하고 다음 내용을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;DATABASE_URL=postgres://test:test1234@localhost:5432/test_db&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;DATABASE_URL&lt;/code&gt;&lt;/b&gt;: 데이터베이스 연결 문자열로, 다음과 같은 형식입니다.
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;postgres://username:password@host:port/database_name&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 환경 변수 로드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dotenv&lt;/code&gt; 크레이트를 사용하여 애플리케이션에서 환경 변수를 로드합니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use dotenv::dotenv;
use std::env;

fn main() {
    // .env 파일 로드
    dotenv().ok();

    // 환경 변수 가져오기
    let database_url = env::var(&quot;DATABASE_URL&quot;).expect(&quot;DATABASE_URL must be set&quot;);

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 데이터베이스 연결 풀 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx::PgPool&lt;/code&gt;을 사용하여 PostgreSQL에 대한 연결 풀을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use sqlx::postgres::PgPoolOptions;

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), sqlx::Error&amp;gt; {
    dotenv().ok();
    let database_url = env::var(&quot;DATABASE_URL&quot;).expect(&quot;DATABASE_URL must be set&quot;);

    // 연결 풀 생성
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&amp;amp;database_url)
        .await?;

    // ...
    Ok(())
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;max_connections&lt;/code&gt;&lt;/b&gt;: 연결 풀에서 유지할 최대 연결 수를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-3. 간단한 CRUD 구현 (Book 엔티티 활용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;Book&lt;/code&gt; 엔티티를 활용하여 데이터베이스에 CRUD(Create, Read, Update, Delete) 기능을 구현해 보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 데이터베이스 테이블 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;code&gt;books&lt;/code&gt; 테이블을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1-1. 마이그레이션 방식 (sqlx-cli 필요)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마이그레이션 파일 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;migrations&lt;/code&gt; 디렉토리를 생성하고, &lt;code&gt;20230101000000_create_books_table.sql&lt;/code&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    author VARCHAR(100) NOT NULL
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;마이그레이션 적용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx-cli&lt;/code&gt;를 설치했다면 다음 명령어로 마이그레이션을 적용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;sqlx migrate run&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 애플리케이션 코드에서 실행 시 마이그레이션을 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1-2.  DB 생성 시 테이블 초기화 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 데이터베이스를 띄울 때 init.db 파일을 활용해 데이터베이스 생성 시점에서 테이블 초기화를 진행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 모델 정의&lt;/h3&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Book {
    id: i32,
    title: String,
    author: String,
}

#[derive(Debug, Deserialize)]
struct CreateBook {
    title: String,
    author: String,
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 핸들러 함수 구현&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1. Book 생성 (Create)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{Extension, Json};
use axum::response::IntoResponse;
use sqlx::PgPool;
use hyper::StatusCode;

async fn create_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;CreateBook&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, impl IntoResponse&amp;gt; {
    let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id, title, author&quot;
    )
    .bind(&amp;amp;payload.title)
    .bind(&amp;amp;payload.author)
    .fetch_one(&amp;amp;pool)
    .await
    .map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!(&quot;데이터베이스 오류: {}&quot;, e),
        )
    })?;

    Ok((StatusCode::CREATED, Json(rec)))
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2. 모든 Book 조회 (Read)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn get_books(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, impl IntoResponse&amp;gt; {
    let recs = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;SELECT id, title, author FROM books&quot;
    )
    .fetch_all(&amp;amp;pool)
    .await
    .map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!(&quot;데이터베이스 오류: {}&quot;, e),
        )
    })?;

    Ok(Json(recs))
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3. 특정 Book 조회&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::extract::Path;

async fn get_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Path(id): Path&amp;lt;i32&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, impl IntoResponse&amp;gt; {
    let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;SELECT id, title, author FROM books WHERE id = $1&quot;
    )
    .bind(id)
    .fetch_one(&amp;amp;pool)
    .await
    .map_err(|e| {
        (
            StatusCode::NOT_FOUND,
            format!(&quot;책을 찾을 수 없습니다: {}&quot;, e),
        )
    })?;

    Ok(Json(rec))
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.4. Book 업데이트 (Update)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn update_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Path(id): Path&amp;lt;i32&amp;gt;,
    Json(payload): Json&amp;lt;CreateBook&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, impl IntoResponse&amp;gt; {
    let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;UPDATE books SET title = $1, author = $2 WHERE id = $3 RETURNING id, title, author&quot;
    )
    .bind(&amp;amp;payload.title)
    .bind(&amp;amp;payload.author)
    .bind(id)
    .fetch_one(&amp;amp;pool)
    .await
    .map_err(|e| {
        (
            StatusCode::NOT_FOUND,
            format!(&quot;책을 업데이트할 수 없습니다: {}&quot;, e),
        )
    })?;

    Ok(Json(rec))
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.5. Book 삭제 (Delete)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn delete_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Path(id): Path&amp;lt;i32&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, impl IntoResponse&amp;gt; {
    let result = sqlx::query(
        &quot;DELETE FROM books WHERE id = $1&quot;
    )
    .bind(id)
    .execute(&amp;amp;pool)
    .await
    .map_err(|e| {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!(&quot;책을 삭제할 수 없습니다: {}&quot;, e),
        )
    })?;

    if result.rows_affected() == 0 {
        return Err((
            StatusCode::NOT_FOUND,
            format!(&quot;책을 찾을 수 없습니다.&quot;),
        ));
    }

    Ok(StatusCode::NO_CONTENT)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 라우터 설정&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{routing::get, routing::post, routing::put, routing::delete, Router};

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), sqlx::Error&amp;gt; {
    dotenv().ok();
    let database_url = env::var(&quot;DATABASE_URL&quot;).expect(&quot;DATABASE_URL must be set&quot;);

    // 연결 풀 생성
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&amp;amp;database_url)
        .await?;

    let app = Router::new()
        .route(&quot;/books&quot;, get(get_books).post(create_book))
        .route(&quot;/books/:id&quot;, get(get_book).put(update_book).delete(delete_book))
        .layer(Extension(pool));

    println!(&quot;서버가 0.0.0.0:3000에서 시작됩니다...&quot;);
    axum::Server::bind(&amp;amp;&quot;0.0.0.0:3000&quot;.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();

    Ok(())
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 매크로 사용에 대한 고려&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매크로를 사용하면 컴파일 시점에 SQL 문법과 타입을 검증할 수 있어 런타임 오류를 줄일 수 있습니다. 그러나 매크로를 사용하지 않아도 &lt;code&gt;query_as::&amp;lt;_, ReturnType&amp;gt;()&lt;/code&gt; 패턴으로 타입 안정성을 확보할 수 있습니다. 매크로를 사용하지 않는 경우에도 다음과 같은 이점이 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드의 명확성&lt;/b&gt;: 매크로 없이 일반 함수 호출로 쿼리를 작성하면 코드가 더 명확해질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유연성&lt;/b&gt;: 런타임에 동적으로 쿼리를 생성해야 하는 경우 매크로를 사용하지 않는 것이 더 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-4. SQLx의 쿼리 빌더와 매핑 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 매크로 없이 쿼리 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sqlx&lt;/code&gt;의 매크로를 사용하지 않고도 &lt;code&gt;query_as::&amp;lt;_, ReturnType&amp;gt;()&lt;/code&gt; 함수를 사용하여 쿼리를 작성하고 결과를 구조체로 매핑할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
    &quot;SELECT id, title, author FROM books WHERE id = $1&quot;
)
.bind(id)
.fetch_one(&amp;amp;pool)
.await?;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;query_as::&amp;lt;_, Book&amp;gt;()&lt;/code&gt;에서 첫 번째 타입 매개변수는 일반적으로 &lt;code&gt;_&lt;/code&gt;로 지정하여 생략할 수 있습니다.&lt;/li&gt;
&lt;li&gt;반환되는 결과를 &lt;code&gt;Book&lt;/code&gt; 구조체로 매핑합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿼리 파라미터 바인딩&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 내에서 &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt; 등의 플레이스홀더를 사용하고, &lt;code&gt;bind()&lt;/code&gt; 메서드를 통해 값을 바인딩합니다.&lt;/li&gt;
&lt;li&gt;이를 통해 SQL 인젝션 공격을 방지할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;sqlx::query(
    &quot;INSERT INTO books (title, author) VALUES ($1, $2)&quot;
)
.bind(&amp;amp;payload.title)
.bind(&amp;amp;payload.author)
.execute(&amp;amp;pool)
.await?;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 타입 매핑과 오류 방지&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매크로를 사용하지 않으면 컴파일 시점에 쿼리 검증은 불가능하지만, 런타임에 타입 불일치로 인한 오류가 발생할 수 있습니다.&lt;/li&gt;
&lt;li&gt;이를 방지하기 위해 쿼리 결과와 구조체 필드의 타입이 일치하는지 주의해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-5. 트랜잭션 처리 및 에러 핸들링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 트랜잭션 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 데이터베이스 작업의 원자성을 보장합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 시작&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;let mut tx = pool.begin().await?;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 내에서 작업 수행&lt;/h4&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;sqlx::query(&quot;INSERT INTO books (title, author) VALUES ($1, $2)&quot;)
    .bind(&amp;amp;book.title)
    .bind(&amp;amp;book.author)
    .execute(&amp;amp;mut tx)
    .await?;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 커밋&lt;/h4&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;tx.commit().await?;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 롤백&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생하면 트랜잭션을 롤백합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;tx.rollback().await?;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 에러 핸들링&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러 타입 정의&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;thiserror&lt;/code&gt; 크레이트를 사용하여 커스텀 에러 타입을 정의할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error(&quot;데이터베이스 오류: {0}&quot;)]
    DatabaseError(#[from] sqlx::Error),
    #[error(&quot;입력 오류: {0}&quot;)]
    ValidationError(String),
    #[error(&quot;내부 서버 오류&quot;)]
    InternalServerError,
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러를 핸들러 함수에서 반환&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn create_book(
    Extension(pool): Extension&amp;lt;PgPool&amp;gt;,
    Json(payload): Json&amp;lt;CreateBook&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, AppError&amp;gt; {
    if payload.title.is_empty() || payload.author.is_empty() {
        return Err(AppError::ValidationError(&quot;제목과 저자는 필수 항목입니다.&quot;.into()));
    }

    let rec = sqlx::query_as::&amp;lt;_, Book&amp;gt;(
        &quot;INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id, title, author&quot;
    )
    .bind(&amp;amp;payload.title)
    .bind(&amp;amp;payload.author)
    .fetch_one(&amp;amp;pool)
    .await?;

    Ok((StatusCode::CREATED, Json(rec)))
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;에러 응답 커스터마이징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;IntoResponse&lt;/code&gt;를 구현하여 에러에 대한 HTTP 응답을 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;

impl IntoResponse for AppError {
    fn into_response(self) -&amp;gt; Response {
        let status = match self {
            AppError::DatabaseError(_) =&amp;gt; StatusCode::INTERNAL_SERVER_ERROR,
            AppError::ValidationError(_) =&amp;gt; StatusCode::BAD_REQUEST,
            AppError::InternalServerError =&amp;gt; StatusCode::INTERNAL_SERVER_ERROR,
        };
        let body = Json(json!({ &quot;error&quot;: self.to_string() }));
        (status, body).into_response()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 SQLx를 활용하여 Rust 애플리케이션과 PostgreSQL 데이터베이스를 연동하는 방법을 살펴보았습니다. 환경 변수 관리부터 데이터베이스 연결, CRUD 구현, 비동기 코드 작성, 쿼리 매핑, 트랜잭션 처리, 에러 핸들링까지 전반적인 내용을 다루었습니다. 특히 매크로를 사용하지 않고도 &lt;code&gt;query_as::&amp;lt;_, ReturnType&amp;gt;()&lt;/code&gt; 패턴을 통해 타입 안정성을 확보하고 쿼리를 실행하는 방법을 배웠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/sqlx/&quot;&gt;SQLx 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/axum/&quot;&gt;Axum 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/&quot;&gt;Tokio 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/crates/dotenv&quot;&gt;dotenv 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/crates/thiserror&quot;&gt;thiserror 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; 이 글은 학습 목적으로 작성되었으며, 실제 애플리케이션 개발 시 보안, 성능, 에러 처리 등에 대한 추가 고려가 필요합니다.&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>rust</category>
      <category>sqlx</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/335</guid>
      <comments>https://wolfy.tistory.com/335#entry335comment</comments>
      <pubDate>Tue, 24 Sep 2024 11:21:39 +0900</pubDate>
    </item>
    <item>
      <title>&amp;amp;Axum SQLx를 활용한 데이터베이스 중급 - Docker를 활용한 PostgreSQL 환경 설정</title>
      <link>https://wolfy.tistory.com/334</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 &lt;b&gt;Docker&lt;/b&gt;와 &lt;b&gt;Docker Compose&lt;/b&gt;를 활용하여 PostgreSQL 데이터베이스를 설정하는 방법을 알아보겠습니다. Docker를 사용하면 로컬 환경에 직접 설치하지 않고도 데이터베이스를 컨테이너로 관리할 수 있어 개발 환경 설정이 간편해지고 일관성을 유지할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Docker와 Docker Compose 소개 및 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Docker 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker&lt;/b&gt;는 애플리케이션을 컨테이너로 패키징하여 배포하고 실행할 수 있는 플랫폼입니다. 컨테이너는 애플리케이션과 그 실행 환경을 함께 묶어 일관된 환경에서 실행할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 Docker 설치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제별 Docker 설치 방법은 다음과 같습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Windows&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker Desktop for Windows&lt;/b&gt;를 설치합니다.&lt;/li&gt;
&lt;li&gt;설치 링크: &lt;a href=&quot;https://docs.docker.com/desktop/windows/install/&quot;&gt;Docker Desktop for Windows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;설치 시 WSL 2(Windows Subsystem for Linux) 기능을 활성화해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;macOS&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker Desktop for Mac&lt;/b&gt;을 설치합니다.&lt;/li&gt;
&lt;li&gt;설치 링크: &lt;a href=&quot;https://docs.docker.com/desktop/mac/install/&quot;&gt;Docker Desktop for Mac&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Linux&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패키지 매니저를 통해 Docker Engine을 설치합니다.&lt;/li&gt;
&lt;li&gt;예시(Ubuntu):&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get update
sudo apt-get install -y docker.io&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;자세한 설치 가이드: &lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker Engine 설치&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 Docker Compose 소개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Docker Compose&lt;/b&gt;는 여러 개의 컨테이너로 구성된 애플리케이션을 정의하고 실행할 수 있는 도구입니다. 단일 YAML 파일로 서비스 구성을 정의하고, 한 번의 명령어로 모든 서비스를 실행할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 Docker Compose 설치&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Docker Desktop&lt;/b&gt;을 설치하면 Docker Compose가 기본적으로 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;Linux에서는 별도로 설치해야 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo curl -L &quot;https://github.com/docker/compose/releases/download/v2.0.1/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;설치 확인:&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose --version&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Docker Compose로 PostgreSQL 설정 및 실행&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 프로젝트 디렉토리 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트를 위한 디렉토리를 생성하고 이동합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;mkdir docker-postgres-setup
cd docker-postgres-setup&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 &lt;code&gt;docker-compose.yml&lt;/code&gt; 파일 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 디렉토리에 &lt;code&gt;docker-compose.yml&lt;/code&gt; 파일을 생성하고 다음 내용을 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;version: &quot;3&quot;

services:
  db:
    image: postgres:latest
    environment:
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test1234
      - POSTGRES_DB=test_db
    ports:
      - &quot;5432:5432&quot;
    volumes:
      - ./db/init/:/docker-entrypoint-initdb.d/
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U test&quot;]
      interval: 5s
      retries: 5&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 설명&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;image&lt;/code&gt;&lt;/b&gt;: 사용할 PostgreSQL Docker 이미지를 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;environment&lt;/code&gt;&lt;/b&gt;: 컨테이너 내부에서 사용할 환경 변수를 설정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_USER&lt;/code&gt;: PostgreSQL 사용자 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;: 해당 사용자의 비밀번호&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_DB&lt;/code&gt;: 초기 생성할 데이터베이스 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/b&gt;: 호스트와 컨테이너의 포트를 매핑합니다. (&lt;code&gt;호스트 포트:컨테이너 포트&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;volumes&lt;/code&gt;&lt;/b&gt;: 호스트의 디렉토리를 컨테이너 내부에 마운트합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;./db/init/&lt;/code&gt; 디렉토리에 있는 스크립트를 컨테이너의 &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;에 마운트하여 컨테이너 시작 시 실행되도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;healthcheck&lt;/code&gt;&lt;/b&gt;: 컨테이너의 상태를 확인하는 방법을 정의합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;pg_isready&lt;/code&gt; 명령어를 사용하여 PostgreSQL이 준비되었는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 초기화 스크립트 작성 (선택 사항)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 데이터베이스 설정이나 테이블 생성을 자동화하려면 &lt;code&gt;./db/init/&lt;/code&gt; 디렉토리에 SQL 스크립트를 작성합니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;mkdir -p db/init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;db/init/init.sql&lt;/code&gt;&lt;/b&gt; 파일을 생성하고 다음 내용을 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 초기 테이블 생성 예시
CREATE TABLE IF NOT EXISTS test_table (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50) NOT NULL
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 Docker Compose로 PostgreSQL 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 다음 명령어를 실행하여 컨테이너를 시작합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;docker-compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; 옵션은 백그라운드에서 컨테이너를 실행합니다.&lt;/li&gt;
&lt;li&gt;컨테이너가 정상적으로 실행되었는지 확인하려면&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose ps&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터베이스 접속 및 기본 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 psql 클라이언트를 사용하여 접속&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 컨테이너에 접속하여 데이터베이스에 접근합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;docker-compose exec db psql -U test -d test_db&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;-U&lt;/code&gt;: 사용자 이름 지정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;: 데이터베이스 이름 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 데이터베이스 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;psql&lt;/b&gt; 프롬프트에서 다음 명령어를 입력하여 데이터베이스와 테이블을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;\l -- 데이터베이스 목록 확인
\dt -- 테이블 목록 확인&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;test_db=# \l
                                   List of databases
   Name    | Owner | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+-------+----------+-------------+-------------+-----------------------
 test_db   | test  | UTF8     | en_US.utf8  | en_US.utf8  |
 postgres  | test  | UTF8     | en_US.utf8  | en_US.utf8  |
 template0 | test  | UTF8     | en_US.utf8  | en_US.utf8  | =c/test              +
           |       |          |             |             | test=CTc/test
 template1 | test  | UTF8     | en_US.utf8  | en_US.utf8  | =c/test              +
           |       |          |             |             | test=CTc/test
(4 rows)

test_db=# \dt
          List of relations
 Schema |   Name    | Type  | Owner
--------+-----------+-------+-------
 public | test_table | table | test
(1 row)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 간단한 쿼리 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테이블 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM test_table;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 초기화 스크립트에서 데이터를 삽입했다면 결과가 나타납니다. 그렇지 않다면 데이터를 삽입해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 삽입&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;INSERT INTO test_table (name) VALUES ('Alice'), ('Bob');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM test_table;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력 예시:&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; id | name
----+-------
  1 | Alice
  2 | Bob
(2 rows)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 종료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;\q&lt;/code&gt; 명령어를 입력하여 &lt;b&gt;psql&lt;/b&gt;을 종료합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 애플리케이션에서의 접속 정보 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션(Rust 등)에서 데이터베이스에 접속하기 위해서는 접속 정보를 설정해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 환경 변수 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 민감한 정보를 관리하기 위해 환경 변수를 사용합니다. &lt;code&gt;.env&lt;/code&gt; 파일을 생성하고 다음과 같이 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;DATABASE_URL=postgres://test:test1234@localhost:5432/test_db&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Rust 애플리케이션에서의 사용 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/b&gt;에 &lt;code&gt;dotenvy&lt;/code&gt;와 &lt;code&gt;sqlx&lt;/code&gt; 의존성을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
dotenvy = &quot;0.15&quot;
sqlx = { version = &quot;0.6&quot;, features = [&quot;postgres&quot;, &quot;runtime-tokio-native-tls&quot;, &quot;macros&quot;] }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;main.rs&lt;/code&gt;&lt;/b&gt; 파일에서 환경 변수를 로드하고 데이터베이스에 연결합니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use sqlx::postgres::PgPoolOptions;
use std::env;
use dotenvy::dotenv;

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), sqlx::Error&amp;gt; {
    // .env 파일 로드
    dotenv().ok();

    // 환경 변수에서 데이터베이스 URL 가져오기
    let database_url = env::var(&quot;DATABASE_URL&quot;).expect(&quot;DATABASE_URL must be set&quot;);

    // 데이터베이스 풀 생성
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect(&amp;amp;database_url)
        .await?;

    // 간단한 쿼리 실행 예시
    let row: (i64,) = sqlx::query_as(&quot;SELECT 1&quot;)
        .fetch_one(&amp;amp;pool)
        .await?;

    println!(&quot;쿼리 결과: {}&quot;, row.0);

    Ok(())
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker와 Docker Compose를 활용하면 PostgreSQL 데이터베이스를 손쉽게 설정하고 관리할 수 있습니다. 이를 통해 개발 환경을 일관되게 유지하고, 팀원들과 동일한 환경에서 개발할 수 있습니다. 또한 Docker를 사용하면 시스템에 직접 데이터베이스를 설치하지 않아도 되므로 환경 간 충돌을 방지할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/&quot;&gt;Docker 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://hub.docker.com/_/postgres&quot;&gt;PostgreSQL Docker 이미지&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/app-psql.html&quot;&gt;psql 사용법&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/sqlx/&quot;&gt;SQLx 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/crates/dotenvy&quot;&gt;dotenvy 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; 이 가이드는 학습 목적을 위해 작성되었으며, 실제 환경에서는 보안 및 구성에 대한 추가 고려 사항이 필요할 수 있습니다.&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>docker</category>
      <category>PostgreSQL</category>
      <category>rust</category>
      <category>sqlx</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/334</guid>
      <comments>https://wolfy.tistory.com/334#entry334comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:56:24 +0900</pubDate>
    </item>
    <item>
      <title>Rust와 Axum을 활용한 웹 백엔드 개발 - 간단한 가계부 웹 API</title>
      <link>https://wolfy.tistory.com/333</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 섹션에서는 Rust와 Axum을 활용하여 간단한 가계부 웹 애플리케이션을 구축합니다. 프로젝트 요구사항을 분석하고, CRUD 엔드포인트를 구현하며, 요청 파라미터와 경로 변수를 처리하는 방법을 배워보겠습니다. 또한 JSON 데이터의 직렬화 및 역직렬화, 에러 처리와 커스텀 에러 응답까지 다루어 실제 서비스 개발에 필요한 기본기를 익힙니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-1. 프로젝트 요구사항 분석 및 설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;목적&lt;/b&gt;: 간단한 가계부 애플리케이션을 구축하여 수입과 지출을 관리할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가계부(Book)&lt;/b&gt; 생성, 조회, 수정, 삭제 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수입/지출 기록(Record)&lt;/b&gt; 추가, 조회, 수정, 삭제 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카테고리(Category)&lt;/b&gt; 관리 (선택 사항)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 모델 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 하기 위해 데이터 모델을 다음과 같이 설계합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테이블 목록&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;tb_book&lt;/b&gt;: 가계부 정보를 저장합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;tb_record&lt;/b&gt;: 수입/지출 기록을 저장합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테이블 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. tb_book&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;height: 56px;&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;컬럼 이름&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;데이터 타입&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;제약 조건&lt;/th&gt;
&lt;th style=&quot;height: 20px;&quot;&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;id&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;SERIAL&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;PRIMARY KEY&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;가계부 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;name&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;VARCHAR(50)&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;NOT NULL&lt;/td&gt;
&lt;td style=&quot;height: 18px;&quot;&gt;가계부 이름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. tb_record&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;컬럼 이름&lt;/th&gt;
&lt;th&gt;데이터 타입&lt;/th&gt;
&lt;th&gt;제약 조건&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;SERIAL&lt;/td&gt;
&lt;td&gt;PRIMARY KEY&lt;/td&gt;
&lt;td&gt;기록 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;book_id&lt;/td&gt;
&lt;td&gt;INTEGER&lt;/td&gt;
&lt;td&gt;FOREIGN KEY(tb_book.id)&lt;/td&gt;
&lt;td&gt;가계부 ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;amount&lt;/td&gt;
&lt;td&gt;INTEGER&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;금액&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memo&lt;/td&gt;
&lt;td&gt;VARCHAR(32)&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;메모&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;target_dt&lt;/td&gt;
&lt;td&gt;DATETIME&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;거래 일시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is_income&lt;/td&gt;
&lt;td&gt;BOOLEAN&lt;/td&gt;
&lt;td&gt;NOT NULL&lt;/td&gt;
&lt;td&gt;income - true&lt;br /&gt;expense - false&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 엔드포인트 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가계부(Book) 엔드포인트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;POST /books&lt;/b&gt;: 새로운 가계부 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books&lt;/b&gt;: 모든 가계부 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books/{id}&lt;/b&gt;: 특정 가계부 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT /books/{id}&lt;/b&gt;: 가계부 정보 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE /books/{id}&lt;/b&gt;: 가계부 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기록(Record) 엔드포인트&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;POST /books/{book_id}/records&lt;/b&gt;: 특정 가계부에 새로운 기록 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books/{book_id}/records&lt;/b&gt;: 특정 가계부의 모든 기록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books/{book_id}/records/{id}&lt;/b&gt;: 특정 기록 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT /books/{book_id}/records/{id}&lt;/b&gt;: 기록 수정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE /books/{book_id}/records/{id}&lt;/b&gt;: 기록 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-2. CRUD 엔드포인트 구현 (예: 가계부 관리)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 생성 및 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cargo.toml&lt;/b&gt;에 필요한 의존성을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
axum = &quot;0.6&quot;
tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }
serde = { version = &quot;1.0&quot;, features = [&quot;derive&quot;] }
serde_json = &quot;1.0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 데이터 구조 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/main.rs&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{
    routing::{get, post, put, delete},
    Router, Json, extract::{Path, Query},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;

// 가계부 구조체
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}

// 애플리케이션 상태: 가계부 목록을 저장
type AppState = Arc&amp;lt;Mutex&amp;lt;Vec&amp;lt;Book&amp;gt;&amp;gt;&amp;gt;;

#[tokio::main]
async fn main() {
    let state = Arc::new(Mutex::new(Vec::new()));

    let app = Router::new()
        .route(&quot;/books&quot;, post(create_book).get(get_books))
        .route(&quot;/books/:id&quot;, get(get_book).put(update_book).delete(delete_book))
        .with_state(state);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!(&quot;서버가 {}에서 시작됩니다...&quot;, addr);
    axum::Server::bind(&amp;amp;addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 가계부 엔드포인트 핸들러 구현&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.1 가계부 생성 (POST /books)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn create_book(
    state: axum::extract::State&amp;lt;AppState&amp;gt;,
    Json(payload): Json&amp;lt;BookInput&amp;gt;,
) -&amp;gt; (StatusCode, Json&amp;lt;Book&amp;gt;) {
    let mut books = state.lock().unwrap();

    let new_id = (books.len() + 1) as u32;
    let book = Book {
        id: new_id,
        name: payload.name,
    };
    books.push(book.clone());

    (StatusCode::CREATED, Json(book))
}

#[derive(Debug, Deserialize)]
struct BookInput {
    name: String,
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.2 가계부 목록 조회 (GET /books)&lt;/h4&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;async fn get_books(state: axum::extract::State&amp;lt;AppState&amp;gt;) -&amp;gt; Json&amp;lt;Vec&amp;lt;Book&amp;gt;&amp;gt; {
    let books = state.lock().unwrap();
    Json(books.clone())
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.3 특정 가계부 조회 (GET /books/:id)&lt;/h4&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;async fn get_book(
    state: axum::extract::State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, StatusCode&amp;gt; {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.4 가계부 수정 (PUT /books/:id)&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn update_book(
    state: axum::extract::State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
    Json(payload): Json&amp;lt;BookInput&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, StatusCode&amp;gt; {
    let mut books = state.lock().unwrap();
    if let Some(book) = books.iter_mut().find(|book| book.id == id) {
        book.name = payload.name.clone();
        Ok(Json(book.clone()))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.5 가계부 삭제 (DELETE /books/:id)&lt;/h4&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;async fn delete_book(
    state: axum::extract::State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; StatusCode {
    let mut books = state.lock().unwrap();
    if books.iter().any(|book| book.id == id) {
        books.retain(|book| book.id != id);
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-3. 요청 파라미터와 경로 변수 처리 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 경로 변수 추출 (&lt;code&gt;Path&lt;/code&gt;)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;경로 변수&lt;/b&gt;는 URL 경로에서 값을 추출합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Path&amp;lt;T&amp;gt;&lt;/code&gt;를 사용하여 경로 변수를 추출할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn get_book(Path(id): Path&amp;lt;u32&amp;gt;) { /* ... */ }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿼리 파라미터 추출 (&lt;code&gt;Query&lt;/code&gt;)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;쿼리 파라미터&lt;/b&gt;는 URL의 쿼리 문자열에서 값을 추출합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Query&amp;lt;T&amp;gt;&lt;/code&gt;를 사용하며, &lt;code&gt;T&lt;/code&gt;는 &lt;code&gt;Deserialize&lt;/code&gt;를 구현해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn search_books(Query(params): Query&amp;lt;SearchParams&amp;gt;) { /* ... */ }

#[derive(Deserialize)]
struct SearchParams {
    name: Option&amp;lt;String&amp;gt;,
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-4. JSON 데이터 직렬화 및 역직렬화&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;serde&lt;/code&gt;&lt;/b&gt; 크레이트를 사용하여 JSON 데이터를 직렬화(Serialize) 및 역직렬화(Deserialize)할 수 있습니다.&lt;/li&gt;
&lt;li&gt;구조체에 &lt;code&gt;#[derive(Serialize, Deserialize)]&lt;/code&gt;를 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핸들러 함수에서 &lt;code&gt;Json&amp;lt;T&amp;gt;&lt;/code&gt;를 사용하여 JSON 데이터를 자동으로 처리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn create_book(Json(payload): Json&amp;lt;BookInput&amp;gt;) { /* ... */ }&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-5. 에러 처리와 커스텀 에러 응답&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기본 에러 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핸들러 함수의 반환 타입에 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;를 사용하여 에러를 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;E&lt;/code&gt;로는 &lt;code&gt;axum::response::IntoResponse&lt;/code&gt;를 구현하는 타입을 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn get_book(
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, StatusCode&amp;gt; {
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 커스텀 에러 타입 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러를 더 자세히 처리하기 위해 커스텀 에러 타입을 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thiserror&lt;/code&gt; 크레이트를 사용하면 편리하게 에러 타입을 정의할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cargo.toml&lt;/b&gt;에 의존성 추가:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
thiserror = &quot;1.0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 타입 정의:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error(&quot;가계부를 찾을 수 없습니다.&quot;)]
    NotFound,
    #[error(&quot;내부 서버 오류가 발생했습니다.&quot;)]
    InternalServerError,
}

impl IntoResponse for AppError {
    fn into_response(self) -&amp;gt; Response {
        let status_code = match self {
            AppError::NotFound =&amp;gt; StatusCode::NOT_FOUND,
            AppError::InternalServerError =&amp;gt; StatusCode::INTERNAL_SERVER_ERROR,
        };
        let body = Json(json!({ &quot;error&quot;: self.to_string() }));
        (status_code, body).into_response()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핸들러 함수에서 사용:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;async fn get_book(
    state: axum::extract::State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, AppError&amp;gt; {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 에러 응답 커스터마이징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 메시지를 JSON 형태로 반환하여 클라이언트가 이해하기 쉽게 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IntoResponse&lt;/code&gt;를 구현하여 에러 응답을 커스터마이징할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;src/main.rs&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{
    routing::{get, post, put, delete},
    Router, Json, extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
use thiserror::Error;
use serde_json::json;

// 데이터 구조체 정의
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}

#[derive(Debug, Deserialize)]
struct BookInput {
    name: String,
}

// 애플리케이션 상태
type AppState = Arc&amp;lt;Mutex&amp;lt;Vec&amp;lt;Book&amp;gt;&amp;gt;&amp;gt;;

// 에러 타입 정의
#[derive(Error, Debug)]
pub enum AppError {
    #[error(&quot;가계부를 찾을 수 없습니다.&quot;)]
    NotFound,
    #[error(&quot;내부 서버 오류가 발생했습니다.&quot;)]
    InternalServerError,
}

impl IntoResponse for AppError {
    fn into_response(self) -&amp;gt; Response {
        let status_code = match self {
            AppError::NotFound =&amp;gt; StatusCode::NOT_FOUND,
            AppError::InternalServerError =&amp;gt; StatusCode::INTERNAL_SERVER_ERROR,
        };
        let body = Json(json!({ &quot;error&quot;: self.to_string() }));
        (status_code, body).into_response()
    }
}

// 핸들러 함수 구현
async fn create_book(
    State(state): State&amp;lt;AppState&amp;gt;,
    Json(payload): Json&amp;lt;BookInput&amp;gt;,
) -&amp;gt; (StatusCode, Json&amp;lt;Book&amp;gt;) {
    let mut books = state.lock().unwrap();

    let new_id = (books.len() + 1) as u32;
    let book = Book {
        id: new_id,
        name: payload.name,
    };
    books.push(book.clone());

    (StatusCode::CREATED, Json(book))
}

async fn get_books(State(state): State&amp;lt;AppState&amp;gt;) -&amp;gt; Json&amp;lt;Vec&amp;lt;Book&amp;gt;&amp;gt; {
    let books = state.lock().unwrap();
    Json(books.clone())
}

async fn get_book(
    State(state): State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, AppError&amp;gt; {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound)
}

async fn update_book(
    State(state): State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
    Json(payload): Json&amp;lt;BookInput&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Book&amp;gt;, AppError&amp;gt; {
    let mut books = state.lock().unwrap();
    if let Some(book) = books.iter_mut().find(|book| book.id == id) {
        book.name = payload.name.clone();
        Ok(Json(book.clone()))
    } else {
        Err(AppError::NotFound)
    }
}

async fn delete_book(
    State(state): State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;u32&amp;gt;,
) -&amp;gt; Result&amp;lt;StatusCode, AppError&amp;gt; {
    let mut books = state.lock().unwrap();
    if books.iter().any(|book| book.id == id) {
        books.retain(|book| book.id != id);
        Ok(StatusCode::NO_CONTENT)
    } else {
        Err(AppError::NotFound)
    }
}

#[tokio::main]
async fn main() {
    let state = Arc::new(Mutex::new(Vec::new()));

    let app = Router::new()
        .route(&quot;/books&quot;, post(create_book).get(get_books))
        .route(&quot;/books/:id&quot;, get(get_book).put(update_book).delete(delete_book))
        .with_state(state);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!(&quot;서버가 {}에서 시작됩니다...&quot;, addr);
    axum::Server::bind(&amp;amp;addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 방법&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서버 실행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;cargo run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;POST /books&lt;/b&gt;: 가계부 생성&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST -H &quot;Content-Type: application/json&quot; -d '{&quot;name&quot;: &quot;내 가계부&quot;}' http://localhost:3000/books&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books&lt;/b&gt;: 가계부 목록 조회&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:3000/books&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GET /books/1&lt;/b&gt;: 특정 가계부 조회&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;curl http://localhost:3000/books/1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PUT /books/1&lt;/b&gt;: 가계부 수정&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X PUT -H &quot;Content-Type: application/json&quot; -d '{&quot;name&quot;: &quot;업데이트된 가계부&quot;}' http://localhost:3000/books/1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DELETE /books/1&lt;/b&gt;: 가계부 삭제&lt;/li&gt;
&lt;li&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X DELETE http://localhost:3000/books/1&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 섹션에서는 Rust와 Axum을 활용하여 간단한 가계부 웹 애플리케이션을 구축하였습니다. 프로젝트 요구사항 분석부터 시작하여, CRUD 엔드포인트를 구현하고, 요청 파라미터와 경로 변수 처리, JSON 데이터의 직렬화 및 역직렬화, 에러 처리와 커스텀 에러 응답까지 다루었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/axum/&quot;&gt;Axum 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://serde.rs/&quot;&gt;Serde 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/crates/thiserror&quot;&gt;Thiserror 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/&quot;&gt;Tokio 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Note: 이 글은 학습 목적으로 작성되었으며, 실제 서비스 개발 시에는 보안, 성능, 에러 처리 등 다양한 측면을 고려해야 합니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>AXUM</category>
      <category>rust</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/333</guid>
      <comments>https://wolfy.tistory.com/333#entry333comment</comments>
      <pubDate>Tue, 24 Sep 2024 10:30:10 +0900</pubDate>
    </item>
    <item>
      <title>Rust와 Axum을 활용한 웹 백엔드 개발 - Axum 프레임워크 소개</title>
      <link>https://wolfy.tistory.com/332</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-1. Axum의 특징과 설치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Axum의 주요 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Axum&lt;/b&gt;은 Rust로 작성된 경량의 웹 애플리케이션 프레임워크로, Tokio 비동기 런타임을 기반으로 합니다. 주요 특징은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;간단한 라우팅&lt;/b&gt;: 함수와 경로를 간단하게 매핑하여 핸들러를 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기 지원&lt;/b&gt;: Tokio 런타임과 함께 비동기 프로그래밍을 효율적으로 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 다른 라이브러리와의 통합이 용이하며, 필요한 기능을 모듈화하여 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 관리 용이성&lt;/b&gt;: 애플리케이션 상태를 쉽게 공유하고 관리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Axum 설치하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axum을 사용하려면 &lt;code&gt;Cargo.toml&lt;/code&gt; 파일에 의존성을 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[dependencies]
axum = &quot;0.7&quot;
tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;axum&lt;/code&gt;&lt;/b&gt;: 웹 프레임워크&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;tokio&lt;/code&gt;&lt;/b&gt;: 비동기 런타임&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-2. 기본 라우팅 설정 및 핸들러 작성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;Hello, World!&quot; 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 Axum 웹 서버를 만들어 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;src/main.rs&lt;/code&gt;&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{
    routing::get,
    Router,
};
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    // 라우터 생성: &quot;/&quot; 경로에 대한 요청을 `hello_world` 핸들러에 연결
    let app = Router::new().route(&quot;/&quot;, get(hello_world));

    // 서버를 바인딩할 주소 설정
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!(&quot;서버가 {}에서 실행 중입니다.&quot;, addr);

    // 서버 실행
    axum::Server::bind(&amp;amp;addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

// 핸들러 함수 정의
async fn hello_world() -&amp;gt; &amp;amp;'static str {
    &quot;Hello, World!&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행하기&lt;/b&gt;:&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;cargo run&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 &lt;code&gt;http://127.0.0.1:3000/&lt;/code&gt;에 접속하면 &quot;Hello, World!&quot; 메시지를 볼 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 경로와 HTTP 메서드를 설정하여 라우팅을 구성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;use axum::routing::{get, post};

let app = Router::new()
    .route(&quot;/get&quot;, get(get_handler))
    .route(&quot;/post&quot;, post(post_handler));&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-3. 요청과 응답 처리 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요청 데이터 추출&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;경로 매개변수 추출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로에서 변수를 추출하여 핸들러 함수의 매개변수로 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{extract::Path, routing::get, Router};

async fn greet(Path(name): Path&amp;lt;String&amp;gt;) -&amp;gt; String {
    format!(&quot;Hello, {}!&quot;, name)
}

let app = Router::new().route(&quot;/greet/:name&quot;, get(greet));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;code&gt;/greet/Alice&lt;/code&gt;로 요청하면 &quot;Hello, Alice!&quot;를 반환합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쿼리 문자열 추출&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{extract::Query, routing::get, Router};
use std::collections::HashMap;

async fn query_params(Query(params): Query&amp;lt;HashMap&amp;lt;String, String&amp;gt;&amp;gt;) -&amp;gt; String {
    format!(&quot;쿼리 파라미터: {:?}&quot;, params)
}

let app = Router::new().route(&quot;/search&quot;, get(query_params));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/search?keyword=rust&amp;amp;sort=asc&lt;/code&gt;로 요청하면 쿼리 파라미터를 추출하여 반환합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JSON 바디 추출&lt;/h4&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{extract::Json, routing::post, Router};
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email: String,
}

async fn create_user(Json(payload): Json&amp;lt;CreateUser&amp;gt;) -&amp;gt; String {
    format!(&quot;사용자 생성: {} ({})&quot;, payload.username, payload.email)
}

let app = Router::new().route(&quot;/users&quot;, post(create_user));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서 JSON 형식의 데이터를 POST로 전송하면 구조체로 매핑하여 사용할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;응답 생성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자열 응답&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핸들러 함수에서 &lt;code&gt;String&lt;/code&gt; 또는 &lt;code&gt;&amp;amp;'static str&lt;/code&gt;을 반환하여 텍스트 응답을 보낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn simple_response() -&amp;gt; &amp;amp;'static str {
    &quot;간단한 응답 메시지&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JSON 응답&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;use axum::{response::Json, routing::get, Router};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u32,
    username: String,
}

async fn get_user() -&amp;gt; Json&amp;lt;User&amp;gt; {
    let user = User {
        id: 1,
        username: &quot;Alice&quot;.to_string(),
    };
    Json(user)
}

let app = Router::new().route(&quot;/user&quot;, get(get_user));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 데이터를 JSON으로 직렬화하여 반환합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1-4. 상태 관리 및 공유 데이터 처리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 상태 공유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 공통으로 사용하는 상태를 공유하려면 &lt;b&gt;&lt;code&gt;Extension&lt;/code&gt;&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{
    extract::Extension,
    routing::get,
    Router,
};
use std::sync::Arc;

struct AppState {
    app_name: String,
}

async fn handler(Extension(state): Extension&amp;lt;Arc&amp;lt;AppState&amp;gt;&amp;gt;) -&amp;gt; String {
    format!(&quot;앱 이름: {}&quot;, state.app_name)
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        app_name: &quot;My Axum App&quot;.to_string(),
    });

    let app = Router::new()
        .route(&quot;/&quot;, get(handler))
        .layer(Extension(state));

    axum::Server::bind(&amp;amp;&quot;127.0.0.1:3000&quot;.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/b&gt;를 사용하여 다중 스레드 환경에서 안전하게 상태를 공유합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Extension&lt;/code&gt;&lt;/b&gt; 레이어를 통해 상태를 애플리케이션에 주입합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 사용 예시: 카운터 증가&lt;/h3&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use axum::{
    extract::Extension,
    routing::get,
    Router,
};
use std::sync::{Arc, Mutex};

struct AppState {
    counter: Mutex&amp;lt;u32&amp;gt;,
}

async fn increment_counter(Extension(state): Extension&amp;lt;Arc&amp;lt;AppState&amp;gt;&amp;gt;) -&amp;gt; String {
    let mut counter = state.counter.lock().unwrap();
    *counter += 1;
    format!(&quot;현재 카운터 값: {}&quot;, counter)
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        counter: Mutex::new(0),
    });

    let app = Router::new()
        .route(&quot;/count&quot;, get(increment_counter))
        .layer(Extension(state));

    axum::Server::bind(&amp;amp;&quot;127.0.0.1:3000&quot;.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/b&gt;를 사용하여 공유 자원의 동시 접근을 제어합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/count&lt;/code&gt; 엔드포인트에 접근할 때마다 카운터가 증가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axum은 Rust로 웹 애플리케이션을 개발할 때 간단하면서도 강력한 기능을 제공합니다. 이번 글에서는 Axum의 특징과 설치 방법부터 기본적인 라우팅 설정, 요청과 응답 처리, 그리고 상태 관리 및 공유 데이터 처리까지 살펴보았습니다. 이러한 기본 개념을 바탕으로 더 복잡한 웹 애플리케이션을 구축할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연습 과제:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;추가 엔드포인트 구현하기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 엔드포인트를 추가하여 다양한 HTTP 메서드와 경로를 연습해보세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태를 활용한 간단한 애플리케이션 만들기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태를 사용하여 방문자 수를 추적하거나, 간단한 메모장 기능을 구현해보세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리 추가하기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청 처리 중 발생할 수 있는 에러를 적절히 처리하고 사용자에게 의미 있는 응답을 반환하도록 해보세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/axum/latest/axum/&quot;&gt;Axum 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/&quot;&gt;Tokio 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://serde.rs/&quot;&gt;Serde 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/&quot;&gt;Rust 언어 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Note: 이 글은 Axum 프레임워크의 기본 사용법을 소개하기 위한 것으로, 더 깊은 이해를 위해서는 공식 문서와 추가 자료를 참고하시기 바랍니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/332</guid>
      <comments>https://wolfy.tistory.com/332#entry332comment</comments>
      <pubDate>Tue, 24 Sep 2024 07:35:16 +0900</pubDate>
    </item>
    <item>
      <title>Rust와 Axum을 활용한 웹 백엔드 개발 - 비동기 프로그래밍</title>
      <link>https://wolfy.tistory.com/331</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 프로그래밍은 프로그램이 동시에 여러 작업을 처리할 수 있도록 해줍니다. 이는 특히 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 효율적인 자원 활용을 가능하게 합니다. Rust는 안전성과 성능을 겸비한 비동기 프로그래밍 모델을 제공합니다. 이번 글에서는 Rust에서의 비동기 프로그래밍에 대해 알아보고, &lt;code&gt;async&lt;/code&gt;와 &lt;code&gt;await&lt;/code&gt; 키워드를 사용하여 비동기 코드를 작성하는 방법을 배워보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비동기 프로그래밍 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 동기와 비동기의 차이점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동기(synchronous) 프로그래밍&lt;/b&gt;: 작업이 순차적으로 실행되며, 이전 작업이 완료되어야 다음 작업을 시작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비동기(asynchronous) 프로그래밍&lt;/b&gt;: 작업이 동시에 진행될 수 있으며, 하나의 작업이 완료되기를 기다리지 않고 다른 작업을 수행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 비동기 프로그래밍의 필요성 및 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;효율적인 자원 활용&lt;/b&gt;: CPU가 대기 시간 없이 작업을 계속 수행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;높은 응답성&lt;/b&gt;: 응용 프로그램이 사용자 입력에 빠르게 반응할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 더 많은 작업을 동시에 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &lt;code&gt;async&lt;/code&gt;와 &lt;code&gt;await&lt;/code&gt; 키워드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 &lt;code&gt;async&lt;/code&gt; 함수 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt; 키워드를 함수 앞에 붙여 비동기 함수를 정의합니다.&lt;/li&gt;
&lt;li&gt;비동기 함수는 &lt;code&gt;Future&lt;/code&gt;를 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn my_async_function() {
    // 비동기 작업 수행
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 &lt;code&gt;await&lt;/code&gt;를 통한 &lt;code&gt;Future&lt;/code&gt; 완료 대기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;await&lt;/code&gt; 키워드를 사용하여 &lt;code&gt;Future&lt;/code&gt;의 완료를 비동기적으로 기다립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn main() {
    my_async_function().await;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 비동기 함수의 반환 타입&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 함수는 컴파일러에 의해 &lt;code&gt;Future&lt;/code&gt;를 반환하는 것으로 변환됩니다.&lt;/li&gt;
&lt;li&gt;반환 타입을 명시하고 싶다면 &lt;code&gt;-&amp;gt; impl Future&amp;lt;Output = T&amp;gt;&lt;/code&gt; 형태로 지정할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use std::future::Future;

fn my_async_function() -&amp;gt; impl Future&amp;lt;Output = ()&amp;gt; {
    async {
        // 비동기 작업 수행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. &lt;code&gt;Future&lt;/code&gt; 이해하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 &lt;code&gt;Future&lt;/code&gt;의 개념과 역할&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Future&lt;/code&gt;는 아직 완료되지 않은 값을 나타내는 객체입니다.&lt;/li&gt;
&lt;li&gt;비동기 작업의 결과를 나타내며, 작업이 완료되면 값을 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 &lt;code&gt;Future&lt;/code&gt; 트레이트의 동작 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Future&lt;/code&gt; 트레이트는 &lt;code&gt;poll&lt;/code&gt; 메서드를 정의하며, 이 메서드는 작업의 진행 상황을 확인합니다.&lt;/li&gt;
&lt;li&gt;일반적으로 &lt;code&gt;poll&lt;/code&gt;은 실행기(executor)에 의해 호출되며, 개발자가 직접 호출하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 비동기 런타임 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 비동기 런타임의 필요성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 함수는 &lt;code&gt;Future&lt;/code&gt;를 반환하며, 이를 실행하려면 실행기(executor)가 필요합니다.&lt;/li&gt;
&lt;li&gt;실행기는 &lt;code&gt;Future&lt;/code&gt;를 &lt;code&gt;poll&lt;/code&gt;하여 작업을 진행시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Tokio 런타임 소개&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Tokio&lt;/b&gt;는 Rust에서 가장 널리 사용되는 비동기 런타임입니다.&lt;/li&gt;
&lt;li&gt;고성능 네트워킹 라이브러리를 포함하며, 비동기 코드를 실행하는 데 필요한 실행기를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 Tokio 설치 및 기본 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;에 &lt;code&gt;tokio&lt;/code&gt; 의존성을 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
tokio = { version = &quot;1.0&quot;, features = [&quot;full&quot;] }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 &lt;code&gt;main&lt;/code&gt; 함수를 정의하기 위해 &lt;code&gt;#[tokio::main]&lt;/code&gt; 어트리뷰트를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;#[tokio::main]
async fn main() {
    // 비동기 코드 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 비동기 함수 작성하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 동기 함수와의 차이점 이해&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기 함수는 즉시 값을 반환하지만, 비동기 함수는 &lt;code&gt;Future&lt;/code&gt;를 반환합니다.&lt;/li&gt;
&lt;li&gt;비동기 함수 내부에서 다른 비동기 함수를 호출할 때는 반드시 &lt;code&gt;await&lt;/code&gt; 키워드를 사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 비동기 함수에서 에러 처리 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 함수에서도 &lt;code&gt;Result&lt;/code&gt; 타입을 사용하여 에러를 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;async fn fetch_data() -&amp;gt; Result&amp;lt;String, reqwest::Error&amp;gt; {
    let response = reqwest::get(&quot;https://example.com&quot;).await?;
    let body = response.text().await?;
    Ok(body)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 비동기 코드 예제 실습&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간단한 HTTP 요청을 보내고 응답을 받는 비동기 함수를 작성해봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use reqwest;

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), reqwest::Error&amp;gt; {
    let response = reqwest::get(&quot;https://www.rust-lang.org&quot;).await?;
    let body = response.text().await?;
    println!(&quot;응답 본문:\n{}&quot;, body);
    Ok(())
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 비동기 트레이트&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 트레이트에서 비동기 함수 사용의 제한 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rust의 현재 안정 버전에서는 트레이트의 메서드에 &lt;code&gt;async&lt;/code&gt;를 직접 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 &lt;code&gt;async-trait&lt;/code&gt; 크레이트를 통한 해결 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;async-trait&lt;/code&gt; 크레이트를 사용하면 트레이트 메서드에서 비동기 함수를 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;에 의존성 추가:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
async-trait = &quot;0.1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;use async_trait::async_trait;

#[async_trait]
trait AsyncTrait {
    async fn perform(&amp;amp;self);
}

struct MyStruct;

#[async_trait]
impl AsyncTrait for MyStruct {
    async fn perform(&amp;amp;self) {
        // 비동기 작업 수행
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 비동기를 활용한 동시성 프로그래밍&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 태스크 생성 및 실행 (&lt;code&gt;tokio::spawn&lt;/code&gt;)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tokio::spawn&lt;/code&gt;을 사용하여 비동기 태스크를 생성하고 실행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // 비동기 작업
    });

    // 태스크 완료 대기
    handle.await.unwrap();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 여러 비동기 작업의 동시 실행&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 비동기 작업을 동시에 실행하여 성능을 향상시킬 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 &lt;code&gt;join!&lt;/code&gt; 매크로를 통한 병렬 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;futures&lt;/code&gt; 크레이트의 &lt;code&gt;join!&lt;/code&gt; 매크로를 사용하여 여러 &lt;code&gt;Future&lt;/code&gt;를 동시에 실행합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;에 의존성 추가:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;[dependencies]
futures = &quot;0.3&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용 예시:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use futures::join;

async fn task_one() {
    // 작업 1
}

async fn task_two() {
    // 작업 2
}

#[tokio::main]
async fn main() {
    let ((), ()) = join!(task_one(), task_two());
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 비동기 I/O 작업&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 파일 읽기/쓰기의 비동기 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tokio&lt;/code&gt;의 파일 시스템 모듈을 사용하여 비동기 파일 I/O를 수행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut file = File::open(&quot;foo.txt&quot;).await?;
    let mut contents = String::new();
    file.read_to_string(&amp;amp;mut contents).await?;
    println!(&quot;파일 내용: {}&quot;, contents);
    Ok(())
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 네트워킹에서의 비동기 처리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tokio&lt;/code&gt;의 네트워킹 모듈을 사용하여 비동기 TCP 서버나 클라이언트를 구현할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;use tokio::net::TcpListener;
use tokio::io::{self, AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let listener = TcpListener::bind(&quot;127.0.0.1:8080&quot;).await?;

    loop {
        let (mut socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            // 소켓으로부터 데이터 읽기
            match socket.read(&amp;amp;mut buf).await {
                Ok(0) =&amp;gt; return, // 연결 종료
                Ok(n) =&amp;gt; {
                    // 에코(Echo) 서버 구현
                    if let Err(e) = socket.write_all(&amp;amp;buf[..n]).await {
                        eprintln!(&quot;데이터 전송 실패: {}&quot;, e);
                    }
                }
                Err(e) =&amp;gt; {
                    eprintln!(&quot;데이터 수신 실패: {}&quot;, e);
                }
            }
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.3 &lt;code&gt;async&lt;/code&gt;를 지원하는 표준 라이브러리와 크레이트 소개&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;표준 라이브러리&lt;/b&gt;: 현재 Rust 표준 라이브러리는 일부 비동기 기능만 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 크레이트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tokio&lt;/code&gt;: 비동기 런타임 및 네트워킹, 파일 I/O 지원&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async-std&lt;/code&gt;: 다른 인기 있는 비동기 런타임&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reqwest&lt;/code&gt;: 비동기 HTTP 클라이언트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hyper&lt;/code&gt;: 고성능 HTTP 라이브러리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 주의사항 및 모범 사례&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 비동기 코드에서의 소유권과 라이프타임 이슈&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 함수에서는 소유권과 라이프타임에 주의해야 합니다.&lt;/li&gt;
&lt;li&gt;비동기 함수 내부에서 참조자를 반환하려면 라이프타임을 명시하거나 &lt;code&gt;Arc&lt;/code&gt; 같은 스마트 포인터를 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 블로킹 코드 피하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 코드에서는 블로킹 함수를 사용하면 전체 스레드가 멈출 수 있으므로 피해야 합니다.&lt;/li&gt;
&lt;li&gt;필요한 경우 &lt;code&gt;spawn_blocking&lt;/code&gt;을 사용하여 별도의 스레드에서 블로킹 작업을 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;tokio::task::spawn_blocking(|| {
    // 블로킹 작업
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.3 비동기 코드 디버깅 팁&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;RUST_BACKTRACE=1&lt;/code&gt; 환경 변수를 설정하여 백트레이스를 활성화합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokio&lt;/code&gt;의 &lt;code&gt;tracing&lt;/code&gt; 기능을 활용하여 로그를 남길 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 프로그래밍은 Rust에서 고성능의 효율적인 프로그램을 작성하는 데 필수적인 기술입니다. 이번 글에서는 &lt;code&gt;async&lt;/code&gt;와 &lt;code&gt;await&lt;/code&gt;의 기본 개념부터 비동기 런타임인 &lt;code&gt;Tokio&lt;/code&gt;를 사용하여 비동기 코드를 작성하는 방법까지 살펴보았습니다. 비동기 프로그래밍을 잘 활용하면 네트워크 요청, 파일 I/O 등 대기 시간이 긴 작업에서 프로그램의 성능과 응답성을 크게 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;연습 과제:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;reqwest&lt;/code&gt; 크레이트를 사용하여 여러 웹사이트에서 동시에 데이터를 가져오는 프로그램을 작성해보세요.&lt;/li&gt;
&lt;li&gt;비동기 TCP 서버를 구현하여 클라이언트로부터 메시지를 받아 에코하는 서버를 만들어보세요.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokio::fs&lt;/code&gt; 모듈을 사용하여 여러 파일을 동시에 읽고 처리하는 프로그램을 작성해보세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://rust-lang.github.io/async-book/&quot;&gt;Asynchronous Programming in Rust (공식 문서)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/tokio/tutorial&quot;&gt;Tokio 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/future/trait.Future.html&quot;&gt;Rust Standard Library API Documentation - Future&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://crates.io/crates/async-trait&quot;&gt;async-trait 크레이트&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;Note: 이 글은 Rust에서의 비동기 프로그래밍을 소개하기 위한 것으로, 더 깊은 이해를 위해서는 공식 문서와 추가 자료를 참고하시기 바랍니다.&lt;/i&gt;&lt;/p&gt;</description>
      <category>슬기로운 개발자생활/Rust</category>
      <category>async</category>
      <category>rust</category>
      <category>비동기</category>
      <author>개발자 소신</author>
      <guid isPermaLink="true">https://wolfy.tistory.com/331</guid>
      <comments>https://wolfy.tistory.com/331#entry331comment</comments>
      <pubDate>Mon, 23 Sep 2024 18:01:53 +0900</pubDate>
    </item>
  </channel>
</rss>