<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>곽의 프로그램</title>
    <link>https://kwakscomputerengineering.tistory.com/</link>
    <description>벡엔드 개발자 최소기준 맞추겠습니다.</description>
    <language>ko</language>
    <pubDate>Thu, 11 Jun 2026 18:05:26 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>과컴</managingEditor>
    <image>
      <title>곽의 프로그램</title>
      <url>https://tistory1.daumcdn.net/tistory/6421826/attach/a64e58c3a4be411d983925d2d1b41b95</url>
      <link>https://kwakscomputerengineering.tistory.com</link>
    </image>
    <item>
      <title>Monew 프로젝트: 뉴스기사 목록조회 PostgreSQL 복합 인덱스로 뉴스 목록 조회 성능 개선</title>
      <link>https://kwakscomputerengineering.tistory.com/77</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번에 조회 쿼리 구조 개선 이후 NAVER 뉴스기사 45,000건에 대해 실행 시간은 약 20ms 수준까지 감소했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 API 응답 속도만 보면 충분히 빠른 수준이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779021685216&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Seq Scan on news_articles
&amp;rarr; Sort
&amp;rarr; Limit 11&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실행 계획을 보면 최종적으로는 11개의 기사만 필요하지만 DB는 여전히 news_artilces 전체를 스캔한 뒤 정렬을 수행하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 메인 화면 뉴스 목록 조회는 서비스에서 가장 자주 호출될 가능성이 높은 API였기때문에 단일 요청의 실행 시간뿐 아니라 반복적인 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;&lt;b&gt;따라서&lt;span style=&quot;background-color: #f6e199;&quot;&gt; 전체 스캔 자체를 줄이고, 정렬 비용을 제거하고 limit 11개를 더 효율적으로 가져오기위해&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&quot;복합 인덱스&quot;를 추가&lt;/span&gt;했다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Redis의 고려?&lt;/b&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;이번 개선에서는 먼저 데이터베이스 조회 구조와 실행 계획자체를 개선하는 방향을 선택하였다.&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;1. DB단에서 개선 여지가 남아있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;병목은 불필요한 JOIN, GROUP BY, 정렬 비용, Seq Scan이고 캐시 이전에 DB가 비효율적으로 동작하는 원인을 먼저 해결하는게 우선이라고 생각했다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 메인 조회 패턴이 고정적이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779021981589&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE source = 'NAVER'
AND is_deleted = false
ORDER BY published_at DESC, created_at DESC
LIMIT 11&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;WHERE + ORDER BY 패턴이 명확해서 복합 인덱스를 통해 DB 레벨에서 충분히 최적화 가능하다고 생각했다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Redis의 운영 복잡도..?&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;i&gt;아직 Redis에 대해 깊이있는 이해가 부족하다.&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;i&gt;캐시 만료 전략, 데이터 동기화, 장애대응등... 운영 고려사항이 필요하다.&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 설계&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779022207924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_news_articles_source_deleted_published_created
ON news_articles (
  source,
  is_deleted,
  published_at DESC,
  created_at DESC
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;source와 is_deleted는 where 필터고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;published_at은 메인 정렬, created_at은 보조정렬이라서 인덱스를 다음과 같이 설계하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스 설계 전 Seq Scan -&amp;gt; Sort -&amp;gt; Limit : 약 22ms&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;인덱스 후: 0.08ms&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt; 99.64% 감소 275배....&lt;/b&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1. 인덱스는 WHERE만이 아니라 ORDER BY까지 고려해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;2. LIMIT 쿼리는 정렬 순서까지 맞는 복합 인덱스에서 효과가 크다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;3. 실행계획에서 Seq Scan -&amp;gt; Index Scan 변화 여부를 반드시 확인해야 한다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;4. 실제 메인 조회 패턴 기준으로 인덱스를 설계해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/77</guid>
      <comments>https://kwakscomputerengineering.tistory.com/77#entry77comment</comments>
      <pubDate>Sun, 17 May 2026 23:07:47 +0900</pubDate>
    </item>
    <item>
      <title>Monew 프로젝트: 뉴스기사 목록 조회 인덱스 없이 구조 개선으로 약 30배 성능 개선</title>
      <link>https://kwakscomputerengineering.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제상황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Monew 프로젝트 메인 화면에서는 뉴스기사 목록을 조회한다.&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;1611&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHqgw7/dJMcadBXAFT/uwvPfzmk7fjcfyct4mBBZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHqgw7/dJMcadBXAFT/uwvPfzmk7fjcfyct4mBBZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHqgw7/dJMcadBXAFT/uwvPfzmk7fjcfyct4mBBZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHqgw7%2FdJMcadBXAFT%2FuwvPfzmk7fjcfyct4mBBZ0%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;1611&quot; height=&quot;816&quot; data-origin-width=&quot;1611&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&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;메일화면의 기본 조회조건은 화면에서 볼 수 있듯이 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬기준: published_at&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 방향: DESC&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: NAVER&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 개수: 10개 + hasNext 확인용 1개&lt;/p&gt;
&lt;pre id=&quot;code_1779019656581&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE na.is_deleted = false
  AND na.source = 'NAVER'
ORDER BY
  na.published_at DESC,
  na.created_at DESC
FETCH FIRST 11 ROWS ONLY&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 조회 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779019681638&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LEFT JOIN article_interests ai
       ON ai.news_article_id = na.id

LEFT JOIN comments c
       ON c.article_id = na.id
      AND c.is_deleted = false

LEFT JOIN article_views av
       ON av.news_article_id = na.id
      AND av.user_id = ...&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;p data-ke-size=&quot;size16&quot;&gt;이후 댓글 수 집계를 위해 COUNT(DISTINCT c.id)와 GROUP BY를 수행했다.&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;문제는 메인 화면에서는 관심사 필터가 없음에도 article_interests 조인이 항상 수행된다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;즉, 최종적으로 필요한 데이터는 11개뿐인데 DB에서는 수만건의 조인과 집계, 정렬을 먼저 수행하고 있었다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 계획 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 확인해보니 다음과 같은 문제가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NAVER 기사 약 45,700 rows 조회&lt;/li&gt;
&lt;li&gt;article_interests 약 79,000 rows 조인&lt;/li&gt;
&lt;li&gt;중간 결과 약 72,000 rows 생성&lt;/li&gt;
&lt;li&gt;이후 GROUP BY, COUNT(DISTINCT id), ORDER BY 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 정렬 단계에서는 external merge가 발생했다.&lt;/p&gt;
&lt;pre id=&quot;code_1779019944480&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Sort Method: external merge
temp read/write 발생&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;결국 최종 결과는 11개 뿐인데도 DB는 그 11개를 얻기위해 수만건의 중간 결과를 처리하고 있던것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개선 방향&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 11개의 row를 뽑기위해 너무 많은 작업을 수행한다는 점.&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;1. news_articles에서 기사 11개를 먼저 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 조회된 기사 ID에 대해서만 댓글 수 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 조회된 기사 ID에 대해서만 사용자 조회 여부 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. java에서 DTO 조립&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;먼저 JOIN과 GROUP BY, ORDER BY로 준비를 해놓고 조회하는방식이 아닌 11개를 조회하고 JOIN을 하는 식으로 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개선 후 구조&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1779020281432&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    na.id,
    na.source,
    na.original_link,
    na.title,
    na.published_at,
    na.summary,
    na.view_count,
    na.created_at
FROM news_articles na
WHERE na.is_deleted = false
  AND na.source = 'NAVER'
ORDER BY
    na.published_at DESC,
    na.created_at DESC
FETCH FIRST 11 ROWS ONLY;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 11개를 조회한 후, 댓글 수 와 조회 여부는 현제 페이지의 기사 ID만 대상으로 별도 조회하는식으로 변경한것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성능 비교&lt;/b&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;NAVER 뉴스기사 약 45000건 기준&lt;/b&gt;&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;&lt;b&gt;기존 JOIN/GROUP BY 구조: 평균 실행시간은 614.825 ms&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개선 구조는 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;20.215 ms&lt;/span&gt;&lt;/b&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;기사 목록조회: 20.067 ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;댓글 수 집계: 0.111ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 조회 여부 조회: 0.038ms&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;개선 후 전체 DB 실행 시간 기준 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;약 30배 수준의 성능 개선&lt;/span&gt;&lt;/b&gt;을 확인할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>트러블 슈팅</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/76</guid>
      <comments>https://kwakscomputerengineering.tistory.com/76#entry76comment</comments>
      <pubDate>Sun, 17 May 2026 21:26:04 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Actions Trigger 유형과 CI/CD 활용 시나리오</title>
      <link>https://kwakscomputerengineering.tistory.com/75</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Trigger는 &quot;언제 워크 플로우를 실행할 것인가&quot;를 결정하는 조건&lt;/b&gt;&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;워크플로우 파일에서는 on 키워드로 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1778631833235&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI

on:
  push:
    branches: [main]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main 브랜치에 push 할때 워크 플로우를 실행하고싶을때는 위와같이 나타냅니다.&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;415&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L3754/dJMcahRUl3C/KSgG0LlJr5VtwWfNentlnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L3754/dJMcahRUl3C/KSgG0LlJr5VtwWfNentlnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L3754/dJMcahRUl3C/KSgG0LlJr5VtwWfNentlnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL3754%2FdJMcahRUl3C%2FKSgG0LlJr5VtwWfNentlnk%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;285&quot; height=&quot;796&quot; data-origin-width=&quot;415&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 공식 문서 기준으로 워크플로우는 GitHub 이벤트, 예약시간, 수동 실행등에 의해 실행될 수 있고 다양한 이벤트 기반 트리거를 제공합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;1. push&lt;/span&gt;: 코드가 브랜치나 태그에 push 될때 실행됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778632269008&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  push:
    branches:
      - main
      - develop&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;develop 브랜치에 push 되면 테스트 실행&lt;/li&gt;
&lt;li&gt;main 브랜치에 merge되면 빌드 후 배포&lt;/li&gt;
&lt;li&gt;v1.0.0 같은 태그가 push 되면 릴리즈 배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 백엔드 프로젝트에서 main 브랜치에 push 될때 Docker 이미지를 빌드하고, ECS나 EC2에 배포하는 흐름에 자주 사용합니다.&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;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;2. pull_request&lt;/span&gt;: PR이 생성되거나, 커밋이 추가되거나, 다시 열릴때 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778632450925&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  pull_request:
    branches:
      - develop
      - main&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;PR 생성시 테스트 실행&lt;/li&gt;
&lt;li&gt;빌드 가능 여부 확인&lt;/li&gt;
&lt;li&gt;리뷰전에 깨진 코드가 들어오는것을 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는&lt;b&gt; pull_request를 CI 검증용&lt;/b&gt;으로 많이 사용한다고 합니다. 배포보다는 이 코드가 merge되어도 안전한가를 확인하는 용도로 많이 사용한다고 합니다.&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;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;3. workflow_dispatch&lt;/span&gt;: GitHub Actions 화면에서 사람이 직접 실행할 수 있는 수동 트리거&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778632714707&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  workflow_dispatch:
    inputs:
      environment:
        description: '배포 환경'
        required: true
        default: 'dev'&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;li&gt;운영 배포를 수동 승인 후 실행&lt;/li&gt;
&lt;li&gt;특정 브랜치를 원하는 시점에 배포&lt;/li&gt;
&lt;li&gt;장애 대응용 재배포&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 자동화는 하되 실행 타이밍은 사람이 통제하고 싶을때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions 탭에 [Run workflow] 버튼이 생기고, 사용자는 dev, prod, stage와 같은 값을 입력하고 실행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;운영 배포는 위험하기 때문에 이러한 과정을 통해서 배포에서 실수를 최소화&lt;/b&gt;한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;4. schedule&lt;/span&gt;: cron 표현식으로 정해진 시간에 워크 플로우를 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778804123941&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  schedule:
    - cron: '0 0 * * *'&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;li&gt;매주 보안 점검&lt;/li&gt;
&lt;li&gt;정기 배치 작업&lt;/li&gt;
&lt;li&gt;오래된 artifact 정리&lt;/li&gt;
&lt;li&gt;주기적인 헬스 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cron은 기본적으로 UTC 기준&lt;/b&gt;이라서 매일 새벽 3시마다 API 테스트를 돌리고 싶다면 한국 시간 기준으로 UTC 변환을 고려해야한다.&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;span style=&quot;background-color: #f6e199;&quot;&gt;5. release&lt;/span&gt;: Github Release가 생성, 수정, 게시될때 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778804294542&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  release:
    types: [published]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1937&quot; data-start=&quot;1873&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1890&quot; data-start=&quot;1873&quot;&gt;릴리즈가 발행되면 운영 배포&lt;/li&gt;
&lt;li data-end=&quot;1902&quot; data-start=&quot;1891&quot;&gt;릴리즈 노트 생성&lt;/li&gt;
&lt;li data-end=&quot;1914&quot; data-start=&quot;1903&quot;&gt;배포 파일 업로드&lt;/li&gt;
&lt;li data-end=&quot;1937&quot; data-start=&quot;1915&quot;&gt;Docker 이미지에 버전 태그 부여&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2016&quot; data-start=&quot;1939&quot; data-ke-size=&quot;size16&quot;&gt;push의 tag 기반 배포와 비슷하지만, release는 GitHub Release라는 명확한 릴리즈 행위를 기준으로 동작합니다.&lt;/p&gt;
&lt;p data-end=&quot;2016&quot; data-start=&quot;1939&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2016&quot; data-start=&quot;1939&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;6. workflow_run&lt;/span&gt;: 다른 워크플로우가 완료된 후 실행&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778804378662&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  workflow_run:
    workflows: [&quot;CI&quot;]
    types:
      - completed&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2222&quot; data-start=&quot;2159&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2175&quot; data-start=&quot;2159&quot;&gt;CI가 성공하면 CD 실행&lt;/li&gt;
&lt;li data-end=&quot;2204&quot; data-start=&quot;2176&quot;&gt;테스트 워크플로우 완료 후 배포 워크플로우 실행&lt;/li&gt;
&lt;li data-end=&quot;2222&quot; data-start=&quot;2205&quot;&gt;빌드와 배포 워크플로우 분리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2300&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;b&gt;CI 워크플로우에서 테스트와 빌드를 수행하고, 성공한 경우에만 Deploy 워크플로우가 실행되도록 구성&lt;/b&gt;할 수 있습니다.&lt;/p&gt;
&lt;p data-end=&quot;2300&quot; data-start=&quot;2224&quot; data-ke-size=&quot;size16&quot;&gt;CI와 CD의 책임을 분리할 수 있다는 장점이 있다.&lt;/p&gt;</description>
      <category>위클리페이퍼</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/75</guid>
      <comments>https://kwakscomputerengineering.tistory.com/75#entry75comment</comments>
      <pubDate>Fri, 15 May 2026 09:21:20 +0900</pubDate>
    </item>
    <item>
      <title>Spring Security에서 왜 Member 대신 UserDetails를 사용하나?</title>
      <link>https://kwakscomputerengineering.tistory.com/74</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security를 처음 학습할때 가장 헷갈렸던 부분 중 하나는 다음이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&quot;DB에는 Member 엔티티가 있는데, 왜 Spring Security는 갑자기 UserDetails를 사용하지?&quot;&lt;/i&gt;&lt;/b&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;실제로 사용자 정보는 Member 엔티티에 저장되어 있는데, 인증과정에서는 UserDetails라는 객체가 등장한다.&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;이번 글에서 Spring Security 인증 구조에서&lt;b&gt; UserDetails가 왜 필요한지,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그리고 Member 엔티티와 어떤 역할 차이가 있는지 정리&lt;/b&gt;해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Member는 비즈니스 도메인 엔티티이고,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UserDetails는 Spring Security가 사용하는 로그인 사용자 표준 객체이다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Security 인증 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1778571206539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;로그인 요청
    &amp;darr;
AuthenticationProvider
    &amp;darr;
UserDetailsService
    &amp;darr;
DB에서 Member 조회
    &amp;darr;
UserDetails 객체 반환
    &amp;darr;
Spring Security 인증 처리&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;DB에서는 Member를 조회하지만, Spring Security 내부에서는 UserDetails를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778571066076&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class HelloUserDetailsServiceV3 implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final HelloAuthorityUtils authorityUtils;

    public HelloUserDetailsServiceV3(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils){
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils;
    }
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 회원조회
        // (6)credential 조회
        // (7)UserDetails 생성
        Member findMember = memberRepository.findByEmail(username)
                .orElseThrow(() -&amp;gt; new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

//        // 내부적으로 List, Set 사용
//        // ? extend GrantedAuthority: GrantedAuthority를 상속받고있는 애들만 들어올 수 있다.
//        Collection&amp;lt;? extends GrantedAuthority&amp;gt; authorities =
//                authorityUtils.createAuthorities(findMember.getEmail());

        // (8)UserDetails 전달
        return new HelloUserDetails(findMember);
    }

    // UserDetails 정보가 너무 빈약하기때문에 Member의 필드를 참조하고 싶은것.
    private final class HelloUserDetails extends Member implements UserDetails{

        public HelloUserDetails(Member member){
            setMemberId(member.getMemberId());
            setEmail(member.getEmail());
            setPassword(member.getPassword());
            setFullName(member.getFullName());

            //권한 추가
            setRoles(member.getRoles());
        }

        @Override
        public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
            return authorityUtils.createAuthorities(this.getRoles());
        }

        @Override
        public String getUsername() {
            return this.getEmail();
        }

        @Override
        public boolean isAccountNonExpired() {
            return true;
        }

        @Override
        public boolean isAccountNonLocked() {
            return true;
        }

        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }

        @Override
        public boolean isEnabled() {
            return true;
        }
    }
}&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;UserDetailsService를 보면 loadUserByUsername() 메서드를 통해 DB에 Member를 조회하고 반환은 UserDetails로 하는걸 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 Member를 직접 사용하지 않을까?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Security는 특정 프로젝트 엔티티 구조를 알지 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 마다 Member, User, Admin등 사용자 엔티티 이름과 구조가 모두 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Spring Security는 &lt;i&gt;&lt;b&gt;&quot;프로젝트마다 다른 사용자 엔티티 대신 내가 필요한 사용자 정보 형식을 정의하자&quot;&lt;/b&gt;&lt;/i&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;i&gt;&lt;b&gt;&quot;추상화&quot;&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;를 위해서 라고 생각하면 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;-&amp;gt; Spring Security는 프로젝트마다 다른 사용자 엔티티가 Member이든, User이든, Admin이든 알 필요가 없다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Security에서 UserDetails를 사용하는 이유는 &lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;프로젝트마다 다른 사용자 엔티티 구조를 통일된 방식으로 처리하기 위해&lt;/span&gt;서이다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>코드잇 스프린트/Spring 이론</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/74</guid>
      <comments>https://kwakscomputerengineering.tistory.com/74#entry74comment</comments>
      <pubDate>Tue, 12 May 2026 16:44:33 +0900</pubDate>
    </item>
    <item>
      <title>AWS RDS를 활용하는 이점과 EC2에 직접 DB 설치 비교</title>
      <link>https://kwakscomputerengineering.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서비스를 운영하다 보면 데이터베이스를 어떤 방식으로 운영할지 고민하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 환경에서는 대표적으로 두가지 선택지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon RDS 사용&lt;/li&gt;
