<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>김원숭</title>
    <link>https://h0wdy.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 20 Jul 2026 06:11:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yooncandooit</managingEditor>
    <image>
      <title>김원숭</title>
      <url>https://tistory1.daumcdn.net/tistory/5560861/attach/6b650d41e0b04d7ebc7a76d5a0c1da75</url>
      <link>https://h0wdy.tistory.com</link>
    </image>
    <item>
      <title>[알고리즘] 가장 짧은 문자거리</title>
      <link>https://h0wdy.tistory.com/90</link>
      <description>&lt;hr&gt;
&lt;p&gt;한 개의 문자열 s와 문자 t가 주어지면 문자열 s의 각 문자가 문자 t와 떨어진 최소거리를 출력하는 프로그램을 작성하세요.&lt;/p&gt;
&lt;h4&gt;입력설명&lt;/h4&gt;
&lt;p&gt;첫 번째 줄에 문자열 s와 문자 t가 주어진다. 문자열과 문자는 소문자로만 주어진다. 문자열의 길이는 100을 넘지 않는다.&lt;/p&gt;
&lt;h4&gt;출력설명&lt;/h4&gt;
&lt;p&gt;첫 번째 줄에 각 문자열 s의 각 문자가 문자t와 떨어진 거리를 순서대로 출력한다.&lt;/p&gt;
&lt;h4&gt;입력예제&lt;/h4&gt;
&lt;p&gt;teachermode, e&lt;/p&gt;
&lt;h4&gt;출력예제&lt;/h4&gt;
&lt;p&gt;10121012210&lt;/p&gt;
&lt;hr&gt;
&lt;h4&gt;문제풀이&lt;/h4&gt;
&lt;p&gt;완전탐색 문제다. 왼쪽에서 오른쪽으로 탐색하는 코드는 비교적 쉽게 작성할 수 있다.&lt;br&gt;이제 다시 오른쪽에서 왼쪽으로 검색하며, 배열 내 숫자를 검색하게 되는데 이때 &lt;code&gt;Math.min()&lt;/code&gt;메소드를 사용하게 된다.&lt;br&gt;카운팅 넘버를 1000으로 잡아둔 후, 왼쪽에서 오른쪽으로 순회하며 답으로 도출할 값을 구한후, 다시 원본 배열을 토대로 오른쪽에서 왼쪽으로 순회를 하며 카운팅넘버를 매긴다.&lt;/p&gt;
&lt;p&gt;마지막으로 값으로 넘긴 배열의 값을 &lt;code&gt;answer[i] = Math.min(answer[i], 카운팅넘버)&lt;/code&gt;로 가장 작은 값을 치환해주면 된다.&lt;/p&gt;
&lt;p&gt;복잡해보이지만, 기준 값인 &lt;code&gt;counting&lt;/code&gt;의 변수를 다루는 기본적인 연습을 하도록 고안된 문제이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function solution(s, e) {
  let answer = [];
  let count = 1000;
  for (let x of s) {
    if (x === n) {
      count = 0;
      answer.push(count);
    } else {
      count++;
      answer.push(count);
    }
  }
  count = 1000; 
  for(let i = s.length - 1; i &amp;gt;= 0;  i--) {
    if (s[i] === n) { 
      count = 0;     
    } else {
      count++;
      answer[i] = Math.min(answer[i], count);
    }
  }
  return answer;
};

const s = &amp;quot;teachermode&amp;quot;
const n = &amp;quot;e&amp;quot;;

console.log(solution(s, n));&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://images.velog.io/images/yunsungyang-omc/post/db0a1b90-519b-43d7-a38e-46c247d898ba/Screen%20Shot%202022-01-24%20at%203.48.28%20PM.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;</description>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/90</guid>
      <comments>https://h0wdy.tistory.com/90#entry90comment</comments>
      <pubDate>Sun, 12 Jul 2026 22:50:28 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 숫자만 추출</title>
      <link>https://h0wdy.tistory.com/89</link>
      <description>&lt;hr&gt;