&lt;li&gt;Amazon EC2에 직접 데이터베이스 설치 및 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 RDS의 주요 장점과 EC2 직접 구축 방식과의 차이점을 비교하고, 반대로 RDS가 적합하지 않을 수 있는 상황까지 정리해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RDS란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS(Relational Database Service)는 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;사용자는 데이터베이스를 직접 설치하거나 운영환경을 구성하지 않아도 되고, AWS가 데이터베이스 운영에 필요한 여러 작업을 대신 관리해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0goUK/dJMcafs0v3x/WAIunpmpfbsRSp6XyEcCbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0goUK/dJMcafs0v3x/WAIunpmpfbsRSp6XyEcCbK/img.png&quot; data-origin-width=&quot;275&quot; data-origin-height=&quot;183&quot; data-is-animation=&quot;false&quot; style=&quot;width: 45.07%; margin-right: 10px;&quot; data-widthpercent=&quot;45.6&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0goUK/dJMcafs0v3x/WAIunpmpfbsRSp6XyEcCbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0goUK%2FdJMcafs0v3x%2FWAIunpmpfbsRSp6XyEcCbK%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;275&quot; height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bni3Qs/dJMcaa6kHYY/1zYkrJUpAKXZ1QZgxTk6WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bni3Qs/dJMcaa6kHYY/1zYkrJUpAKXZ1QZgxTk6WK/img.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;714&quot; data-is-animation=&quot;false&quot; width=&quot;455&quot; height=&quot;254&quot; style=&quot;width: 53.7672%;&quot; data-widthpercent=&quot;54.4&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bni3Qs/dJMcaa6kHYY/1zYkrJUpAKXZ1QZgxTk6WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbni3Qs%2FdJMcaa6kHYY%2F1zYkrJUpAKXZ1QZgxTk6WK%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;1280&quot; height=&quot;714&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BNEIV/dJMcacwjF4t/fymWlyqzXD5haVDTrfjoOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BNEIV/dJMcacwjF4t/fymWlyqzXD5haVDTrfjoOk/img.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;280&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.245%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;52.86&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BNEIV/dJMcacwjF4t/fymWlyqzXD5haVDTrfjoOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBNEIV%2FdJMcacwjF4t%2FfymWlyqzXD5haVDTrfjoOk%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;610&quot; height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YqgjB/dJMcaiwuk83/crBnB7qnVk1PzKTr3enjYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YqgjB/dJMcaiwuk83/crBnB7qnVk1PzKTr3enjYk/img.png&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;175&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.5922%; margin-top: 10px;&quot; data-widthpercent=&quot;47.14&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YqgjB/dJMcaiwuk83/crBnB7qnVk1PzKTr3enjYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYqgjB%2FdJMcaiwuk83%2FcrBnB7qnVk1PzKTr3enjYk%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;340&quot; height=&quot;175&quot;/&gt;&lt;/span&gt;&lt;/div&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;대표적으로 이런 DB 엔진을 지원한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1 data-end=&quot;524&quot; data-start=&quot;503&quot; data-section-id=&quot;flu4ng&quot;&gt;EC2에 직접 DB를 설치하는 방식&lt;/h1&gt;
&lt;p data-end=&quot;574&quot; data-start=&quot;526&quot; data-ke-size=&quot;size16&quot;&gt;EC2에 직접 DB를 구축하는 경우에는 사용자가 서버 운영 전반을 직접 관리해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;576&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 PostgreSQL을 직접 설치한다면 다음 작업들을 모두 직접 수행해야 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;EC2 생성
&amp;rarr; PostgreSQL 설치
&amp;rarr; 사용자 및 권한 설정
&amp;rarr; 백업 스크립트 작성
&amp;rarr; 장애 복구 구성
&amp;rarr; 모니터링 환경 구축
&amp;rarr; 패치 및 버전 관리&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;753&quot; data-start=&quot;727&quot; data-ke-size=&quot;size16&quot;&gt;즉, 자유도는 높지만 운영 부담도 매우 커진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1 data-end=&quot;772&quot; data-start=&quot;760&quot; data-section-id=&quot;s61ecm&quot;&gt;RDS의 주요 장점&lt;/h1&gt;
&lt;h2 data-end=&quot;786&quot; data-start=&quot;774&quot; data-section-id=&quot;fgsfde&quot; data-ke-size=&quot;size26&quot;&gt;1. 운영 자동화&lt;/h2&gt;
&lt;p data-end=&quot;835&quot; data-start=&quot;788&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;RDS의 가장 큰 장점은 데이터베이스 운영 작업을 AWS가 대신 관리&lt;/b&gt;&lt;/span&gt;해준다는 점이다.&lt;/p&gt;
&lt;p data-end=&quot;860&quot; data-start=&quot;837&quot; data-ke-size=&quot;size16&quot;&gt;대표적으로 다음 기능들이 자동화되어 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;930&quot; data-start=&quot;862&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;877&quot; data-start=&quot;862&quot; data-section-id=&quot;5a8mm0&quot;&gt;DB 설치 및 초기 구성&lt;/li&gt;
&lt;li data-end=&quot;887&quot; data-start=&quot;878&quot; data-section-id=&quot;vw8gw5&quot;&gt;운영체제 패치&lt;/li&gt;
&lt;li data-end=&quot;898&quot; data-start=&quot;888&quot; data-section-id=&quot;ohc3bi&quot;&gt;DB 엔진 패치&lt;/li&gt;
&lt;li data-end=&quot;906&quot; data-start=&quot;899&quot; data-section-id=&quot;1hmcol&quot;&gt;자동 백업&lt;/li&gt;
&lt;li data-end=&quot;915&quot; data-start=&quot;907&quot; data-section-id=&quot;1q1mamq&quot;&gt;스냅샷 관리&lt;/li&gt;
&lt;li data-end=&quot;923&quot; data-start=&quot;916&quot; data-section-id=&quot;m7lzsk&quot;&gt;장애 복구&lt;/li&gt;
&lt;li data-end=&quot;930&quot; data-start=&quot;924&quot; data-section-id=&quot;1sysgmx&quot;&gt;모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;972&quot; data-start=&quot;932&quot; data-ke-size=&quot;size16&quot;&gt;덕분에 개발자는 인프라 운영보다 애플리케이션 개발에 더 집중할 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;977&quot; data-start=&quot;974&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;998&quot; data-start=&quot;979&quot; data-section-id=&quot;1havijr&quot; data-ke-size=&quot;size26&quot;&gt;2. 자동 백업 및 복구 기능&lt;/h2&gt;
&lt;p data-end=&quot;1020&quot; data-start=&quot;1000&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 자동 백업 기능을 제공한다.&lt;/p&gt;
&lt;p data-end=&quot;1028&quot; data-start=&quot;1022&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1089&quot; data-start=&quot;1029&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1063&quot; data-start=&quot;1029&quot; data-section-id=&quot;u6omp9&quot;&gt;특정 시점 복구(Point-In-Time Recovery)&lt;/li&gt;
&lt;li data-end=&quot;1075&quot; data-start=&quot;1064&quot; data-section-id=&quot;1auaxmj&quot;&gt;자동 스냅샷 생성&lt;/li&gt;
&lt;li data-end=&quot;1089&quot; data-start=&quot;1076&quot; data-section-id=&quot;8x4qlp&quot;&gt;백업 보관 기간 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1106&quot; data-start=&quot;1091&quot; data-ke-size=&quot;size16&quot;&gt;등을 쉽게 사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1124&quot; data-start=&quot;1108&quot; data-ke-size=&quot;size16&quot;&gt;EC2 직접 구축 환경에서는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1161&quot; data-start=&quot;1125&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1136&quot; data-start=&quot;1125&quot; data-section-id=&quot;86rdnw&quot;&gt;pg_dump&lt;/li&gt;
&lt;li data-end=&quot;1148&quot; data-start=&quot;1137&quot; data-section-id=&quot;ilug4a&quot;&gt;WAL 로그 관리&lt;/li&gt;
&lt;li data-end=&quot;1161&quot; data-start=&quot;1149&quot; data-section-id=&quot;1y30woh&quot;&gt;백업 스크립트 작성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1180&quot; data-start=&quot;1163&quot; data-ke-size=&quot;size16&quot;&gt;등을 모두 직접 구성해야 한다.&lt;/p&gt;
&lt;hr data-end=&quot;1185&quot; data-start=&quot;1182&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1207&quot; data-start=&quot;1187&quot; data-section-id=&quot;xjv3ew&quot; data-ke-size=&quot;size26&quot;&gt;3. 고가용성(HA) 구성 용이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;-&amp;gt; 고가용성: DB 서버 한대가 죽어도 서비스가 계속 동작하게 만드는 구조&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1247&quot; data-start=&quot;1209&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 Multi-AZ 기능을 통해 고가용성 구성을 쉽게 지원한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Primary DB: 실제 서비스용
↕ 동기 복제
Standby DB: 대기용&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1353&quot; data-start=&quot;1292&quot; data-ke-size=&quot;size16&quot;&gt;장애 발생 시 자동으로 Standby DB가 승격(Failover)되기 때문에 서비스 안정성을 높일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1353&quot; data-start=&quot;1292&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1371&quot; data-start=&quot;1355&quot; data-ke-size=&quot;size16&quot;&gt;반면 EC2 직접 구축에서는 아래내용을 직접 구축해야한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1413&quot; data-start=&quot;1372&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1388&quot; data-start=&quot;1372&quot; data-section-id=&quot;e7v4x5&quot;&gt;Replication 구성&lt;/li&gt;
&lt;li data-end=&quot;1402&quot; data-start=&quot;1389&quot; data-section-id=&quot;19ek2wg&quot;&gt;Failover 처리&lt;/li&gt;
&lt;li data-end=&quot;1413&quot; data-start=&quot;1403&quot; data-section-id=&quot;1lf0akg&quot;&gt;장애 감지 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1429&quot; data-start=&quot;1415&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;1434&quot; data-start=&quot;1431&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1454&quot; data-start=&quot;1436&quot; data-section-id=&quot;1q7xqqh&quot; data-ke-size=&quot;size26&quot;&gt;4. 모니터링 및 성능 관리&lt;/h2&gt;
&lt;p data-end=&quot;1485&quot; data-start=&quot;1456&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RDS는 AWS CloudWatch와 쉽게 연동&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-end=&quot;1493&quot; data-start=&quot;1487&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1545&quot; data-start=&quot;1494&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1503&quot; data-start=&quot;1494&quot; data-section-id=&quot;1wkp42q&quot;&gt;CPU 사용량&lt;/li&gt;
&lt;li data-end=&quot;1513&quot; data-start=&quot;1504&quot; data-section-id=&quot;1g5b0ac&quot;&gt;메모리 사용량&lt;/li&gt;
&lt;li data-end=&quot;1524&quot; data-start=&quot;1514&quot; data-section-id=&quot;14n7t5g&quot;&gt;Disk I/O&lt;/li&gt;
&lt;li data-end=&quot;1532&quot; data-start=&quot;1525&quot; data-section-id=&quot;ybt8bt&quot;&gt;커넥션 수&lt;/li&gt;
&lt;li data-end=&quot;1545&quot; data-start=&quot;1533&quot; data-section-id=&quot;5idb45&quot;&gt;Slow Query&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1565&quot; data-start=&quot;1547&quot; data-ke-size=&quot;size16&quot;&gt;등을 손쉽게 모니터링할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1613&quot; data-start=&quot;1567&quot; data-ke-size=&quot;size16&quot;&gt;또한 Performance Insights 기능을 통해 쿼리 병목 분석도 가능하다.&lt;/p&gt;
&lt;hr data-end=&quot;1618&quot; data-start=&quot;1615&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;1629&quot; data-start=&quot;1620&quot; data-section-id=&quot;wdvl5r&quot; data-ke-size=&quot;size26&quot;&gt;5. 확장성&lt;/h2&gt;
&lt;p data-end=&quot;1671&quot; data-start=&quot;1631&quot; data-ke-size=&quot;size16&quot;&gt;트래픽 증가 시 인스턴스 스펙 변경이나 스토리지 확장이 비교적 간단하다.&lt;/p&gt;
&lt;p data-end=&quot;1679&quot; data-start=&quot;1673&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;db.t3.micro &amp;rarr; db.r6g.large&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1746&quot; data-start=&quot;1721&quot; data-ke-size=&quot;size16&quot;&gt;처럼 콘솔에서 인스턴스 타입 변경이 가능하다.&lt;/p&gt;
&lt;p data-end=&quot;1783&quot; data-start=&quot;1748&quot; data-ke-size=&quot;size16&quot;&gt;또한 Read Replica를 통해 읽기 부하 분산도 지원한다.&lt;/p&gt;
&lt;hr data-end=&quot;1788&quot; data-start=&quot;1785&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1809&quot; data-start=&quot;1790&quot; data-section-id=&quot;1p1jqve&quot;&gt;RDS와 EC2 직접 구축 비교&lt;/h1&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2054&quot; data-start=&quot;1811&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody data-end=&quot;2054&quot; data-start=&quot;1850&quot;&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;RDS&lt;/td&gt;
&lt;td&gt;EC2 직접 구축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1876&quot; data-start=&quot;1850&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1858&quot; data-start=&quot;1850&quot;&gt;DB 설치&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1867&quot; data-start=&quot;1858&quot;&gt;AWS 관리&lt;/td&gt;
&lt;td data-end=&quot;1876&quot; data-start=&quot;1867&quot; data-col-size=&quot;sm&quot;&gt;직접 설치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1906&quot; data-start=&quot;1877&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1888&quot; data-start=&quot;1877&quot;&gt;OS/DB 패치&lt;/td&gt;
&lt;td data-end=&quot;1897&quot; data-start=&quot;1888&quot; data-col-size=&quot;sm&quot;&gt;AWS 관리&lt;/td&gt;
&lt;td data-end=&quot;1906&quot; data-start=&quot;1897&quot; data-col-size=&quot;sm&quot;&gt;직접 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1929&quot; data-start=&quot;1907&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1915&quot; data-start=&quot;1907&quot;&gt;자동 백업&lt;/td&gt;
&lt;td data-end=&quot;1920&quot; data-start=&quot;1915&quot; data-col-size=&quot;sm&quot;&gt;지원&lt;/td&gt;
&lt;td data-end=&quot;1929&quot; data-start=&quot;1920&quot; data-col-size=&quot;sm&quot;&gt;직접 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1964&quot; data-start=&quot;1930&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1938&quot; data-start=&quot;1930&quot;&gt;장애 복구&lt;/td&gt;
&lt;td data-end=&quot;1955&quot; data-start=&quot;1938&quot; data-col-size=&quot;sm&quot;&gt;자동 Failover 가능&lt;/td&gt;
&lt;td data-end=&quot;1964&quot; data-start=&quot;1955&quot; data-col-size=&quot;sm&quot;&gt;직접 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;1985&quot; data-start=&quot;1965&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1974&quot; data-start=&quot;1965&quot;&gt;운영 난이도&lt;/td&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1979&quot; data-start=&quot;1974&quot;&gt;낮음&lt;/td&gt;
&lt;td data-end=&quot;1985&quot; data-start=&quot;1979&quot; data-col-size=&quot;sm&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2009&quot; data-start=&quot;1986&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;1995&quot; data-start=&quot;1986&quot;&gt;커스터마이징&lt;/td&gt;
&lt;td data-end=&quot;2001&quot; data-start=&quot;1995&quot; data-col-size=&quot;sm&quot;&gt;제한적&lt;/td&gt;
&lt;td data-end=&quot;2009&quot; data-start=&quot;2001&quot; data-col-size=&quot;sm&quot;&gt;자유로움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2032&quot; data-start=&quot;2010&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2020&quot; data-start=&quot;2010&quot;&gt;Root 접근&lt;/td&gt;
&lt;td data-end=&quot;2026&quot; data-start=&quot;2020&quot; data-col-size=&quot;sm&quot;&gt;불가능&lt;/td&gt;
&lt;td data-end=&quot;2032&quot; data-start=&quot;2026&quot; data-col-size=&quot;sm&quot;&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2054&quot; data-start=&quot;2033&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;2043&quot; data-start=&quot;2033&quot;&gt;유지보수 부담&lt;/td&gt;
&lt;td data-end=&quot;2048&quot; data-start=&quot;2043&quot; data-col-size=&quot;sm&quot;&gt;낮음&lt;/td&gt;
&lt;td data-end=&quot;2054&quot; data-start=&quot;2048&quot; data-col-size=&quot;sm&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1 data-end=&quot;2083&quot; data-start=&quot;2061&quot; data-section-id=&quot;9qwooh&quot;&gt;RDS가 적합하지 않을 수 있는 상황&lt;/h1&gt;
&lt;p data-end=&quot;2119&quot; data-start=&quot;2085&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 매우 편리하지만 모든 상황에서 최선의 선택은 아니다.&lt;/p&gt;
&lt;h2 data-end=&quot;2143&quot; data-start=&quot;2121&quot; data-section-id=&quot;28k04y&quot; data-ke-size=&quot;size26&quot;&gt;1. OS 레벨 튜닝이 필요한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;-&amp;gt;데이터베이스 자체 설정이 아니라, DB가 돌아가는 서버 운영체제(OS)까지 직접 조정하는것.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;[EC2에 직접 PostgreSQL 설치 구조]&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778546026293&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[애플리케이션]
        &amp;darr;
[PostgreSQL]
        &amp;darr;
[Linux OS]
        &amp;darr;
[디스크 / 메모리 / CPU]&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 data-end=&quot;2164&quot; data-start=&quot;2145&quot;&gt;&lt;b&gt;&lt;i&gt;메모리 설정, 디스크 설정, 파일 시스템등등 직접 수정 가능하다.&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2164&quot; data-start=&quot;2145&quot; data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;[RDS 구조]&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778546145767&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[애플리케이션]
        &amp;darr;
[RDS(PostgreSQL)]
        &amp;darr;
[AWS가 관리하는 OS]&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 data-end=&quot;2164&quot; data-start=&quot;2145&quot;&gt;&lt;b&gt;&lt;i&gt;여기서 사용자는 DB만 접근 가능하고 아래 OS에는 접근 자체가 안된다.&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;2164&quot; data-start=&quot;2145&quot;&gt;&lt;b&gt;&lt;i&gt;즉, SSH 접속 불가, sudo 불가, root 권한 없다.&lt;/i&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2164&quot; data-start=&quot;2145&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2164&quot; data-start=&quot;2145&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 관리형 서비스이기 때문에:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2203&quot; data-start=&quot;2165&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2177&quot; data-start=&quot;2165&quot; data-section-id=&quot;1bjm5oe&quot;&gt;Root 권한 제한: EC2 직접 구축에서는 sudo su 같은 명령으로 서버 관리자(root)가 될 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;2188&quot; data-start=&quot;2178&quot; data-section-id=&quot;madkc5&quot;&gt;커널 튜닝 불가&lt;/li&gt;
&lt;li data-end=&quot;2203&quot; data-start=&quot;2189&quot; data-section-id=&quot;1rubk8k&quot;&gt;파일 시스템 제어 제한&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2215&quot; data-start=&quot;2205&quot; data-ke-size=&quot;size16&quot;&gt;등의 제약이 있다.&lt;/p&gt;
&lt;p data-end=&quot;2263&quot; data-start=&quot;2217&quot; data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;특수한 성능 튜닝이 필요한 경우에는 EC2 직접 구축이 더 적합할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2263&quot; data-start=&quot;2217&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2263&quot; data-start=&quot;2217&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;i&gt;&lt;b&gt;-&amp;gt; 즉, DB가 동작하는 OS/메모리/디스크 환경까지 세밀하게 최적화 할 수 있다는 장점.&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr data-end=&quot;2268&quot; data-start=&quot;2265&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2295&quot; data-start=&quot;2270&quot; data-section-id=&quot;191wvnk&quot; data-ke-size=&quot;size26&quot;&gt;2. 특정 확장 기능 사용이 필요한 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;-&amp;gt; PostgreSQL 기본 기능만으로 부족해서 추가 기능을 설치하는 경우&lt;/b&gt;&lt;/i&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;ex)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;uuid-ossp: UUID 랜덤 생성&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;pg_trgm: 문자열 유사도 검색 기능&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;등등의 확장 기능을 추가하고싶을경우 RDS에서는 어렵다.&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2342&quot; data-start=&quot;2297&quot; data-ke-size=&quot;size16&quot;&gt;일부 PostgreSQL Extension이나 커스텀 플러그인은 제한될 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2350&quot; data-start=&quot;2344&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2396&quot; data-start=&quot;2351&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2368&quot; data-start=&quot;2351&quot; data-section-id=&quot;shhcto&quot;&gt;특수 Extension 설치&lt;/li&gt;
&lt;li data-end=&quot;2382&quot; data-start=&quot;2369&quot; data-section-id=&quot;pkoqjt&quot;&gt;커스텀 바이너리 사용&lt;/li&gt;
&lt;li data-end=&quot;2396&quot; data-start=&quot;2383&quot; data-section-id=&quot;zg519b&quot;&gt;특정 DB 엔진 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2427&quot; data-start=&quot;2398&quot; data-ke-size=&quot;size16&quot;&gt;등이 필요한 경우에는 직접 구축 환경이 더 유연하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-end=&quot;2454&quot; data-start=&quot;2434&quot; data-section-id=&quot;q9ctxk&quot; data-ke-size=&quot;size26&quot;&gt;3. 비용 최적화가 중요한 경우&lt;/h2&gt;
&lt;p data-end=&quot;2468&quot; data-start=&quot;2456&quot; data-ke-size=&quot;size16&quot;&gt;소규모 프로젝트에서는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2488&quot; data-start=&quot;2469&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2488&quot; data-start=&quot;2469&quot; data-section-id=&quot;4pibfj&quot;&gt;EC2 한 대에 직접 DB 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2505&quot; data-start=&quot;2490&quot; data-ke-size=&quot;size16&quot;&gt;방식이 더 저렴할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2546&quot; data-start=&quot;2507&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 관리형 서비스 비용이 포함되기 때문에 상대적으로 비용이 높다.&lt;/p&gt;
&lt;p data-end=&quot;2551&quot; data-start=&quot;2548&quot; data-ke-size=&quot;size16&quot;&gt;특히:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2586&quot; data-start=&quot;2552&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2562&quot; data-start=&quot;2552&quot; data-section-id=&quot;2n4bhz&quot;&gt;Multi-AZ&lt;/li&gt;
&lt;li data-end=&quot;2571&quot; data-start=&quot;2563&quot; data-section-id=&quot;oavuvp&quot;&gt;백업 저장소&lt;/li&gt;
&lt;li data-end=&quot;2586&quot; data-start=&quot;2572&quot; data-section-id=&quot;n7sd96&quot;&gt;Read Replica&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2613&quot; data-start=&quot;2588&quot; data-ke-size=&quot;size16&quot;&gt;등을 사용하면 비용 증가 폭이 커질 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h1 data-end=&quot;2625&quot; data-start=&quot;2620&quot; data-section-id=&quot;20h5rq&quot;&gt;마무리&lt;/h1&gt;
&lt;p data-end=&quot;2671&quot; data-start=&quot;2627&quot; data-ke-size=&quot;size16&quot;&gt;RDS는 데이터베이스 운영 부담을 크게 줄여주는 매우 강력한 관리형 서비스이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2714&quot; data-start=&quot;2677&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2688&quot; data-start=&quot;2677&quot; data-section-id=&quot;1ncc46t&quot;&gt;빠른 서비스 구축&lt;/li&gt;
&lt;li data-end=&quot;2698&quot; data-start=&quot;2689&quot; data-section-id=&quot;4wbg1l&quot;&gt;안정적인 운영&lt;/li&gt;
&lt;li data-end=&quot;2714&quot; data-start=&quot;2699&quot; data-section-id=&quot;hcwspa&quot;&gt;자동 백업 및 장애 대응&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2748&quot; data-start=&quot;2716&quot; data-ke-size=&quot;size16&quot;&gt;이 중요한 일반적인 웹 서비스 환경에서는 매우 효율적이다.&lt;/p&gt;
&lt;p data-end=&quot;2748&quot; data-start=&quot;2716&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2753&quot; data-start=&quot;2750&quot; 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-end=&quot;2796&quot; data-start=&quot;2754&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2768&quot; data-start=&quot;2754&quot; data-section-id=&quot;1a9lest&quot;&gt;OS 레벨 커스터마이징&lt;/li&gt;
&lt;li data-end=&quot;2780&quot; data-start=&quot;2769&quot; data-section-id=&quot;1xayjq8&quot;&gt;특수한 성능 튜닝&lt;/li&gt;
&lt;li data-end=&quot;2796&quot; data-start=&quot;2781&quot; data-section-id=&quot;t53yyr&quot;&gt;고도의 DBA 운영 환경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2834&quot; data-start=&quot;2798&quot; data-ke-size=&quot;size16&quot;&gt;이 필요한 경우에는 EC2 직접 구축 방식이 더 적합할 수 있다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2875&quot; data-start=&quot;2836&quot; data-ke-size=&quot;size16&quot;&gt;결국 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;중요한 것은 서비스 규모와 운영 목적에 맞는 선택&lt;/b&gt;&lt;/span&gt;이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>위클리페이퍼</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/73</guid>
      <comments>https://kwakscomputerengineering.tistory.com/73#entry73comment</comments>
      <pubDate>Tue, 12 May 2026 10:00:31 +0900</pubDate>
    </item>
    <item>
      <title>Monew 프로젝트 개선: 뉴스기사 목록 조회 성능 개선 - 복합 인덱스 적용기</title>
      <link>https://kwakscomputerengineering.tistory.com/72</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Monew 프로젝트 메인 페이지]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4N5Q7/dJMcagrSu8N/0rJuJAYfgEPBDclEvOW0B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4N5Q7/dJMcagrSu8N/0rJuJAYfgEPBDclEvOW0B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4N5Q7/dJMcagrSu8N/0rJuJAYfgEPBDclEvOW0B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4N5Q7%2FdJMcagrSu8N%2F0rJuJAYfgEPBDclEvOW0B0%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;1281&quot; height=&quot;816&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&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;pre id=&quot;code_1778132393527&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WHERE is_deleted = false
  AND source = 'NAVER'