&lt;p&gt;문자와 숫자가 섞여있는 문자열이 주어지면 그 중 숫자만 추출해 그 순서대로 자연수를 만드는 함수를 작성하세요.&lt;/p&gt;
&lt;p&gt;만약 &amp;#39;g0en2t0s8eSoft&amp;#39;에서 숫자만 추출하면 0, 2, 0, 8이고 이것을 자연수로 만들면 208이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;문제풀이&lt;/h3&gt;
&lt;p&gt;정규표현식을 이용해서 숫자를 제외하고 나머지 알파벳을 모두 공백으로 제거해준다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function solution(s) {
  let answer;
  let s2 = s.replace(/[^0-9]/g, &amp;#39;&amp;#39;).split(&amp;#39;&amp;#39;);

  s2.filter(item =&amp;gt; item &amp;gt; 0);
  return answer =  Number(s2.join(&amp;#39;&amp;#39;));
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://images.velog.io/images/yunsungyang-omc/post/7cf20cb3-b55e-446e-b007-227dea748260/Screen%20Shot%202022-01-21%20at%204.30.10%20PM.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;다른 문제풀이&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function solution(str) {
  let answer=&amp;#39;&amp;#39;;
  for (let x of str) {
    if (!isNaN(x)) answer+=x;
  }
  return parseInt(answer);
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;parseInt를 이용하지 않고 풀기&lt;/h4&gt;
&lt;p&gt;```js&lt;br&gt;function solution(str) {&lt;br&gt;let answer=0;&lt;br&gt;for (let x of str) {&lt;br&gt;if (!isNaN(x)) answer=answer*10+; // +를 더하면 숫자형으로 변환되는 원리를 이용했다.&lt;br&gt;}&lt;br&gt;return answer;&lt;br&gt;}&lt;/p&gt;</description>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/89</guid>
      <comments>https://h0wdy.tistory.com/89#entry89comment</comments>
      <pubDate>Sun, 12 Jul 2026 22:50:16 +0900</pubDate>
    </item>
    <item>
      <title>[26년 상반기 회고] Connecting the Dots</title>
      <link>https://h0wdy.tistory.com/73</link>
      <description>&lt;p&gt;작성 중 ...&lt;br&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dna/cn0KwU/dJMb99NLNDI/AAAAAAAAAAAAAAAAAAAAAE5wz8TZzdXygmp8Tq034xhfQIht8zxj8W6vP4Jvtkat/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;expires=1785509999&amp;allow_ip=&amp;allow_referer=&amp;signature=kQNXJPUnvGTG3HC4XCrbck1KGvo%3D&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;올해  Recap&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AWS Student Builder Group at DGU 1기&lt;/li&gt;
&lt;li&gt;BOAZ 26기 [데이터 분석] 파트 수료&lt;/li&gt;
&lt;li&gt;Notion 공식 자격증 (Advance/Workflows) 취득&lt;/li&gt;
&lt;li&gt;ETS 자격증 TOEIC Speaking IH (140) 취득&lt;/li&gt;
&lt;li&gt;한국공학교육인증원 반도체 (Fundamental/ 시스템SW General) 자격증 취득&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;정보통신공학과 캡스톤 디자인 학생/교수평가 1위&lt;/li&gt;
&lt;li&gt;2026 AX 아이디어 경진대회 출품&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;상반기 블로그 작성 17개! &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;여행&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;책: &lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;나는 1-2부터 현재 4-1까지 교내동아리, 9oormthonUNIV, UMC, SOPT 등 많은 IT 커뮤니티(= 동아리)를 경험해왔다&lt;/p&gt;
&lt;p&gt;동아리는 네트워킹과 인맥이라는 큰 장점이 있지만, 점점 네트워킹과 과제에 시간을 너무 많이 쏟게 되고, 좁은 분야의 트랙(BE/FE/DS/PM)을 따라가다 보니 나 스스로도 한계를 *&amp;quot;프론트엔드 개발자&amp;quot;* 로만 규정하게 된다는 느낌이 들었다&lt;br&gt;올해 초에 SOPT 합숙까지 죽어라 달린 이후, 조금 환기하면서 정신 차려보니, 전공 공부를 하며 흥미로웠던 부분이나 그외 용어만 들어보고 궁금했던 것들도 많았는데 커리어 스펙을 쌓는 개발자의 나와 평소의 호기심 많은 나를 너무 분리해서 살지 않았나? 싶었다 &lt;/p&gt;</description>
      <category>recap</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/73</guid>
      <comments>https://h0wdy.tistory.com/73#entry73comment</comments>
      <pubDate>Sat, 11 Jul 2026 10:52:00 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] AWS Student Community Day 회고</title>
      <link>https://h0wdy.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 7월 4일, &lt;a href=&quot;https://www.meetup.com/ko-kr/aws-sbg-at-konkuk-university/events/315165017/&quot;&gt;&lt;b&gt;AWS Student Community Day 2026&lt;/b&gt;&lt;/a&gt;에 다녀왔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Student Builder Groups가 주최하는 대학생 컨퍼런스로, Dev / Startup / Career 세 개 트랙이 동시에 진행되고 Time마다 하나씩 골라 들어가는 방식이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Career와 Startup 트랙을 골랐다. 기술 세션이 궁금하지 않았던 건 아닌데, 요즘 취준을 하다보니 주변 취준생들의 이야기도 듣고 연사님들께 조언도 구하고, 무엇보다 꼭 듣고 싶은 Career 섹션이 있어서 해당 행사에 참여하게 되었다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Career Track] 비선형의 가치 - 영상학과에서 클라우드 엔지니어까지, 16년의 비선형 커리어, 강전희 연사님&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/9983951f81726ddf.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CJ ENM에서 티빙 인프라를 운영하시는 분이었는데, 첫 회사가 도산(?)했고, 다음에서 게임 서버 개발자로 시작해 CJ로 옮겨 게임 스트리밍을 하시다가, 다이아TV라는 유튜브 사업을 만들고 광고 비즈니스를 거쳐 지금은 FinOps로 근무한다고 하셨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FinOps는 처음 들어봤는데, DevOps나 AIOps처럼 &lt;b&gt;비용 최적화 관점에서 시스템을 운영&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;i&gt;(티빙에 오신 첫해에 불필요한 비용을 80억 원어치 줄이셨다고 &amp;hellip; )&lt;/i&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;p data-ke-size=&quot;size16&quot;&gt;야구 중계가 있는 날에는 서버를 500대씩 미리 띄워둔다고 하셨다. 안 들어올 수도 있으니 낭비처럼 보이지만, 고객이 들어왔는데 안 뜨면 계속 새로고침을 할 거고 서버는 더 큰 부하를 받게 되니까 전체 비용 관점에서는 _&lt;b&gt;서버를 미리 띄우는 게 이득&lt;/b&gt;_이라는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담을 섞어보자면, 처음에 &lt;i&gt;쓴 만큼만 내는 클라우드에서 왜 온프레미스 환경처럼 말씀하시는지&lt;/i&gt; 의문이었는데&lt;br /&gt;'서버를 미리 띄운다'는 말을 '서버를 미리 사둔다'로 착각한 거였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드는 &quot;원할 때 늘리고 줄일 수 있다&quot;를 보장하지, &quot;원하는 순간 즉시 존재한다&quot;를 보장하지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 서버를 필요 몇 분 전에 미리 빌려둔다는 의미였다&lt;br /&gt;온프레미스에서는 애초에 이미 사놨으니까 띄운다/안 띄운다를 고민할 수 없는데 클라우드가 탄력성을 줬기 때문에 &lt;b&gt;&quot;언제 얼마나 미리 빌릴 것인가&quot;&lt;/b&gt;라는 새로운 의사결정이 생긴 거고, 그 판단을 하는 게 FinOps라는 걸 다시 상기했다  &lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엔지니어란 무엇일까?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술은 계속 바뀌지만, 문제를 해결한다는 본질은 변하지 않는다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 세션에서 들은 문장 중에 가장 오래 남은 말이었다&lt;br /&gt;ECS든 EKS든 결국 도구일 뿐이고, 예전에도 Docker가 있었고 Docker Compose가 있었고 오케스트레이션이 있었듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 바뀌었지 본질은 다르지 않다는 것이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 해결하려면 그 문제를 이해해야 한다 &amp;rarr; 이해하는 것이 곧 &lt;b&gt;도메인&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&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;p data-ke-size=&quot;size16&quot;&gt;ai 얘기도 같은 맥락이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 프롬프트를 넣고 ~를 추천해줘라고 하면 원하는 답이 안 나오지만, 좀 더 자세히 입력하면 원하는 것 이상의 답이 나온다는 것을 통해, 도메인을 알아야 ai를 시키더라도 제대로 시킬 수 있다는 얘기였다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커리어는 사다리가 아니라 정글짐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000000625387?LINK=PLP&quot;&gt;책&lt;/a&gt;을 인용하시면서 하신 말인데, 커리어는 게임처럼 테크트리를 찍어가는 게 아니라고 하시며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부서 이동을 하거나 엔지니어에서 기술 영업으로 넘어가는 걸 &quot;망했다&quot;고 생각할 필요가 없다는 것이다&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;취업만 생각하지 말고 자유롭게 살아봐라&lt;/b&gt;였다. (ex. 취업 전에 배낭여행 다녀오기)&lt;br /&gt;두 번째는 여러 분야에 관심 갖기,&lt;br /&gt;세 번째는 부서 이동을 두려워하지 말기!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;[Career Track] 제대로 된 곳에 취업할 수 없을 거라 믿던 사람이 7곳 이상에서 먼저 연락 받기까지, 안다혜 연사님&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/8605e4ae7961c432.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 Product Engineer로 일하시고, 그전에는 SRE, 그전에는 데이터 엔지니어로 근무했다고 하셨는데, AWSKRUG의 Women In Cloud 소모임 Organizer로 원래 알고 있던 분이었고, &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7475344257671647232/&quot;&gt;LinkedIn&lt;/a&gt;을 통해 세션을 진행하신다는 말을 듣고 해당 세션을 듣고 싶어서 커뮤니티 데이에 간 거였다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/ef05b81ce1c64eb2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모교(!) 영화과를 나오셨고, 6년 전에는 채용 공고에 2차 면접이 있으면 &lt;b&gt;너무 어려워 보여서 뒤로 가기를 눌렀다&lt;/b&gt;고 하셨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 첫 회사는 이력서와 링크드인에서 아예 빼셨다고 한다. 배운 것이 없진 않았지만&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 회사에 있던 모든 시간이 저의 커리어 자산이 되지 않았다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 하신 부분이 나도 지금 하고 있는 인턴이나 프로젝트들이 나중에 자산으로 남을지 계속 저울질하고 있어서 이 말이 인상 깊었다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커리어를 투자의 관점으로 보는 질문 3개&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1년 만에 첫 회사를 나오게 만든 질문들이라고 소개하셨다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;지금 이 회사를 1년 더 다니면 나한테 뭐가 남지?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내가 만약 투자자라면, 지금 이 회사에 투자할 건가?&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;돈을 투자할 수 없다면, 내 시간은 더 투자하고 싶나?&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 질문에 전부 아니라는 답이 나와서 1년 만에 나오셨다고 하는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천해주신 책인 곧 &lt;a href=&quot;https://www.yes24.com/product/goods/76070893&quot;&gt;당신은 더 좋은 회사를 다닐 자격이 있다&lt;/a&gt;를 읽어보고 싶다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기록하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연사님은 &lt;a href=&quot;https://aimb.tistory.com/&quot;&gt;블로그&lt;/a&gt;에 264편의 글이 있는데 그중 25편이 회고글이셨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접관분들이 남의 회고 읽는 걸 되게 재밌어하시거든요. 면접에서 티를 내주시는 분들도 계세요&lt;/p&gt;
&lt;/blockquote&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 커뮤니티와 스터디&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔지니어 경력 8개월 차부터 AWS 사용자 모임을 다니기 시작하셨는데, 처음엔 무슨 소리인지 하나도 모르셨다고. 기술 얘기를 하는데 이해를 못 해서 기죽고 주눅이 드셨다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데도 꾸준히 다니셨고, 스터디를 하셨다고 한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디는 남이 만들어주는 걸 참여하는 거라고만 생각하시는 분들이 계신데, 스터디를 만드는 것 자체가 브랜딩이 된다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &quot;이 사람은 스터디를 되게 많이 하네&quot;라는 브랜딩&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무에서 elk 로그 스택을 다뤄야 해서 공부하신 뒤, 그걸 다른 주니어분들에게 알려주는 강의형 스터디를 여셨다는데, 물론 브랜딩이 목적은 아니었지만 그게 본인의 코어가 됐다는 얘기였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 세션도 하나 신청해보라고 하시며, 컨테이너 소모임이든 쿠버네티스 소모임이든 &lt;b&gt;&quot;내가 가도 되나&quot; 하는 자기 검열 전에 먼저 참여해보라고 하셨다&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 드러내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경력 1년 차 때 회사를 너무 떠나고 싶어서 &lt;b&gt;공개 구직 글&lt;/b&gt;을 sns에 올리셨다는데 5년 전엔 공개 구직이라는 게 흔하지 않아서 꽤 용기가 필요했다고 하셨다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나는 지금까지 뭘 해온 사람인가&lt;/li&gt;
&lt;li&gt;뭘 잘할 수 있는가&lt;/li&gt;
&lt;li&gt;내 장점은 무엇인가. 그리고 왜 그게 장점인지 등등을 작성했다고 하셨다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일도 잘할 뿐더러 웹사이트도 기획해봤고, 토익 980점도 받아봤고, 교육도 해봤다고 소프트 스킬까지 셀링 포인트로 넣으셨다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 글 하나로 경력 1년 차에 &lt;b&gt;7곳에서 먼저 연락&lt;/b&gt;이 왔다고 말씀해주셨다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 두 번째 회사도 커뮤니티에서 알게 된 분이 어느 날 &quot;우리 팀 공고 났는데 오면 인프라랑 백엔드 다 밟을 수 있다&quot;고 알려주셨고, 호기심에 지원했다가 덜컥 합격하셨다는 것&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜딩을 쌓으려고 한 게 아니었고 그냥 열심히 쌓았을 뿐인데, 그게 브랜딩이 됐다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가만히 있으면 뛰어난 사람은 알아주지만, 보통은 말하는 사람을 알아주는 것 같다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 계정 &lt;a href=&quot;https://x.com/mmmmmmmdev&quot;&gt;트위터&lt;/a&gt; 팔로워가 n천 명이고 링크드인까지 합치면 만 명 정도 된다고 하시면서, 그 곳에 무엇을 쓰는 지를 말씀해주셨는데,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 이야기를 전해주셔서 &lt;b&gt;기존에는 단순 재미로 블로그에 뜨문뜨문 기록을 해왔는데, 조금 더 목적을 갖고 더 꾸준히, 열심히 기록해 나가야겠다&lt;/b&gt;고 생각했다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;cf. 그리고 AI 얘기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 Take Away 세 가지를 정리하시면서&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요새 ai로 쓴 글들이 너무 많아서 피로하다 &amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 내가 배운 거, 느낀 거를 적으면 아직은 인간적인 콘텐츠가 그 ai 법망 속에서 살아남는 것 같다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 말에 크게 동의했다 .. 정보만 정리한 글은 이제 누구나 빨리 작성할 수 있고, 남는 건 결국 &lt;b&gt;내가 그때 무슨 생각을 했는가&lt;/b&gt;뿐인 것 같다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 환경에 나를 두고 어떤 행동을 시작할지는 나 자신만이 바꿀 수 있다!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그리고 이외 Career, StartUp Track 세션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DevOps는 이분법이 아니라 스펙트럼이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신입 데브옵스를 뽑는 곳이 사실상 거의 없다는 얘기로 시작하셨는데, 결정이 조직 전체로 퍼지기 때문에 경험이 필요한 직무라서 그렇다고..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;스펙트럼을 넓혀가는 접근&lt;/b&gt;을 제안하셨다&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;순수 애플리케이션 &amp;rarr; 관측성(메트릭&amp;middot;트레이스) &amp;rarr; 컨테이너 &amp;rarr; IaC / 네트워크 설계&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 기반 서버에서 메모리 부족 이슈가 났을 때, 노드로 개발해본 경험이 있어서 v8 엔진 힙 문제를 바로 추측하고 잡아내신 사례를 들려주셨다. 결국 중요한 건 &lt;b&gt;펀더멘탈&lt;/b&gt;이라는 결론이었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 분도 기술 블로그로 취업하셨다고 했다&lt;br /&gt;입사 후에 팀장님께 물어보니 블로그를 보자마자 뽑아야겠다고 다짐하셨다는데, 인프라에 관심이 많아 관련된 걸 전부 포스팅해두신 게 플러스 포인트였다고..!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;좋은 사용자 반응이 좋은 비즈니스 모델은 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Startup 트랙에서 들은 이화여대 학생분의 발표였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대학생과 기업을 협찬으로 연결하는 플랫폼 mvp를 돌리셨는데, 학생회 반응도 분명 좋았는데, 유료 전환을 하려니 아무도 사지 않았다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반응이 좋았던 학생 단체는 &lt;b&gt;유저&lt;/b&gt;였고 그들의 pain point는 돈이 없다는 것 &amp;rarr; 거기서 비즈니스 모델이 나올 수가 없었던 것이다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 검증은 잘했는데, 고객 검증을 못 했다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ai로 누구나 만들 수 있게 된 시대라 &lt;b&gt;&quot;누가 왜 돈을 내는가&quot;&lt;/b&gt;가 훨씬 중요해졌다는 얘기였다. 사이드 프로젝트는 &quot;있으면 좋다&quot;까지 검증하지만, 창업은 &quot;없으면 불편하다(must have)&quot;까지 가야 한다고 하며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창업은 아이디어를 증명하는 일이 아니라 &lt;b&gt;틀린 과정을 줄여나가는 일&lt;/b&gt;이라는 마무리가 좋았다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Done is better than perfect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 트랙의 다음 세션이었는데, 완벽주의가 출발을 막는다는 이야기였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽하게 준비되면 시작하겠다는 생각으로 대학원에 가셨는데, 지금 돌아보면 공부는 공부대로 하면서 도전은 동시에 할 수 있었다고. 실행은 순서대로(sequential)가 아니라 &lt;b&gt;병렬로(parallel)&lt;/b&gt; 가능한 거였다고 하셨다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 인생에서 준비가 끝나는 날은 영영 오지 않는다&lt;/p&gt;
&lt;/blockquote&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;Y Combinator : 거절당할 수 있는 자리에 나를 노출시킬수록 운이 들어올 표면적이 넓어진다&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;/b&gt;를 스스로 만들어야 끈기가 저절로 온다는 뜻이라고&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오픈소스로 기술을 습득하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 gpu 클라우드 회사에서 일하시는 분이었는데, Ray와 KubeRay가 회사 아키텍처의 핵심인데 쿠버네티스 컨텍스트가 전혀 없는 상태로 이해하려니 너무 어려웠다고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책도 사보고 문서도 번역해봤지만 &lt;b&gt;정적인 자료로는 학습이 안 되더라&lt;/b&gt;는 것 &amp;rarr; 그래서 오픈소스 기여를 시작하셨다고 한다&lt;br /&gt;Ray, KubeRay, 그리고 Go 기반 Redis 클라이언트에 계속 PR을 넣고 계신다고 하셨다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 지금 velog와 티스토리에 글을 쓰고 있고 링크드인도 하고 있지만, 회고는 쓰고 있지 않아서 행사 끝나고 돌아오면서 안다혜 연사님의 세 가지를 그대로 적어봤다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;회고 쓰기&lt;/b&gt; : 월 단위는 무리여도 분기는 해보기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스터디 만들기&lt;/b&gt; : 참여 말고 만들기도 좋음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;드러내기&lt;/b&gt; : 잘 쓰려고 하지 않아도 되니 배운 것, 느낀 것을 그냥 적기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;del&gt;세션 듣고 온 날 바로 적어보라고 하셨는데 며칠 늦었지만 ..&lt;/del&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 위로와 동기부여가 된 세션이었고, 앞으로 더욱 열심히 도움되는 방향으로 기록을 해나가야겠다고 생각했다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여담&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/e0cb329ca16df78d.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당첨돼서 AWSKRUG 텀블러도 받았다 ㅎㅎ&lt;/p&gt;</description>
      <category>aws</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/71</guid>
      <comments>https://h0wdy.tistory.com/71#entry71comment</comments>
      <pubDate>Fri, 10 Jul 2026 13:17:00 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] CloudTrail을 이용한 AWS 사용자 활동 추적과 로그 분석</title>
      <link>https://h0wdy.tistory.com/70</link>
      <description>&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약&lt;/b&gt;: aws에서 &lt;b&gt;&lt;i&gt;누가, 언제, 무엇을 했는지&lt;/i&gt;&lt;/b&gt; 알고 싶다면 CloudTrail 로그를 보기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서는 &lt;b&gt;누구에게 무엇을 허용할 것인지&lt;/b&gt;를 다뤘는데, 이번 글은&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;실제로 누가 무엇을 했는가&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 대한 내용이다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;Presigned URL&lt;/th&gt;
&lt;th&gt;CloudTrail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&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;사건 전&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;권한을 아무리 잘 설계해도 누가 언제 그걸 바꿔버렸는지 추적할 수 없으면 의미가 없고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 로그만 잔뜩 쌓아둬도 의미가 없으니 막을 수단이 있어야한다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CloudTrail이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws 작업을 감사하는 서비스로, api 호출과 사용자 활동을 자동으로 기록해준다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/b08e71594106b36f.png&quot; alt=&quot;&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;IAM 권한, 버킷 정책 등 설계한 통제 장치들이 &lt;b&gt;누군가에 의해 바뀌었는지를 감시하는 게 CloudTrail이다&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 AWS 작업은 API 호출이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 CloudTrail을 이해하는 핵심인 것 같다&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;콘솔에서 버튼 클릭
cli 명령어 입력      ──▶  StopInstances api 호출  ──▶  CloudTrail json 기록
terraform apply&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔의 예쁜 버튼 아래엔 결국 api가 있을 뿐이니, 어떤 도구를 쓰든 &lt;code&gt;eventName&lt;/code&gt;은 똑같이 기록된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail 하나로 도구와 무관하게 모든 활동을 잡아낼 수 있는 이유!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습은 &lt;b&gt;직접 사건을 만들고, 그 흔적을 읽는&lt;/b&gt; 순서로 진행했다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/c3d6470011b458c2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. IAM 계정 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 계정으로 실습해도 되긴 하지만, 감사 로그를 다루는 실습에서 루트를 쓰는 건 좀 아이러니하니까 &lt;code&gt;lab-user&lt;/code&gt;를 따로 만들자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt;가 붙어 있는지 확인하고, 없다면 직접 정책 연결로 붙여주면 된다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cf. 실무에서는 &lt;code&gt;AdministratorAccess&lt;/code&gt;를 이렇게 막 주면 안 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그를 볼 사람과 로그를 끌 수 있는 사람은 반드시 분리&lt;/b&gt;되어야 하기 때문&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/21f5f930009b2c6b.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/abc8cce426fd880f.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/ad3e2a561e31b426.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. EC2 인스턴스 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;CloudTrail-Lab&lt;/code&gt;이라는 이름으로 t3.micro (혹은 t2.micro) 하나만 띄운다. Amazon Linux 2023에 키 페어 없음, 스토리지는 기본 8GiB로 두면 됨&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/3b4131446d0b2a9b.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/5f1bdadffba62e52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. CloudTrail 추적 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Security-Lab&lt;/code&gt;이라는 이름으로 빠른 추적 생성을 누르면 끝난다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/ae4b8db21ad38416.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그가 저장될 s3 버킷은 자동으로 만들어지고, 이벤트를 로깅하는 것 자체에는 비용이 들지 않지만 &lt;b&gt;로그를 저장하는 s3 버킷에는 요금이 발생&lt;/b&gt;한다는 안내가 뜬다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 인스턴스 중지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ec2로 돌아가서 방금 만든 인스턴스를 중지시키고,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/20b140331115a0e3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 CloudTrail의 이벤트 기록으로 이동해 &lt;code&gt;StopInstances&lt;/code&gt;를 검색해보자&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/008cd14f4f2e542e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방금 내가 한 행동(StopInstances)이 로그로 남아 있다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;json 코드 분석하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 클릭하면 아래 같은 json이 나온다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/7b28501b634fa436.png&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/72acacebeea37a42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 이벤트 하나에 육하원칙이 거의 다 들어 있었다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;질문&lt;/th&gt;
&lt;th&gt;필드&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;누가&lt;/td&gt;
&lt;td&gt;userIdentity.type / arn / userName&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;언제&lt;/td&gt;
&lt;td&gt;eventTime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;어디서&lt;/td&gt;
&lt;td&gt;sourceIPAddress / awsRegion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무엇을&lt;/td&gt;
&lt;td&gt;eventName / eventSource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무엇에 대해&lt;/td&gt;
&lt;td&gt;resources[].ARN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;어떻게&lt;/td&gt;
&lt;td&gt;userAgent / sessionCredentialFromConsole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과는&lt;/td&gt;
&lt;td&gt;responseElements.currentState&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;위험한가&lt;/td&gt;
&lt;td&gt;readOnly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;readOnly를 보자!&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;&quot;readOnly&quot;: false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 전용 작업인지 알려주는 필드로,&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;api&lt;/th&gt;
&lt;th&gt;readOnly&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DescribeInstances&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;조회만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GetBucketPolicy&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;조회만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StopInstances&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;상태 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeleteBucket&lt;/td&gt;
&lt;td&gt;false&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;수만 건의 로그에서 &quot;진짜 문제가 될 만한 것&quot;만 걸러내는 첫 번째 필터라서, 보안 관제에서 제일 먼저 거는 조건이라고 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;code&gt;readOnly: true&lt;/code&gt;인 이벤트를 아예 안 봐도 되는 건 아니다. &lt;code&gt;DescribeInstances&lt;/code&gt;나 &lt;code&gt;ListBuckets&lt;/code&gt; 같은 조회 api가 &lt;b&gt;비정상적으로 폭주&lt;/b&gt;하는 건 정찰의 전형적인 신호이기도 하다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;requestID와 eventID의 차이&lt;/h3&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;code&gt;requestID&lt;/code&gt; : aws가 해당 api 요청에 부여한 id로, aws에 문의할 때 쓴다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eventID&lt;/code&gt; : CloudTrail 이벤트 자체의 고유 id로, 로그 추적하고 분석할 때 쓴다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;previousState &amp;rarr; currentState&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;currentState&quot;:  { &quot;code&quot;: 64, &quot;name&quot;: &quot;stopping&quot; },
&quot;previousState&quot;: { &quot;code&quot;: 16, &quot;name&quot;: &quot;running&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 변화의 전후를 통째로 담고 있어서, 로그만 보고도 &quot;실행 중이던 인스턴스가 중지되었다&quot;는 걸 알 수 있다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mfaAuthenticated&lt;/h3&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;mfaAuthenticated&quot;: &quot;false&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 운영 계정이었다면 이 &lt;code&gt;false&lt;/code&gt;가 경보 대상이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 조직이 CloudTrail 로그에서 &lt;code&gt;mfaAuthenticated: false&lt;/code&gt;인 콘솔 로그인을 알람 조건으로 걸어둔다고 한다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실패한 API 호출이 더 많은 걸 말해준다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail은 성공한 호출만 기록하는 게 아니라 &lt;b&gt;실패한 api 호출도 기록&lt;/b&gt;한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 없이 s3 버킷을 삭제하려고 시도하면 &lt;code&gt;AccessDenied&lt;/code&gt;와 함께 이벤트가 남는데, 사실 이게 CloudTrail의 진짜 가치인 것 같다&lt;/p&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;탈취한 키로 접속
  &amp;rarr; GetCallerIdentity   (성공)      &quot;나는 누구지?&quot;
  &amp;rarr; ListUsers           (AccessDenied)
  &amp;rarr; ListRoles           (AccessDenied)
  &amp;rarr; ListBuckets         (성공)      &amp;larr; 여기서 뚫림
  &amp;rarr; GetObject &amp;times; 수백 건  (성공)      &amp;larr; 데이터 유출&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 실패가 없어서 위험한 경우도 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt;를 가진 계정이 탈취되면 아무것도 실패하지 않아서 로그만으론 정상 관리자와 구분이 안 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최소 권한 원칙이 곧 탐지 가능성&lt;/b&gt;이기도 한 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GuardDuty가 하는 일이 정확히 이건데, &lt;code&gt;Recon:IAMUser/MaliciousIPCaller&lt;/code&gt; 같은 탐지 항목이 전부 CloudTrail의 실패 이벤트를 먹고 동작한다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그를 실제로 쓰는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에서는 콘솔의 이벤트 기록까지만 봤지만, 콘솔은 &lt;b&gt;90일치 관리 이벤트&lt;/b&gt;만 보여주기 때문에 실무에서는 단계가 올라간다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;도구&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;CloudTrail 콘솔 이벤트 기록&lt;/td&gt;
&lt;td&gt;최근 90일, 단건 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;s3 + Athena&lt;/td&gt;
&lt;td&gt;장기 로그를 sql로 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CloudWatch Logs + Metric Filter + Alarm&lt;/td&gt;
&lt;td&gt;특정 이벤트 발생 시 즉시 알림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;EventBridge + Lambda&lt;/td&gt;
&lt;td&gt;위험 이벤트 자동 차단&amp;middot;롤백&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;GuardDuty / Security Hub&lt;/td&gt;
&lt;td&gt;ml 기반 이상 탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 3번이 가성비가 좋다고 한다. &lt;code&gt;DeleteTrail&lt;/code&gt;, &lt;code&gt;StopLogging&lt;/code&gt;, &lt;code&gt;DeleteBucket&lt;/code&gt;, Root 계정 로그인 같은 이벤트가 발생하면 sns로 즉시 알림을 받는 것인데, 이 몇 줄짜리 설정이 실무 보안의 기본이라고 한다&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- Athena로 Root 계정의 위험 작업만 뽑아보기
SELECT eventtime, useridentity.arn, eventname, errorcode
FROM cloudtrail_logs
WHERE useridentity.type = 'Root'
  AND readonly = false&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습 화면 상단에 &lt;b&gt;&quot;CloudTrail Lake가 2026년 5월 31일부터 신규 고객에게 제공되지 않는다&quot;&lt;/b&gt;는 배너가 떠 있었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유사 기능은 CloudWatch를 참고하라고 하니, 앞으로는 CloudWatch Logs 쪽 파이프라인에 익숙해지는 게 유리해 보인다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관리 이벤트 vs 데이터 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail 기본 설정으로는 &lt;b&gt;s3 파일을 누가 다운로드했는지가 기록되지 않는다&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;code&gt;PutBucketPolicy&lt;/code&gt;, &lt;code&gt;CreateBucket&lt;/code&gt; 처럼 리소스를 관리하는 작업으로 기본 활성화되어 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 이벤트&lt;/b&gt; : &lt;code&gt;GetObject&lt;/code&gt;, &lt;code&gt;PutObject&lt;/code&gt; 처럼 데이터를 다루는 작업인데 &lt;b&gt;별도로 켜야 하고 유료&lt;/b&gt;다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(지난 글의 presigned url과 연결하면, 데이터 이벤트를 켜야 발급된 url로 누가 언제 파일을 받아갔는지 추적할 수 있다)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그 버킷이 공격당하면 어떡할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그를 지울 수 있는 사람은 흔적도 지울 수 있다&lt;/b&gt;는 게 감사 시스템의 근본 딜레마다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에서도 마지막에 추적을 삭제하고 s3 버킷을 비우고 지웠는데, 공격자가 계정 권한을 얻으면 똑같이 할 수 있는 것 아닌가?&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;1. 계정 분리 (제일 중요)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[운영 계정 A] ─┐
[운영 계정 B] ─┼──▶ [로그 아카이브 전용 계정]  &amp;larr; 로그 버킷은 여기에만
[운영 계정 C] ─┘        (운영팀은 접근 권한 없음)&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;b&gt;2. s3 Object Lock&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지정 기간 동안 &lt;b&gt;Root 계정조차 삭제/수정 불가&lt;/b&gt;하도록 만든다. worm(Write Once Read Many) 스토리지가 되는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Log File Validation&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail이 1시간마다 로그 파일의 sha-256 해시를 모아 &lt;b&gt;다이제스트 파일&lt;/b&gt;을 만들고 aws 비공개 키로 서명한다&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;&lt;b&gt;4. scp로 봉쇄&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Organizations 서비스 제어 정책으로 &lt;code&gt;cloudtrail:StopLogging&lt;/code&gt;, &lt;code&gt;cloudtrail:DeleteTrail&lt;/code&gt;을 조직 전체에서 deny&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 데드맨 스위치&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;CloudWatch Alarm
  Metric: CloudTrail 이벤트 수
  조건: 15분간 이벤트 0건
  TreatMissingData: breaching &amp;larr; 데이터가 없으면 경보&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;그리고 애초에 CloudTrail을 끄는 행위인 &lt;code&gt;StopLogging&lt;/code&gt;, &lt;code&gt;DeleteTrail&lt;/code&gt;도 CloudTrail에 기록되기 때문에 완벽한 은폐는 어려운 구조다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 RDS가 아니라 S3일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail 추적을 만들면 s3 버킷이 자동으로 생기는데, 왜 하필 s3일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로그의 성격이 관계형 데이터베이스와 안 맞기 때문&lt;/b&gt;이다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특성&lt;/th&gt;
&lt;th&gt;CloudTrail 로그&lt;/th&gt;
&lt;th&gt;rds가 잘하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;쓰기&lt;/td&gt;
&lt;td&gt;한 번 쓰고 안 고침&lt;/td&gt;
&lt;td&gt;잦은 update/delete&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;서비스마다 구조가 다름&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;gb~tb&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;b&gt;비용&lt;/b&gt; : s3 Standard는 gb당 월 $0.023인데 rds 스토리지는 그 5~10배에 인스턴스 시간당 요금이 상시 발생해서, 1년에 몇 번 볼 로그를 위해 db를 24시간 켜둘 이유가 없음&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;b&gt;스키마 부재&lt;/b&gt; : ec2는 &lt;code&gt;instancesSet&lt;/code&gt;, s3는 &lt;code&gt;bucketName&lt;/code&gt;, iam은 또 다른 구조라서 수백 개 서비스의 파라미터를 하나의 테이블로 정규화하는 건 불가능함&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;b&gt;불변성&lt;/b&gt; : s3는 Object Lock으로 worm 보장이 되는데 rds는 db 관리자가 update 한 방이면 조작 가능해서 감사 로그에는 치명적임&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;- &lt;b&gt;수명 주기&lt;/b&gt; : 90일 후 Glacier로, 7년 후 삭제. s3 Lifecycle 규칙 몇 줄이면 끝남&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색은 &lt;b&gt;Athena&lt;/b&gt;가 담당하고, s3에 있는 json을 그 자리에서 sql로 조회하고 스캔한 데이터 양만큼만 과금하는 방식&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;저장은 싸게, 조회는 필요할 때만&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;헷갈리는 이웃 서비스들&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;서비스&lt;/th&gt;
&lt;th&gt;기록 대상&lt;/th&gt;
&lt;th&gt;한 줄 요약&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CloudTrail&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;api 호출&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;누가 무엇을 했나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch Logs&lt;/td&gt;
&lt;td&gt;애플리케이션&amp;middot;시스템 로그&lt;/td&gt;
&lt;td&gt;앱이 무슨 말을 했나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch Metrics&lt;/td&gt;
&lt;td&gt;성능 지표&lt;/td&gt;
&lt;td&gt;얼마나 바쁜가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vpc Flow Logs&lt;/td&gt;
&lt;td&gt;네트워크 트래픽&lt;/td&gt;
&lt;td&gt;어떤 패킷이 오갔나&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&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;saa 시험에서도 이 구분이 단골로 나오는데, &lt;b&gt;&quot;감사&quot;, &quot;누가(who)&quot;, &quot;규정 준수&quot;, &quot;api 호출&quot;&lt;/b&gt; 키워드가 보이면 CloudTrail이고 &lt;b&gt;&quot;구성 규칙 위반&quot;&lt;/b&gt;이면 Config, &lt;b&gt;&quot;거부된 트래픽&quot;&lt;/b&gt;이면 vpc Flow Logs다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;cf. 그럼 EC2 안에서 rm -rf 한 것도 남을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 남는다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudTrail은 AWS API &lt;b&gt;경계에서 일어난 일&lt;/b&gt;만 보기 때문에, 인스턴스 내부에서 무슨 명령을 실행했는지, 파일 내용이 무엇이었는지는 기록하지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이건 CloudWatch Agent나 auditd 같은 별도의 os 감사 도구의 영역)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리소스 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EC2 인스턴스 &lt;b&gt;종료(삭제)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;CloudTrail 추적 삭제&lt;br /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/40ea241a379b22dd.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;S3 버킷 비우기 &amp;rarr; 버킷 삭제&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/27caa96fa0cbafab.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;IAM 사용자 삭제&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/466c8ea506969ddd.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;aws 아키텍처 아이콘은 서비스 카테고리별로 색이 정해져 있다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;카테고리&lt;/th&gt;
&lt;th&gt;색&lt;/th&gt;
&lt;th&gt;예시&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;주황&lt;/td&gt;
&lt;td&gt;ec2, Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;초록&lt;/td&gt;
&lt;td&gt;s3, ebs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;파랑&lt;/td&gt;
&lt;td&gt;rds, DynamoDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Networking&lt;/td&gt;
&lt;td&gt;보라&lt;/td&gt;
&lt;td&gt;vpc, CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security, Identity&lt;/td&gt;
&lt;td&gt;빨강&lt;/td&gt;
&lt;td&gt;iam, GuardDuty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management &amp;amp; Governance&lt;/td&gt;
&lt;td&gt;핑크&lt;/td&gt;
&lt;td&gt;CloudTrail, CloudWatch, CloudFormation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글의 Presigned URL 자료와 비교해보면 재밌다. S3는 초록, Lambda는 주황이었으니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 흥미로운 건 CloudTrail이 보안 서비스처럼 느껴지는데도 색은 &lt;b&gt;빨강(Security)이 아니라 핑크(Management &amp;amp; Governance)&lt;/b&gt;라는 점이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, CloudTrail 자체는 아무것도 막지 않고 그저 기록할 뿐이며, 그 기록을 보안에 쓸지 비용 추적에 쓸지 규정 준수 증빙에 쓸지는 전부 쓰는 사람의 몫이기 때문에 핑크색이 맞는 거 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 Presigned URL이 &lt;b&gt;막는 쪽&lt;/b&gt;이었다면 CloudTrail은 &lt;b&gt;보는 쪽&lt;/b&gt;인데, 색깔 하나가 서비스의 성격을 정확히 말해주고 있었다는 게 신기했다!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>aws</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/70</guid>
      <comments>https://h0wdy.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 10 Jul 2026 13:15:44 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] S3 Presigned URL: 비공개 파일 안전하게 공유하기</title>
      <link>https://h0wdy.tistory.com/69</link>
      <description>&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약&lt;/b&gt;: S3에 파일을 올려뒀을 때, 그 파일을 &lt;i&gt;&lt;b&gt;특정한 사람에게만, 잠깐&lt;/b&gt;&lt;/i&gt; 보여주고 싶다면 S3 Presigned URL을 쓰기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3의 기본 원칙: &lt;b&gt;모든 객체는 기본적으로 비공개이다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷을 만들고 파일을 올리면 그 순간부터 아무도 접근할 수 없음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 어떻게 버킷을 공유할지인데, 이때 아래 세가지 방법이 있다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버킷을 공개하는 3가지 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 버킷을 Public으로 열기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크만 주면 끝이고 관리할 게 없다는 점에서 편하긴 하지만, 링크를 아는 사람이면 누구든 받아갈 수 있어서 회사 내부 문서나 사용자 이력서를 관리해야한다면 적합하지 않은 방법이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 버킷 유출 사고는 대개 해당 부분에서 발생한다고 한다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/be1dd0d42a5dfed3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Access Key 나눠주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IAM 자격증명을 주는 방식으로, Access Key와 Secret Access Key를 발급해서 필요한 사람에게 건네면 이제 지정한 사람만 파일을 받을 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 만약 한 명이 실수로 깃허브에 커밋하는 것처럼 키가 유출되면? 공유받은 사람 &lt;b&gt;전원에게&lt;/b&gt; 다시 발급해줘야 하기 떄문에 관리 부담이 사람 수에 정비례해서 늘어나는 방법이다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/290ab75ee8023bd8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. IAM 사용자를 만들어주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키를 통째로 주는 게 위험하니, 사람마다 IAM 사용자를 만들어주자. 권한도 개별로 조절할 수 있어서 꽤 그럴 듯해보이지만&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 계정당 IAM 사용자는 5,000명이 한계이고, 우선 100명만 넘어가도 누가 들어오고 누가 나갔는지, 이 사람 권한을 회수했는지 안 했는지를 사람 손으로 좇는 순간부터 관리 부담이 커진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그리고 애초에 파일 하나 받으려고 aws 계정을 만들라고 할 순 없는 노릇)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/16b4ff628c88cd1e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;public은 위험하고, 개별 권한 부여는 번거롭다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4번째 방법 Presigned URL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지 방법 이외에 Presigned URL이라는 4번째 방법으로, &lt;b&gt;비공개 S3 객체에 한시적 접근을 부여하는, 서명된 URL이다&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/489b6075f107c59f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;우선 자격증명이 필요 없다&lt;/b&gt;&lt;br /&gt;URL을 받은 사람은 AWS 계정이나 Access Key, &lt;code&gt;aws configure&lt;/code&gt;도 필요 없이 그냥 브라우저에 붙여넣으면 접속할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;당연히 비밀 키는 URL에 들어가지 않는다&lt;/b&gt;&lt;br /&gt;SigV4로 서명하는데, 서명 결과만 URL에 실리고, 원래의 Secret Key는 절대 노출되지 않는다&lt;br /&gt;또한 특정 객체와 동작, 특정 만료 시간에만 유효하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 정리하면,&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;누구에게 &amp;rarr; URL을 가진 사람애게만
무엇을 &amp;rarr; 특정 key (flower.png 이미지와 같은 전달하고 싶은 것)
어떤 동작 &amp;rarr; get 또는 put
언제까지 &amp;rarr; expiresIn (특정 만료 시간) 초 동안&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cf. url을 발급받는데 정작 S3한테는 물어보지 않는다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL 생성은 API 호출이 아니라 &lt;b&gt;로컬에서 HMAC-SHA256 서명을 계산하는 순수 연산이므로,&lt;/b&gt; 인터넷이 끊겨 있어도 만들어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3는 나중에 그 URL로 요청이 들어왔을 때 서명이 유효한지 검증할 뿐, 발급 시점에는 무슨 url이 만들어졌는지조차 모르는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Presigned URL 발급 아키텍처&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/5812c37edc70712e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API Gateway:&lt;/b&gt; 요청을 받는 입구, 클라이언트의 요청을 Lambda로 라우팅하며 해당 실습에서는 presigned URL 발급 요청을 받는 API&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/7d4debc4a06eb427.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Lambda:&lt;/b&gt; 요청한 사람이 해당 파일을 받을 자격이 있는지 확인하고, 자격이 있다면 URL에 서명해서 돌려주는 역할&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/abb79cc5d4eb25db.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서명용 자격증명이 Lambda 함수 밖으로 절대 나가지 않는다&lt;/b&gt;는 것이 핵심!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 서명된 결과물만 받고, 전달할 파일은 s3 버킷 안에만 있고, 잠깐 문만 열어주는 셈이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 굳이 Lambda일까? &amp;rarr; 서버리스라 요청이 있을 때만 실행되고 유휴 시간에는 비용이 0이기 때문, url 발급은 몇 밀리초짜리 연산이니 람다가 적합하다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CloudFormation 템플릿을 하나 받아서 스택으로 배포하면 위 아키텍처가 통째로 생긴다&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;yaml 템플릿 업로드&lt;/li&gt;
&lt;li&gt;스택 이름 &lt;code&gt;presigned-url-lab&lt;/code&gt; 입력&lt;/li&gt;
&lt;li&gt;IAM 리소스 생성 승인 체크&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;2~3분쯤 지나 &lt;code&gt;CREATE_COMPLETE&lt;/code&gt;가 뜨면 출력 탭에 두 개가 나온다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ApiEndpoint:&lt;/b&gt; Presigned URL 발급 API 주소&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BucketName:&lt;/b&gt; 파일이 들어갈 S3 버킷 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/d87c797280b6344d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/53bd4135cb7b8d6e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;flower.png&lt;/code&gt;를 버킷에 업로드하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 콘솔에서 &lt;code&gt;flower.png&lt;/code&gt;를 클릭하면 &quot;객체 URL&quot;이 보인다. 그걸 새 탭에서 열어보면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/c36b72558117ffc0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 에러가 뜬다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/76b71c66c33fb958.png&quot; alt=&quot;&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;&lt;b&gt;아래에서 똑같은 파일에 똑같은 방식으로 접근했는데 성공하는&lt;/b&gt; 장면을 통해 무엇이 달라졌는지 알아보자&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GET: 다운로드용 URL 받아보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아까 확인한 &lt;code&gt;ApiEndpoint&lt;/code&gt;를 새 탭에서 열어도면 응답이 이렇게 온다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/a1688dd4f382a2e1.png&quot; alt=&quot;&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;code&gt;operation:&lt;/code&gt; 수행할 작업, &lt;code&gt;get&lt;/code&gt;은 다운로드/조회&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; : 버킷 안에서 접근할 객체 경로&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expiresIn&lt;/code&gt; : 유효 시간, 초 단위&lt;/li&gt;
&lt;li&gt;&lt;code&gt;presignedUrl&lt;/code&gt; : 발급된 임시 접근 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;presignedUrl&lt;/code&gt;의 쿼리스트링을 잘 보면 서명 알고리즘(&lt;code&gt;X-Amz-Algorithm&lt;/code&gt;), 발급 시각(&lt;code&gt;X-Amz-Date&lt;/code&gt;), 유효 기간(&lt;code&gt;X-Amz-Expires&lt;/code&gt;), 그리고 서명값(&lt;code&gt;X-Amz-Signature&lt;/code&gt;)이 다 들어 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Secret Key는 없이 서명 결과만 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 발급된 Presigned URL을 복사해서 새 탭에 붙여넣으면 방금 전과 달리 꽃 사진이 뜬다!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/9ccf483662a9b0b8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;바뀐 건 url 뒤에 붙은 쿼리스트링뿐인데, S3에 접근한 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 60초 뒤에 다시 눌러보면&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/291db703b7738bf5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Request has expired&lt;/code&gt;가 뜨며 만료 시각과 서버 시각까지 친절하게 알려준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PUT: 업로드용 URL은 뭐가 다를까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창의 &lt;code&gt;op=get&lt;/code&gt;&amp;rarr; &lt;code&gt;op=put&lt;/code&gt;으로 바꾸고, &lt;code&gt;key=flower.png&lt;/code&gt;&amp;rarr; &lt;code&gt;key=flower_upload.png&lt;/code&gt;로 바꿔서 다시 접속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 업로드용 url이 나오는데&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/fdf3980972e5b0bc.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널을 열어서 아래의 명령을 입력하고&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# macOS / Linux
curl -X PUT --upload-file ./flower_upload.png &quot;발급받은 Presigned URL&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Windows (명령 프롬프트)
curl -X PUT --upload-file flower_upload.png &quot;발급받은 Presigned URL&quot;
# PowerShell이라면 curl 대신 curl.exe&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/1a7c059108a805f8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 버킷을 새로고침하면 &lt;code&gt;flower_upload.png&lt;/code&gt;가 생겨 있다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 콘솔로 안 하고 굳이 터미널을 썼을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 콘솔 업로드는 Presigned URL을 안 쓴다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S3 콘솔에서 업로드 버튼을 누르는 순간, 브라우저는 &lt;b&gt;내가 로그인한 IAM 자격증명&lt;/b&gt;으로 S3에 요청을 보낸다&lt;br /&gt;성공해도 그건 &quot;나는 원래 권한이 있는 사람&quot;이라는 증명일 뿐이고, Presigned URL이 동작한다는 걸 하나도 보여주지 못한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;curl&lt;/code&gt;은 aws 자격증명이 전혀 설정되지 않은 상태에서 url 하나만으로 PUT을 성공시킨다. &lt;code&gt;aws configure&lt;/code&gt;를 한 번도 안 돌린 컴퓨터에서 해봐도 똑같이 성공한다 (&lt;b&gt;-&amp;gt; Presigned URL의 존재 이유)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 브라우저 주소창으로는 PUT을 보낼 수가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주소창에 url을 치면 브라우저는 무조건 GET을 보내기 때문이다&lt;br /&gt;GET용으로 서명된 url은 주소창으로 확인이 되지만, PUT용은 HTTP 메서드를 지정할 수 있는 도구가 필요하다 (ex. curl, postman, FE의 fetch 같은 것들 )&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;await fetch(presignedUrl, {
  method: 'PUT',
  body: file,
});&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 어디에 쓰나?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 파일 업로드&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[기존]  Client ──파일 전체──▶ API 서버 ──파일 전체──▶ S3
[개선]  Client ──요청──▶ API 서버 ──URL만──▶ Client
        Client ─────────────파일 전체─────────────▶ S3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식에선 3MB짜리 이미지든 2GB짜리 영상이든 전부 API 서버의 메모리를 통과했기 때문에 동시 업로드가 몰리면 서버 죽고 타임아웃, OOM, 대역폭 요금과 같은 문제가 발생했지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL을 쓰면 API 서버는 &lt;b&gt;인증하고 URL 하나 발급하는 일&lt;/b&gt;만 해서 프로필 이미지, 게시글 첨부, 이력서 pdf, 강의 영상과 같은 파일 데이터는 서버를 통과하지 않는다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Private 자산 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제한 사람만 받을 수 있는 자료, 이메일로 보내는 청구서 링크, 관리자만 볼 수 있는 리포트 등등&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 파이프라인의 입구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 RAG 시스템을 만들다 보면 사용자가 문서를 올리는 지점이 늘 첫 관문인데, 여기서도 결국 같은 패턴으로 수렴한다&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Presigned PUT &amp;rarr; S3 Object Created 이벤트 &amp;rarr; Lambda
    &amp;rarr; 파싱 / 청킹 / 임베딩 &amp;rarr; Vector DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bedrock Knowledge Base도 S3를 데이터 소스로 받으니 그대로 이어진다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미디어 처리 위임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 원본 영상을 S3에 직접 PUT하고, MediaConvert가 트랜스코딩하고, 결과물은 다시 Presigned GET으로 내려준다 (서버가 받지 않도록)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 반드시 알아야 할 것들&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL이 자격증명이 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Presigned URL이 bearer token이기 때문에&lt;/b&gt; URL을 가진 사람이 곧 권한자가 된다&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;CloudWatch Logs, Nginx access log에 URL 통째로 남기기&lt;/li&gt;
&lt;li&gt;Slack이나 이메일에 붙여넣기&lt;/li&gt;
&lt;li&gt;클라이언트 코드에 하드코딩&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유출되면 그냥 임시 공개 링크가 되기 때문에&lt;br /&gt;Presigned URL은 그 자체로 안전한 물건이 아니라, &quot;짧게 살고, 좁은 범위만 열고, 흔적을 남긴다&quot;는 전제 위에서 안전한 기능이다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만료 시간은 짧게 설정하자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드는 몇 분, 업로드는 길어야 십수 분으로 설정해서, 유출돼도 피해를 제한하는 방식으로 사용하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 알아둘 한계가 하나 있는데, &lt;b&gt;S3 Presigned URL은 일회용으로 만들 수 없다.&lt;/b&gt; 만료 전까지는 몇 번이든 재사용된다. 진짜 1회성이 필요하면 Lambda@Edge나 CloudFront Function으로 사용 횟수를 추적하는 레이어를 따로 얹어야 한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발급자의 권한을 넘을 수 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda 실행 역할에 &lt;code&gt;s3:DeleteObject&lt;/code&gt;가 없으면, DELETE용 URL을 만들어봐야 호출 시점에 &lt;code&gt;AccessDenied&lt;/code&gt;가 난다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL은 &lt;b&gt;권한을 새로 만드는 게 아니라 내 권한을 잠깐 빌려주는 것이므로&lt;/b&gt; Lambda 역할부터 최소 권한으로 두어야한다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제대로 보안을 챙기려면 URL이 아니라 발급하는 문을 지켜야 한다&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침할 때마다 URL이 재생성되는데, DoS 아닌가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말했듯 url 생성 자체는 로컬 서명 연산이라 S3에는 부하가 0이다. 진짜 위험한 건,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;발급 엔드포인트:&lt;/b&gt; API Gateway와 Lambda는 요청당 과금되고 동시성 한도가 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발급된 URL로 실제 GET이 몰릴 때:&lt;/b&gt; S3 요청 비용 + 데이터 전송 비용&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제를 막기 위해선 레이어를 여러 겹 쌓아 해결할 수 있다&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;계층&lt;/th&gt;
&lt;th&gt;조치&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;인증&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;가장 중요! Cognito나 Lambda Authorizer로 로그인한 사용자만 받도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;레이트 리밋&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;API Gateway Throttling, Usage Plan, WAF Rate-based rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;캐싱&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;같은 사용자, 같은 객체면 남은 TTL 동안 기존 URL 재사용하면 새로고침해도 새 URL이 안 나간다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Lambda 보호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Reserved concurrency로 상한선 고정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비용 방어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CloudWatch 비용 알람, 앞단에 CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CloudFront Signed URL&lt;/b&gt; &amp;rarr;대규모 배포, 엣지 캐싱, IP 제한이 필요한 경우에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lambda가 &quot;권한 확인&quot;을 한다고 되어 있는데 실제 실습상의 api 주소를 보면 &lt;code&gt;token=dem&lt;/code&gt;로, 데모용 문자열 하나로 통과시켜서 간단히 실습해보았지만&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실서비스라면 그 자리에 뭐가 들어갈까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cognito Authorizer? JWT 검증? Lambda Authorizer로 세션 확인?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL 발급 코드는 사실 boto3 세 줄이면 끝나고, 나머지 전부는 &quot;이 사람이 이 파일을 받을 자격이 있는가&quot;를 판단하는 로직일 것 같다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리소스 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;S3 버킷 안의 객체 삭제 (버킷 목록에서 &quot;비어 있음&quot; 사용 가능)&lt;/li&gt;
&lt;li&gt;S3 버킷 삭제&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/205d058182f49a02.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;CloudFormation 스택 삭제&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/be59e4a6226f3be7.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버킷에 객체가 남아 있으면 스택 삭제가 실패하니 위 순서대로 고고&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Presigned URL을 처음 봤을 땐 최소 권한을 url이라는 형태로 구현했다는 점에서 보안 역할이라 생각했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실무에서 이걸 도입하는 진짜 이유는 서버를 파일 데이터의 통로에서 빼내어 무거운 것을 서버 대신 S3가 받게 한다는 점에서 &lt;b&gt;아키텍처가 얇아진다는 것&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 이유 때문에 Presigned URL의 보안 역할은 오히려 사이드 이펙트에 가깝다는 생각이 들었다!&lt;/p&gt;</description>
      <category>aws</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/69</guid>
      <comments>https://h0wdy.tistory.com/69#entry69comment</comments>
      <pubDate>Fri, 10 Jul 2026 13:15:30 +0900</pubDate>
    </item>
    <item>
      <title>Frontend 인턴 필기 시험 문제 공유</title>
      <link>https://h0wdy.tistory.com/62</link>
      <description>&lt;p&gt;총 6문제 / 25분&lt;br&gt;모두 서술형&lt;/p&gt;
&lt;p&gt;필기 시험이라고 해서 &lt;a href=&quot;https://github.com/jbee37142/Interview_Question_for_Beginner#%EB%A9%B4%EC%A0%91%EC%97%90%EC%84%9C-%EB%B0%9B%EC%95%98%EB%8D%98-%EC%A7%88%EB%AC%B8%EB%93%A4&quot;&gt;운영체제와 컴퓨터구조 CS문제&lt;/a&gt;를 보고 갔는데,&lt;br&gt;Frontend 직무다보니, JavaScript 기초, CSS 구현, 함수 구현, 결과 출력 예시와 이유를 서술하는 유형이 출제되었다&lt;/p&gt;
&lt;p&gt;회사의 기술 스택은 Vue+Typescript, SCSS였지만 해당 기술 스택으로 구현하라고 명시되어있진 않았다&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Q1. CSS 작성&lt;/h2&gt;
&lt;p&gt;아래 조건을 만족하는 CSS를 작성하시오.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&amp;quot;page&amp;quot;&amp;gt;
  &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;Login&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;조건&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.page&lt;/code&gt;는 뷰포트를 가득 채울 것&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.card&lt;/code&gt;는 페이지 내에서 좌우, 위아래 가운데 정렬&lt;/li&gt;
&lt;li&gt;모바일 뷰에서는 최대 너비 &lt;code&gt;360px&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PC 뷰에서는 너비 &lt;code&gt;100%&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;좌우 마진은 &lt;code&gt;16px&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Q2. 얕은 복사&lt;/h2&gt;
&lt;p&gt;아래 코드의 출력 결과를 작성하고,&lt;br&gt;원본 &lt;code&gt;user&lt;/code&gt;를 보존할 수 있도록 &lt;code&gt;copyUser&lt;/code&gt;를 다시 구현하시오.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const user = {
  name: &amp;#39;noah&amp;#39;,
  age: 25,
  city: &amp;#39;seoul&amp;#39;
}

const copyUser = { ...user }

copyUser.name = &amp;#39;Kim&amp;#39;
copyUser.city = &amp;#39;busan&amp;#39;

console.log(user.name)
console.log(user.city) &lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Q3. 실행 순서&lt;/h2&gt;
&lt;p&gt;아래 코드의 출력 순서를 작성하고, 이유를 설명하시오.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;console.log(&amp;#39;1&amp;#39;)

setTimeout(() =&amp;gt; {
  console.log(&amp;#39;2&amp;#39;)
}, 0)

Promise.resolve().then(() =&amp;gt; {
  console.log(&amp;#39;3&amp;#39;)
})

console.log(&amp;#39;4&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Q4. 함수 구현&lt;/h2&gt;
&lt;p&gt;원하는 출력 결과가 나오도록 함수를 구현하시오.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const users = [
  { userId: 1, amount: 12000, status: &amp;#39;pending&amp;#39; },
  { userId: 2, amount: 7000,  status: &amp;#39;pending&amp;#39; },
  { userId: 1, amount: 2000,  status: &amp;#39;process&amp;#39; },
  { userId: 1, amount: 3000,  status: &amp;#39;pending&amp;#39; }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;출력 결과&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;userId 1: 15000
userId 2: 7000&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;
&lt;h2&gt;Q5. 클로저 카운터&lt;/h2&gt;
&lt;p&gt;아래 코드의 출력 결과를 작성하고, 이유를 설명하시오.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createCounter() {
  let count = 0

  return function() {
    return ++count
  }
}

const counterA = createCounter()
const counterB = createCounter()

console.log(counterA())
console.log(counterB())
console.log(counterA())
console.log(counterB())&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Q6. 상태값 &amp;amp; 주의사항&lt;/h2&gt;
&lt;p&gt;상품 목록 API를 호출해 상품 목록을 받아온다.&lt;br&gt;로딩 중에는 로딩 상태를 노출하고, 실패 시 에러 상태를 노출한다.&lt;br&gt;사용자는 검색어를 빠르게 여러 번 입력할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 필요한 상태값 3가지를 작성하시오.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 이 구현에서 주의할 점을 2가지 이상 작성하시오.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;회사 이름은 명시하지 않았습니다&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;</description>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/62</guid>
      <comments>https://h0wdy.tistory.com/62#entry62comment</comments>
      <pubDate>Fri, 10 Jul 2026 13:01:04 +0900</pubDate>
    </item>
    <item>
      <title>velog에서 tistory로 블로그 이전 자동화 하기 (+ 코드 공유)</title>
      <link>https://h0wdy.tistory.com/60</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://h0wdy.tistory.com/&quot;&gt;나의 티스토리&lt;/a&gt; 글 목록을 보면 알다싶이, 고3때 대학 입시를 위해 딥러닝을 짧게 공부했었다. 이후 대학에 와서 개발에 관심을 가진 이후로는 기록의 중요성을 느끼고 블로그를 다시 쓰게 되었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;velog는 home에 추천 게시물이 있고, 최신 트렌드 및 취준 인사이트를 쉽게 얻을 수 있어서 참 좋다&lt;br /&gt;또한 따로 초기 세팅 없이 기본 테마가 적용되어서 심플하고, 마크다운을 지원하기 때문에 코드 입력도 편하다&lt;br /&gt;그래서 &amp;lsquo;글을 쓰는 것&amp;rsquo;에만 집중할 수 있는게 가장 큰 장점인 거 같다&lt;br /&gt;덕분에 약 2년동안 나름 열심히 기록을 하며 벌써 9개의 시리즈를 썼다는 걸 처음 알았다&lt;/p&gt;
&lt;h1&gt;옮기는 이유&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 고3 때 블로그를 운영하던 당시, 방문자 통계를 보던 재미가 있었는데, velog는 방문자의 경로를 볼 수는 없고 방문자 수만 볼 수 있다는게 아쉬웠다 (근데 본인까지 카운트 됨..!)&lt;br /&gt;또한 velog는 html 기반인지라 구글 서치에 등록이 안되기 때문에 블로그 노출도 되지 않는다&lt;br /&gt;또한 티스토리는 테마를 마음껏 커스터마이징 할 수 있기 때문에 올해부터 천천히 마음이 기울었다&lt;br /&gt;그치만 그동안 써운 게시물을 수작업으로 바꾸는 건 너무 시간이 오래 걸릴 거 같아서 고민하다가&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;h1&gt;자동화할 순 없을까?&lt;/h1&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 생각을 하고, 해당 글을 작성하게 되었다&lt;/p&gt;
&lt;h1&gt;전체 그림부터 그리기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 코드를 작성하기 전에, 문제를 세 단계로 나눠봤다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;velog에서 내 글을 마크다운으로 뽑아온 뒤,&lt;/li&gt;
&lt;li&gt;본문 속 이미지를 어딘가에 다시 올리고 링크를 바꾸고,&lt;/li&gt;
&lt;li&gt;뽑아온 마크다운을 tistory에 글로 올린다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로는 여기서 가장 먼저 막힌 게 3번이었다. &lt;a href=&quot;https://notice.tistory.com/2664&quot;&gt;티스토리 Open API가 2024년 2월에 완전히 종료&lt;/a&gt;됐기 때문에 글을 프로그래밍으로 발행하는 공식 경로가 사라진 셈이다. 그래서 발행은 어쩔 수 없이 &lt;b&gt;Playwright를 사용했고&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;&lt;b&gt;추출&lt;/b&gt;: velog GraphQL API &amp;rarr; 글 본문(마크다운)을 그대로 가져올 수 있었다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지&lt;/b&gt;: velog 이미지를 내려받아 GitHub에 올리고 jsDelivr(CDN)로 서빙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;발행&lt;/b&gt;: Playwright로 tistory 에디터를 열어 제목&amp;amp;본문 입력 후 발행&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;문제1. 마크다운 모드로 전환이 안 됨&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 테스트를 해보니 제목까지는 잘 입력되는데, 본문 에디터를 마크다운 모드로 바꾸는 데서 에러가 났다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/87720fb457cc063e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;monkey&quot;&gt;&lt;code&gt;strict mode violation: locator(&quot;.CodeMirror textarea&quot;) resolved to 2 elements&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러메세지를 보면 CodeMirror textarea 요소가 2개라고 뜬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Playwright는 셀렉터가 &lt;b&gt;정확히 하나의 요소&lt;/b&gt;를 가리킬 때만 동작하기 때문에 오류가 발생하는 것이었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 CodeMirror ID를 명시적으로 박아서 해결하려 했는데, 계속 타임아웃이 났다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;Playwright Inspector&lt;/b&gt;를 띄워서 &lt;code&gt;--debug&lt;/code&gt; 상태에서 브라우저가 멈춰 있을 때, Inspector의 &lt;b&gt;&quot;Pick locator&quot;&lt;/b&gt; 버튼으로 화면 요소를 직접 클릭하면 정확한 셀렉터를 찾았다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/ad60658e1b6d256d.png&quot; alt=&quot;&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;본문 편집기는 처음 생각했던 것과 구조가 달랐다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(기본 모드는 iframe 기반 리치 텍스트고, 마크다운 모드는 CodeMirror였다.) 엉뚱한 셀렉터를 찾고 있었으니 될 리가 없었다&lt;/p&gt;
&lt;h1&gt;문제2. 한 번에 터진 네 가지 문제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀렉터를 맞추고 나니 첫 글이 &lt;b&gt;발행됐다!&lt;/b&gt; 그런데 결과물이 엉망이었다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/7f3d7e386927fcea.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;#&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt; 같은 마크다운 문법이 적용되지 않고 글자 그대로 노출됨&lt;/li&gt;
&lt;li&gt;이미지가 마크다운 문법(&lt;code&gt;![]()&lt;/code&gt;) 텍스트 그대로 보임&lt;/li&gt;
&lt;li&gt;글 발행 순서가 최신순이 아닌 알파벳순으로 정렬&lt;/li&gt;
&lt;li&gt;이상하게 본문 전체에 취소선이 그어진 채 발행됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 네 개가 다 다른 문제인 줄 알았지만 파고들수록, 12는 하나의 원인에서 나온 거였다&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/f8fbcd03c86a4525.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 마크다운 모드로 전환이 실제로는 안 됐던 것인데 .. 마크다운 버튼을 누르면 &lt;code&gt;작성 모드를 변경하시겠습니까?&lt;/code&gt;라는 확인창이 떴다가 0.1초 만에 사라지고, 모드는 그대로였던 거 였다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 확인창은 &lt;b&gt;브라우저 네이티브 &lt;code&gt;confirm()&lt;/code&gt; 다이얼로그&lt;/b&gt;라서, 일반 DOM 버튼처럼 클릭할 수 없었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Playwright는 다이얼로그 핸들러가 없으면 그걸 &lt;b&gt;자동으로 취소(dismiss)&lt;/b&gt;해버리기 때문에 매번 전환이 안됐던 것이다&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;# 네이티브 confirm 다이얼로그는 이렇게 수락해야 함
page.once(&quot;dialog&quot;, lambda d: d.accept())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위를 스크립트에 추가하니 마크다운 모드가 제대로 켜졌고, 12가 해결됐다 (3 발행 순서는 별개 문제라, 파일명순 정렬을 글 작성일(date) 기준 오름차순으로 바꿔서 오래된 글부터 발행되게 고쳤다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 4번 취소선 같은 문제는 아직 해결이 안돼서 수동으로 취소해주고 있다 ... 짐작가는 원인은 캐시 문제인 거 같은데 좀 더 시도해봐야겟다&lt;/p&gt;
&lt;h1&gt;문제3. 에디터에선 멀쩡한데, 발행하면 빈 본문&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/d9da898b5f9ab12c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/d34c4e4623a0a872.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마크다운도 잘 적용되었고, 이제 모두 발행하려하니 발행하거나 미리보기를 누르면 본문이 텅 비어 있었다&lt;br /&gt;에디터 화면에는 본문 1807자가 멀쩡히 들어가 있는데, 다시 원인을 파악해보았다 ..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 &lt;code&gt;CodeMirror.setValue()&lt;/code&gt;로 값을 넣으면 &lt;b&gt;에디터 화면(표시 레이어)에는&lt;/b&gt; 텍스트가 들어가지만, &lt;b&gt;티스토리가 발행 시 실제로 읽어가는 저장 필드에는 동기화가 안 걸린다&lt;/b&gt; 프로그래밍으로 값을 주입한 거라, 티스토리 내부의 &amp;lsquo;사용자가 뭔가 입력했다&amp;rsquo;는 변경 감지가 작동하지 않았던 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은 &lt;b&gt;실제 사용자가 타이핑한 것처럼&lt;/b&gt; 만드는 것이었다&lt;br /&gt;에디터를 클릭해 포커스를 준 뒤 키보드 입력으로 본문을 넣으니, 변경 감지가 정상적으로 걸려 저장 필드까지 동기화됐다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 과정에서 ui 상태와 저장 상태는 별개라는 걸 다시 상기했다&lt;/b&gt;&lt;br /&gt;화면에 보이는 것과 시스템이 저장하는 것은 다르다  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;문제4.이미지 서빙 안됨&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 관문은 이미지였다&lt;br /&gt;분명 깃헙에 잘 올렸는데, jsDelivr URL을 열면 404가 떴다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 메세지:&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Couldn't find the requested file /assets/....png in yooncandooit/velog2tistory.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소는 찾았는데 그 파일은 못 찾는다는 메시지가 뜨길래, 확인해보니 파일은 로컬에도 있고, &lt;code&gt;git ls-files&lt;/code&gt;에도 잡히고, 원격에도 push돼 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 웹에서는 이미지가 멀쩡히 보이기까지 했다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단서는 GitHub blob 페이지의 raw 링크가 &lt;code&gt;@main&lt;/code&gt;이 아니라 &lt;b&gt;커밋 해시&lt;/b&gt;로 뜬다는 점이었다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;.../blob/7c2971372864a8ce.../assets/...   &amp;larr; @main 이 아니라 커밋 해시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 &lt;b&gt;저장소가 Private일 때&lt;/b&gt; 나타나는 특징이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jsDelivr나 raw.githubusercontent.com은 Private 저장소를 서빙하지 않는다. 그렇기 때문에 인증이 없는 CDN 입장에선 그 파일에 접근할 수 없으니 404였던 것이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(알고보니 내가 로그인돼 있어서 깃헙 웹에서만 보였던 거)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 호스팅 용도이니 저장소를 Public으로 전환했고, 그제야 CDN이 이미지를 정상적으로 서빙한 걸 확인 할 수 있었다&lt;/p&gt;
&lt;h1&gt;  이전 성공&lt;/h1&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;h1&gt;배운 점&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;setValue&lt;/code&gt;로 화면은 채워도 저장 필드는 비어 있을 수 있다&lt;br /&gt;프레임워크가 값을 언제 읽는지를 이해해야 하는 디버깅 과정이었다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네이티브 다이얼로그는 DOM이 아니다&lt;/b&gt;&lt;br /&gt;&lt;code&gt;confirm()&lt;/code&gt;은 버튼 클릭이 아니라 dialog 이벤트로 처리해야 하고, 핸들러가 없으면 자동 취소된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;여러 증상이 한 원인에서 나올 수 있다.&lt;/b&gt; 마크다운 미적용, 이미지 노출이 전부 모드 전환 실패 하나였다&lt;br /&gt;원인을 찾을 때 공통 원인일 수도 있다는 걸 배웠다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원인은 이분법으로 좁힌다 &amp;rarr;&lt;/b&gt; raw와 jsDelivr를 나눠 테스트해서 캐시 문제인지 경로 문제인지 갈랐고, 커밋 해시라는 작은 단서가 private 저장소를 짚어줬다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 메시지를 정확히 읽자.&lt;/b&gt; &quot;resolved to 2 elements&quot;, &quot;저장소는 찾았으나 파일 없음&quot;등등 메시지가 이미 원인을 절반쯤 알려주고 있어서 빠른 문제해결에 큰 도움이 되었다&lt;/li&gt;
&lt;li&gt;마지막으로, 혼자하는 프로젝트에서 issue/PR 생성은 오버 엔지인듯 해서 issue 칸을 트러블 슈팅 기록 칸으로 썼다, 바로바로 이슈를 기록하는 습관을 기르기에 편한 방법인 듯하여 앞으로 개인 프로젝트 시 해당 방식으로 기록해도 좋을 거 같다&lt;br /&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/ebcd2506626d49a8.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;  자동화 코드 오픈소스로 공개 &lt;a href=&quot;https://github.com/yooncandooit/velog2tistory&quot;&gt;(링크!)&lt;/a&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 만든 스크립트는 주석, 임시 코드와 여러 번 고친 흔적이 많아서,&lt;br /&gt;조만간 불필요한 부분을 정리하고, 남이 바로 따라 할 수 있는 형태로 다듬어 오픈소스로 공개하려 합니다.&lt;br /&gt;취소선 이슈 or 혹시 쓰다가 오류가 나는 부분이 있으면 &lt;b&gt;이슈로 남겨주시면&lt;/b&gt; 감사하겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cf. 이 방식은 브라우저 자동화라 티스토리 에디터 UI가 바뀌면 셀렉터가 깨질 수 있고, 어디까지나 &lt;b&gt;본인 계정에서 본인 글을 옮기는 개인 백업/이전 용도입니다.&lt;/b&gt; 각 플랫폼의 이용약관은 사용자 책임 하에 확인하시길 바랍니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>python</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/60</guid>
      <comments>https://h0wdy.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 10 Jul 2026 12:50:19 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] DMS, Route 53, ACM</title>
      <link>https://h0wdy.tistory.com/59</link>
      <description>&lt;h1&gt;5. 온프레미스와 AWS 아키텍처 비교해보기&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;온프레미스&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사가 직접 서버&amp;amp;장비를 사서 자체 건물(데이터센터)에 설치해 운영&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/f207c017fc0a94ff.png&quot; alt=&quot;&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;클라우드(aws)의 반대말이라고 생각하면 됨&lt;/li&gt;
&lt;li&gt;서버, 네트워크 장비, 전기, 냉각, 유지보수까지 전부 회사가 직접 관리&lt;/li&gt;
&lt;li&gt;ex: 회사 지하 서버실에 직접 산 델(Dell) 서버 놓고 운영&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS와 비교&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;온프레미스&lt;/th&gt;
&lt;th&gt;AWS(클라우드)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&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;회사가 직접 (전기, 냉각, 하드웨어 교체)&lt;/td&gt;
&lt;td&gt;AWS가 대신 함&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;고가용성 (HA)&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 일부가 고장 나도 &lt;b&gt;서비스 전체는 계속 돌아가게&lt;/b&gt; 만드는 설계&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS에서 HA를 만드는 대표 방법&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;서버(EC2)를 여러 가용 영역(AZ)에 나눠 배치 &amp;rarr; 한 AZ가 통째로 죽어도 다른 AZ가 살아있음&lt;/li&gt;
&lt;li&gt;로드밸런서(ELB)로 여러 서버에 트래픽 분산 &amp;rarr; 서버 1대 죽어도 나머지가 처리&lt;/li&gt;
&lt;li&gt;DB도 다중 AZ(Multi-AZ) 배포 &amp;rarr; 주 DB 죽으면 자동으로 대기 DB가 이어받음&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;6. DMS(Database Migration Service) 설명&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/bb92265f6f165e09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 DB에 있는 데이터를 목적지 DB(RDS)로 복사해서 옮겨주는 서비스&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 DB 주소/ID/비번, 목적지 DB(RDS) 주소/ID/비번을 입력하면&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[온프레미스 원본 DB] &amp;rarr; (DMS 인스턴스가 읽어서) &amp;rarr; [AWS RDS]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DMS가 중간에서 데이터 읽고 옮겨쓰는 &lt;code&gt;복사&lt;/code&gt; 역할을 하나 만들어주는 거라고 생각하면 됨 (사람이 직접 데이터를 export/import 안 해도 됨)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/df26c8a1ef4f9d37.png&quot; alt=&quot;&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;빠르고 안전하게 DB를 AWS로 마이그레이션하며, 복원성이 좋고 자가 복구 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;마이그레이션 중에도 원본 DB는 사용 가능&lt;/li&gt;
&lt;li&gt;동종 (Homogeneous) 마이그레이션: ex. Oracle에서 Oracle로&lt;/li&gt;
&lt;li&gt;이기종 (Heterogeneous) 마이그레이션: ex. Microsoft SQL Server에서 Aurora로&lt;/li&gt;
&lt;li&gt;CDC(Continuous Data Replication)를 사용한 지속적인 데이터 복제, 마이그레이션하는 동안에도 원본 DB를 계속 운영하면서, 그 사이 바뀐 데이터까지 실시간으로 따라 옮겨주는 기능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;다운타임 최소화&quot;, &quot;무중단 마이그레이션&quot; 같은 문구가 지문에 나오면 &amp;rarr; CDC&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;DMS를 사용하려면 복제 작업을 수행하기 위해 EC2 인스턴스를 생성해야 함&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;온프레미스에서 운영하던 쇼핑몰을 AWS로 그대로 옮기는 중&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;쇼핑몰 코드 &amp;rarr; EC2 인스턴스에 배포&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 데이터 &amp;rarr; RDS로 이전&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;도메인 주소 &amp;rarr; AWS 로드밸런서로 연결&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. Route 53과 ACM으로 도메인 연결하기&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/aa83b7be7a6e1493.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;07. Route 53과 ACM으로 도메인 연결하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Route 53&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도메인 이름을 IP 주소로 바꿔주는 AWS의 DNS 서비스&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사람은 example.com 같은 이름을 치지만, 컴퓨터끼리는 IP 주소로만 통신함&lt;/li&gt;
&lt;li&gt;이 둘을 연결(변환)해주는 시스템을 DNS라고 부르고, AWS 서비스 = Route 53
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 DNS 서버를 구축하면 네임서버 설정, 안정성 유지까지 다 회사가 관리해야 해서 복잡함 &amp;rarr; Route 53은 AWS가 대신 관리해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Route 53의 3가지 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;도메인 등록&lt;/td&gt;
&lt;td&gt;Route 53에서 직접 도메인을 사고 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;헬스 체크&lt;/td&gt;
&lt;td&gt;서버가 살아있는지 주기적으로 확인 &amp;rarr; 장애 나면 자동으로 다른 정상 리전으로 트래픽 우회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트래픽 라우팅&lt;/td&gt;
&lt;td&gt;지리적 위치 / 지연 시간 / 가중치 기반 등 다양한 방식으로 트래픽 분산&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트래픽 라우팅 &amp;rarr; 라우팅 정책 종류&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;요구사항 키워드&lt;/th&gt;
&lt;th&gt;라우팅 정책&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;사용자와 가장 가까운 서버로&lt;/td&gt;
&lt;td&gt;Geoproximity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;특정 국가/지역 사용자만 다른 곳으로&lt;/td&gt;
&lt;td&gt;Geolocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;응답 속도가 가장 빠른 리전으로&lt;/td&gt;
&lt;td&gt;Latency-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 서버에 비율 나눠서(예: 90:10 카나리 배포)&lt;/td&gt;
&lt;td&gt;Weighted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 시 대체 서버로 자동 전환&lt;/td&gt;
&lt;td&gt;Failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 IP를 무작위로 반환해 단순 분산&lt;/td&gt;
&lt;td&gt;Multivalue Answer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Certificate Manager(ACM)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트 주소창의 자물쇠, 즉 HTTPS를 가능하게 하는 SSL/TLS 인증서를 발급하고 자동으로 관리해주는 서비스&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTPS = 사용자 데이터가 암호화돼서 안전하게 오가는 연결. 그걸 가능하게 하는 게 SSL/TLS 인증서&lt;/li&gt;
&lt;li&gt;원래는 인증서를 직접 사고, 서버에 설치하고, 만료 전에 갱신까지 다 챙겨야 해서 번거롭고, 깜빡하면 서비스가 갑자기 끊길 위험도 있음&lt;/li&gt;
&lt;li&gt;ACM이 이 전 과정(발급 &amp;rarr; 설치 &amp;rarr; 갱신)을 자동화해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ACM의 4가지 주요 기능&lt;/b&gt;&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;무료 발급&lt;/td&gt;
&lt;td&gt;퍼블릭 도메인용 SSL/TLS 인증서 무료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;ELB, CloudFront, API Gateway 등에 클릭 몇 번으로 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프라이빗 인증서&lt;/td&gt;
&lt;td&gt;내부 서비스 전용 인증서도 발급 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;문제&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q1&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 회사가 온프레미스에서 단일 서버로 웹 애플리케이션을 운영 중이며, 서버 장애 시 서비스가 완전히 중단되는 문제를 겪고 있습니다. AWS로 이전하면서, 한 가용 영역(AZ)에 장애가 발생해도 애플리케이션이 계속 동작하도록 만들고자 합니다. 트래픽 증가에도 자동으로 대응해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 하나의 AZ에 대형 EC2 인스턴스 1대를 배치하고 정기적으로 스냅샷을 생성한다.&lt;br /&gt;B. 여러 AZ에 걸친 EC2 인스턴스를 Auto Scaling 그룹으로 구성하고, 앞단에 Application Load Balancer를 둔다.&lt;br /&gt;C. 하나의 AZ에 EC2 인스턴스 2대를 배치하고 그 앞에 ALB를 둔다.&lt;br /&gt;D. EC2 인스턴스 1대에 Elastic IP를 할당하고 장애 시 수동으로 교체한다.&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;&lt;b&gt;1. 요구사항 분석&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;AZ 장애에도 &lt;b&gt;무중단&lt;/b&gt; &amp;rarr; 다중 AZ 분산 필수&lt;/li&gt;
&lt;li&gt;트래픽 증가 &lt;b&gt;자동 대응&lt;/b&gt; &amp;rarr; Auto Scaling&lt;/li&gt;
&lt;li&gt;서버 앞단에서 트래픽 &lt;b&gt;분산&lt;/b&gt; &amp;rarr; 로드 밸런서&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 관련 AWS 서비스&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;Auto Scaling Group&lt;/b&gt;: 부하에 따라 인스턴스 수를 자동 조절, 여러 AZ에 분산 배치 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ELB(ALB)&lt;/b&gt;: 여러 인스턴스로 트래픽 분산 + 비정상 인스턴스 자동 제외&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가용 영역(AZ)&lt;/b&gt;: 서로 물리적으로 분리 &amp;rarr; 한 AZ 장애가 다른 AZ에 영향 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 선택지 분석&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;A &amp;rarr; 단일 AZ + 단일 인스턴스라 SPOF(단일 장애점). 스냅샷은 백업일 뿐 가용성 해결책이 아님&lt;/li&gt;
&lt;li&gt;B &amp;rarr; &lt;b&gt;다중 AZ + ASG + ALB로 고가용성과 자동 확장을 모두 충족&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;C &amp;rarr; 인스턴스는 2대지만 &lt;b&gt;단일 AZ&lt;/b&gt;라 그 AZ 전체 장애에 취약&lt;/li&gt;
&lt;li&gt;D &amp;rarr; 여전히 단일 인스턴스 + 수동 복구라 무중단 요건 미충족&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 도메인 이름 레코드를 호스팅하는 DNS 공급자가 AWS에서 실행되는 웹 사이트의 서비스 중단을 초래하는 중단을 겪고 있습니다. 회사는 보다 탄력적인 관리형 DNS 서비스로 마이그레이션해야 하며 해당 서비스가 AWS에서 실행되기를 원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 호스팅 서비스를 신속하게 마이그레이션하려면 솔루션 설계자가 무엇을 해야 합니까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 도메인 이름에 대한 Amazon Route 53 퍼블릭 호스팅 영역을 생성합니다. 이전 공급자가 호스팅하는 도메인 레코드가 포함된 영역 파일을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. 도메인 이름에 대한 Amazon Route 53 프라이빗 호스팅 영역을 생성합니다. 이전 공급자가 호스팅하는 도메인 레코드가 포함된 영역 파일을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C. AWS에서 Simple AD 디렉터리를 생성합니다. 도메인 레코드에 대해 DNS 공급자와 Microsoft Active Directory용 AWS Directory Service 간의 영역 전송을 활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D. VPC에 Amazon Route 53 Resolver 인바운드 엔드포인트를 생성합니다. 공급자의 DNS가 DNS 쿼리를 전달할 IP 주소를 지정합니다. 도메인에 대한 DNS 쿼리를 인바운드 엔드포인트에 지정된 IP 주소로 전달하도록 공급자의 DNS를 구성합니다.&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;외부 인터넷 사용자가 접속하는 웹 사이트의 DNS 장애를 해결하고 AWS로 전환하려면, 인터넷 트래픽을 라우팅할 수 있는 &lt;b&gt;Amazon Route 53 퍼블릭 호스팅 영역&lt;/b&gt;이 필요합니다. 기존 DNS 공급자로부터 영역 파일(Zone File)을 추출하여 Route 53에 가져오기(Import)하면 레코드를 수동으로 하나씩 생성할 필요 없이 신속하게 마이그레이션을 완료할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 문제의 요구사항 분석하기&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;DNS 서비스 중단 신속 해결&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;탄력적인 &lt;b&gt;AWS 관리형 DNS 서비스&lt;/b&gt;로 마이그레이션.&lt;/li&gt;
&lt;li&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;2. 관련 AWS 서비스 생각하기&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;Amazon Route 53 :&lt;/b&gt; 가용성과 확장성이 뛰어난 AWS의 클라우드 DNS 웹 서비스로, &lt;b&gt;퍼블릭 호스팅 영역&lt;/b&gt;(인터넷 트래픽 라우팅용)과 &lt;b&gt;프라이빗 호스팅 영역&lt;/b&gt;(VPC 내부용) 두 가지 유형을 제공합니다. 또한 BIND 형식의 &lt;b&gt;영역 파일 가져오기(Import Zone File)&lt;/b&gt; 기능을 통해 기존 DNS 공급자의 레코드를 한 번에 옮겨 올 수 있어 마이그레이션 시간을 단축할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Amazon Route 53 Resolver :&lt;/b&gt; VPC와 온프레미스 네트워크 간 DNS 쿼리를 해석할 수 있도록 하는 하이브리드 DNS 기능으로, 인바운드/아웃바운드 엔드포인트를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS Directory Service (Simple AD 등) :&lt;/b&gt; AWS에서 관리형 디렉터리 서비스를 제공하며, 주로 사용자 인증 및 내부 AD 통합 용도로 활용됩니다.&lt;/li&gt;
&lt;/ul&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;A. 도메인 이름에 대한 Amazon Route 53 퍼블릭 호스팅 영역을 생성합니다. 이전 공급자가 호스팅하는 도메인 레코드가 포함된 영역 파일을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 인터넷 사용자를 위한 퍼블릭 호스팅 영역을 생성하고, 영역 파일을 가져와 신속하게 마이그레이션할 수 있는 해결책입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. 도메인 이름에 대한 Amazon Route 53 프라이빗 호스팅 영역을 생성합니다. 이전 공급자가 호스팅하는 도메인 레코드가 포함된 영역 파일을 가져옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 프라이빗 호스팅 영역은 하나 이상의 특정 VPC 내에서만 내부적으로 트래픽을 라우팅할 때 사용되므로, 퍼블릭 웹 사이트용으로는 적합하지 않습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C. AWS에서 Simple AD 디렉터리를 생성합니다. 도메인 레코드에 대해 DNS 공급자와 Microsoft Active Directory용 AWS Directory Service 간의 영역 전송을 활성화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; Simple AD는 내부 디렉터리 서비스 및 인증 용도이며, 일반적인 인터넷 웹 사이트의 글로벌 DNS 호스팅을 위한 서비스가 아닙니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D. VPC에 Amazon Route 53 Resolver 인바운드 엔드포인트를 생성합니다. 공급자의 DNS가 DNS 쿼리를 전달할 IP 주소를 지정합니다. 도메인에 대한 DNS 쿼리를 인바운드 엔드포인트에 지정된 IP 주소로 전달하도록 공급자의 DNS를 구성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; Route 53 Resolver 인바운드 엔드포인트는 온프레미스 네트워크에서 AWS VPC 내부 리소스의 DNS를 해석할 때 사용하는 하이브리드 구성이므로 퍼블릭 웹 호스팅 목적과 무관합니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>aws</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/59</guid>
      <comments>https://h0wdy.tistory.com/59#entry59comment</comments>
      <pubDate>Fri, 10 Jul 2026 12:50:05 +0900</pubDate>
    </item>
    <item>
      <title>LawLens 서비스 오픈소스 기여! + 스터디 회고</title>
      <link>https://h0wdy.tistory.com/57</link>
      <description>&lt;h1&gt; &amp;zwj;⚖️ 들어가기에 앞서&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;무슨 프로젝트인가요?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소개&lt;/h3&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;zwj;⚖️ &lt;b&gt;&amp;ldquo;법제처 공식 DB + 자체 RAG (예규&amp;middot;판례 + LLM 컴파일 wiki)&lt;/b&gt;로 모든 인용을 교차검증해서 LLM 환각을 차단하는 한국 법령&amp;middot;세무 Q&amp;amp;A 어시스턴트&amp;rdquo;&lt;br /&gt;&amp;rarr; 단일 Next.js 프로젝트로 두 개 도메인(LawLens + SalesLens)을 운영하는 한국 법령 AI SaaS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Github&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/IISweetHeartII/lawlens&quot;&gt;https://github.com/IISweetHeartII/lawlens&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Url&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://lawlens.log8.kr/&quot;&gt;https://lawlens.log8.kr/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://saleslens.log8.kr/&quot;&gt;https://saleslens.log8.kr/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구조 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리뷰하기 전에 구조 먼저 짚고 가자면!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 10개의 도메인, 14개의 페이지&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;❯ tree
.
├── app
│   ├── _components
│   ├── account
│   │   ├── organization
│   │   │   ├── [id]
│   │   │   │   └── page.tsx
│   │   │   └── page.tsx
│   │   └── page.tsx
│   ├── billing
│   │   └── return
│   │       └── page.tsx
│   ├── chat
│   │   ├── [id]
│   │   │   └── page.tsx
│   │   └── page.tsx
│   ├── disclaimer
│   │   └── page.tsx
│   ├── layout.tsx
│   ├── login
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── page.tsx
│   ├── pricing
│   │   └── page.tsx
│   ├── privacy
│   │   └── page.tsx
│   ├── refund
│   │   └── page.tsx
│   ├── saleslens
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── terms
│       └── page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;문제 상황&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 페이지 렌더링 속도는 무난했지만, 유독 사이드바에서 대화를 클릭하면, 채팅 창으로 전환되기까지 미묘한 딜레이가 있었어요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 분량이 너무 방대한 관계로, &quot;채팅 부분의 페이지 전환 속도가 느리다&quot;는 문제에 집중해서 &lt;code&gt;/src/chat&lt;/code&gt; 부분만 리팩토링을 진행했어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;원인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LawLens는 새 대화를 시작할 때 아래와 같은 흐름이에요&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;/chat&lt;/code&gt; 에서 메시지 입력&lt;/li&gt;
&lt;li&gt;
&lt;div&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/5d1f65ebe8fc5e17.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;서버에서 AI 응답 스트리밍 시작&lt;/li&gt;
&lt;li&gt;응답이 끝나면 페이지가 리렌더링 되면서(깜빡 후) &amp;rarr; URL이 &lt;code&gt;/chat/uuid-1234&lt;/code&gt; 로 바뀜&lt;br /&gt;
&lt;div&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/47dc40b709d29935.png&quot; alt=&quot;&quot; /&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 보면 알 수 있듯, &lt;b&gt;응답이 끝나는 순간 메시지가 위에서부터 다시 로드되며,&lt;/b&gt; 깜빡이면서 이미 다 그려졌던 내용이 처음부터 다시 렌더링되는 걸 확인할 수 있었어요.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  if (isNew &amp;amp;&amp;amp; !navigatedRef.current &amp;amp;&amp;amp; status === 'ready' &amp;amp;&amp;amp; messages.length &amp;gt; 0) {
    navigatedRef.current = true;
    router.replace(`/chat/${conversationId}`);
  }
}, [isNew, status, messages.length, router, conversationId]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;router.replace()&lt;/code&gt; 는 URL을 바꾸는 함수가 아닌 App Router에서는 &quot;이 경로로 네비게이션하는 함수&quot;예요. 따라서 재패치가 따라와요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js App Router는 서버 컴포넌트 &lt;code&gt;RSC&lt;/code&gt; 기반이므로, router.replace()를 사용하면 새 URL에 해당하는 &lt;b&gt;서버 컴포넌트를 다시 패치&lt;/b&gt;해요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 URL이 바뀌었으므로 서버에서는 바뀐 URL의 RSC를 다시 패치하고 ChatLayout.tsx 파일에 해당하는 쿼리를 모두 다시 실행 후 이후 새로 리렌더링을 하게 되는 흐름이었어요.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변 이후에는 URL만 바꾸고 싶은 의도를 유지하려면, Next.js를 우회해서 &lt;code&gt;window.history.replaceState&lt;/code&gt; 를 써주면 돼요.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Before
router.replace(`/chat/${conversationId}`);

// After
window.history.replaceState(null, '', `/chat/${conversationId}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 한가지 주의할 점은, &amp;lsquo;Next.js를 우회해서&amp;rsquo; URL을 변경하는 것이기 때문에 라우터 상태에 의존하는 뒤로가기 동작 등을 수행했을 때 내부 상태가 예상과 다를 수 있다는 트레이드 오프가 있어요.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/yooncandooit/my_velog2tistory@main/assets/9224dc08360ccaa9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;  React 스터디 회고&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10주 동안 React 중급 스터디를 마쳤다. 컴포넌트 설계부터 테스트 전략까지, 늘 궁금했지만 혼자서는 파보지 못했던 문제들을 스터디원들과 하나씩 짚어가는 시간이었다. &quot;왜 이렇게 써야 하는가&quot;를 계속 묻고, 어떻게 하면 더 쉽게 설명할 수 있을지 고민하는 재미가 있던 스터디였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;커리큘럼 한눈에 보기&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;주차&lt;/th&gt;
&lt;th&gt;주제&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1주차&lt;/td&gt;
&lt;td&gt;컴포넌트 설계 원칙&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2주차&lt;/td&gt;
&lt;td&gt;커스텀 훅과 책임 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3주차&lt;/td&gt;
&lt;td&gt;상태 관리 전략 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4주차&lt;/td&gt;
&lt;td&gt;TanStack Query 심화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5주차&lt;/td&gt;
&lt;td&gt;렌더링 최적화 실전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6주차&lt;/td&gt;
&lt;td&gt;에러 핸들링 &amp;amp; 안전성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7주차&lt;/td&gt;
&lt;td&gt;폴더 구조 &amp;amp; 아키텍처&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8주차&lt;/td&gt;
&lt;td&gt;테스트 전략&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9주차&lt;/td&gt;
&lt;td&gt;포트폴리오 사이트 만들어오기 (MVP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10주차&lt;/td&gt;
&lt;td&gt;스터디한 내용을 기반으로 서로 코드 리뷰해주기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5주차 렌더링 최적화 실전 (발표 주차) 회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링 최적화는 내가 직접 발표를 맡은 주차이자, 실제 10주차 코드리뷰까지 사용된 개념이었다.&lt;br /&gt;&lt;b&gt;렌더링 최적화 실전 가이드&lt;/b&gt;: React DevTools Profiler 활용법, &lt;code&gt;useMemo&lt;/code&gt; / &lt;code&gt;useCallback&lt;/code&gt; / &lt;code&gt;memo&lt;/code&gt; 적용 기준, 불필요한 렌더링 찾아서 없애기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;준비하면서 가장 크게 느낀 건 &lt;b&gt;측정 없이 최적화하면 안 된다&lt;/b&gt;는 것이었다. 처음에는 &quot;모든 곳에 &lt;code&gt;useMemo&lt;/code&gt;와 &lt;code&gt;useCallback&lt;/code&gt;을 붙이면 빨라지지 않을까&quot;라는 생각이 있었는데, 실제로 Profiler를 돌려보면 병목이 예상과 다른 곳에 있는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Profiler의 Flame Graph에서 셀프 시간이 높은 컴포넌트를 찾고, &quot;왜 이 컴포넌트가 렌더링됐는가&quot;를 확인한 뒤에 최적화 도구를 적용하는 순서가 핵심이었다. 실제 LawLens 코드에서는 불필요한 &lt;code&gt;useCallback&lt;/code&gt;을 제거하는 것이 오히려 성능 개선으로 이어지는 사례도 있었다. 최적화란 무언가를 추가하는 것이 아니라 불필요한 것을 없애는 것이기도 하다는 걸 실감했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발표 준비 과정에서 팀원분이 남겨주신 질문이 기억에 남는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;모든 부분에 메모이제이션 vs 필요한 부분만 메모이제이션 중 어떤 의견이 더 본인과 맞는 것 같았나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 &quot;필요한 부분만&quot;이라고 답했지만, 그 기준이 뭔지 스스로도 흐릿했다. 발표를 준비하면서 그 기준이 조금씩 명확해졌다. &lt;b&gt;비싼 계산인지&lt;/b&gt;, &lt;b&gt;함수 참조의 안정성이 필요한지&lt;/b&gt;, &lt;b&gt;부모 리렌더링이 자식에게 전파되는지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 가지를 확인하는 것이 앞으로 최적화의 기준이 되었다&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가장 크게 성장한 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;왜&quot;를 먼저 묻는 습관&lt;/b&gt;이 생겼다. 도구를 사용하기 전에 왜 이 도구가 필요한지, 다른 선택지는 무엇인지를 먼저 고민하게 됐다. 렌더링 최적화 발표를 준비하면서 특히 그 습관이 강해졌다고 생각한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아쉬웠던 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험기간에는 학교 공부에 집중하다 보니 4, 8주차의 내용을 깊이 있게 따라가지 못한 부분이 있다. Tanstack Query, 에러 핸들링과 테스트 전략은 별도로 더 공부하고 싶은 주제로 남아 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스터디에서 배운 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 지식만큼이나 &lt;b&gt;함께 공부하는 방식&lt;/b&gt;도 배웠다. 질문하는 것을 두려워하지 않는 분위기, 다른 사람의 코드에서 배우는 경험, 발표를 준비하면서 개념을 다시 정리하는 과정 같은 모든 것이 혼자 공부할 때는 얻기 어려운 것들이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;마치며&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10주가 생각보다 빠르게 지나갔다. 매 주차마다 &quot;이건 나중에 다시 봐야겠다&quot;는 주제가 쌓였고, 그게 지금의 학습 로드맵이 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스터디는 끝났지만, 각 주차에서 다룬 주제들은 계속 이어서 공부할 예정이다. 특히 렌더링 최적화와 테스트 전략은 실제 프로젝트에 더 깊이 적용해보고 싶다!&lt;/p&gt;</description>
      <category>recap</category>
      <author>yooncandooit</author>
      <guid isPermaLink="true">https://h0wdy.tistory.com/57</guid>
      <comments>https://h0wdy.tistory.com/57#entry57comment</comments>
      <pubDate>Wed, 8 Jul 2026 15:57:31 +0900</pubDate>
    </item>
  </channel>
</rss>