ORDER BY published_at DESC, created_at DESC
LIMIT 11&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 뉴스기사 목록 조회 API는 위의 조건을 기본으로 사용하고있다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-section-id=&quot;jtqorj&quot; data-end=&quot;517&quot; data-start=&quot;508&quot;&gt;댓글 수 조회&lt;/li&gt;
&lt;li data-section-id=&quot;y0fbsn&quot; data-end=&quot;542&quot; data-start=&quot;518&quot;&gt;사용자별 조회 여부(viewedByMe)&lt;/li&gt;
&lt;li data-section-id=&quot;4x9g51&quot; data-end=&quot;555&quot; data-start=&quot;543&quot;&gt;관심사 기반 필터링&lt;/li&gt;
&lt;li data-section-id=&quot;xkc5zo&quot; data-end=&quot;567&quot; data-start=&quot;556&quot;&gt;커서 페이지네이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[실제 QueryDSL 기반 조회 쿼리]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778132568369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
    na1_0.id,
    na1_0.source,
    na1_0.original_link,
    na1_0.title,
    na1_0.published_at,
    na1_0.summary,
    count(distinct c1_0.id),
    na1_0.view_count,
    av1_0.id is not null,
    na1_0.created_at
from news_articles na1_0
left join article_interests ai1_0
    on ai1_0.news_article_id = na1_0.id
left join comments c1_0
    on c1_0.article_id = na1_0.id
left join article_views av1_0
    on av1_0.news_article_id = na1_0.id
where
    na1_0.is_deleted = false
    and na1_0.source = 'NAVER'
group by ...
order by
    na1_0.published_at desc,
    na1_0.created_at desc
fetch first 11 rows only;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;source, is_deleted, published_at, created_at의 컬럼들이 반복적&lt;/b&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;별도의 복합 인덱스가 존재하지 않아 DB는 조회시 &lt;b&gt;조건 필터링, 정렬 수행, LIMIT 적용 과정을 매번 수행&lt;/b&gt;해야했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 ORDER BY published_at DESC, created_at DESC 구간에서 정렬 비용이 증가할 가능성이 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;왜 인덱스를 선택했는가?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 개선 방법은 여러 선택지가 있었다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 캐싱, QueryDSL 동적 조인 제거, 댓글 수 비정규화, 복합 인덱스 추가등 ..&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;Redis를 우선 적용하지 않은 이유&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;현재 뉴스기사 데이터 수는 약 200건 수준으로 많지 않았고, 캐싱보다 DB 조회 구조 자체를 먼저 최적화하는게 우선.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redis는 캐시 키 설계, TTL 관리, 캐시 무효화 전략등 추가 고려사항이 존재&lt;/b&gt;했다.&lt;/li&gt;
&lt;li&gt;현재 &lt;b&gt;병목은 반복되는 WHERE + ORDER BY 패턴&lt;/b&gt;에 가깝기때문에 DB 레벨에서 조회 경로를 최적화 하는 인덱스 추가가 가장 직접적인 해결 방법이라고 판단했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인덱스의 이해&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본키(PK) 만들면 DB가 자동으로 인덱스를 생성한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778135335782&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE users (
    id UUID PRIMARY KEY,
    nickname VARCHAR(50)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778135353715&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;id
 ├── a123
 ├── b234
 ├── c345&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK는 유일성을 띄니까 이런식으로 자료구조를 만든다. 그래서 &lt;b&gt;Where id='a123'을 하면 테이블 전체를 안뒤지고 바로 a123으로 간다&lt;/b&gt;. 인덱스 설정이 안돼있으면 테이블 1행부터 하나하나 찾아서 조회한다.&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;[인덱스가 없는경우 보통 DB의 조회]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778135900375&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;1행 읽기
&amp;rarr; source=CHOSUN
&amp;rarr; 조건 불만족
&amp;rarr; 다음 행

2행 읽기
&amp;rarr; source=NAVER 만족
&amp;rarr; is_deleted=true
&amp;rarr; 조건 불만족
&amp;rarr; 다음 행

3행 읽기
&amp;rarr; source=NAVER 만족
&amp;rarr; is_deleted=false 만족
&amp;rarr; 결과 포함

4행 읽기
&amp;rarr; source=YONHAP
&amp;rarr; 불만족

5행 읽기
&amp;rarr; source=NAVER 만족
&amp;rarr; is_deleted=false 만족
&amp;rarr; 결과 포함&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[인덱스 설계]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778135051292&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_news_articles_source_deleted_published_created
ON news_articles (
    source,
    is_deleted,
    published_at DESC,
    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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1778135590992&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;source
 └── NAVER
      └── is_deleted=false
           └── published_at DESC
                └── created_at DESC&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;&lt;span&gt;복합 인덱스는 source &amp;rarr; is_deleted &amp;rarr; published_at DESC &amp;rarr; created_at DESC&lt;/span&gt;&lt;br /&gt;&lt;span&gt;순서 기준으로 정렬된 B-Tree 구조를 생성한다. &lt;/span&gt;&lt;span&gt;따라서 PostgreSQL은 테이블 전체를 순차 탐색하지 않고,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;조건에 해당하는 범위로 빠르게 접근할 수 있다.&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;&lt;b&gt; &lt;span&gt;PostgreSQL의 B-Tree 인덱스는 트리 구조 기반으로 데이터를 관리하기 때문에, &lt;/span&gt;&lt;span&gt;탐색 비용이 O(log N) 수준으로 감소한다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span&gt;예를 들어 데이터가 많아질수록 Sequential Scan은 전체 row를 순차 탐색해야 하지만,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;B-Tree 인덱스는 소수의 트리 탐색만으로 원하는 범위에 접근할 수 있다.&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;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;[인덱스 추가 결과]&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;현재 286개 뉴스기사 데이터가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같이 복합 인덱스를 추가하고 돌렸을때 순차 스캔 방식이 비용이 더 싸다고 PostgreSQL의 옵티마이저가 판단해서&amp;nbsp;INDEX를 적용한 조회를 하지 않았다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;[마무리]&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실제로 EXPLAIN ANALYZE 결과를 확인했을 때, &lt;/span&gt;&lt;span&gt;현재 데이터 수가 적은 환경에서는 Seq Scan이 선택되었지만,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;정렬 비용(Sort)과 그룹 집계(GroupAggregate)가 주요 비용 요소로 나타나는 것을 확인&lt;/b&gt;&lt;/span&gt;할 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;-&amp;gt; 그룹 집계:&amp;nbsp; ex)뉴스기사별 댓글 수 구하는 작업&lt;/i&gt;&lt;/b&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;span&gt;특히 뉴스기사 데이터는 지속적으로 증가하는 특성이 있기 때문에, &lt;/span&gt;&lt;span&gt;향후 대량 데이터 환경에서 Index Scan을 활용할 수 있도록&lt;/span&gt;&lt;br /&gt;&lt;span&gt;기본 조회 패턴에 맞춘 복합 인덱스를 선제적으로 적용했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;span&gt;실제 Seq Scan은 0.1ms 수준이였지만 GroupAggregate은 0.9ms/ Sort도 1.096ms 을 사용했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;-&amp;gt; &lt;i&gt;&lt;b&gt;테이블 읽기 자체보다 Group By + 정렬 작업이 더 무겁다.&lt;/b&gt;&lt;/i&gt;&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;size14&quot;&gt;&lt;span&gt;GroupAggregate: 같은 기사끼리 묶기, 댓글 개수 계산&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;Sort: published_at DESC 정렬&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;b&gt;[그래도 인덱스 만들었는데 강제 적용해봤다.]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1778164573192&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT
    na.id,
    na.source,
    na.original_link,
    na.title,
    na.published_at,
    na.summary,
    COUNT(DISTINCT c.id),
    na.view_count,
    av.id IS NOT NULL,
    na.created_at
FROM news_articles na
         LEFT JOIN article_interests ai
                   ON ai.news_article_id = na.id
         LEFT JOIN comments c
                   ON c.article_id = na.id
                       AND c.is_deleted = false
         LEFT JOIN article_views av
                   ON av.news_article_id = na.id
                       AND av.user_id = '71a099cb-4f16-471e-844a-a1d51248dc32'
WHERE na.is_deleted = false
  AND na.source = 'NAVER'
GROUP BY
    na.id,
    na.source,
    na.original_link,
    na.title,
    na.published_at,
    na.summary,
    na.view_count,
    av.id,
    na.created_at
ORDER BY
    na.published_at DESC,
    na.created_at DESC
    FETCH FIRST 11 ROWS ONLY;&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;1346건 기준,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 적용전: Execution Time: 14.334ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;인덱스 적용후: Execution Time: 13.728 ms&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778165366834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Index Cond: (((source)::text = 'NAVER'::text) AND (is_deleted = false))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;source와 is_deleted는 인덱스를 탔는데 published_at DESC와 created_at DESC는 정렬까지는 쓰지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778165453235&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LEFT JOIN comments
COUNT(DISTINCT c.id)
GROUP BY ...
LEFT JOIN article_interests
LEFT JOIN article_views&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조인하고 집계하면서 row 순서가 깨져서 PostgreSQL이 인덱스 순서를 그대로 최종 정렬에 쓰기 어려워진것.&lt;/p&gt;</description>
      <category>코드잇 스프린트/실습</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/72</guid>
      <comments>https://kwakscomputerengineering.tistory.com/72#entry72comment</comments>
      <pubDate>Thu, 7 May 2026 23:52:01 +0900</pubDate>
    </item>
    <item>
      <title>Docker 컨테이너 포트를 80으로 쓰면 안되는 이유</title>
      <link>https://kwakscomputerengineering.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[Dockerfile]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777468648834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#애플리케이션 포트
EXPOSE 80

# 컨테이너 시작 시 Spring Boot 실행
ENTRYPOINT [&quot;sh&quot;, &quot;-lc&quot;, &quot;exec java $JAVA_TOOL_OPTIONS -jar /app/app.jar --server.port=80&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 내부에서 애플리케이션을 80번 포트로 실행하도록 구성했다.&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;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;i&gt;&quot;컨테이너 내부 포트를 80으로 고정하면 non-root 사용자 실행이 불가능합니다.&quot;&lt;/i&gt;&lt;/span&gt;&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.Linux의 &quot;특권 포트&quot; 개념&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0 ~ 1023 포트 = 특권포트&lt;/li&gt;
&lt;li&gt;이 포트들은 root 권한이 있어야만 바인딩 가능하다.&lt;/li&gt;
&lt;li&gt;Spring Boot -&amp;gt; 80 포트 사용 -&amp;gt; root 권한 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.컨테이너 보안 문제&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Docker 컨테이너는 보안상 non-root 사용자로 실행하는 방식을 권장한다.&lt;/li&gt;
&lt;li&gt;지금 구조에서는 --server.port=80 이기때문에 &lt;b&gt;non-root로 실행&lt;/b&gt;하면 Permission denied / BindException 발생 -&amp;gt; &lt;b&gt;애플리케이션 기동 실패 문제가 발생한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※root 권한이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리눅스에서 모든걸 할 수 있는 &quot;최고 관리자 권한&quot;&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;파일 삭제/수정/권한 변경 전부 가능&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;네트워크, 프로세스, 시스템 설정 전부 제어.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;root = 시스템의 주인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;non-root = 일반사용자(제한된 권한)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[이해 Time]&lt;/b&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;gt; http://서버IP:80 -&amp;gt; 컨테이너:80 -&amp;gt; Spring Boot&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사람들이 80번 문으로 들어온다.&lt;/li&gt;
&lt;li&gt;사용자가 들어온다고 해서 서버를 마음대로 건드릴 수는 없다.&lt;/li&gt;
&lt;li&gt;GET /api/articles 이런 API 호출뿐 ,,,&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;IF 취약한 코드 존재.&lt;/p&gt;
&lt;pre id=&quot;code_1777469519086&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/run&quot;)
public String run(@RequestParam String cmd) throws Exception {

	//문자열을 OS명령어로 넘겨서 실행하는 코드
    return Runtime.getRuntime().exec(cmd).toString();
}&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;사용자가 GET /run/cmd=ls 요청 보내면 서버 내부 명령 실행된다.&lt;/li&gt;
&lt;li&gt;GET /run?cmd=rm -rf / 이런식으로 보내면 서버 파일 삭제 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금처럼 80 포트 = root로 실행중이면 서버 파일 삭제 가능하다. DB 접근이나 시스템 명령 실행 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;root로 실행중이 아니라면 권한부족으로 실패돼서 피해가 제한된다.&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 id=&quot;code_1777469876988&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /run?cmd=rm -rf /&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777469887599&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Runtime.getRuntime().exec(cmd);&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;Spring Boot -&amp;gt; OS(리눅스) 에게 명령 요청 -&amp;gt; rm -rf /실행&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;[root]&lt;/p&gt;
&lt;pre id=&quot;code_1777470098785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring Boot (root)
   &amp;darr;
exec(&quot;rm -rf /&quot;)
   &amp;darr;
컨테이너 내부 파일 삭제
   &amp;darr;
앱 죽음&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;[non-root]&lt;/p&gt;
&lt;pre id=&quot;code_1777470124889&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Spring Boot (appuser)
   &amp;darr;
exec(&quot;rm -rf /&quot;)
   &amp;darr;
권한 없음
   &amp;darr;
실행 실패&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;</description>
      <category>코드잇 스프린트/실습</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/71</guid>
      <comments>https://kwakscomputerengineering.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 29 Apr 2026 22:57:43 +0900</pubDate>
    </item>
    <item>
      <title>뉴스기사 조회수 증가 동시성 문제 트러블 슈팅</title>
      <link>https://kwakscomputerengineering.tistory.com/70</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[문제 상황]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뉴스기사 조회 API에서 사용자가 기사를 클릭하면 이 두 작업을 하나의 트랜잭션에서 처리하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 조회 이력 1건 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 기사 조회수 1증가&lt;/p&gt;
&lt;pre id=&quot;code_1776904105430&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ===== 비즈니스 로직 =====
    public void increaseViewCount() {
        this.viewCount++;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt; 동시성 문제 발생&lt;/b&gt; 할 수 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[고려한 해결 방법]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. DB 레벨 원자적 증가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776905338189&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE news_article
SET view_count = view_count + 1
WHERE 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;&lt;b&gt;2. 비관적 락&lt;/b&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;&lt;b&gt;3. 낙관적 락&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버전 컬럼(@Version)을 두고, 수정 충돌이 발생하면 예외를 발생시켜 재시도 하는 방식&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;1번 방식을 선택하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776904371934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//뉴스기사 조회수 증가
  @Modifying
  @Query(&quot;UPDATE NewsArticle n SET n.viewCount = n.viewCount + 1 WHERE n.id = :articleId&quot;)
  void incrementViewCount(@Param(&quot;articleId&quot;) UUID articleId);&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;엔티티를 읽지 않고 바로 UPDATE 쿼리를 날린다.&lt;/li&gt;
&lt;li&gt;@Modifying: 기본적으로 @Query는 SELECT용이라서 UPDATE시 붙여줘야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[기존 방식]&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776904519686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;NewsArticle article = findById(id);
article.increaseViewCount(); // viewCount++&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;DB에서 조회하고, 메모리에서 +1한다음, 다시 DB에 저장하는 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 초기값 10, 동시에 2개 요청&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A: 10읽음 -&amp;gt; 11 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B: 10읽음 -&amp;gt; 11 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과: 11 (Lost Update)&lt;/b&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB가 직접 현재 값 기준으로 +1 수행하기때문에 읽기 없이 바로 반영된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) A: DB -&amp;gt; 10 -&amp;gt; &lt;b&gt;11&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; B: DB -&amp;gt; &lt;b&gt;11&lt;/b&gt; -&amp;gt; 12&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 각 UPDATE를 순차적으로 처리하면서 값 누락이 없다.&lt;/p&gt;</description>
      <category>코드잇 스프린트/실습</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/70</guid>
      <comments>https://kwakscomputerengineering.tistory.com/70#entry70comment</comments>
      <pubDate>Thu, 23 Apr 2026 13:45:10 +0900</pubDate>
    </item>
    <item>
      <title>MoNew 프로젝트: ERD 설계 데이터 타입에 대한 고민(VARCHAR vs TEXT)</title>
      <link>https://kwakscomputerengineering.tistory.com/69</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZBTBh/dJMcajogLTB/QdPfQo6e6aK6svrpVgeGh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZBTBh/dJMcajogLTB/QdPfQo6e6aK6svrpVgeGh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZBTBh/dJMcajogLTB/QdPfQo6e6aK6svrpVgeGh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZBTBh%2FdJMcajogLTB%2FQdPfQo6e6aK6svrpVgeGh0%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;418&quot; height=&quot;233&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;233&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. VARCHAR(255)가 최대 아닌가? : 그냥 관례이다.&lt;/b&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;VARCHAR(1000),10000도 가능하다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;PostgreSQL의 VARCHAR(n)은 최대 약 1GB까지 가능하다.&lt;/li&gt;
&lt;li&gt;극단적으로 보면 PostgreSQL에서 VARCHAR는 약 &lt;b&gt;VARCHAR(1073741824)까지 가능&lt;/b&gt;하다. 약 10억자.&lt;/li&gt;
&lt;li&gt;뉴스기사 제공 API문서에 최대 데이터 수를 CHECK&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 요약의 TEXT는 적절한가?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;summary가 10자일수도, 500자일수도 있다.&lt;/li&gt;
&lt;li&gt;PostgreSQL에서 TEXT는 가변길이 그대로 저장된다.&lt;/li&gt;
&lt;li&gt;따라서 VARCHAR로 지정하는것보다 TEXT가 좋다고 생각한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그럼 가변으로 저장되는거면, title과 original_link도 TEXT타입으로 저장하는게 좋은거 아니냐?&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;무결성때문에&lt;/b&gt;&lt;/span&gt; VARCHAR로 저장하는게 좋다.&lt;/li&gt;
&lt;li&gt;ex) 제목은 길어봐야 2,300자 수준이다. 근데 제목이 1000자가 들어오면 비정상 데이터로 판단할 줄 알아야한다.&lt;/li&gt;
&lt;li&gt;따라서 VARCHAR로 설정함으로써 DB레벨에서 막을 수 있어야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;*데이터 무결성: 데이터가 정확하고, 일관되고, 신뢰할 수 있는 상태를 유지하는것.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;ex) emal: &quot;abc&quot; x / age: -5/ title: 1000짜리 문자열: 비정상 데이터&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 조회수의 BIGINT는 적절한가?&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BIGINT는 9경정도까지의 숫자가 가능하다.&lt;/li&gt;
&lt;li&gt;같은사용자가 여러번 조회해도 1회로 친다는 요구사항에 봤을때 BIGINT는 과하다.&lt;/li&gt;
&lt;li&gt;INT의 범위가 21억까지 이므로 INT만해도 충분하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론: INT로 변경&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코드잇 스프린트/실습</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/69</guid>
      <comments>https://kwakscomputerengineering.tistory.com/69#entry69comment</comments>
      <pubDate>Wed, 15 Apr 2026 11:33:18 +0900</pubDate>
    </item>
    <item>
      <title>애플리케이션의 각 계층에서 수행되는 입력값 검증의 범위와 책임</title>
      <link>https://kwakscomputerengineering.tistory.com/68</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. Controller 계층&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적:&amp;nbsp;&lt;/b&gt;사용자 경험 보호, 빠른 피드백&lt;/p&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;필수 필드 존재 여부(null, empty)&lt;/li&gt;
&lt;li&gt;형식검증(이메일 형식, 날짜형식, 전화번호 패턴 ..)&lt;/li&gt;
&lt;li&gt;길이/범위 제함(글자수, 숫자 점위)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원칙:&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;이 계층의 검증은 신뢰할 수 없다.&lt;/b&gt;&lt;/span&gt; 클라이언트 검증은 언제든 우회 가능하므로, 보안을 위한 게이트로 삼으면 안된다.&lt;/p&gt;
&lt;pre id=&quot;code_1775089423978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Controller: 형식과 필수값만 검증
@PostMapping(&quot;/users&quot;)
public ResponseEntity&amp;lt;?&amp;gt; createUser(@Valid @RequestBody CreateUserRequest request) {
    // @Valid: @NotNull, @Email, @Size 등 포맷 검증만 수행
    return userService.createUser(request);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Valid: @NotNull, @Email, @Size등 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;포맷 검증만 수행한다.&lt;/b&gt;&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;&lt;b&gt;2. Service 계층&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적:&amp;nbsp;&lt;/b&gt;비즈니스 규칙 수호, 도메인 불변식 보장&lt;/p&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;비즈니스 규칙(ex: 만 19세 이상만 가입, 잔액 초과 출금 불가 ..)&lt;/li&gt;
&lt;li&gt;*도메인 불변식(주문 상태 전이 유효성)&lt;/li&gt;
&lt;li&gt;*교차 엔티티 일관성(A와 B가 동시에 존재해야하는 관계)&lt;/li&gt;
&lt;li&gt;*권한/컨텍스트 기반 규칙&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;*도메인 불변식 : &lt;/b&gt;&quot;불변식&quot;이란 객체가 살아있는 동안 항상 참이어야 하는 규칙.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;주문 상태 전이를 예로 들면: 주문생성 -&amp;gt; 결제완료 -&amp;gt; 배송중 -&amp;gt; 배송완료&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;-&amp;gt; 취소 가능/ 취소 불가&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;&lt;b&gt;이 흐름을 절대 어겨서는 안된다.&lt;/b&gt; 배송완료된 주문을 갑자기 결제완료로 되돌리거나, 주문 생성에서 바로 배송완료로 건너뛰는건 불가능해야한다.&lt;/i&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;*교차 엔티티 일관성 :&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;예) 팀 - 팀장의 관계&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;팀이 존재하면 -&amp;gt; 팀장 한명이 있어야한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;팀장이 퇴사하면 -&amp;gt; 팀을 없애거나 팀장을 교체해야한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;예2) Order - OrderItem 관계&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;주문은 반드시 1개 이상의 상품을 가져야한다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;마지막 상품을 지우면 주문 자체도 취소. 빈 주문은 존재할 수 없다.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt; -&amp;gt; 단순한 null 체크나 비즈니스 규칙이 아니라 &quot;두 엔티티의 관계 자체가 깨지면 안된다&quot;&lt;/i&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;&lt;i&gt;*권한/컨텍스트 기반 규칙 :&amp;nbsp;&lt;/i&gt;&lt;/b&gt;&lt;i&gt;같은 행위라도 누가 하느냐에 따라 달라짐.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;동일한 요청이라도 호출자의 역할이나 상황에 따라 허용 여부가 달라지는 규칙.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;예) &lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;권한기반: 본인 글이거나 관리자만 삭제 가능.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;컨텍스트 기반: 관리자도 공지상항은 슈퍼관리자만 삭제 가능.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;예2) 같은 API라도 시간, 상태에 따라 달라지는 컨텍스트 규칙.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;경매가 진행 중일 때만 입찰가능.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;i&gt;본인이 만든 경매에는 입찰 불가.&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775089606990&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Service: 비즈니스 규칙 검증
public void withdraw(Long accountId, Money amount) {
    Account account = accountRepository.findById(accountId);

    // 비즈니스 규칙: 잔액 부족, 계좌 상태, 일일 한도
    if (account.isLocked()) throw new AccountLockedException();
    if (account.getBalance().isLessThan(amount)) throw new InsufficientFundsException();
    if (account.getDailyWithdrawn().plus(amount).exceeds(DAILY_LIMIT))
        throw new DailyLimitExceededException();

    account.withdraw(amount);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. Repository 계층&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;목적: &lt;/b&gt;데이터 무결성 최후 방어선&lt;/p&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;DB제약조건(UNIQUE, NOT NULL, FK, CHECK)&lt;/li&gt;
&lt;li&gt;*트랜잭션 격리 수준에 따른 동시성 보호&lt;/li&gt;
&lt;li&gt;*외부 시스템 연동시 응답값 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1775090628247&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- DB는 최후의 안전망으로서 제약을 유지
ALTER TABLE users
    ADD CONSTRAINT uq_email UNIQUE (email),
    ADD CONSTRAINT chk_age CHECK (age &amp;gt;= 19);&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;size14&quot;&gt;&lt;b&gt;*트랜잭션 격리 수준에 따른 동시성 보호&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&quot;동시에 여러 요청이 들어왔을때 데이터가 꼬이지 않게 보호하는 것&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;1. 비관적 락(Pessimistic Lock)으로 해결: 내가 읽는 순간 다른 트랜잭션은 이 행에 손대지 마.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;2. 낙관적 락(Optimistic Lock)으로 해결: 충돌이 드물거라 가정하고, 저장할때 버전 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;b&gt;*외부 시스템 연동시 응답값 검증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;외부 API는 내코드가 아니므로, 응답을 믿지 말고 반드시 검증해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;중복검증의 트레이드 오프&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복 검증을 완전히 없애는것이 항상 옳지는 않다. &lt;b&gt;의도적 중복과 우발적 중복을 구분해야한다.&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;의도적 중복&lt;/td&gt;
&lt;td&gt;우발적 중복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;계층별 방어, 보안 강화&lt;/td&gt;
&lt;td&gt;코드 복붙, 설계 부재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;이메일 형식을 Controller&amp;middot;DB 모두 검증&lt;/td&gt;
&lt;td&gt;동일한 비즈니스 규칙을 Controller&amp;middot;Service 양쪽에 작성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;유지보수&lt;/td&gt;
&lt;td&gt;각 계층이 독립적으로 역할 수행&lt;/td&gt;
&lt;td&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;원칙1. 비즈니스 규칙은 Service에만.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;비즈니스 규칙이 Controller에 있으면 API가 바뀔 때마다 규칙도 흩어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원칙2. DB제약은 절대 제거 X&lt;/b&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;원칙3. 검증 로직을 도메인 객체로 응집.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775436287141&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;public class Email {
    private final String value;

    public Email(String value) {
        if (value == null || !value.matches(&quot;^[\\w.]+@[\\w.]+\\.[a-z]{2,}$&quot;))
            throw new InvalidEmailException(value);
        this.value = value;
    }
}

// Service, Controller 어디서 생성해도 동일한 규칙 적용
Email email = new Email(request.getEmail());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;</description>
      <category>코드잇 스프린트/Spring 이론</category>
      <author>과컴</author>
      <guid isPermaLink="true">https://kwakscomputerengineering.tistory.com/68</guid>
      <comments>https://kwakscomputerengineering.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 6 Apr 2026 09:46:18 +0900</pubDate>
    </item>
  </channel>
</rss>