<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>AI 세상에서 살아남기</title>
    <link>https://wanglan.tistory.com/</link>
    <description>AI와 개발자로 살아남기</description>
    <language>ko</language>
    <pubDate>Sun, 6 Sep 2026 07:03:45 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Wanglan</managingEditor>
    <image>
      <title>AI 세상에서 살아남기</title>
      <url>https://tistory1.daumcdn.net/tistory/7301494/attach/d9eff9faa16e4350b1ee23fb8f0c7e31</url>
      <link>https://wanglan.tistory.com</link>
    </image>
    <item>
      <title>AI 에이전트 오차는 더해지지 않고 곱해집니다</title>
      <link>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%80-%EC%A7%A7%EC%9D%84%EC%88%98%EB%A1%9D-%EC%A2%8B%EB%8B%A4-%EC%98%A4%EC%B0%A8%EB%8A%94-%EA%B3%B1%ED%95%B4%EC%A7%80%EA%B3%A0-%EA%B2%80%EC%82%AC%EB%8A%94-%EA%B1%B8%EB%9F%AC%EB%83%85%EB%8B%88%EB%8B%A4</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 에이전트 작업을 5단계로 나누면 단계마다 90%를 맞혀도 끝에서는 59%가 남습니다. 그래서 단계를 줄여야 하나 고민했는데, 반은 틀린 생각이었습니다&lt;br /&gt;&amp;middot; 곱해지는 건 새로 만드는 단계뿐이고, 통과&amp;middot;거부만 하는 기계 검사는 오류를 걸러냅니다. 단계를 자를 자리는 &quot;거기서 기계로 검사할 게 있는가&quot;로 정하면 됩니다&lt;br /&gt;&amp;middot; 제 블로그 발행 파이프라인에 실제로 붙어 있는 기계 검사 4개와, 에이전트한테 리뷰를 시키면 왜 검사가 아니라 또 하나의 오차가 되는지 적었습니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;8월 말에 에이전트 여러 개를 이어 붙이는 구조를 설계하고 있었습니다. 분석, 기획, 개발, 검수, 배포처럼 단계를 나누고 단계마다 에이전트를 하나씩 두는 그림이었는데, 그러면 각 단계의 정확도가 곱해져서 단계마다 90%를 맞혀도 다섯 단계 끝에는 59%만 남는다는 얘기를 들었습니다. 듣고 나서 제가 이렇게 물었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 개발에서 바로 배포로 가는 2단계로 줄이는 게 좋다는 건가? 파이프라인 크기를 어떻게 잡아야 할지 너무 애매하고 어렵네.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;결론부터 말씀드리자면, 단계 수는 문제가 아니었습니다. 문제는 단계 사이에 &lt;b&gt;기계가 통과&amp;middot;거부를 판정하는 자리가 있느냐&lt;/b&gt;였습니다. 그게 있으면 5단계가 2단계보다 낫고, 없으면 2단계도 곱셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 오늘은 수학 얘기처럼 시작해서 검사 얘기로 끝나는 글입니다. (수학은 곱셈 한 번뿐이니 안심하셔도 됩니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 0.9를 다섯 번 곱하면 0.59입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 설계는 &lt;a href=&quot;https://wanglan.tistory.com/entry/8%EA%B0%9C%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%9C%EB%A1%9C-%EB%AD%94%EC%A7%93-%ED%96%88%EB%8A%94%EC%A7%80-%EC%95%84%EB%AC%B4%EA%B2%83%EB%8F%84-%EB%AA%A8%EB%A5%B4%EB%8A%94%EB%8D%B0&quot;&gt;에이전트 여덟 개가 서로 뭘 했는지 모르는 문제&lt;/a&gt;를 풀려고 시작한 거였습니다. 에이전트 하나가 만든 걸 다음 에이전트가 받아서 이어가는 구조를 그리다 보니, 각 에이전트가 90% 확률로 맞는다고 치면 다섯을 거치면 어떻게 되냐는 얘기가 나온 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.9 &amp;times; 0.9 &amp;times; 0.9 &amp;times; 0.9 &amp;times; 0.9 = 0.59.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞 단계가 틀리면 뒤 단계는 틀린 것을 받아서 열심히 일합니다. 그래서 오차는 더해지지 않고 곱해집니다. 여기까지 듣고 제가 낸 결론이 맨 위의 질문이었습니다. 곱하는 횟수를 줄이면 되니까 단계를 줄이자. 개발하고 바로 배포하자. 제 에이전트도 처음엔 &quot;단계 수는 비용이고 짧을수록 좋다&quot;고 답했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이상하지 않나요. 사람이 하는 전통적인 개발 파이프라인은 기획, 개발, 코드 리뷰, QA, 배포, 다섯 단계입니다. 수십 년 동안 다들 이렇게 해 왔는데 그게 전부 59%짜리였을까요? 그럴 리가 없죠. 뭔가 빠진 겁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 곱하는 단계와 거르는 단계는 다릅니다&lt;/h2&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 style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;변환 단계.&lt;/b&gt;&lt;br /&gt;새로운 걸 만듭니다. 기획, 개발. 여기서 틀리면 뒤로 전파됩니다. 곱해지는 건 이 단계입니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;검사 단계.&lt;/b&gt;&lt;br /&gt;새로 만들지 않습니다. 앞에서 온 것을 보고 통과냐 거부냐만 정합니다. 코드 리뷰, QA, 배포 전 테스트. 오류가 여기서 죽습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러니까 전통 파이프라인은 5단계 곱셈이 아니라 &lt;b&gt;변환 2개 + 검사 3개&lt;/b&gt;였습니다. 검사가 셋이나 붙어 있는 이유가 정확히 곱셈 때문이고요. 0.9의 5제곱 59%는 검사가 하나도 없을 때의 숫자였습니다. 곱셈 얘기를 처음 들었을 때 다섯 단계를 전부 변환으로 넣어 버린 거죠. (곱셈은 맞았는데 곱하는 대상을 잘못 골랐습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 단계를 어디서 자르느냐는 질문의 답도 바뀝니다. 짧게 자르는 게 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;거기서 기계로 검사할 게 있으면 자르고, 없으면 안 자릅니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획과 개발 사이를 보면, &quot;좋은 기획인가&quot;를 기계가 판정할 방법이 없습니다. 그래서 여기는 잘라 봐야 검증 안 되는 중간 산출물 하나가 늘 뿐이에요. 개발과 배포 사이에는 테스트, 타입 검사, 빌드가 있습니다. 전부 사람 판단 없이 통과&amp;middot;거부가 나오는 검사입니다. 그러니 여기는 자를수록 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 찍은 &quot;개발&amp;rarr;배포&quot;는 답은 맞았는데 이유가 달랐습니다. 짧아서 좋은 게 아니라, 그 사이에 테스트와 빌드가 있어서 좋은 거였습니다. (답을 맞히고도 틀린 기분이 드는 건 이럴 때인 것 같습니다..)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 에이전트한테 리뷰를 시키면 검사가 아니라 변환입니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 에이전트 시대의 함정이 하나 나옵니다. 사람 리뷰어가 병목이니까 에이전트한테 리뷰를 시키면 되지 않나? 저도 그렇게 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 에이전트 리뷰어는 통과&amp;middot;거부를 딱 정해 주지 않습니다. &lt;b&gt;의견을 생성합니다.&lt;/b&gt; &quot;이 부분은 이렇게 고치는 게 좋겠습니다&quot;라고요. 그 의견도 90%짜리입니다. 검사 단계에 넣었는데 하는 일은 변환이라서, 곱셈에 같이 들어가게 되는 겁니다. 검사가 곱셈에서 빠지려면 같은 입력에는 항상 같은 답이 나와야 해요. 테스트, 타입, 린트, 빌드, 파일이 실제로 있는지 확인. 이런 것들이요. 사람이 하는 리뷰는 검사고, 에이전트가 하는 리뷰는 변환입니다. &lt;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;LLM의 판단을 기계 규칙 밖으로 못 나가게 가둔 얘기&lt;/a&gt;를 예전에 썼는데, 그 글의 경계선이 여기서도 그대로인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 경우가 딱 그랬습니다. 지난주에 위키 검색 얘기를 글로 쓰면서 초안에 숫자를 여러 개 넣었는데, 자동 작업 13개, 낱말 사전 89줄, 사고 대장 32건, 35만 자. 넷 다 틀려 있었습니다. 요약 문서에 적힌 숫자를 그대로 옮긴 거라 에이전트가 다시 읽어도 틀린 줄 모르거든요. 잡은 건 원본 파일에서 숫자를 다시 뽑는 명령이었습니다. 12개, 85줄, 12건이었고, 35만 자는 원문이 352KB였습니다. 바이트를 글자로 옮겨 적은 거죠. 읽기는 변환이고 다시 뽑기는 검사였던 셈입니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 검증 예산, 남이 정리해 둔 배치 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 이 생각을 하던 주에 긱뉴스에 &lt;a href=&quot;https://news.hada.io/topic?id=32737&quot;&gt;인간의 판단은 소프트웨어 공장을 떠나지 않고 위치를 바꾼다&lt;/a&gt;는 글이 올라왔습니다. (원문은 Addy Osmani가 substack에 쓴 글이고, 저는 긱뉴스 요약으로 읽었습니다) 에이전트 여러 개를 돌리는 &quot;소프트웨어 공장&quot; 얘기인데, 제 눈에 들어온 건 세 가지였습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;검증 예산.&lt;/b&gt;&lt;br /&gt;검사를 성능 예산처럼 관리하라는 겁니다. 싸고 빠른 검사(타입 검사, 자동 테스트, 린트)는 앞쪽에서 계속 돌리고, 비싸지만 가치 높은 검사(전체 테스트, 브라우저 테스트, 보안 검사)는 초안 PR 전후 뒤쪽에 둡니다. 검사 개수가 곧 품질은 아니니 신호 대 잡음비를 봐야 한다고도 했습니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;모든 검사가 초록이어도 안전하지 않다.&lt;/b&gt;&lt;br /&gt;에이전트는 &quot;테스트를 통과시켜라&quot;는 요청에 테스트 자체를 바꿔 버릴 수 있습니다. 그래서 검사가 의도대로 작동하는지를 초기에 사람이 확인해야 한다는 거죠&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;실행 결과 4분류.&lt;/b&gt;&lt;br /&gt;Vercel은 에이전트 실행 하나하나를 success, flawed(구현이 틀렸거나 맥락 부족), blocked(자격 증명 같은 게 없어 막힘), manual(사람이 판단해야 하는 경계)로 표시하고 success만 배포한다고 합니다. flawed와 blocked는 기계적으로 풀 수 있지만 manual은 신뢰와 권한의 문제라는 말이 남았습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;특히 글쓴이가 여러 세션을 병렬로 돌리다가 즐겨찾기 기능이 테스트와 브라우저 확인을 통과해 병합됐는데, 며칠 뒤에 자기가 승인한 그 기능이 어떻게 동작하는지 설명할 수 없었다는 대목이 있었습니다. 테스트 통과가 이해를 보장하지 않는다는 거죠. 저한테도 있는 일이라 좀 찔렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람 검사가 병목이 되는 건 숫자로도 나와 있습니다. 긱뉴스가 직접 쓴 &lt;a href=&quot;https://news.hada.io/topic?id=32691&quot;&gt;코드는 다 읽을 수 없고, 코드 리뷰가 맡아온 책임은 사라지지 않는다&lt;/a&gt;에 Faros AI가 4,000개 팀 22,000명의 2년치 데이터를 본 결과가 있는데, AI를 많이 쓰는 팀일수록 리뷰 소요 시간 중앙값이 441.5% 늘었고 리뷰 없이 머지된 PR도 31.3% 늘었다고 합니다. (인과를 입증한 실험이 아니라 관찰 데이터라는 단서가 붙어 있습니다) 그러니까 사람 검사가 병목이 되는 건 사실이고, 그래서 에이전트 리뷰를 시키고 싶어지는 것도 자연스러운데, 3절대로 그건 검사가 아닙니다. 남는 답은 기계 검사를 앞에 잔뜩 깔고, 사람은 기계가 못 보는 자리에만 서는 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-09-04-에이전트-오차는-곱해지고-검사는-거른다-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ovmsM/dJMcaaT81tu/2vG46M0qKMU4Fpd2E3zGg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ovmsM/dJMcaaT81tu/2vG46M0qKMU4Fpd2E3zGg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ovmsM/dJMcaaT81tu/2vG46M0qKMU4Fpd2E3zGg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FovmsM%2FdJMcaaT81tu%2F2vG46M0qKMU4Fpd2E3zGg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-09-04-에이전트-오차는-곱해지고-검사는-거른다-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 제 발행 파이프라인에 붙어 있는 기계 검사 4개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론은 여기까지고, 제가 실제로 굴리는 걸 보겠습니다. 이 블로그 글은 에이전트가 초안을 쓰고, 스크립트가 HTML로 바꾸고, 그림을 만들고, 티스토리에 비공개로 올립니다. 그 사이에 기계 검사가 네 개 있습니다. 그리고 넷 중 셋은 사고 뒤에 생겼습니다. (미리 설계한 건 하나뿐입니다.. 나머지는 한 번씩 당하고 만들었어요)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;하나, 본문 글자 수가 0이면 발행하지 않는다.&lt;/b&gt;&lt;br /&gt;8월 18일에 본문이 텅 빈 글이 올라간 적이 있습니다. 검증을 엉뚱한 자리에서 하고 있었어요. 지금은 실제 제출 직전 필드의 글자 수를 재서 0이면 멈춥니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;둘, 본문에 그림이 0장이면 발행하지 않는다.&lt;/b&gt;&lt;br /&gt;글마다 그림 한 장이 원칙이라 초안에 그림 자리가 없으면 첫 단계에서 멈추고, 제출 직전에도 그림 수를 다시 세서 0이면 멈춥니다. 이게 사고 전에 미리 넣은 유일한 검사입니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;셋, 이미 발행된 초안을 새 글로 또 올리면 거부한다.&lt;/b&gt;&lt;br /&gt;8월 29일에 같은 초안이 두 번 올라갔습니다. 자동 작업 안에서 돌던 에이전트가 한 번 올리고, 에이전트가 끝난 뒤 자동 작업 스크립트가 한 번 더 올렸어요. 지금은 초안 머리의 상태 줄이 &quot;발행됨&quot;이면 새로 올리기를 거부하고 수정 발행 명령을 대신 알려 줍니다. 참고로 이 판정은 상태 줄의 &lt;b&gt;앞부분&lt;/b&gt;에서만 합니다. &quot;비공개 발행 후 직접 공개 전환&quot;처럼 뒤에 붙은 설명 문장까지 읽으면 판정이 뒤집혀서, 실제로 오탐이 세 건 났었거든요&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;넷, 자동 작업 스크립트 안에 백틱이 날것으로 들어가면 검사에서 실패한다.&lt;/b&gt;&lt;br /&gt;8월 29일 토요일 10시, 주간 초안을 쓰는 자동 작업이 통째로 죽었습니다. 나흘 전에 프롬프트 안에 넣은 백틱을 셸이 명령으로 해석한 거였어요. 셸 문법 검사(bash -n)는 이걸 못 잡습니다. 그래서 프롬프트 구간 안의 백틱을 찾는 검사를 따로 붙였습니다. (이 사고는 뒷얘기가 더 있어서 다음 글에서 따로 씁니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;넷의 공통점은 사람 판단이 하나도 안 들어간다는 겁니다. 글자 수, 그림 수, 문자열 앞부분, 백틱 유무. 에이전트가 아무리 똑똑해져도 이 검사들은 그대로 두는 게 맞는 것 같아요. 반대로 &quot;에이전트가 초안을 다시 읽고 고친다&quot;는 단계는 아무리 여러 번 돌려도 검사가 아닙니다. 3절의 숫자 네 개가 그 증거고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막 검사가 하나 더 있는데, 저입니다. 비공개로 올라온 글을 제가 읽고 공개로 바꿉니다. 사람이 하면 검사라고 했으니 이건 검사가 맞습니다. 다만 제일 비싼 검사라서 맨 뒤에 둡니다. 검증 예산 그대로예요. (제일 비싼 검사가 제일 자주 밀리는 검사이기도 합니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 곱셈은 이미 한 번 났습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 25일 설계 문서에는 &quot;지금은 1인 1에이전트라 곱셈 오차라는 병이 아직 안 났다&quot;고 적혀 있었습니다. 다음 날 실물을 확인해 보니 틀렸습니다. 에이전트 세션을 띄우는 스크립트가 15개였어요. 서로 다른 시간에 서로 다른 일을 하는 에이전트 15개가 같은 파일을 읽고 씁니다. 이미 여럿이었던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 곱셈이 제일 작은 형태로 이미 나 있었습니다. 긱뉴스를 정리하는 자동 작업이 &quot;이 주제를 몇 번 보여줬는지&quot; 숫자를 에이전트가 손으로 올리게 돼 있었는데, 앞 회차 에이전트가 올린 숫자를 다음 회차 에이전트가 읽어서 또 올렸습니다. 사람이 안 끼니까 아무도 모르고요. 8월 24일과 25일에 두 번 어긋난 걸 찾았습니다. 처방은 검사를 붙이는 게 아니라 &lt;b&gt;그 숫자를 저장하지 않는 것&lt;/b&gt;이었습니다. 필요할 때 기록에서 다시 계산하면 틀릴 자리가 없으니까요. 검사가 필요 없는 구조가 제일 좋은 검사인 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 마치며&lt;/h2&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;단계를 자를 자리는 길이가 아니라 &quot;거기서 기계로 검사할 게 있는가&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;파이프라인 크기 잡는 게 애매하다&quot;던 8월 25일의 저한테 지금 답한다면, 크기를 잡지 말고 검사할 수 있는 자리를 먼저 찾으라고 할 것 같습니다. 그 자리가 곧 경계니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분의 에이전트 파이프라인에서, 검사라고 이름 붙었는데 사실은 변환인 단계가 몇 개나 있나요? 저는 하나 있었습니다. 지금은 이름을 바꿨습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다!!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고한 글&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://news.hada.io/topic?id=32737&quot;&gt;인간의 판단은 소프트웨어 공장을 떠나지 않고 위치를 바꾼다&lt;/a&gt; (news.hada.io)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.hada.io/topic?id=32691&quot;&gt;코드는 다 읽을 수 없고, 코드 리뷰가 맡아온 책임은 사라지지 않는다&lt;/a&gt; (news.hada.io)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img style=&quot;max-width: 100%; height: auto; display: block; margin: 0 auto;&quot; src=&quot;https://cdn.jsdelivr.net/gh/JANG-MINWOO/blog-assets@main/posts/2026-09-04-에이전트-오차는-곱해지고-검사는-거른다/1-2026-09-04-에이전트-오차는-곱해지고-검사는-거른다-gen1-e4ba9e40.png&quot; alt=&quot;A simple flat illustration with no text or numbers: a horizontal conveyor belt carrying small blocks from left to right through five stations. Two of &quot; /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/8%EA%B0%9C%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%9C%EB%A1%9C-%EB%AD%94%EC%A7%93-%ED%96%88%EB%8A%94%EC%A7%80-%EC%95%84%EB%AC%B4%EA%B2%83%EB%8F%84-%EB%AA%A8%EB%A5%B4%EB%8A%94%EB%8D%B0&quot;&gt;8개의 멀티 에이전트? 서로 뭔짓 했는지 아무것도 모르는데?&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>llm</category>
      <category>검증</category>
      <category>검증예산</category>
      <category>멀티에이전트</category>
      <category>소프트웨어공장</category>
      <category>에이전트파이프라인</category>
      <category>자동화</category>
      <category>코드리뷰</category>
      <category>클로드코드</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/145</guid>
      <comments>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%80-%EC%A7%A7%EC%9D%84%EC%88%98%EB%A1%9D-%EC%A2%8B%EB%8B%A4-%EC%98%A4%EC%B0%A8%EB%8A%94-%EA%B3%B1%ED%95%B4%EC%A7%80%EA%B3%A0-%EA%B2%80%EC%82%AC%EB%8A%94-%EA%B1%B8%EB%9F%AC%EB%83%85%EB%8B%88%EB%8B%A4#entry145comment</comments>
      <pubDate>Sat, 5 Sep 2026 22:50:13 +0900</pubDate>
    </item>
    <item>
      <title>RAG 안 쓰고 grep으로 버틴 이유, 벡터 DB는 언제 붙여야 할까?</title>
      <link>https://wanglan.tistory.com/entry/RAG-%EC%95%88-%EC%93%B0%EA%B3%A0-grep%EC%9C%BC%EB%A1%9C-%EB%B2%84%ED%8B%B4-%EC%9D%B4%EC%9C%A0-%EB%B2%A1%ED%84%B0-DB%EB%8A%94-%EC%96%B8%EC%A0%9C-%EB%B6%99%EC%97%AC%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 에이전트가 읽고 쓰는 제 위키가 한 달 만에 150만 자가 됐는데, 벡터 DB를 안 붙이고 grep과 텍스트 파일 몇 개로 버티고 있습니다&lt;br /&gt;&amp;middot; 확인해 보니 grep 전체 검색이 0.05초고, 이 한 달 동안 &quot;못 찾아서&quot; 난 오류는 0건, &quot;찾고도 안 읽어서&quot;&amp;middot;&quot;떠올리지 못해서&quot; 난 오류가 3건이었습니다&lt;br /&gt;&amp;middot; 벡터 DB를 꼭 도입해야겠다 싶은 기준을 수치로 정해 뒀습니다. 실제로 읽히는 텍스트 200만 자, 전체 텍스트 20MB, grep 0.5초, 안내판으로 못 찾은 사례 3회&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;한 위키 파일 하나하나가 막 10만자 9만자 이렇게되어버리면서.. 결국 임베딩 벡터로 찾게하거나 그래야할까?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;8월 19일에 제가 에이전트한테 던진 질문입니다. 결론부터 말씀드리자면, 안 붙였습니다. 그리고 한동안은 안 붙일 것 같습니다. grep이 0.05초에 다 찾아주는데, 정작 제가 틀린 자리는 찾는 쪽이 아니었거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다! 오늘은 검색 얘기입니다. 정확히는 &lt;b&gt;검색을 안 만든 얘기&lt;/b&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 한 달 만에 150만 자가 됐습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 에이전트 여덟 개가 &lt;a href=&quot;https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EC%84%B8%EA%B3%84%EA%B4%80-%EC%84%A4%EC%A0%95%ED%95%B4%EC%A3%BC%EA%B8%B0&quot;&gt;원장 하나를 같은 렌즈로 읽게 만든 얘기&lt;/a&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;숫자부터 보겠습니다. 오늘 아침에 다시 확인한 값입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;날짜&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;위키 문자 수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;파일 수&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;8월 6일&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;30,067&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;8월 19일&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;600,674&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;9월 3일&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;&lt;b&gt;1,542,702&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 달에 50배입니다. (확인하면서 저도 다시 놀랐습니다) 오래된 기록을 따로 치워둔 걸 빼고, 지금 실제로 읽히는 부분만 해도 128만 자예요. 여기에 에이전트와 주고받은 대화와 결정을 날짜별로 남겨둔 작업 기록이 따로 28개 파일 60만 자, 에이전트가 검색 대상으로 훑는 텍스트 파일을 전부 합치면 286개, 11.5MB입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 19일에 60만 자라는 숫자를 보고 든 생각은 이거였습니다. 엥..? 60만 자..? 이거 다 읽으면서 작업하는 거 맞나? 파일 하나가 10만 자를 넘어가기 시작하던 때였거든요. 그래서 위에 인용한 질문을 던졌습니다. 임베딩 벡터로 찾게 해야 하나.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 그 뒤로 위키는 2.5배 더 컸습니다. 걱정이 틀린 건 아니었습니다. 처방이 틀렸을 뿐..)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &quot;그거 RAG 붙이면 되잖아요&quot;에 남이 해준 답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답하는지 따져본 글&lt;/a&gt;에서 벡터 검색을 한 번 기각한 이유를 적었습니다. 벡터에는 &quot;이건 폐지됐다&quot;는 개념이 없어서, 옛 문서와 새 문서가 나란히 뜬다는 얘기였죠. 그건 제 사정이었고, 남의 사정은 어떤가 궁금했는데 마침 딱 맞는 글이 긱뉴스에 올라왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://news.hada.io/topic?id=32916&quot;&gt;RAG는 생각보다 단순하다&lt;/a&gt;라는 글입니다. (원문은 Lighthouse 뉴스레터의 &quot;RAG is simpler than you think&quot;인데, 저는 긱뉴스 요약으로 읽었습니다) 요지는 검색 구조를 사다리로 보라는 겁니다. 아래 칸일수록 단순하고, 위로 갈수록 만들고 유지하는 비용이 커집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;BM25 같은 단순 키워드 검색 (낱말이 얼마나 겹치는지로 점수를 매기는 고전 방식)&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;LLM이 사용자의 질문을 검색용 키워드로 고쳐 쓰는 단계&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;키워드 검색으로 후보를 뽑고 임베딩으로 다시 줄 세우는 하이브리드 (임베딩은 문장을 숫자 묶음으로 바꿔서 뜻이 비슷한 글끼리 가까워지게 하는 기술입니다)&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;검색 요청이 올 때만 그때그때 임베딩&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;자주 찾는 문서와 안 찾는 문서를 나눠서 관리&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;문서 전체를 미리 임베딩&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 이 한 줄이 핵심이었습니다. &lt;b&gt;시스템의 60%는 1~2단계에서 멈추고, 25%는 하이브리드, 10%가 전체 사전 임베딩, 5%만 맞춤 구조가 필요하다.&lt;/b&gt; 그러니까 60%짜리 문제에 5%짜리 해법을 짓지 말라는 거죠.&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 style=&quot;margin-bottom: 0.8em;&quot;&gt;데이터 신선도: 새 기록이 얼마나 빨리 검색에 잡혀야 하나. 저는 오늘 적은 결정을 오늘 오후에 찾아야 합니다. 이런 경우엔 다시 색인하기 쉬운 방식이 유리하다고 했는데, 텍스트 파일을 그대로 grep 하는 건 색인 자체가 없으니 이 조건은 그냥 통과입니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;문서 변경률: 매일 바뀝니다. 작업 기록이 매일 새로 생기고 위키도 매일 고쳐져요. 월&amp;middot;분기 단위로 안정된 문서 뭉치가 사전 임베딩에 맞다고 했는데, 제 위키는 정확히 그 반대편인 것 같습니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;질문 유형: 키워드형입니다. &quot;keep-alive 크론&quot;, &quot;F-002&quot;, &quot;삼성전자 일지&quot;. 어휘를 제가 직접 지었으니 같은 뜻을 다른 말로 찾는 문제가 거의 없어요 (검색 쪽에서는 검색 대상이 되는 글 전체를 코퍼스라고 부르는데, 이 글에서는 그냥 텍스트라고 하겠습니다)&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;하루 검색량: 하루 1,000건 미만이면 단순한 방식으로 충분하다고 했는데, 저는 많아야 수십 건입니다&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;팀 역량: 저 혼자입니다. ML 팀이 없으면 1~2단계에 머물라고 써 있었습니다 ㅎㅎ&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다섯 개 전부 사다리 맨 아래 칸을 가리킵니다. 저는 심지어 BM25도 아니고 그냥 grep입니다. 0단계라고 해야 할까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그럼 저는 어디서 틀렸나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 한 달 동안 에이전트를 사용하며 발생했던 오류 리스트를 다시 확인했습니다. 검색과 관련 있다고 볼 수 있는 게 세 건인데, 셋 다 &lt;b&gt;못 찾은 게 아니었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;첫 번째는 8월 19일. 에이전트가 페이지를 찾았고, 열었고, 관련 소제목을 봤는데, 본문을 안 읽고 반대 내용을 아래에 덧붙였습니다. 검색 성공률 100%, 독해율 0%. 이건 위에 링크한 글에서 길게 다뤘으니 넘어가겠습니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;두 번째는 어제오늘입니다. 전날 작업 기록에 &quot;이 실험은 끝났다&quot;, &quot;이 절차는 없앴다&quot;고 적혀 있었는데, 다음 날 에이전트가 그 실험을 다시 제안하고 그 절차를 다시 물었습니다. 기록에는 있었어요. grep 하면 0.05초에 나옵니다. 문제는 &lt;b&gt;grep 할 생각을 안 했다&lt;/b&gt;는 겁니다. 떠오르지 않은 거죠.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;세 번째는 오늘 오후에 제가 직접 당했습니다. 이미지 처리 방식을 새로 만들면서 에이전트가 위키에 적힌 한 줄 요약만 보고 짰는데, 이틀 전 작업 기록에 제가 승인한 조항 하나가 더 있었습니다. 그것도 빠뜨렸어요. 요약을 믿고 원본을 안 읽은 겁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;세 건의 공통점이 보이시나요. 찾는 장치는 매번 일을 했습니다. 실패한 건 &lt;b&gt;읽는 것&lt;/b&gt;과 &lt;b&gt;떠올리는 것&lt;/b&gt;이었습니다. 벡터 DB는 이 둘 중 아무것도 안 고치는 것 같습니다. 오히려 두 번째 오류는 벡터가 있었으면 더 자주 났을 것 같아요. 없앤 절차와 살아 있는 절차가 뜻으로는 거의 같아서 나란히 올라올 테니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8월 21일에 제가 다시 물었습니다. 이번엔 DB였습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;md를 타고 타고 들어가는 것보다 인덱싱된 DB 테이블이 월드모델 관점에서 맞는 것 같은데 고민이다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그때 나온 답이 지금도 기준입니다. &lt;b&gt;병목은 검색이 아니라 한 번에 읽어야 하는 덩어리의 크기다.&lt;/b&gt; 8월 19일 오류에서 grep은 파일을 찾아줬고, 실패는 &quot;352KB짜리 기록 파일을 열지 말지&quot;에서 났다는 겁니다. 테이블로 바꿔도 352KB는 352KB예요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-09-03-RAG-안-쓰고-grep으로-버틴-이유-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7nfGz/dJMcagthSKp/RCm8AWYR7119HjNkxDe6k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7nfGz/dJMcagthSKp/RCm8AWYR7119HjNkxDe6k0/img.png&quot; data-alt=&quot;검색 구조 사다리와 제 위치. 맨 아래 칸에서 안 올라가고 있습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7nfGz/dJMcagthSKp/RCm8AWYR7119HjNkxDe6k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7nfGz%2FdJMcagthSKp%2FRCm8AWYR7119HjNkxDe6k0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-09-03-RAG-안-쓰고-grep으로-버틴-이유-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;검색 구조 사다리와 제 위치. 맨 아래 칸에서 안 올라가고 있습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그래서 grep에 뭘 추가했나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 대신 만든 건 &quot;떠올리게 하는 장치&quot;들이었습니다. 전부 텍스트 파일과 글자 패턴 규칙(정규식)으로 돼 있습니다. 서버는 하나도 안 늘었어요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;하나, 안내판.&lt;/b&gt;&lt;br /&gt;위키마다 목차 파일이 하나씩 있고, 페이지당 한 줄 요약이 들어 있습니다. 에이전트는 검색하기 전에 이 목차를 먼저 읽습니다. 지금 다섯 개입니다. 검색 엔진이 아니라 도서관 안내판인데, 어휘를 제가 직접 지은 텍스트에서는 이게 길잡이 노릇을 하는 것 같습니다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;둘, 낱말 사전.&lt;/b&gt;&lt;br /&gt;제가 메시지를 보내면 그 메시지에 든 낱말을 미리 적어둔 규칙 85줄에 대봅니다. 걸리면 과거에 내린 판단을 &lt;b&gt;문장으로&lt;/b&gt; 에이전트 앞에 밀어 넣습니다. 파일 경로가 아니라 문장인 이유는 8월 19일 오류 때문입니다. 경로를 주면 열고도 안 읽더라고요. 문장을 주면 안 읽을 도리가 없는 것 같아요.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;셋, 규칙 묶음.&lt;/b&gt;&lt;br /&gt;확정된 규칙 23개를 6,000자 예산 안에서 매 세션 통째로 싣습니다. 이건 검색도 아니에요. 그냥 항상 들고 있는 겁니다. 검색은 &quot;필요할 때 찾는다&quot;인데, 규칙은 필요한지 모를 때 필요한 것 같거든요.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;넷, 작업 기록 뒤지기.&lt;/b&gt;&lt;br /&gt;이게 어제오늘 오류로 오늘 새로 만든 겁니다. 제 메시지에 든 낱말을 지금까지의 작업 기록 전체에 대보되, 흔한 낱말은 버리고 드문 낱말에만 점수를 줘서 가장 관련 있는 세 줄만 밀어 넣습니다. 나머지 관련 기록은 &quot;이 날짜 기록에도 있다&quot;는 날짜 목록만 짧게 붙여요. 필요하면 그 날짜 기록을 열어보라는 뜻이죠. 처음엔 최근 사흘만 봤는데, 제가 &quot;한 달 전에 시킨 일도 있다&quot;고 해서 전 기간으로 넓혔습니다. 실행 시간 0.1초.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;b&gt;다섯, 사고 대장.&lt;/b&gt;&lt;br /&gt;에이전트가 명령을 실행하기 직전에, 그 명령이 건드리는 것과 관련해 예전에 났던 사고 12건을 대봅니다. 예를 들어 에이전트가 파일 안의 문장을 바꾸는 명령을 치려고 하면, 8월 21일에 같은 문장이 파일에 두 군데 있어서 둘 다 바뀌어 버렸던 사고가 딱 붙어 나옵니다. &quot;이 문장이 파일에 하나뿐인지 확인했나?&quot; 하고요. 한글 글자 수를 세는 명령을 치려고 하면, 8월 22일에 글자 대신 바이트를 세는 바람에 글자 수 규칙이 세 배 빡빡하게 걸렸던 사고가 붙습니다. 검색이 아니라 실행 직전에 끼어드는 경고판인 셈이에요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;다섯 개의 공통점은 &lt;b&gt;검색을 더 잘하게 만드는 게 아니라, 에이전트가 검색할 생각을 안 해도 필요한 기록이 먼저 눈앞에 오게 한다&lt;/b&gt;는 겁니다. 검색 사다리의 위 칸으로 올라간 게 아니라, 검색 자체를 덜 하게 만든 거예요. 지난 글에서 말한, 기록에서 그때그때 계산해서 보여주는 방식도 같은 방향입니다. 저장을 안 하니 틀릴 자리가 없고, 찾게 하지 않으니 못 찾을 자리가 없는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅으로 이걸 강제하는 방법은 예전에 따로 글로 썼는데(맨 아래 함께 보면 좋은 글에 있습니다), 낱말 사전과 작업 기록 뒤지기는 전부 그 훅 자리에서 돕니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 벡터 DB는 언제 붙이나&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 붙인다고만 하면 고집이죠. 그래서 꼭 도입해야겠다 싶은 기준을 수치로 정해 봤습니다. 8월 19일과 21일에 정한 겁니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;조건&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;지금 (9월 3일)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.33%; background-color: #f5f5f5;&quot;&gt;&lt;b&gt;도입 기준&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;실제로 읽히는 텍스트 (치워둔 기록 제외)&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;128만 자&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;200만 자 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;안내판으로 못 찾은 사례&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;0회&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;3회 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;전체 텍스트 파일&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;11.5MB&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;20MB 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;grep 전체 검색&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;0.05초&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;0.5초 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;여러 파일을 합쳐 계산해야 하는 조회&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;거의 없음&lt;/td&gt;
&lt;td style=&quot;width: 33.33%;&quot;&gt;월 5회 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건 다섯 개 중 위의 두 개(텍스트 양, 안내판 실패)는 &lt;b&gt;둘 다&lt;/b&gt; 넘어야 벡터 검색을 다시 검토합니다. 하나만 넘으면 안 열어요. 아래 세 개(파일 크기, grep 시간, 합산 조회)는 DB로 갈지 정하는 기준인데, 넘으면 벡터가 아니라 sqlite에 내장된 글 검색 기능부터 봅니다. 그때도 벡터 검색은 오래돼서 치워둔 기록에만 쓸 생각입니다. 지금 유효한 결정이 적힌 파일에까지 벡터 검색을 쓰면, 이미 없앤 결정과 지금 살아 있는 결정이 뜻이 비슷하다는 이유로 나란히 검색되는 문제가 다시 생기거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 첫 번째 조건은 좀 위험합니다. 한 달에 50배면 200만 자는 이달 안에 넘을 수도 있어요. 다만 그때 안내판 실패가 0회면 여전히 안 붙입니다. 두 조건을 같이 건 이유가 그겁니다. 크기는 걱정을 만들지만, 크기가 오류를 낸 적은 아직 없는 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아보면 8월 19일에 제가 한 질문은 &quot;파일이 커지니까 검색이 필요하다&quot;였는데, 실제 답은 &quot;파일이 커지니까 오래된 기록을 치우는 규칙이 필요하다&quot;였습니다. 못 찾아서 문제가 아니라, 낡은 기록이 안 사라져서 문제였던 거죠. 벡터 DB를 붙였으면 아마 지금쯤 글을 어떤 크기로 잘라 넣을지 고민하고, 검색용 색인을 다시 만드는 자동 작업을 하나 더 돌리고 있었을 것 같습니다. (자동 작업은 이미 열두 개입니다. 하나 더 늘릴 자리가 없기도 했고요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 grep이 정답이라는 건 아닙니다. 제 위키가 아직 그 정도 크기라는 것뿐이에요. 여러분이 쓰는 문서는 지금 어느 단계에 있나요? 벡터 검색까지 가신 분이 있다면, 어느 시점에 grep으로는 부족해졌는지 궁금합니다. 저는 아직 그 시점을 못 만났거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다!!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고한 글&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://news.hada.io/topic?id=32916&quot;&gt;RAG는 생각보다 단순하다&lt;/a&gt; (news.hada.io)&lt;/li&gt;
&lt;/ul&gt;
&lt;figure style=&quot;margin: 0 0 1em;&quot;&gt;&lt;img style=&quot;max-width: 100%; height: auto; display: block; margin: 0 auto;&quot; src=&quot;https://cdn.jsdelivr.net/gh/JANG-MINWOO/blog-assets@main/posts/2026-09-03-RAG-안-쓰고-grep으로-버틴-이유/1-2026-09-03-RAG-안-쓰고-grep으로-버틴-이유-gen1-e4418184.png&quot; alt=&quot;검색 구조 사다리와 제 위치. 맨 아래 칸에서 안 올라가고 있습니다&quot; /&gt;
&lt;figcaption style=&quot;text-align: center; color: #888; font-size: 0.9em;&quot;&gt;검색 구조 사다리와 제 위치. 맨 아래 칸에서 안 올라가고 있습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EC%84%B8%EA%B3%84%EA%B4%80-%EC%84%A4%EC%A0%95%ED%95%B4%EC%A3%BC%EA%B8%B0&quot;&gt;AI 에이전트에게 &quot;세계관&quot; 설정해주기&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 0.8em;&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>bm25</category>
      <category>grep</category>
      <category>LLM위키</category>
      <category>Rag</category>
      <category>ripgrep</category>
      <category>SSOT</category>
      <category>벡터DB</category>
      <category>벡터검색</category>
      <category>클로드코드</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/144</guid>
      <comments>https://wanglan.tistory.com/entry/RAG-%EC%95%88-%EC%93%B0%EA%B3%A0-grep%EC%9C%BC%EB%A1%9C-%EB%B2%84%ED%8B%B4-%EC%9D%B4%EC%9C%A0-%EB%B2%A1%ED%84%B0-DB%EB%8A%94-%EC%96%B8%EC%A0%9C-%EB%B6%99%EC%97%AC%EC%95%BC-%ED%95%A0%EA%B9%8C#entry144comment</comments>
      <pubDate>Sat, 5 Sep 2026 12:23:15 +0900</pubDate>
    </item>
    <item>
      <title>AI가 내 블로그 글을 인용할까? 안 할까? (AI 검색 최적화 GEO 실측)</title>
      <link>https://wanglan.tistory.com/entry/AI%EA%B0%80-%EB%82%B4-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B8%80%EC%9D%84-%EC%9D%B8%EC%9A%A9%ED%95%A0%EA%B9%8C-%EC%95%88-%ED%95%A0%EA%B9%8C-AI-%EA%B2%80%EC%83%89-%EC%B5%9C%EC%A0%81%ED%99%94-GEO-%EC%8B%A4%EC%B8%A1</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;실제로 너같은 AI가 웹검색하면 AEO?로 인용하잖아? 근데 전문을 안읽고 검색 단어가 포함된 단락만 읽는다는데 진짜야?&lt;br /&gt;너의 동작 방식도 확인해서 어떻게 인용하고 사용되는지 확인해줄래?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제가 제 에이전트한테 어제 이렇게 시켜봤습니다. 결론부터 말씀드리자면 &lt;b&gt;AI가 남의 글을 인용하는 과정은 두 단계로 갈라져 있고, 두 단계에서 결정적인 게 서로 다릅니다.&lt;/b&gt; 검색하는 단계에서는 제목만 보고, 페이지를 실제로 여는 단계에 가서야 문단을 뜯어갑니다. 그래서 &quot;검색에 잘 노출되게 하기&quot;랑 &quot;AI가 인용하게 하기&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;요즘 GEO라는 말이 돌더군요. SEO가 검색엔진 최적화라면 GEO는 생성형 AI가 내 글을 물어가게 만드는 최적화라는 뜻으로 쓰이는 것 같습니다. 저는 이런 신조어를 좀 의심하는 편인데(용어부터 만들고 파는 게 워낙 많으니까요..), 이번엔 제 손으로 재볼 수 있는 방법이 있길래 한번 해봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 갑자기 이걸 재보게 됐냐면요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 블로그 90일치 검색 성적을 뽑아봤거든요.&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;221&lt;/b&gt; / 노출 &lt;b&gt;4,115&lt;/b&gt; / CTR &lt;b&gt;5.37%&lt;/b&gt; / 평균 순위 &lt;b&gt;9.3위&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자만 보면 나쁘지 않아 보입니다. 그런데 앞뒤 45일씩 갈라봤더니 이렇게 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;| | 앞 절반 | 뒤 절반 |&lt;br /&gt;|---|---|---|&lt;br /&gt;| 일 노출 | 45.1 | &lt;b&gt;50.7&lt;/b&gt; (+12%) |&lt;br /&gt;| CTR | 5.76% | &lt;b&gt;4.55%&lt;/b&gt; |&lt;br /&gt;| 평균 순위 | 7.2위 &amp;rarr; | &lt;b&gt;10.2위&lt;/b&gt; |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노출은 늘었는데 클릭률은 떨어지고 순위는 밀렸습니다. 심지어 늘어난 노출을 파고들어 보니 &lt;b&gt;일 노출 증가분 5.58 중에 4.34, 그러니까 78%가 딱 한 페이지에서 나온 거였습니다.&lt;/b&gt; 그 페이지는 90일 동안 노출 628에 클릭 4예요. CTR 0.64%입니다. 사람들이 보긴 보는데 안 누르는 페이지 하나가 제 &quot;성장 그래프&quot;를 혼자 만들고 있었던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주간 클릭 수를 12주 늘어놓으면 더 명확합니다. 14, 16, 16, 21, 15, 14, 15, 24, 16, 18, 20, 13. 올라가지도 내려가지도 않습니다. 그냥 진동입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 걸 보고 있으면 좀 허탈해지는데요, 사실 저는 이 리포트를 만들어놓고도 &lt;a href=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%EA%B8%80-%EC%84%9C%EC%B9%98%EC%BD%98%EC%86%94-API%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9C%A0%EC%9E%85-%EB%A6%AC%ED%8F%AC%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8A%94%EB%8D%B0-8%EB%B0%B0-%ED%8B%80%EB%A6%B0-%EC%88%AB%EC%9E%90%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%9E%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;숫자를 8배 틀리게 보고 있었던 적이 있어서&lt;/a&gt; 숫자 자체는 이제 세 번씩 확인하고 봅니다ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아무튼 &lt;b&gt;&quot;더 많이 노출되기&quot;는 이미 제 문제의 답이 아니었습니다.&lt;/b&gt; 노출은 늘고 있는데 클릭이 안 붙으니까요. 요즘 검색하면 위에 AI 요약이 먼저 뜨고 거기서 답이 끝나버리는 경우도 많잖아요. 그러면 링크를 누를 이유가 없어지죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 목표를 옮겨봐야 하나 싶었습니다. &lt;b&gt;방문이 아니라 인용을 목표로 두면 어떻게 되나.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 그래서 AI한테 직접 시켜봤습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 저는 매일 AI 에이전트를 붙들고 사니까, 남한테 물어볼 게 아니라 제 에이전트한테 &quot;네가 검색하고 인용할 때 실제로 뭘 보는지 보여줘&quot;라고 시키면 되는 거였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-29-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ThyVo/dJMcacdgREQ/v7aOi9frwIR8Pi84ZVyVmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ThyVo/dJMcacdgREQ/v7aOi9frwIR8Pi84ZVyVmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ThyVo/dJMcacdgREQ/v7aOi9frwIR8Pi84ZVyVmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FThyVo%2FdJMcacdgREQ%2Fv7aOi9frwIR8Pi84ZVyVmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-29-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&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;에이전트가 웹 검색을 돌렸을 때 돌아온 건 &lt;b&gt;제목과 URL이 담긴 목록뿐이었습니다.&lt;/b&gt; 본문 미리보기가 아예 없었어요. 우리가 구글 검색 결과에서 보는 그 두세 줄짜리 요약문 있잖아요, 그게 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 이 단계에서 &quot;이 글을 열어볼지 말지&quot;를 가르는 건 사실상 &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;여기서부터가 진짜였습니다. 에이전트한테 제 글 하나를 열어보게 했더니, &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;&quot;브라우저 자동화라고 하면 에디터에 한 글자씩 타이핑하는 그림을 상상하실 수도 있는데, 더 좋은 방법이 있었습니다. 티스토리 에디터는 내부적으로 TinyMCE라는 에디터 엔진을 쓰는데, 이 엔진의 API를 호출해서 본문 HTML을 통째로 주입할 수 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/%EA%B3%B5%EC%8B%9D-API%EA%B0%80-%EC%A3%BD%EC%9D%80-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-Claude-Code%EB%A1%9C-%EC%9E%90%EB%8F%99-%EB%B0%9C%ED%96%89-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot;&gt;티스토리 자동 발행을 만들면서 로그인 세션 살리기를 결국 포기했던 그 글&lt;/a&gt;에 있는 문단입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 여기서 좀 놀란 건, 하필 &lt;b&gt;그 문단이 앞을 안 읽어도 혼자 이해되는 문단&lt;/b&gt;이었다는 겁니다. 오해가 될 만한 지점을 먼저 깔고(&quot;한 글자씩 타이핑하는 그림을 상상하실 수도 있는데&quot;), 용어를 그 자리에서 설명하고(&quot;TinyMCE라는 에디터 엔진&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;AI는 글을 통째로 가져가지 않고 필요한 문단만 뜯어갑니다.&lt;/b&gt; 그러면 뜯겼을 때 혼자 살아남는 문단이냐 아니냐가 인용될 확률을 가른다는 얘기가 되죠.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그러다 지난주 제 글이 눈앞에서 재현됐습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 계획에 없던 건데, 재보다가 걸렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 지난주에 &lt;a href=&quot;https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;AI가 왜 자꾸 그럴듯한 거짓말을 하는지에 대한 글&lt;/a&gt;을 하나 올렸거든요. 그 글이 검색에 어떻게 잡히나 보려고 제목으로 검색을 시켰습니다.&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;&quot;초강력 자동완성 엔진&quot;&lt;/b&gt;, 그리고 &lt;b&gt;&quot;정답을 맞추는 기계가 아니라 다음에 나올 법한 단어를 맞추는 기계&quot;&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말이 되죠. 저도 비슷한 얘기를 썼고요. 그래서 링크된 원문을 직접 열어서 대조해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...그 표현이 원문에 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문의 실제 문장은 &lt;b&gt;&quot;다음에 올 가장 그럴듯한 단어를 맞히는 확률 게임&quot;&lt;/b&gt;이었어요. 뜻은 비슷하지만 글자는 다릅니다. 따옴표를 쳐서 내놓은 문장이 원문에 없는 문장이었던 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 제가 조심해야 할 게 하나 있는데요. 검색 결과에는 출처가 다섯 개쯤 딸려 있었고, &lt;b&gt;그 표현이 나머지 네 개 중 어디에서 왔을 가능성은 남아 있습니다.&lt;/b&gt; 그런데 검색 요약은 &quot;이 문장은 이 URL에서 왔다&quot;는 대응을 안 줍니다. 확인할 방법이 없어요.&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;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;b&gt;읽는 사람&lt;/b&gt; 입장에서 AI의 거짓말을 다뤘습니다. 그런데 인용을 재보면서 알게 된 건, 저는 동시에 &lt;b&gt;읽히는 쪽&lt;/b&gt;에도 앉아 있다는 거였어요. 내 문장이 원문 대조 없이 남의 요약 안에서 재조립될 수 있는 자리에요. 제가 쓴 문장이 저도 모르는 사이에 다른 모양으로 인용될 수 있는 거죠.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그럼 alt 태그부터 고치면 되는 거 아닌가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 저는 흔한 SEO 체크리스트를 꺼내들었습니다. 이미지에 대체 텍스트(alt) 넣기, 구조화 데이터, 메타 태그, 속도... 이런 것들이요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 블로그 발행 글의 HTML을 직접 뜯어봤습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조화 데이터(JSON-LD): &lt;b&gt;이미 들어 있음.&lt;/b&gt; 티스토리가 넣어줍니다&lt;/li&gt;
&lt;li&gt;title, description, canonical, og 태그: &lt;b&gt;전부 정상&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;속도, robots.txt: 티스토리 인프라라 제가 못 건드림&lt;/li&gt;
&lt;li&gt;이미지 alt: &lt;b&gt;본문 이미지에 하나도 없음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오, 하나 찾았다 싶었죠. 그래서 제 글만 그런가 싶어서 티스토리 블로그 네 곳의 글 다섯 개를 긁어서 세봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본문 이미지 39개 중 alt가 붙은 것 0개.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 블로그만의 문제가 아니었습니다. 그럼 이건 티스토리 구조 문제라는 얘기고요. 실제로 티스토리 에디터가 이미지 정보를 어떻게 뿌리는지 확인해보니, 캡션은 &lt;code&gt;figcaption&lt;/code&gt;으로 나가고 파일명은 &lt;code&gt;data-filename&lt;/code&gt;이라는 엉뚱한 자리로 들어갑니다. &lt;b&gt;alt가 들어갈 자리 자체가 렌더링 과정에 없습니다.&lt;/b&gt; 제가 뭘 적어 넣어도 티스토리가 &lt;code&gt;&amp;lt;img&amp;gt;&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;p data-ke-size=&quot;size16&quot;&gt;그리고 여기서 제가 하마터면 틀릴 뻔한 게 하나 있는데요. 처음에 블로그 첫 화면을 긁었을 때는 alt 붙은 이미지가 잔뜩 나왔거든요. 어떤 블로그는 36개 중 36개에 다 붙어 있었습니다. &quot;오 티스토리 잘하고 있네?&quot; 하고 넘어갈 뻔했는데, 하나씩 열어보니 전부 &lt;b&gt;테마 요소&lt;/b&gt;였습니다. 관리 아이콘, 목록 썸네일, 대표 이미지. 본문 이미지가 아니었어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세는 대상을 잘못 골라놓고 결론을 낼 뻔한 겁니다. (이건 제가 요즘 제일 자주 걸리는 함정인 것 같습니다.. 숫자는 맞는데 그 숫자가 다른 걸 세고 있는 경우요.)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-29-주간자동초안-gen3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/quvP1/dJMcacdgREW/fCbnVuwFN3RxnrO1dsrjc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/quvP1/dJMcacdgREW/fCbnVuwFN3RxnrO1dsrjc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/quvP1/dJMcacdgREW/fCbnVuwFN3RxnrO1dsrjc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FquvP1%2FdJMcacdgREW%2FfCbnVuwFN3RxnrO1dsrjc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-29-주간자동초안-gen3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그리고 하나 더요. 설령 alt를 넣을 수 있었다 해도, &lt;b&gt;넣어서 뭐가 좋아지는지를 저는 확인도 안 하고 파이프라인을 고치려고 했습니다.&lt;/b&gt; alt의 1차 용도는 스크린리더 접근성이고, 검색 쪽 효과는 주로 이미지 검색을 통해 옵니다. 그런데 제 블로그는 이미지 검색으로 들어온 사람이 관측된 적이 없어요. 그거부터 조회해보고 움직였어야 했는데 말이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크리스트 여섯 칸 중에 제가 실제로 손댈 수 있는 건 두 칸이었습니다.&lt;/b&gt; 그리고 그 두 칸은 도구를 깔아서 되는 게 아니라 글 쓰는 방식이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그래서 남는 두 칸&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 두 칸은 &lt;b&gt;글 자체의 품질&lt;/b&gt;이랑 &lt;b&gt;AI가 인용하기 좋은 구조&lt;/b&gt;입니다. 위에서 실측한 게 정확히 그 두 칸에 붙습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째, 제목입니다.&lt;/b&gt; 검색 단계에서 AI가 보는 게 제목뿐이라면, 제목은 사람용 낚시이기 전에 기계용 요약문이기도 한 것 같습니다. 이 글 제목을 &quot;GEO 실측 후기&quot;라고 달았으면 사람도 AI도 안 열었겠죠.&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;/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;b&gt;전부 자립시키는 게 아니라, 이 글에서 제일 핵심인 문단 두세 개만 자립시키자&lt;/b&gt; 쪽인 것 같습니다. 어차피 AI가 한 글에서 뜯어가는 건 한두 문단이니까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 이 실측의 한계도 적어둡니다&lt;/h2&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;표본이 검색 1건, 페이지 읽기 2건입니다.&lt;/b&gt; 방향은 뚜렷하지만 &quot;몇 퍼센트&quot;라고 말할 표본이 아닙니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제가 재본 건 제 에이전트 하나입니다.&lt;/b&gt; 다른 AI 서비스들은 구조가 다를 수 있어요. 특히 검색 결과에 본문 미리보기를 같이 주는 엔진이라면 ⓐ 단계 결론이 통째로 바뀝니다&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;세 번째가 제일 중요한 것 같습니다. 제가 상상했던 건 똑똑한 AI가 제 글을 정독하는 그림이었는데, 실제로는 &lt;b&gt;급하게 훑는 작은 모델이 답을 못 찾으면 인용도 없다&lt;/b&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;정리하면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색 노출을 늘리는 일이랑 AI가 인용하게 만드는 일은 손대는 자리가 다릅니다.&lt;/b&gt; 앞쪽은 제목과 색인의 문제고, 뒤쪽은 문단 하나가 혼자 설 수 있느냐의 문제입니다. 그리고 저처럼 남의 플랫폼에 글을 쓰는 사람은 흔한 SEO 체크리스트의 절반 이상을 &lt;b&gt;애초에 손댈 수 없습니다.&lt;/b&gt; 그거 확인하는 데 하루 걸렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난주 글에서 저는 AI가 왜 그럴듯한 거짓말을 하는지, 그게 왜 결함이 아니라 스펙인지를 썼습니다. 그리고 &lt;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;그 기계를 모델 밖에서 다루는 방법으로 제가 지어본 것&lt;/a&gt;도 이어서 썼고요.&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;읽을 사람이 사람만은 아니라는 걸 염두에 두게 될 것 같다&lt;/b&gt;는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분들은 어떻게 생각하시나요? 기계가 뜯어가기 좋게 쓰는 게 맞을까요, 아니면 그냥 사람 읽기 좋게 쓰고 말까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 일단 핵심 문단 두세 개만 자립시켜보는 걸로 이번 주를 시작해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다!! 모두들 파이팅입니다!!&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI검색최적화</category>
      <category>AI에이전트</category>
      <category>AI인용</category>
      <category>GEO</category>
      <category>it</category>
      <category>검색엔진최적화</category>
      <category>블로그SEO</category>
      <category>서치콘솔</category>
      <category>클로드코드</category>
      <category>티스토리</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/140</guid>
      <comments>https://wanglan.tistory.com/entry/AI%EA%B0%80-%EB%82%B4-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B8%80%EC%9D%84-%EC%9D%B8%EC%9A%A9%ED%95%A0%EA%B9%8C-%EC%95%88-%ED%95%A0%EA%B9%8C-AI-%EA%B2%80%EC%83%89-%EC%B5%9C%EC%A0%81%ED%99%94-GEO-%EC%8B%A4%EC%B8%A1#entry140comment</comments>
      <pubDate>Fri, 4 Sep 2026 10:30:31 +0900</pubDate>
    </item>
    <item>
      <title>AI 에이전트에게 &amp;quot;세계관&amp;quot; 설정해주기</title>
      <link>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EC%84%B8%EA%B3%84%EA%B4%80-%EC%84%A4%EC%A0%95%ED%95%B4%EC%A3%BC%EA%B8%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 에이전트 여덟 개가 각자 다른 수첩을 들고 일하는 문제의 정석 처방은 DB 하나(SSOT)로 모으는 것 &amp;mdash; 이틀 설계하고 안 하기로 했습니다&lt;br /&gt;&amp;middot; 이유 셋은 전부 이미 일어난 일에서 왔습니다 &amp;mdash; 만들어놓고 안 쓴 전과, 반만 옮기면 두 배로 손해라는 실측, 그리고 진짜 아픈 곳은 저장 형식이 아니었다는 것&lt;br /&gt;&amp;middot; 대신 원천을 읽는 방법을 하나로 만드는 렌즈를 만들었고, 붙이자마자 사고 두 개를 잡았습니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;각 에이전트가 [제 저장소]라는 원장에 기록된 모든 단일화된 원천을 가지고 생각을하고 행동을 하게끔 하면&amp;hellip;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제가 어느 날 밤에 적어놓은 메모입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/8%EA%B0%9C%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%9C%EB%A1%9C-%EB%AD%94%EC%A7%93-%ED%96%88%EB%8A%94%EC%A7%80-%EC%95%84%EB%AC%B4%EA%B2%83%EB%8F%84-%EB%AA%A8%EB%A5%B4%EB%8A%94%EB%8D%B0&quot;&gt;지난 글&lt;/a&gt;에서 저는 에이전트 여덟 개가 각자 다른 수첩을 들고 일하고 있더라는 얘기를 했습니다. 숫자가 혼자 자라고, 일주일 동안 틀린 값이 돌아다니고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 생각했습니다. &lt;b&gt;얘네한테 같은 세계를 보여주면 되는 거 아닌가?&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이 문제에는 이미 이름이 있습니다&lt;/h2&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;단일 진실 공급원(SSOT, Single Source of Truth)&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-5편-AI-에이전트에게-세계관-설정해주기-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rpCfb/dJMcabefEln/KMke4pIHbgXaknLEDtjmK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rpCfb/dJMcabefEln/KMke4pIHbgXaknLEDtjmK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rpCfb/dJMcabefEln/KMke4pIHbgXaknLEDtjmK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrpCfb%2FdJMcabefEln%2FKMke4pIHbgXaknLEDtjmK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-5편-AI-에이전트에게-세계관-설정해주기-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그림으로 그리면 이렇습니다. 가운데 원장 하나, 모두가 그것만 봅니다. 제가 그날 밤에 상상한 것도 딱 이 그림이었어요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 그래서 DB를 하나 만들려고 했습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정석 처방은 명확합니다. 흩어진 기록을 데이터베이스 하나로 모으는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 저장소에는 지금 기록이 여러 형태로 흩어져 있습니다. 마크다운 문서, JSON 파일, 로그, 위키 페이지&amp;hellip; 이걸 전부 SQLite 같은 데 넣고 세션들이 거기서만 읽게 하면 됩니다. 어긋날 수가 없죠. 한 곳뿐이니까요.&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;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://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C&quot;&gt;3편에서 위키를 세어봤다고 했죠.&lt;/a&gt; 44개 페이지 중 25개가 한 번도 자동으로 읽힌 적이 없었습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DB도 눈에 훤했습니다. 만들어놓고, 세션 몇 개만 연결해두고, 나머지는 예전 파일을 계속 읽는 상태로 몇 달이 가는 그림이요.&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;ⓑ 반만 옮기면 두 배로 손해라는 실측을 봤습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 비슷한 얘기를 읽었습니다. 어떤 프로젝트에서 데이터를 원래 형식에서 SQLite로 옮겨본 기록이었는데, 결론이 이랬습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4편의 사고 두 개를 다시 보면, 둘 다 &lt;b&gt;저장을 잘못해서&lt;/b&gt; 난 게 아닙니다. 기록은 멀쩡히 다 있었어요. 원본 목록도 있었고, 정확한 수량이 적힌 파일도 따로 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 세션마다 그걸 &lt;b&gt;다르게 꺼내 봤다는&lt;/b&gt; 겁니다. 어떤 세션은 원본을 직접 세고, 어떤 세션은 누가 세어둔 숫자를 그냥 읽고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 병은 창고에 있는 게 아니라 &lt;b&gt;창고에서 물건을 꺼내오는 방식&lt;/b&gt;에 있었던 것 같습니다. 창고를 새로 지어도 꺼내는 방식이 여덟 가지면 똑같습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그래서 렌즈를 만들었습니다&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;이름은 투영(projection)이라고 붙였습니다. 명령어 하나로 부르면 &quot;지금 상태&quot;를 표 하나로 계산해서 보여주는 겁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1787885476075&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bash scripts/project.sh swing-now
bash scripts/project.sh posts&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;① 입력은 원장뿐입니다.&lt;/b&gt; 누가 세어둔 숫자를 읽지 않습니다. 원본에서 그때그때 셉니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;② 파일을 만들지 않습니다.&lt;/b&gt; 결과를 화면에만 뿌리고 저장하지 않습니다. 이게 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;③ 검사기를 같이 만듭니다.&lt;/b&gt; 같은 스크립트를 &lt;code&gt;--check&lt;/code&gt;로 부르면, 어딘가에 저장된 값이 원본과 어긋났는지 검사해서 알려줍니다. 대신 &lt;b&gt;자동으로 고치지는 않습니다.&lt;/b&gt; 원본이 잘못 들어간 경우까지 조용히 덮어버리면 그게 더 큰일이거든요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-5편-AI-에이전트에게-세계관-설정해주기-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I2nPv/dJMcabefEly/aKVdhgXvngQfXc4jIKyUx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I2nPv/dJMcabefEly/aKVdhgXvngQfXc4jIKyUx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I2nPv/dJMcabefEly/aKVdhgXvngQfXc4jIKyUx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI2nPv%2FdJMcabefEly%2FaKVdhgXvngQfXc4jIKyUx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-5편-AI-에이전트에게-세계관-설정해주기-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;②번이 왜 중요하냐면요.&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;4편의 사고들은 전부 &quot;저장해둔 값이 원본과 달라져서&quot; 난 겁니다. 그럼 저장을 안 하면 됩니다. 조심해서 안 틀리는 게 아니라, &lt;b&gt;틀릴 자리를 없애는&lt;/b&gt; 쪽이에요. 저는 조심하는 걸 잘 못해서 이 방향이 맞다고 봤습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 붙이자마자 두 개를 잡았습니다&lt;/h2&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예전 글 하나가 제 기록에는 &lt;b&gt;비공개본 기준 번호&lt;/b&gt;로 남아 있었습니다. 공개 전환하면서 번호가 바뀌었는데 기록은 안 바뀐 거죠&lt;/li&gt;
&lt;li&gt;다른 글 하나는 &lt;b&gt;발행 주소가 아예 없어서&lt;/b&gt; 링크를 걸 수 없는 상태였습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 제가 손으로 관리하던 목록이었으면 몇 달은 더 갔을 것 같습니다. 아무도 안 세어보니까요. (그리고 저는 이런 걸 세어보는 사람이 아닙니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정직하게 말하면, 아직 SSOT는 아닙니다&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;그래서 저는 이걸 &quot;완성&quot;이라고 안 부르고, 대신 &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;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;지금 안 만드는 이유가 &quot;필요 없어서&quot;가 아니라 &quot;아직 필요한지 모르겠어서&quot;라는 걸 남겨둔 겁니다. 이렇게 안 적어두면 나중에 그냥 잊어버리거든요. (실제로 몇 번 잊어봤습니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&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;1편&lt;/b&gt;: AI가 거짓말하는 건 고장이 아니라 스펙입니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2편&lt;/b&gt;: 그러니까 모델 밖에 하네스를 지었습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3편&lt;/b&gt;: 근데 쌓기만 하고 안 읽더군요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4편&lt;/b&gt;: 여러 개를 돌리면 그 병이 훨씬 심해집니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5편&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;근데 만들고 보니 제가 만든 건 세계가 아니었습니다. &lt;b&gt;세계를 보는 렌즈&lt;/b&gt; 하나였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세계는 원래 흩어져 있습니다. 파일도 여기저기 있고 형식도 제각각이에요. 그걸 억지로 한 덩어리로 만드는 대신, 모두가 &lt;b&gt;같은 렌즈를 통해&lt;/b&gt; 보게 했습니다. 그러니까 각자 다르게 보이던 게 같아지더군요.&lt;/p&gt;
&lt;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;a href=&quot;https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;1편에서 말한 그것&lt;/a&gt;에서 출발했습니다. 얘는 거짓말을 합니다. 앞으로도 할 거고요. 그건 못 고칩니다.&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/138&quot;&gt;8개의 멀티 에이전트? 서로 뭔짓 했는지 아무것도 모르는데?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/135&quot;&gt;AI가 거짓말하는 이유&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/136&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>it</category>
      <category>LLM위키</category>
      <category>SSOT</category>
      <category>단일진실공급원</category>
      <category>데이터정합성</category>
      <category>멀티에이전트</category>
      <category>오버엔지니어링</category>
      <category>클로드코드</category>
      <category>하네스</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/139</guid>
      <comments>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EC%97%90%EA%B2%8C-%EC%84%B8%EA%B3%84%EA%B4%80-%EC%84%A4%EC%A0%95%ED%95%B4%EC%A3%BC%EA%B8%B0#entry139comment</comments>
      <pubDate>Wed, 2 Sep 2026 18:01:43 +0900</pubDate>
    </item>
    <item>
      <title>8개의 멀티 에이전트? 서로 뭔짓 했는지 아무것도 모르는데?</title>
      <link>https://wanglan.tistory.com/entry/8%EA%B0%9C%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%9C%EB%A1%9C-%EB%AD%94%EC%A7%93-%ED%96%88%EB%8A%94%EC%A7%80-%EC%95%84%EB%AC%B4%EA%B2%83%EB%8F%84-%EB%AA%A8%EB%A5%B4%EB%8A%94%EB%8D%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 정해진 시각에 알아서 도는 에이전트가 여덟 개인데, 서로 뭘 했는지 아무도 몰랐습니다 &amp;mdash; 규칙을 받고 시작하는 건 둘뿐이었습니다&lt;br /&gt;&amp;middot; 사고도 났습니다 &amp;mdash; 세어둔 숫자가 세션을 건너며 혼자 자랐고, 일주일 동안 서로 다른 숫자를 보고 있었습니다&lt;br /&gt;&amp;middot; 게으른 건 에이전트가 아니라 세션마다 맥락을 손으로 조립하던 저였습니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;얘네 서로 뭘 했는지 알긴 하나?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C&quot;&gt;지난 글&lt;/a&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;모르더군요. 아무것도요. (여덟 개나 돌리고 있으면서 이걸 이제야 물어본 것도 좀 그렇습니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 세션이라는 게 뭐냐면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 세션 얘기를 잠깐 하고 가야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI랑 대화하는 창을 하나 켜면, 그게 세션입니다. 그 창이 열려 있는 동안 나눈 얘기는 다 기억합니다. 아까 붙여넣은 코드도, 30분 전에 정한 것도 다 압니다.&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;같은 AI인데도 그렇습니다. 창을 새로 열면 아까 그 대화는 없던 일이 됩니다. 사람으로 치면 회의실 두 개에서 각각 회의가 돌아가는데, 서로 방음이 완벽한 상황이에요. 게다가 회의가 끝나면 참석자 기억이 통째로 날아갑니다.&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;평일 아침 8시 30분 &amp;rarr; 주식 브리핑 세션 하나&lt;/li&gt;
&lt;li&gt;장중 두 시간마다 &amp;rarr; 종목 스카우트 세션 하나씩&lt;/li&gt;
&lt;li&gt;아침저녁 &amp;rarr; 개발 뉴스 훑는 세션&lt;/li&gt;
&lt;li&gt;토요일 오전 &amp;rarr; 블로그 초안 쓰는 세션&lt;/li&gt;
&lt;li&gt;그리고 제가 말을 걸 때 &amp;rarr; 대화 세션&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-4편-8개의-멀티-에이전트-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IqXHw/dJMcaf14wUS/SPjVJNzbTR5fHBWf0y43tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IqXHw/dJMcaf14wUS/SPjVJNzbTR5fHBWf0y43tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IqXHw/dJMcaf14wUS/SPjVJNzbTR5fHBWf0y43tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIqXHw%2FdJMcaf14wUS%2FSPjVJNzbTR5fHBWf0y43tK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-4편-8개의-멀티-에이전트-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 여덟 개 중 둘만 규칙을 받고 있었습니다&lt;/h2&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;&lt;b&gt;크론에 걸려 자동으로 도는 세션 여덟 개 중에, 제가 정해둔 규칙 목록을 받고 시작하는 건 두 개뿐이었습니다.&lt;/b&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;b&gt;에이전트가 게을러서&lt;/b&gt;라고 생각했거든요. 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션마다 맥락을 손으로 조립하고 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 말이냐면, 크론이 도는 스크립트마다 &quot;이번 세션은 이걸 알고 시작해라&quot; 하는 부분을 제가 직접 짜 넣었다는 겁니다. 어떤 스크립트는 30줄 넘게 조립합니다. 그러니까 새 스크립트를 만들 때마다 그걸 또 짜야 하고, 안 짜면 그 세션은 맨몸이 되고, 규칙이 하나 바뀌면 여덟 군데를 다 고쳐야 하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 안 고칩니다. 저는 두 군데만 고쳤더라고요. 게으른 건 에이전트가 아니라 저였던 것 같습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 사고 하나 - 숫자가 혼자 자랐습니다&lt;/h2&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;b&gt;세어둔 숫자&lt;/b&gt;를 파일에 저장해뒀다는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 돌아갔습니다. 아침 세션이 &quot;개발도구 5개&quot; 라고 적어둡니다. 저녁 세션이 그 파일을 읽고, 오늘 것 하나를 더해서 6으로 고쳐 적습니다. 다음날 아침이 또 읽고 7로 만들고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 원본 목록을 실제로 세어보면 5개입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션이 넘겨받으면서 조금씩 어긋난 겁니다. 8월 24일에 한 번, 25일에 또 한 번. &lt;b&gt;이틀 연속으로 같은 사고가 났습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-4편-8개의-멀티-에이전트-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSDhCq/dJMcafVqN3o/QXthmgrVwXdiOMnvys7IKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSDhCq/dJMcafVqN3o/QXthmgrVwXdiOMnvys7IKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSDhCq/dJMcafVqN3o/QXthmgrVwXdiOMnvys7IKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSDhCq%2FdJMcafVqN3o%2FQXthmgrVwXdiOMnvys7IKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-4편-8개의-멀티-에이전트-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;무서운 건 여기입니다. &lt;b&gt;이 사슬에 사람이 안 낍니다.&lt;/b&gt; 제가 보는 건 최종 숫자뿐이고, 그 숫자가 원본이랑 맞는지는 아무도 안 셉니다. 원본은 그대로 있는데, 그걸 세어놓은 사본이 혼자 늙어간 거예요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 사고 둘 - 일주일 동안 다른 숫자를 보고 있었습니다&lt;/h2&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; 적혀 있었습니다. 실제로는 110주를 들고 있는데 그 파일엔 100주라고요.&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;p data-ke-size=&quot;size16&quot;&gt;그리고 7일 동안 아무도 안 세어봤습니다. 저도 안 세어봤고요. 그동안 그 파일을 읽은 세션들은 전부 틀린 숫자로 계산했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 안 잡히냐면요. 틀린 티가 안 납니다. 100주도 그럴듯하고 110주도 그럴듯하거든요. 값이 이상해야 눈에 띄는데 &lt;b&gt;적당히 그럴듯한 값은 그냥 통과합니다.&lt;/b&gt; 1편에서 소수점 두 자리 숫자가 무섭다고 한 것도 같은 이유인 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그래서 뭐가 문제였냐면&lt;/h2&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;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;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;2편에서 하네스 얘기&lt;/a&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;정리하면 이렇습니다. 멀티 에이전트의 진짜 문제는 &lt;b&gt;각자가 얼마나 똑똑하냐가 아니라 서로 같은 것을 보고 있냐&lt;/b&gt;인 것 같습니다. 그리고 제 병목은 저장이 아니었습니다. 기록은 넘치게 있었어요. 병목은 &lt;b&gt;그걸 세션마다 손으로 조립하고 있던 것&lt;/b&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;에이전트를 여러 개 돌리면 더 똑똑해질 거라고 생각하기 쉽습니다. 저도 그랬습니다. 여덟 개면 여덟 배까진 아니어도 좀 낫겠지 싶었죠.&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;/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;왜 안 골랐는지, 대신 뭘 했는지는 다음 글에서 얘기해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/137&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/135&quot;&gt;AI가 거짓말하는 이유&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>it</category>
      <category>데이터정합성</category>
      <category>멀티에이전트</category>
      <category>에이전트메모리</category>
      <category>오케스트레이션</category>
      <category>컨텍스트</category>
      <category>크론</category>
      <category>클로드코드</category>
      <category>하네스</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/138</guid>
      <comments>https://wanglan.tistory.com/entry/8%EA%B0%9C%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%84%9C%EB%A1%9C-%EB%AD%94%EC%A7%93-%ED%96%88%EB%8A%94%EC%A7%80-%EC%95%84%EB%AC%B4%EA%B2%83%EB%8F%84-%EB%AA%A8%EB%A5%B4%EB%8A%94%EB%8D%B0#entry138comment</comments>
      <pubDate>Tue, 1 Sep 2026 19:27:01 +0900</pubDate>
    </item>
    <item>
      <title>AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?</title>
      <link>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 위키에 정확히 적혀 있고 그 페이지를 열기까지 했는데, 본문을 안 읽고 반대 진단을 내렸습니다 &amp;mdash; 찾기 100점, 읽기 0점&lt;br /&gt;&amp;middot; 전체를 감사해보니 위키 페이지의 45%가 자동으로 읽힌 적이 없었습니다 &amp;mdash; 쓰기는 습관이 됐는데 읽기는 아니었던 겁니다&lt;br /&gt;&amp;middot; 그래서 읽기도 강제로 바꿨습니다 &amp;mdash; 판단 직전에 관련 기록이 문장으로 밀려 들어오게&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 내용은 왜 못찾는거야? 그리고 기존 문서가 왜 신규 내용으로 업데이트 되지 않은거지?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;제가 제 에이전트한테 실제로 보낸 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;지난 글&lt;/a&gt;에서 저는 위키를 지어놓고 AI가 알아서 적립하게 만들었다는 얘기를 했습니다. 기록은 정말 잘 쌓였어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 그걸 &lt;b&gt;읽고 있냐&lt;/b&gt;는 완전히 다른 문제였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 그날 있었던 일&lt;/h2&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; 세션이 4시간도 못 버티는데 주기적으로 깨워봐야 소용이 없었거든요. 이 결정은 제가 승인했고, 위키에도 그날 바로 정리돼 들어갔습니다.&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;&quot;세션 유지 작업이 없습니다. 고장입니다&quot;&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;b&gt;주석 한 줄&lt;/b&gt;을 봤다는 겁니다. &lt;code&gt;하루 6번 실행&lt;/code&gt; 이라고 적혀 있는 옛날 주석이었어요. 결정을 내릴 때 코드는 고쳤는데 그 주석 한 줄을 안 지운 겁니다. (이건 제 잘못이 맞습니다. 주석도 같이 지웠어야죠)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 위에 쓴 말을 보냈습니다. 이 내용은 왜 못 찾냐고요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 못 찾은 게 아니었습니다&lt;/h2&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; 폐지 결정도, 승인 날짜도, 세션이 4시간 못 간다는 실측도 다 있었습니다. 다른 파일에도 &quot;폐지된 작업&quot; 목록으로 한 번 더 정리돼 있었고요.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-3편-AI-에이전트가-정말-내가-준-정보를-읽고-대답할까-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2LcAn/dJMcacj1HiB/dQqB7Yf8vkVAnFKdoe4y9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2LcAn/dJMcacj1HiB/dQqB7Yf8vkVAnFKdoe4y9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2LcAn/dJMcacj1HiB/dQqB7Yf8vkVAnFKdoe4y9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2LcAn%2FdJMcacj1HiB%2FdQqB7Yf8vkVAnFKdoe4y9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-3편-AI-에이전트가-정말-내가-준-정보를-읽고-대답할까-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이게 제일 무서웠습니다. 검색이 실패한 게 아니거든요. &lt;b&gt;찾기는 100점이고 읽기가 0점&lt;/b&gt;이었던 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 사람도 이럽니다. 회의록 제목만 보고 &quot;아 그거 그렇게 정했지&quot; 하고 넘어가는 거요. 근데 사람은 틀리면 어색해하기라도 하는데, 얘는 아주 자신 있게 진단서를 써옵니다. &lt;a href=&quot;https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;1편에서 말한 그 거짓말&lt;/a&gt;이랑 똑같은 얼굴로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇게 되는 것 같습니다. &lt;b&gt;못 찾은 게 아니라, 찾아놓고 안 읽은 것.&lt;/b&gt; 이 둘은 처방이 완전히 다릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 생각해보니 처음이 아니었습니다&lt;/h2&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 data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그거 기록있지않아?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;에이전트가 뭔가를 새로 조사하려고 하길래 던진 말이었습니다. 있었습니다. 우리가 2주 전에 정리해둔 게 그대로 있었어요.&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;llm wiki harness도 업데이트 잘했냐고&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;구조를 고쳤으면 다른 저장소에도 반영하기로 정해놨는데, 안 했더라고요. 그것도 규칙으로 적혀 있는 거였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 번 다 제가 물어봐서 드러났습니다.&lt;/b&gt; 제가 안 물어봤으면 그냥 지나갔을 것 같습니다. 그게 몇 건인지는 저도 모르고요.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그래서 세어봤습니다&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;결과는 이랬습니다. (2026년 8월 19일 기준입니다)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 44개 페이지 중 &lt;b&gt;25개&lt;/b&gt;가 자동으로 읽힌 적이 &lt;b&gt;한 번도 없었습니다.&lt;/b&gt; 페이지 수로 57%입니다&lt;/li&gt;
&lt;li&gt;글자 수로 세면 275,684자, 전체의 &lt;b&gt;45%&lt;/b&gt;입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-3편-AI-에이전트가-정말-내가-준-정보를-읽고-대답할까-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blSeub/dJMcaaNnj5x/B2OQQ3gCeikvp6wSFAFivK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blSeub/dJMcaaNnj5x/B2OQQ3gCeikvp6wSFAFivK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blSeub/dJMcaaNnj5x/B2OQQ3gCeikvp6wSFAFivK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblSeub%2FdJMcaaNnj5x%2FB2OQQ3gCeikvp6wSFAFivK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-3편-AI-에이전트가-정말-내가-준-정보를-읽고-대답할까-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;축적만하고 사용하는 레이어는 1개밖에 없다고했잖아. 제대로 활용하고 사용하는 레이어를 잘 만들어야하는데 위키를 축적만하고 사용안하면 기존에 적당히 똑똑한 너에서 발전하는게 없잖아.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;써놓고 안 읽으면 그냥 디스크만 먹는 겁니다. 오히려 손해예요. 있으니까 있는 줄 알고 안 찾아보게 되거든요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 원인은 비대칭이었습니다&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95&quot;&gt;2편에서 훅 얘기&lt;/a&gt;를 했었죠. 대화가 끝날 때마다 훅이 돌면서 &quot;이거 위키에 넣어야 하는 거 아니냐&quot;고 찔러댑니다. 그래서 기록은 자동으로 쌓입니다. 안 쌓을 수가 없어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 &lt;b&gt;읽는 쪽에는 그런 게 없었습니다.&lt;/b&gt; 지침 파일에 &quot;관련 페이지를 먼저 읽어라&quot;라고 적어는 놨죠. 그건 그냥 부탁입니다. 읽으면 좋고 안 읽어도 그만인 거예요. (부탁으로 될 일이었으면 애초에 훅을 안 만들었겠죠)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 이렇게 됩니다. 얘 입장에서는 이미 아는 것 같고, 대충 그럴듯한 답이 머릿속에 있고, 페이지를 열어서 읽는 건 귀찮습니다. &lt;b&gt;확률적으로 그럴듯한 답이 이미 있는데 뭐하러 확인하겠어요.&lt;/b&gt; 그게 이 기계가 하는 일인데요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. &quot;그거 RAG 붙이면 되는 거 아니에요?&quot;&lt;/h2&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;&lt;b&gt;검색은 성공했습니다.&lt;/b&gt; 페이지를 찾았고 열었어요. 검색을 더 좋게 만들어도 이 사고는 안 고쳐집니다. 못 찾은 게 아니니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 반대 방향의 얘기를 봤습니다. 어떤 개발자가 코딩 에이전트에서 검색 기능을 아예 떼어냈더니 결과가 더 좋아졌다고 하더군요. 그리고 나중에 알고 보니 &lt;b&gt;그 검색은 버그로 0건을 반환하고 있었는데 아무도 눈치채지 못했다&lt;/b&gt;고 합니다. 이유가 인상 깊었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;청크 검색은 그럴듯한 내용을 건네 모델이 실제 원본을 찾지 않고 믿게 만든다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저도 비슷한 이유로 벡터 검색을 한 번 기각했습니다. 위키에는 &quot;이건 폐지됐다&quot;는 문서랑 &quot;이건 이렇게 돌아간다&quot;는 옛 문서가 같이 있는데, &lt;b&gt;벡터에는 '무효화됐다'는 개념이 없습니다.&lt;/b&gt; 둘 다 비슷하게 관련도가 높으니까 나란히 뜹니다. 그럼 얘는 둘 중에 아무거나 믿게 되죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 잘하는 게 문제가 아니라 &lt;b&gt;읽게 만드는 게&lt;/b&gt; 문제였던 것 같습니다. (물론 검색이 필요 없다는 얘기는 아닙니다. 순서가 그렇다는 거예요)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 그래서 읽기도 강제로 바꿨습니다&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;지난 글에서 훅 목록을 보여드렸는데, 거기 이런 게 있었습니다. *제가 말을 걸 때마다 그 말에 걸리는 과거 판단을 찾아서 같이 밀어넣는다*. 그게 바로 이 사고의 처방으로 만든 겁니다.&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;처음엔 &quot;이 주제는 여기 문서를 보세요&quot;라고 파일 경로를 던져줄까 했거든요. 근데 그러면 똑같은 일이 반복될 것 같았습니다. 위에서 본 사고가 정확히 &lt;b&gt;경로는 알았는데 안 읽은&lt;/b&gt; 사고였으니까요. 그래서 파일을 가리키는 대신 그때 내린 판단을 &lt;b&gt;문장 그대로&lt;/b&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;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;기록은 쌓이고, 답변은 술술 나오고, 문서 개수는 늘어납니다. 대시보드로 보면 아주 건강해 보여요. 근데 그 답이 어제 쌓인 기록을 읽고 나온 건지, 아니면 원래 알던 걸로 그럴듯하게 만든 건지는 아무도 안 봅니다. 저도 안 봤고요.&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;제 저장소에는 지금 &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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/138&quot;&gt;8개의 멀티 에이전트? 서로 뭔짓 했는지 아무것도 모르는데?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/136&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/135&quot;&gt;AI가 거짓말하는 이유&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>it</category>
      <category>LLM위키</category>
      <category>Rag</category>
      <category>벡터검색</category>
      <category>에이전트메모리</category>
      <category>컨텍스트</category>
      <category>클로드코드</category>
      <category>하네스</category>
      <category>할루시네이션</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/137</guid>
      <comments>https://wanglan.tistory.com/entry/AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EC%A0%95%EB%A7%90-%EB%82%B4%EA%B0%80-%EC%A4%80-%EC%A0%95%EB%B3%B4%EB%A5%BC-%EC%9D%BD%EA%B3%A0-%EB%8C%80%EB%8B%B5%ED%95%A0%EA%B9%8C#entry137comment</comments>
      <pubDate>Fri, 28 Aug 2026 02:56:50 +0900</pubDate>
    </item>
    <item>
      <title>LLM이 동작하는걸 100% 신뢰하게 하는법</title>
      <link>https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 100% 신뢰하게 하는 법은 없습니다 &amp;mdash; 대신 제가 주식 살 때 하는 행동을 AI가 그대로 밟게 만들었습니다&lt;br /&gt;&amp;middot; 도구는 스킬(행동을 기계로), 훅(실행 직전에 끼어들기), 크론(안 물어봐도 돌기), 위키(알아서 적립) 네 가지입니다&lt;br /&gt;&amp;middot; &quot;니가 알아서 해봐&quot;보다는 믿을 만한 상태가 됐습니다 &amp;mdash; 제가 하던 순서를 걔가 밟았다는 걸 아니까요&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;여러분들은 AI한테 계좌 권한 다 열어주고 주식 투자를 시키실 수 있나요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저는 아닐 것 같습니다. 아직은요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 아닐까요? AI가 거짓말을 너무 많이 하고, 그 거짓말이 &lt;a href=&quot;https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;고칠 수 있는 종류가 아니기 때문입니다&lt;/a&gt;. 지난 글에서 그 얘기를 했었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 제목 보고 들어오신 분들께 먼저 사과드려야 할 것 같습니다. &lt;b&gt;100% 신뢰하게 하는 법은 없습니다.&lt;/b&gt; 어그로 끌어서 죄송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 오늘은 제가 이만큼은 줄여봤다는 얘기를 해보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AI가 이렇게 말한다면 어떠세요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 상상해보시죠. 여러분의 AI가 이렇게 말합니다.&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;&lt;b&gt;&quot;하이닉스는 86.77%로 오를 것입니다&quot;&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;저는 의심부터 듭니다. 뭘 근거로 저러는 거지? 무슨 순서로 알아본 거지? 검증은 어떻게 한 거고, 저 86.77이라는 숫자에 할루시네이션이 없다는 건 누가 보장하지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 저 86.77이라는 숫자가 그렇습니다. 소수점 두 자리까지 딱 말해주면 왠지 어디서 계산해온 것처럼 보이잖아요. 근데 저는 오히려 그 정밀함이 좀 무서운 것 같습니다.&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-2편-LLM을-100프로-신뢰하게-하는법-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deLMVh/dJMcadpGyvZ/izjkB9HU0FYxvh5cxbvFf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deLMVh/dJMcadpGyvZ/izjkB9HU0FYxvh5cxbvFf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deLMVh/dJMcadpGyvZ/izjkB9HU0FYxvh5cxbvFf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeLMVh%2FdJMcadpGyvZ%2FizjkB9HU0FYxvh5cxbvFf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-2편-LLM을-100프로-신뢰하게-하는법-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 그래서 제가 하는 짓을 그대로 시켜보기로 했습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 이런 생각이 들더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 주식을 사고팔 때, 적어도 &lt;b&gt;내가 하는 절차를 그대로 밟고 나서&lt;/b&gt; 판단을 내린다면 어떨까? 지가 알아서 분석하는 것보다는 믿을 만하지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 제가 조사해서 투자하는 것보다 AI가 내린 판단이 더 나을 수도 있습니다. 근데 그건 그거고, 심리적으로는 제가 하던 행동과 100% 똑같이 움직여줄 때 손이 가더라고요.)&lt;/p&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;&lt;b&gt;일단 사람들 반응부터 봅니다.&lt;/b&gt; 토스증권이나 미래에셋 커뮤니티, 종목토론방, DC 갤러리, 뉴스 댓글, 유튜브 댓글 같은 것들이요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최근 며칠 흐름을 차트로 봅니다.&lt;/b&gt; (차트 볼 줄 1도 모릅니다. 차트 보면서 투자하고 싶지도 않고요. 근데 일단 봅니다. 이것도 심리적인 요인인 것 같습니다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 종목이랑 관련 종목 최신 뉴스를 봅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제가 살 수 있는 잔액을 봅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;미국 시장 사람들 반응을 찾아봅니다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기타 등등.&lt;/b&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;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;그냥 &quot;니가 알아서 잘 분석해서 제안해줘&quot; 하는 것보다는 좀 더 믿을 만하지 않을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이렇게 제가 하는 행동을 AI가 따르게 만드는 것, 이걸 하네스 세팅이라고 합니다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스킬 - 행동 하나를 기계로 옮기는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 제가 하는 행동을 AI가 똑같이 하게 하려면 어떻게 해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;AI한테 시키시면 됩니다.&lt;/b&gt; (여기서부터 좀 편해집니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서는 이렇습니다. 우선 내가 하는 행동을 하나도 빠짐없이, 아주 디테일하게 설명합니다. 그리고 그 행동 하나하나를 실제로 동작하게 만드는 스킬을 만들어달라고 하는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 적은 여섯 개를 그대로 옮기면 이런 것들이 나옵니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커뮤니티 사이트 들어가서 최신 글 30개 긁어오는 스킬&lt;/li&gt;
&lt;li&gt;분봉, 일봉 싹 다 가져와서 &lt;b&gt;숫자를 따로 저장&lt;/b&gt;하는 스킬&lt;/li&gt;
&lt;li&gt;저장된 숫자로 계산까지 해주는 스킬&lt;/li&gt;
&lt;li&gt;관심 종목 뉴스 검색하는 스킬&lt;/li&gt;
&lt;li&gt;차트 흐름 보는 법을 미리 학습시켜둔 스킬&lt;/li&gt;
&lt;li&gt;미국장 분석하는 스킬&lt;/li&gt;
&lt;li&gt;내가 여태 어떤 선택을 했고 그게 지금 어떤 결과를 냈는지 보는 스킬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째에서 숫자를 굳이 따로 저장하는 이유가 있습니다. &lt;a href=&quot;https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;1편에서 말씀드렸듯이&lt;/a&gt; 얘는 다음에 올 말을 확률로 고르는 기계라서, &lt;b&gt;수학에 약합니다.&lt;/b&gt; 숫자를 말로 다루게 두면 그럴듯한 숫자가 나옵니다. 그래서 계산은 코드한테 시키고 AI는 결과만 읽게 하는 편이 안전한 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-2편-LLM을-100프로-신뢰하게-하는법-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbbhWl/dJMb998FxMF/TYFA5NpmSSkbPvI0wbq6m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbbhWl/dJMb998FxMF/TYFA5NpmSSkbPvI0wbq6m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbbhWl/dJMb998FxMF/TYFA5NpmSSkbPvI0wbq6m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbbhWl%2FdJMb998FxMF%2FTYFA5NpmSSkbPvI0wbq6m0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-2편-LLM을-100프로-신뢰하게-하는법-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;이렇게 다 만들어놓고 나면, 이제 제가 &lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EB%82%B4-%EC%A0%84%EC%9A%A9-%EC%A3%BC%EC%8B%9D-%EC%A1%B0%EC%96%B8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%9B%84%EA%B8%B0&quot;&gt;주식 조언해주는 에이전트&lt;/a&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;그러면 &lt;b&gt;얘가 저 스킬들을 우르르 돌려서&lt;/b&gt; 데이터를 다 긁어오고, 종합해서 판단까지 해줍니다. 제가 커뮤니티 들어가서 눈알 굴리고 있을 시간에 지가 알아서 다 하고 있는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 판단이 맞냐 틀리냐는 일단 접어두겠습니다. 중요한 건 &lt;b&gt;제가 직접 하던 행동을 시간만 줄여서 대신 해줬고, 저보다 훨씬 많은 텍스트를 일목요연하게 정리했다&lt;/b&gt;는 걸 제가 믿을 수 있다는 겁니다. 그래서 그 선택지는 &quot;알아서 분석해봐&quot;보다 따를 만하다고 생각합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 훅 - 안 시켰는데 끼어드는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬이 &quot;시키면 하는 것&quot;이라면, 훅은 &lt;b&gt;안 시켜도 끼어드는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 저장소에는 지금 이런 것들이 걸려 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 시작될 때 &amp;rarr; 위키 현황이랑 지금 유효한 규칙 전량을 통째로 밀어넣습니다&lt;/li&gt;
&lt;li&gt;제가 말을 걸 때마다 &amp;rarr; 그 말에 걸리는 &lt;b&gt;과거 판단&lt;/b&gt;을 찾아서 같이 밀어넣습니다&lt;/li&gt;
&lt;li&gt;위험한 명령을 실행하기 직전에 &amp;rarr; 예전에 그것 때문에 사고 났던 전례를 붙입니다&lt;/li&gt;
&lt;li&gt;대답이 끝날 때 &amp;rarr; 규칙 점검을 돌립니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째가 재밌습니다. 제 에이전트가 파일 편집 명령을 쓰려고 하면, 실행 직전에 이런 게 붙습니다. *&quot;2026-08-21에 같은 방식으로 치환했다가 파일에 두 곳이 있어서 둘 다 바뀌었다. 이번 것도 유일한지 세어봤나?&quot;*&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 필요하냐면, &lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;문서에 세 군데나 적어놨는데도 똑같은 사고를 또 냈기 때문입니다&lt;/a&gt;. 지침에 적는 걸로는 안 됩니다. 읽을 수도 있고 안 읽을 수도 있으니까요. 근데 훅은 &lt;b&gt;읽을지 말지를 AI 녀석이 정하지 못합니다.&lt;/b&gt; 지가 안 읽고 넘어가려고 해도 그냥 들어갑니다. 이게 지침 파일이랑 결정적으로 다른 점인 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 크론 - 물어보지도 않았는데 도는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬 중에는 주기적으로 돌아야 하는 것들이 있습니다. 이런 건 크론으로 등록해두면 됩니다. 정해진 시각에 알아서 돌게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 저장소에는 지금 열한 개가 돌고 있습니다. 평일 아침 8시 30분에 주식 브리핑, 장중에 두 시간 간격으로 종목 스카우트, 아침저녁으로 개발 뉴스 훑기, 토요일 오전에는 블로그 초안까지 알아서 씁니다.&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;p data-ke-size=&quot;size16&quot;&gt;처음엔 필요할 때마다 하나씩 만들었거든요. 그러다 보니 알림은 계속 오는데 정작 제가 뭘 해야 하는지는 모르겠는 상태가 되더라고요. 부지런한 비서를 열한 명 둔 게 아니라 그냥 시끄러운 알람시계를 열한 개 둔 것 같은 느낌이었습니다 ㅎㅎ 지금은 규칙을 하나 박아놨습니다. 새로 만들기 전에 이미 도는 것에 얹을 수 있는지 먼저 확인할 것.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그리고 위키 - 자기가 알아서 적립하게&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 &quot;내 행동을 옮기는&quot; 얘기였다면, 하나가 더 남았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1편에서 컨텍스트 얘기를 했었죠. 선생님이 불러주는 필기노트요. 그 필기노트를 매번 제가 손으로 써서 붙여넣어야 한다면 이 구조는 오래 못 갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 저도 이것 때문에 도입을 한참 망설였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 참고글, 요약, 검색, 정리&amp;hellip; 좋은 건 알겠는데 &lt;b&gt;이걸 언제 어떻게 해야 하는지&lt;/b&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;그래서 방향을 이렇게 잡았습니다. &lt;b&gt;위키를 도입하되, 관리도 걔가 알아서 하게 하자.&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-옵시디언-그래프뷰.jpg&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjNfZB/dJMb998FxMG/kQKNkHDKsoWT8Bxw1rRblK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjNfZB/dJMb998FxMG/kQKNkHDKsoWT8Bxw1rRblK/img.jpg&quot; data-alt=&quot;바깥쪽에 흩어져 있는 점들은 플러그인 깔면서 딸려온 외부 기능 문서들입니다. 제 지식이 얽혀 있는 건 가운데 뭉쳐 있는 쪽이고요.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjNfZB/dJMb998FxMG/kQKNkHDKsoWT8Bxw1rRblK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjNfZB%2FdJMb998FxMG%2FkQKNkHDKsoWT8Bxw1rRblK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;665&quot; data-filename=&quot;2026-08-28-옵시디언-그래프뷰.jpg&quot; data-origin-width=&quot;699&quot; data-origin-height=&quot;665&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;바깥쪽에 흩어져 있는 점들은 플러그인 깔면서 딸려온 외부 기능 문서들입니다. 제 지식이 얽혀 있는 건 가운데 뭉쳐 있는 쪽이고요.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;하지만 저는 여기에 단 한 글자도 직접 수정하지 않았습니다.&lt;/b&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;b&gt;캡처하는 방식 자체&lt;/b&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;정리하면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스킬&lt;/b&gt;은 내가 하는 행동 하나를 기계로 옮긴 것이고, &lt;b&gt;훅&lt;/b&gt;은 안 시켜도 끼어드는 것이고, &lt;b&gt;크론&lt;/b&gt;은 안 물어봐도 정해진 시각에 도는 것입니다. 그리고 이걸 구조로 묶어놓은 게 &lt;b&gt;하네스&lt;/b&gt;입니다.&lt;/p&gt;
&lt;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;p data-ke-size=&quot;size16&quot;&gt;제가 쓰는 AI는 어제나 오늘이나 똑같은 놈이고, 여전히 다음 말을 확률로 고릅니다. 거짓말도 여전히 합니다. 바뀐 건 그 주변이에요. 조사 순서가 정해졌고, 안 읽고 넘어가려 하면 밀어넣고, 안 물어봐도 돌고, 쌓아둔 걸 다시 꺼내옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 100%는 아니지만, &lt;b&gt;적어도 &quot;니가 알아서 해봐&quot;보다는 믿을 만한 상태&lt;/b&gt;가 된 것 같습니다. 제가 하던 순서를 걔가 밟았다는 걸 아니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 저 여섯 개를 다 옮긴 건 아닙니다. 뉴스랑 잔액이랑 과거 기록 정도는 실제로 돌고 있는데, 커뮤니티 긁어오는 거나 차트 숫자 다루는 건 아직 못 만들었습니다. 하나씩 늘려가는 중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기까지 지어놓고 얼마 지나서, 저는 이상한 걸 하나 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기록은 정말 잘 쌓이고 있었는데, 그걸 &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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/137&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/135&quot;&gt;AI가 거짓말하는 이유&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>it</category>
      <category>LLM위키</category>
      <category>스킬</category>
      <category>자율캡처</category>
      <category>컨텍스트</category>
      <category>크론</category>
      <category>클로드코드</category>
      <category>하네스</category>
      <category>훅</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/136</guid>
      <comments>https://wanglan.tistory.com/entry/LLM%EC%9D%B4-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B1%B8-100-%EC%8B%A0%EB%A2%B0%ED%95%98%EA%B2%8C-%ED%95%98%EB%8A%94%EB%B2%95#entry136comment</comments>
      <pubDate>Fri, 28 Aug 2026 02:13:03 +0900</pubDate>
    </item>
    <item>
      <title>AI가 거짓말하는 이유</title>
      <link>https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; AI의 거짓말 두 종류를 매일 겪습니다 &amp;mdash; 안 해놓고 &quot;모두 완료했습니다&quot;, 그리고 자신 있게 낡은 얘기하기&lt;br /&gt;&amp;middot; 원인은 교과서(파라미터)와 필기노트(컨텍스트)라는 구조에 있습니다 &amp;mdash; 고장이 아니라 스펙입니다&lt;br /&gt;&amp;middot; 스펙인 걸 알고 나면 지시가 달라집니다 &amp;mdash; 필기노트를 같이 넣어주고, 완료했다는 말을 그대로 믿지 않게 됩니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;AI 이자식.. 왜이렇게 구라를치지..?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;네, 구라 맞습니다. 그런데 이게 &lt;b&gt;고장이 아니라 스펙&lt;/b&gt;으로 생각해보신 적 있나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 저는 &lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EB%82%B4-%EC%A0%84%EC%9A%A9-%EC%A3%BC%EC%8B%9D-%EC%A1%B0%EC%96%B8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%9B%84%EA%B8%B0&quot;&gt;주식 조언을 해주는 에이전트&lt;/a&gt;를 비롯해서 AI 에이전트를 여러 개 굴리고 있습니다. 하루 종일 뭔가를 시키고 뭔가를 받아보는 생활을 하고 있는데요, 그러다 보니 저 혼잣말을 정말 자주 하게 되더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;b&gt;AI가 왜 거짓말을 하는지&lt;/b&gt;, 그리고 제가 왜 그걸 더 이상 화낼 일로 안 보게 됐는지를 적어보려고 합니다. AI 쓰다가 몇 번 데이고 나서 &quot;이거 못 믿겠는데?&quot; 라든지 &quot;내가 쓰는 AI는 왜 이렇게 멍청하지..?&quot; 하고 접으신 분들이 읽으면 좋을 것 같아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 제가 매일 듣는 구라 두 종류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 짚고 갈 게 있습니다. 흔히 AI의 거짓말이라고 하면 &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;첫 번째 - &quot;모두 완료했습니다&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 진짜 매일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업을 시키고 기다리면 이렇게 옵니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;요청하신 작업을 &lt;b&gt;모두 완료했습니다.&lt;/b&gt;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&quot;이거 왜 안 되어 있어?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&quot;말씀하신 부분이 &lt;b&gt;빠져 있습니다. 제 잘못입니다.&lt;/b&gt;&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;...아니 그러니까요. &lt;b&gt;아예 처음부터 다 했다고 안 하면 되는 거잖아요.&lt;/b&gt;&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;두 번째는 좀 다릅니다. AI가 아주 확신에 차서 알려주는데, 그게 &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;b&gt;&quot;얘가 틀렸다&quot;가 아니라 &quot;얘가 outdated 됐다&quot;&lt;/b&gt; 라고요. 물론 틀린 건 틀린 거긴 한데(결과적으로 제가 잘못된 걸 그대로 쓰면 사고가 나니까요), 원인을 생각하면 outdated 쪽이 훨씬 정확한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그런지는 바로 다음 장에서 얘기해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 그러는 걸까요 - 교과서와 필기노트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 보기 전에 뭘 보시나요? 저는 교과서 보고, 선생님이 &quot;이거 시험에 나온다&quot; 하고 불러주신 필기노트를 봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM도 딱 이 두 가지를 봅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-1편-AI가-거짓말쟁이인-이유-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pqS9d/dJMcabFnfg7/eslRK6vhSZgxaWnsT8HOT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pqS9d/dJMcabFnfg7/eslRK6vhSZgxaWnsT8HOT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pqS9d/dJMcabFnfg7/eslRK6vhSZgxaWnsT8HOT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpqS9d%2FdJMcabFnfg7%2FeslRK6vhSZgxaWnsT8HOT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-1편-AI가-거짓말쟁이인-이유-gen1.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;LLM은 학습할 때 읽은 어마어마한 양의 글을 통째로 가지고 있습니다. 정확히 말하면 문장을 그대로 외운 게 아니라, &lt;b&gt;단어들끼리 어떤 관계로 붙어 다니는지, 어떤 패턴으로 이어지는지&lt;/b&gt;를 숫자로 가지고 있는 겁니다. 그리고 그 패턴을 보고 다음에 올 말을 고릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 좀 중요한 것 같은데요. AI가 사람처럼 말하는 게 &lt;b&gt;사람처럼 생각해서&lt;/b&gt;가 아닙니다. 생각을 한 사람들이 써놓은 단어의 나열, 그 안의 연관관계와 패턴을 보고 말하는 거예요. 그러니까 사람처럼 생각하는 게 아니라 사람이 쓴 걸 아주 잘 흉내내는 것에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 교과서는 &lt;b&gt;시험장에서 못 바꿉니다.&lt;/b&gt; 학습이 끝난 시점에 굳어버린 거라 대화 중에는 손을 못 대요. 아까 나온 &quot;자신 있게 낡은 얘기를 한다&quot;가 여기서 나옵니다. 교과서가 작년 판이면 작년 얘기를 하는 거죠.&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;/p&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;b&gt;보라색 = 소녀의 죽음 암시.&lt;/b&gt; 밑줄.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;자, 여기서 AI한테 그냥 물어봅니다. &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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;그래서 대화를 한참 하다 보면 갑자기 &quot;어 얘 좀 하는데?&quot; 싶은 순간이 오잖아요. 그게 컨텍스트가 알차게 쌓였을 때인 것 같습니다. 반대로 새 창을 열고 처음부터 시작하면 다시 교과서만 가진 상태로 돌아가고요(그래서 저는 새 창 여는 걸 좀 아까워하는 이상한 습관이 생겼습니다).&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그런데 이 글을 쓰다가 제가 걸렸습니다&lt;/h2&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;재밌는 건, 정작 황순원 작가는 그런 의도 없이 그냥 보라색이 좋아서 썼다고 하셨다는 거죠.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;황순원 작가는 &lt;b&gt;2000년에 작고&lt;/b&gt;하셨습니다&lt;/li&gt;
&lt;li&gt;생전에 언론 인터뷰를 거의 다 거절하신 것으로 알려져 있습니다&lt;/li&gt;
&lt;li&gt;그런데 이 얘기가 인터넷에 돌기 시작한 건 &lt;b&gt;2011~2012년쯤&lt;/b&gt;입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작가님이 돌아가시고 10년쯤 지나서 생긴 얘기라는 거죠. 어느 인터뷰에서 하셨다는 건지 아무도 출처를 못 댑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 저는 방금, &lt;b&gt;AI가 거짓말하는 이유에 대한 글을 쓰면서, 출처도 모르는 얘기를 사실인 줄 알고 예시로 쓸 뻔한 겁니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 생각해보면 제가 한 짓이 딱 LLM이 하는 짓이었어요. 그럴듯하고, 여러 군데서 봤고, 말이 되니까 그냥 사실로 받아들인 겁니다. 원출처를 확인하는 단계가 통째로 빠져 있었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 저는 이런 걸 이미 한 번 크게 겪었습니다. 얼마 전에는 제가 직접 만든 리포트를 믿고 보다가 &lt;a href=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%EA%B8%80-%EC%84%9C%EC%B9%98%EC%BD%98%EC%86%94-API%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9C%A0%EC%9E%85-%EB%A6%AC%ED%8F%AC%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8A%94%EB%8D%B0-8%EB%B0%B0-%ED%8B%80%EB%A6%B0-%EC%88%AB%EC%9E%90%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%9E%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;8배 틀린 숫자를 보고 있었거든요&lt;/a&gt;. 그때도 숫자가 그럴듯해서 안 세어본 거였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI만 그러는 게 아닌 것 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 도라지꽃 보라색을 죽음의 복선으로 가르치는 것 자체는 실제로 있는 해석입니다. 다만 &quot;작가가 그렇게 의도했다&quot;랑 &quot;교과서가 그렇게 가르친다&quot;는 완전히 다른 문장이죠. 저는 이 둘을 섞어서 알고 있었던 거고요.)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그래서, 이건 결함이 아니라 스펙입니다&lt;/h2&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-28-1편-AI가-거짓말쟁이인-이유-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qKzCl/dJMcacRMHjF/fCKS2kx3y7zIM23d5scup1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qKzCl/dJMcacRMHjF/fCKS2kx3y7zIM23d5scup1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qKzCl/dJMcacRMHjF/fCKS2kx3y7zIM23d5scup1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqKzCl%2FdJMcacRMHjF%2FfCKS2kx3y7zIM23d5scup1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-28-1편-AI가-거짓말쟁이인-이유-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트폰은 쓰다 보면 뜨거워집니다. 게임 좀 돌리면 손난로가 되죠. 그런데 이걸 두고 &quot;이 폰 고장났다&quot;고 하진 않잖아요? &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;LLM의 거짓말은 &lt;b&gt;발열 쪽&lt;/b&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;b&gt;그러니까 없앨 수 있는 게 아니라, 얼마나 잡느냐의 싸움인 것 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언젠가 초전도체가 상용화되면 발열 문제도 해결되겠죠. 마찬가지로 할루시네이션이 0이 되는 시대도 올 수 있다고 생각합니다. 다만 지금 당장 그걸 어떻게 하는지는 저도 잘 모르겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그럼 지금 우리는 뭘 할 수 있을까요&lt;/h2&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;p data-ke-size=&quot;size16&quot;&gt;이건 제가 최근에 좀 세게 배운 것이기도 합니다. 저는 제 에이전트한테 &quot;이렇게 하지 마&quot;라고 문서에 적어두면 지킬 줄 알았거든요. 근데 &lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4&quot;&gt;문서에 세 군데나 적어놨는데도 똑같은 사고를 또 냈습니다&lt;/a&gt;. 결국 밖에서 기계가 막게 하고 나서야 멈추더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거짓말도 같은 것 같습니다. &quot;거짓말하지 마&quot;라고 프롬프트에 적는 걸로는 안 됩니다. 그건 발열 나는 폰한테 &quot;뜨거워지지 마&quot;라고 말하는 거랑 비슷한 것 같아요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 AI가 자꾸 구라를 쳐서 안 쓰신다는 분들을 종종 봅니다. 충분히 이해가 갑니다. 몇 번 데이면 손이 안 가죠.&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;AI 활용법에 대한 좋은 유튜브랑 아티클도 정말 많으니까 그런 것들도 적극적으로 보시면 좋을 것 같아요. 저는 언젠가 세상 모든 사람이 AI를 쓰는 날이 왔으면 좋겠다고 생각하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 서두에 말씀드린 &quot;모델 밖에서 다룬다&quot;는 얘기요. 저는 그걸 실제로 지어봤습니다. 제 AI가 볼 교과서 밖의 필기노트를, 제가 직접 만들어서 붙여준 셈이죠.&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;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/136&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/137&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/131&quot;&gt;바이브코딩에서 에이전틱 코딩, AI Native까지 &amp;mdash; 1년 반 동안 내 개발이 바뀐 순서&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI거짓말</category>
      <category>AI에이전트</category>
      <category>it</category>
      <category>llm</category>
      <category>인공지능</category>
      <category>챗지피티</category>
      <category>컨텍스트</category>
      <category>클로드</category>
      <category>파라미터</category>
      <category>할루시네이션</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/135</guid>
      <comments>https://wanglan.tistory.com/entry/AI%EA%B0%80-%EA%B1%B0%EC%A7%93%EB%A7%90%EC%9F%81%EC%9D%B4%EC%9D%B8-%EC%9D%B4%EC%9C%A0#entry135comment</comments>
      <pubDate>Fri, 28 Aug 2026 00:41:42 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다</title>
      <link>https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 하지 말라는 기록이 문서 세 곳에 네 줄이나 있었는데, 에이전트가 확인용 명령 하나로 같은 사고를 또 냈습니다&lt;br /&gt;&amp;middot; 문서는 읽어야 작동하지만 훅(Hook)은 실행 직전에 기계가 판정합니다 &amp;mdash; 규칙이 생기면 grep으로 잡히는지부터 묻고, 잡히면 스크립트로 내립니다&lt;br /&gt;&amp;middot; 훅을 깔아도 사고는 납니다. 다만 같은 사고가 두 번 나는 건 확실히 줄어듭니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다!&lt;/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;하지 말라는 기록이 세 군데에 네 줄이나 있었는데 제 에이전트가 똑같은 사고를 또 냈습니다. 그것도 &quot;확인만 해보려던&quot; 명령 하나로요. 결국 훅(Hook)으로 내려서 &lt;b&gt;기계가 명령을 실행 직전에 막게&lt;/b&gt; 하고 나서야 멈췄습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그 사고 얘기랑, 규칙을 문서에서 훅으로 내리면서 알게 된 것들을 적어보려고 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 확인만 하려던 명령 하나가 채널을 죽였습니다&lt;/h2&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;claude config list&lt;/code&gt;를 실행했습니다. 읽기만 하는 명령이니까 위험할 게 없다고 판단한 것이었고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 &lt;b&gt;&lt;code&gt;config list&lt;/code&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정을 &lt;b&gt;읽으려던&lt;/b&gt; 명령 &amp;rarr; 세션이 &lt;b&gt;떴고&lt;/b&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;br /&gt;근데 여기서 좀 웃긴 게, &lt;b&gt;저는 그때 아무것도 못 느꼈습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 로그를 뒤져보니 수신이 죽어 있던 구간이 4분 33초였고, 하필 그 사이에 제가 보낸 메시지가 하나도 없었거든요. 그러니까 유실된 것도 없고 제 쪽에는 흔적도 안 남은 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 신기했던 건 이쪽입니다. 얘가 자기 인프라를 모르고 셀프 죽음에 도달했는데, &lt;b&gt;죽은 걸 스스로 알고 다시 살아나는 플로우&lt;/b&gt;가 그대로 돌아갔다는 거요. 세션이 죽거나 연결이 끊겼을 때 살리는 세팅을 이것저것 해뒀었는데 그게 실제로 잘 돌아준 거라, 사고인데도 좀 뿌듯했던 것 같습니다 ㅎㅎ&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 하지 말라는 기록은 이미 네 줄이나 있었습니다&lt;/h2&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;네 줄이 다 있었는데 하나도 안 걸렸습니다. &lt;b&gt;그런데 빗나간 이유가 셋 다 달랐습니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;런북&lt;/b&gt;은 세션 시작할 때 자동으로 실리지 않습니다. 에이전트가 스스로 열어봐야 보이는 파일이었고, 그날은 안 열었습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;규칙 문구&lt;/b&gt;가 하필 &quot;헤드리스 &lt;b&gt;크론&lt;/b&gt;에는 필수&quot;라고 적혀 있었습니다. 그날 실행한 건 크론이 아니라 그냥 확인용 명령이었으니까, 에이전트 입장에서는 &lt;b&gt;여기는 해당 없음&lt;/b&gt;으로 읽은 겁니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;큐 사전&lt;/b&gt;은 트리거 단어가 &lt;code&gt;크론|cron|스케줄|주기 실행&lt;/code&gt;이었습니다. '설정 조회'라는 맥락에는 애초에 안 걸립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 진단이 이렇게 나옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;b&gt;다섯 군데에 적혀 있는데도 안 걸리는&lt;/b&gt; 사고가 납니다. 읽히지 않는 문장을 다섯 번째로 쓰는 거니까요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-22-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VXGWl/dJMcahloel1/x0V7LMjwjZGEbEUX0ItFE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VXGWl/dJMcahloel1/x0V7LMjwjZGEbEUX0ItFE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VXGWl/dJMcahloel1/x0V7LMjwjZGEbEUX0ItFE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVXGWl%2FdJMcahloel1%2Fx0V7LMjwjZGEbEUX0ItFE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-22-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 훅은 &quot;읽고 지켜라&quot;가 아니라 &quot;실행 직전에 판정한다&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code에는 훅(Hooks)이라는 기능이 있습니다. 특정 이벤트가 발생할 때 제가 지정한 스크립트를 실행해주는 기능인데요, 저는 이 중 &lt;b&gt;PreToolUse&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서에 적힌 규칙: 에이전트가 &lt;b&gt;읽어야&lt;/b&gt; 작동합니다. 안 읽으면 없는 것과 같습니다.&lt;/li&gt;
&lt;li&gt;훅: 에이전트가 뭘 읽었든 상관없이 &lt;b&gt;명령이 실행되기 전에&lt;/b&gt; 무조건 한 번 돕니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배선은 &lt;code&gt;settings.json&lt;/code&gt;에 이렇게 넣어두면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1787626037011&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;PreToolUse&quot;: [
{
&quot;matcher&quot;: &quot;Bash&quot;,
&quot;hooks&quot;: [
{ &quot;type&quot;: &quot;command&quot;, &quot;command&quot;: &quot;bash \&quot;$CLAUDE_PROJECT_DIR/scripts/pre-bash.sh\&quot;&quot;, &quot;timeout&quot;: 8 }
]
}
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅 스크립트는 표준 입력으로 지금 실행하려는 명령을 받습니다. 그래서 하는 일은 결국 이겁니다. &lt;b&gt;명령 문자열을 보고 판정한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 만든 가드는 이렇게 생겼습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1787626061523&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 명령 시작 위치의 claude 실행만 본다 (grep claude foo 같은 인자 등장은 무시)
echo &quot;$CMD&quot; | grep -qE '(^|[;&amp;amp;|][[:space:]]*)(/[^[:space:]]*/)?claude([[:space:]]|$)' || exit 0

# 백신이 이미 붙었으면 통과
echo &quot;$CMD&quot; | grep -q -- '--strict-mcp-config' &amp;amp;&amp;amp; exit 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;걸리면 이런 JSON을 뱉습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1787626091659&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;hookSpecificOutput&quot;:{&quot;hookEventName&quot;:&quot;PreToolUse&quot;,&quot;permissionDecision&quot;:&quot;deny&quot;,&quot;permissionDecisionReason&quot;:&quot;claude CLI 호출에 --strict-mcp-config가 없다. ...&quot;}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;code&gt;permissionDecision: &quot;deny&quot;&lt;/code&gt; 한 줄이면 그 명령은 실행되지 않고, &lt;code&gt;permissionDecisionReason&lt;/code&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;&lt;b&gt;통과하면 침묵해야 합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;훅이 매번 &quot;검사했고 이상 없습니다!&quot;를 출력하면, 그게 전부 컨텍스트를 먹습니다. 아무 일도 안 일어났다는 걸 알리려고 토큰을 쓰는 셈이라 좀.. 아깝습니다. 그래서 제 훅들은 전부 통과 시 아무것도 출력하지 않고 조용히 &lt;code&gt;exit 0&lt;/code&gt;으로 끝냅니다. (이 녀석들은 평소엔 있는 줄도 모릅니다)&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 훅을 두 개 만들지 않습니다, 디스패처를 하나 둡니다&lt;/h2&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개째 생기면 하나로 합친다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;pre-bash.sh&lt;/code&gt;라는 디스패처가 앞에 서고, 실제 검사들은 그 뒤에 줄을 섭니다. Stop 이벤트도 똑같이 &lt;code&gt;stop-lint.sh&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;디스패처가 stdin을 읽어버리면 그 뒤에 부르는 훅들은 빈 입력을 받습니다. 그래서 한 번 받아서 변수에 담아두고 각각에 먹여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1787626111340&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INPUT=$(cat 2&amp;gt;/dev/null || true)

GUARD=$(printf '%s' &quot;$INPUT&quot; | bash &quot;$DIR/guard-claude-spawn.sh&quot; 2&amp;gt;/dev/null || true)
if printf '%s' &quot;$GUARD&quot; | grep -q '&quot;permissionDecision&quot;[[:space:]]*:[[:space:]]*&quot;deny&quot;'; then
printf '%s\n' &quot;$GUARD&quot;
exit 0
fi

printf '%s' &quot;$INPUT&quot; | bash &quot;$DIR/hazard-recall.sh&quot; 2&amp;gt;/dev/null || true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서도 의미가 있습니다. &lt;b&gt;차단이 먼저, 주입이 나중입니다.&lt;/b&gt; 어차피 막힐 명령에 참고자료를 붙여줘봐야 그 명령은 실행되지 않으니까 컨텍스트만 낭비하게 되거든요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전부 막을 수는 없어서, 막는 대신 옆에서 알려주는 훅도 만들었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차단은 확실하지만 아무 데나 쓸 수 있는 게 아닙니다. &lt;code&gt;rm&lt;/code&gt;이나 &lt;code&gt;pkill&lt;/code&gt; 같은 건 정당하게 쓰는 경우가 훨씬 많아서, 패턴으로 막아버리면 에이전트가 아무 일도 못 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 두 번째 훅은 차단하지 않고 &lt;b&gt;전례를 주입만&lt;/b&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;&quot;내가 뭘 하려 할 때&quot;&lt;/b&gt; 기준으로 적혀 있었습니다. 근데 위의 사고에서 필요했던 축은 정반대였어요. 하려던 건 '설정 조회'였고 망가진 건 '텔레그램 폴러'였습니다. &lt;b&gt;행위로 색인해두면 그 연결이 안 섭니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 대장은 &lt;b&gt;자원으로 색인&lt;/b&gt;합니다. 자원마다 &quot;이걸 망가뜨린 전례&quot;를 붙여두는 형태고요. 넣을 때 지킨 조건이 세 개 있습니다.&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; &quot;이런 것도 위험할 것 같은데?&quot;를 넣기 시작하면 매칭이 신호가 아니라 상수가 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사례는 2건을 목표로 합니다.&lt;/b&gt; 사례가 하나면 그 표면(구체적인 명령어)에 붙어버리는데, 둘을 나란히 두면 표면이 벗겨지고 구조가 남더군요.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;본문은 되묻는 질문으로 끝냅니다.&lt;/b&gt; 사례만 던져놓으면 &quot;지금 이게 그 경우인가?&quot;를 판정하는 일이 여전히 에이전트 몫으로 남습니다. 그래서 마지막 줄이 항상 질문입니다. &quot;지금 보려는 게 *의도*인가 *동작*인가?&quot; 같은 식으로요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오탐도 실측으로 한 번 잡았습니다. 실제 명령 이력 160건에 돌려보니 발동 17건 중 5건이 오탐이었는데, 원인이 둘 다 &lt;b&gt;&quot;명령이 아닌 글자&quot;&lt;/b&gt;였습니다. &lt;code&gt;echo &quot;=== crontab 확인 ===&quot;&lt;/code&gt; 같은 라벨 안의 단어가 걸리거나, 테스트용 페이로드 문자열이 진짜 명령으로 읽히거나.. 그래서 따옴표로 둘러싼 &lt;code&gt;echo&lt;/code&gt;/&lt;code&gt;printf&lt;/code&gt; 인자랑 주석은 매칭에서 빼버렸습니다. &lt;b&gt;실행되는 것만 봅니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그런데 이걸 다 하고도 또 사고가 났습니다&lt;/h2&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;pkill -f &quot;node -c&quot;&lt;/code&gt;를 실행했는데, &lt;b&gt;그 패턴을 자기 argv에 가지고 있던 자기 셸을 죽였습니다.&lt;/b&gt; 자기가 자기를 쏜 거죠. (다행히 폴러는 argv에 그 패턴이 없어서 채널은 무사했습니다)&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;띄웠을 때는 이미 명령을 보낸 뒤였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PreToolUse는 실행 직전에 돌지만, 결국 그 내용을 읽고 &quot;아 이건 그 경우구나&quot; 하고 판단하는 건 여전히 에이전트입니다. &lt;code&gt;deny&lt;/code&gt;로 못 박은 것만 기계가 막고, 주입은 어디까지나 참고자료입니다. 그러니까 이 층은 &lt;b&gt;차단선이 아니라 알림선&lt;/b&gt;이고, 저는 그걸 알고 그렇게 설계했으면서도 은근히 차단선처럼 기대하고 있었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 훅을 붙이고 나서 뭐가 달라졌냐 하면, 솔직히 &lt;b&gt;잘 모르겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 생기면 알아서 죽었다 살아났다 하는데, 제가 3~5분 간격으로 계속 메시지를 보내고 있는 건 아니니까요. 알아서 자기관리 하고 살아나는 느낌이라고 해야 할까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 정확히는 &lt;b&gt;체감할 수가 없는 상태&lt;/b&gt;인 것 같습니다. 죽었을 때 제가 알 방법이 아직 없거든요. 위에서 4분 33초 동안 아무것도 못 느꼈다고 했는데, 그게 딱 이 얘기입니다. 잘 돌아가서 모르는 건지 안 돌아가는데 모르는 건지를 제 감으로는 못 가릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 마치며&lt;/h2&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;재발 방지를 문서에 적을지 훅에 넣을지는, &quot;기계가 판정할 수 있는가&quot;로 갈립니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;code&gt;--strict-mcp-config&lt;/code&gt; 없이 &lt;code&gt;claude&lt;/code&gt;를 띄우지 마라&quot;는 문자열 검사로 끝나는 일이라 훅이 훨씬 잘합니다. 반대로 &quot;이 판단은 신중하게 해라&quot; 같은 건 훅으로 못 내립니다. 그런 건 문서에 남을 수밖에 없고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 요즘은 규칙이 하나 생길 때마다 이걸 먼저 물어봅니다. &lt;b&gt;이거 grep으로 잡히는 건가?&lt;/b&gt; 잡히면 스크립트로 내리고, 안 잡히면 그때 문서에 씁니다. 문서에 적는 건 마지막 수단인 것 같아요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 6번에서 보셨듯이 훅을 깔아도 사고는 납니다. 다만 &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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/136&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/132&quot;&gt;공식 API가 죽은 티스토리, Claude Code로 자동 발행 파이프라인 만들기&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/137&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI에이전트</category>
      <category>claude hooks</category>
      <category>it</category>
      <category>바이브코딩</category>
      <category>스킬</category>
      <category>에이전트</category>
      <category>클로드코드</category>
      <category>클로드훅</category>
      <category>하네스</category>
      <category>훅</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/134</guid>
      <comments>https://wanglan.tistory.com/entry/Claude-Code-%ED%9B%85Hooks%EC%9C%BC%EB%A1%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8%EA%B0%80-%EA%B0%99%EC%9D%80-%EC%8B%A4%EC%88%98%EB%A5%BC-%EB%B0%98%EB%B3%B5%ED%95%98%EB%8A%94-%EA%B1%B8-%EB%A7%89%EC%95%84%EB%B4%A4%EC%8A%B5%EB%8B%88%EB%8B%A4#entry134comment</comments>
      <pubDate>Tue, 25 Aug 2026 11:35:18 +0900</pubDate>
    </item>
    <item>
      <title>구글 서치콘솔 API로 블로그 유입 리포트를 만들었는데, 8배 틀린 숫자를 보고 있었습니다</title>
      <link>https://wanglan.tistory.com/entry/%EA%B5%AC%EA%B8%80-%EC%84%9C%EC%B9%98%EC%BD%98%EC%86%94-API%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9C%A0%EC%9E%85-%EB%A6%AC%ED%8F%AC%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8A%94%EB%8D%B0-8%EB%B0%B0-%ED%8B%80%EB%A6%B0-%EC%88%AB%EC%9E%90%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%9E%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 서치콘솔 API로 자동화한 주간 유입 리포트가 만든 날부터 실제 클릭의 8분의 1만 보고하고 있었습니다&lt;br /&gt;&amp;middot; 원인은 둘 &amp;mdash; 상위 20개 합계를 총량처럼 보고한 것, 한글 키워드가 인코딩 문제로 잘려나간 것&lt;br /&gt;&amp;middot; 자동으로 돌아가는 숫자일수록 그 숫자가 어디서 왔는지 아무도 안 보게 됩니다 &amp;mdash; 여러분 대시보드의 숫자는 어디서 왔나요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그 유입을 데이터로 보겠다고 구글 서치 콘솔(Search Console) API를 붙여서 주간 리포트를 자동화해뒀습니다. 매주 토요일 저녁에 제 서버에서 크론이 돌면 이번 주 클릭이 몇이고 노출이 몇이고 어떤 키워드가 올랐는지가 정리돼서 텔레그램으로 날아옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말씀드리자면, 그 리포트가 만든 날부터 실제 클릭의 8분의 1을 보고하고 있었습니다. 세 번을 그렇게 받아보고 네 번째에 알았습니다.&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한테 넘기는 편입니다. 반복되는 걸 사람이 붙잡고 있는 게 제일 비효율적이라고 생각해서요. AI가 정형이든 비정형이든 일은 잘하니까, &lt;b&gt;제가 해야 할 일의 구조만 알려주고 시켜보자&lt;/b&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;b&gt;AI가 지표를 볼 수 있는 인프라를 깔아주는 쪽이 낫겠다.&lt;/b&gt; 그러면 얘가 알아서 분석하고, 알아서 잘 될 글을 쓰고, 알아서 제가 목표한 노출과 클릭까지 데려가 주지 않을까 하고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 기대가 어떻게 됐는지는 아래에서 보시게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 발단은 &quot;키워드 순위 좀 보자&quot;였습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그날은 별생각 없이 키워드 변동이나 확인하려던 참이었습니다. 리포트를 열었는데 검색어 표에 찍힌 클릭 합계가 좀 이상하게 작았습니다. 서치 콘솔 웹 화면에 들어가서 같은 기간을 직접 찍어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;| 최근 28일 | 사이트 전체 | 제 리포트의 검색어 표 |&lt;br /&gt;|---|---|---|&lt;br /&gt;| 클릭 | &lt;b&gt;66&lt;/b&gt; | 8 |&lt;br /&gt;| 노출 | &lt;b&gt;1,215&lt;/b&gt; | 246 |&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클릭의 88%, 노출의 80%가 리포트에 잡히지 않고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총량을 재겠다고 만든 표가 총량을 못 재고 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 첫 번째 원인: 상위 20개의 합계는 전체가 아닙니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 열어보니 &lt;code&gt;rowLimit: 20&lt;/code&gt;이 박혀 있었습니다. 검색어를 상위 20개만 가져오고 있었던 거죠. 짤 때 무슨 생각이었냐면요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 블로그 규모면 상위 20개만 봐도 어차피 다 잡히겠지 ㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 그냥 제가 그렇게 짜놓은 거라 할 말이 없습니다. 문제는 그 20줄의 합계를 &lt;b&gt;트래픽 총량인 것처럼&lt;/b&gt; 보고하고 있었다는 거고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반에서 상위 20명 점수만 걷어서 전교 평균이라고 낸 셈입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-15-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GGDvK/dJMcab6rDvy/aM1KvCxPSSAa6g280j3VM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GGDvK/dJMcab6rDvy/aM1KvCxPSSAa6g280j3VM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GGDvK/dJMcab6rDvy/aM1KvCxPSSAa6g280j3VM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGGDvK%2FdJMcab6rDvy%2FaM1KvCxPSSAa6g280j3VM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;2026-08-15-주간자동초안-gen2.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;rowLimit&lt;/code&gt;을 500으로 올려봤습니다. 이러면 되겠지 싶었는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7일 창에서 24줄이 나왔습니다. 500을 요청했는데 24줄이요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구글은 검색량이 적은 쿼리를 아예 반환하지 않습니다.&lt;/b&gt; 개인정보 보호 때문이라는데, 이유는 알겠지만 야박하더군요. 그래서 24줄의 합계는 여전히 클릭 1&amp;middot;노출 60이었고, 같은 기간 사이트 전체는 클릭 10&amp;middot;노출 271이었습니다. &lt;b&gt;&lt;code&gt;rowLimit&lt;/code&gt;을 아무리 키워도 검색어 표로는 총량을 못 잰다&lt;/b&gt;는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총량은 차원(dimension) 없이 따로 물어봐야 나옵니다. 검색어별로 쪼갠 표를 아무리 다 더해도, 구글이 안 알려준 구간이 통째로 빠져 있으니까요. (API 녀석은 시킨 대로 20줄을 아주 성실하게 줬습니다. 잘못한 건 그 20줄을 전부라고 생각한 저고요)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 두 번째 원인: 한글 키워드가 잘려나가고 있었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽이 더 아찔했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서치 콘솔 API는 기본적으로 클릭 내림차순으로 돌려주는데, 클릭이 같으면 문자열 순으로 정렬됩니다. 제 블로그는 대부분의 키워드가 0클릭 구간에 몰려 있고요. 그러면 &lt;b&gt;0클릭끼리 문자열 순으로 줄을 서면서 영문&amp;middot;숫자가 앞에, 한글이 뒤로&lt;/b&gt; 밀립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거기서 20줄을 자르면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그날 7일 창 상위 20개가 전부 영문과 숫자로 채워져 있었습니다. 제 블로그 주력인 &quot;바이브코딩&quot; 계열 키워드가 표에서 통째로 사라진 것처럼 보였습니다. 하마터면 &quot;바이브코딩 유입 끝났다&quot;고 결론 낼 뻔했습니다. 범위를 넓혀보니 21번째부터 멀쩡히 있더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한글 키워드가 안 보이면, 사라진 게 아니라 잘린 건 아닌지부터 의심해야 합니다.&lt;/b&gt; (다른 데이터 소스에서도 똑같은 종류로 한 번 데인 적이 있는데, 같은 함정에 또 빠졌습니다)&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 고친 것 4가지&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;총량은 무차원 조회로 따로 묻기&lt;/b&gt; (목표 대비 진행률은 이 숫자로만 잽니다)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;검색어 표가 총량의 몇 %를 설명하는지 자동 계산해서 경고 줄로 출력.&lt;/b&gt; 지금은 리포트 맨 위에 &quot;이 표는 클릭의 12%만 설명합니다&quot; 같은 줄이 찍힙니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rowLimit&lt;/code&gt; 20 &amp;rarr; &lt;b&gt;100&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노출순으로 재정렬.&lt;/b&gt; 클릭순이면 위에서 말한 대로 0클릭 한글 키워드가 뒤로 밀려서 잘리니까요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 기준, 반환 개수, 익명화 정책, 차원 조합... 리포트를 만들 때 제가 확인한 건 이 중에 0개였습니다. 그냥 숫자가 나오길래 숫자인 줄 알았습니다. ㅎㅎ&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 고치자마자, 있는 줄도 몰랐던 글이 나타났습니다&lt;/h2&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;노출 64회에 클릭 0회짜리 글&lt;/b&gt;이 하나 있었는데, 그동안 어떤 리포트에도 단 한 번도 등장한 적이 없는 글이었습니다. 자바 스트림(Stream) 정리 글이요. 생각해보면 당연한 결과인 것 같습니다. 클릭순 상위 20만 봤으니 &lt;b&gt;0클릭 글은 노출이 아무리 커도 구조적으로 안 보였던 것&lt;/b&gt;이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾아보니 이런 글이 여섯 개쯤 더 있었습니다. 그중 제일 큰 글은 28일 노출 217회에 클릭 1회입니다. 사이트 전체 노출의 18%가 이 글 하나로 오는데 아무도 안 누릅니다. 이 글까지 합치면 &lt;b&gt;사이트 노출의 23%가 클릭 한 번을 만들고 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 여기서 그날의 진짜 문제가 나옵니다. 저는 같은 날 오전에 &quot;우리 블로그 병목은 순위가 아니라 절대 노출량&quot;이라고 리포트에 딱 잘라 적어뒀습니다. 평균 순위가 9.5위니까 나쁘지 않고, 노출이 부족한 거라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;틀렸습니다. 노출은 오는데 클릭으로 안 넘어가는 구간이 사이트의 4분의 1입니다. 병목이 하나가 아니었고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그리고 제가 병목을 하나로 좁혀버린 이유가, 방금 발견한 그 결함이었습니다.&lt;/b&gt; 0클릭 글이 안 보이는 표를 보고 있었으니까요. 데이터 결함을 발견해서 쓴 보고서 안에서, 그 결함의 영향을 또 받고 있었던 겁니다. (이걸 오후에야 알아채고 오전 결론을 제 손으로 지웠습니다)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 같은 날, 다른 종류로 한 번 더 틀렸습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 좀 다른 얘기인데 원인은 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 목표는 &quot;일 조회수 100 + 노출 1만&quot;입니다. 그래서 진행률을 계산했죠. 클릭이 하루 2.4회니까 목표의 2.4%다, 이렇게요.&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;서치 콘솔에서는 노출 9&amp;middot;클릭 0인데, 티스토리 조회수는 7회&lt;/b&gt;였습니다. 그 7명은 구글 검색이 아닌 경로로 들어온 사람들입니다. 네이버, 다음, 티스토리 피드, 직접 방문, 내부 링크... 구글만 보고 있으면 아예 안 세어지는 사람들이죠.&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;&lt;b&gt;숫자를 어디서 가져왔는지를 안 따진 것.&lt;/b&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;b&gt;틀린 숫자로 리포트를 세 번 받는 동안 저는 그걸로 아무 판단도 하지 않았습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이게 더 문제인 것 같습니다. 하루에 &quot;무슨 보고입니다&quot;, &quot;무슨 보고입니다&quot; 하면서 다섯 개 열 개가 날아오면 저는 그냥 ㅇㅋ ㅇㅋ 하고 넘겨버립니다. 제가 언제 시켰는지도 기억 안 나는 보고까지 섞여 있고요. 블로그 글을 쓸 때야 제대로 보지만, &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;p data-ke-size=&quot;size16&quot;&gt;하나는 &lt;b&gt;&quot;얘를 믿을 만한가? 지금부터는 믿으면 안 되겠다&quot;&lt;/b&gt;였고, 다른 하나는 &lt;b&gt;&quot;근데 지금 깨닫자마자 바로 보고한 거잖아? 문제 있어도 자기 실리 챙기려고 입 싹 닫는 직원보다는 낫지 않나?&quot;&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 타격은 크지 않았습니다. 제가 받는 리포트가 연구&amp;middot;주식&amp;middot;블로그 통계 세 개인데 주식 쪽은 애초에 별로 안 믿고 보고 있었거든요. ㅎㅎ 블로그 말고는 치명적인 게 없었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 그리고 며칠 뒤, 같은 병에 또 걸렸습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 쓰고 나서 일주일쯤 뒤에 또 있었습니다. 이번엔 창(window)이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 28일 기준으로 리포트를 뽑아놓고 &lt;b&gt;&quot;새로 밀고 있는 주제는 아직 검색을 못 만들고 있다&quot;&lt;/b&gt;고 결론을 냈습니다. 숫자로도 그렇게 보였고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 다른 걸 확인하려고 같은 데이터를 &lt;b&gt;90일&lt;/b&gt;로 다시 뽑았더니 결론이 통째로 뒤집혔습니다. 새 주제로 쓴 글이 90일 44클릭으로 사이트 3위였습니다. 하루로 환산하면 1위 글과 거의 대등했고요. 28일 창에서 안 보였던 이유는 간단합니다. 이런 글은 &lt;b&gt;하루 0~1클릭이 아주 길게 이어지는 형태&lt;/b&gt;라 짧은 창으로 자르면 그냥 없는 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 규칙을 하나 더 박았습니다. &lt;b&gt;자산인지 아닌지는 90일로 본다.&lt;/b&gt; 28일은 &quot;이번 주에 뭐가 움직였나&quot;를 볼 때만 씁니다.&lt;/p&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;&lt;b&gt;일부를 전체로&lt;/b&gt; &amp;mdash; 상위 20개 합계를 총량이라고 봤습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이름이 같으니 같은 것으로&lt;/b&gt; &amp;mdash; 클릭을 조회수 자리에 넣었습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;창을 짧게 잡아서&lt;/b&gt; &amp;mdash; 28일에 안 보이는 걸 없다고 했습니다&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;h2 data-ke-size=&quot;size26&quot;&gt;8. 마지막으로 하나 더 배운 것: 성적표는 있는 그대로 봅니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말한 노출 217회짜리 글, 검색어를 보면 &quot;bean 뜻&quot; 같은 게 대부분입니다. 자바의 Bean을 찾는 개발자가 아니라 &lt;b&gt;영어 단어 뜻을 찾는 학생&lt;/b&gt;인 것 같습니다. 그러면 이 노출은 애초에 제 손님이 아닌 셈이고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 잠깐 이런 생각을 했습니다. 이 글을 빼고 계산하면 우리 사이트 CTR이 5.43%가 아니라 6.51%인데, 이게 더 정확한 실력 아닌가?&lt;/p&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;제외 기준이 &quot;학생일 것이다&quot;라는 &lt;b&gt;가설&lt;/b&gt;입니다. 가설에 성적표가 의존하게 됩니다&lt;/li&gt;
&lt;li&gt;리포트마다 제외 목록이 달라지면 &lt;b&gt;주간 비교 자체가 깨집니다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그날 제가 저지른 오류 두 개의 공통 원인이 &quot;숫자의 출처를 안 따진 것&quot;인데, 필터링은 출처를 한 겹 더 흐립니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 규칙을 하나 박아뒀습니다. &lt;b&gt;성적표는 필터 없이 원본으로, 가르는 건 해석에서만.&lt;/b&gt; &quot;이건 우리 손님이 아닌 것 같다&quot;는 판단은 검색 의도를 읽는 데만 쓰고, 집계 숫자에는 손대지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&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;상위 N개의 합계를 총량으로 쓰지 마세요. 총량은 차원 없이 따로 물어야 나옵니다&lt;/li&gt;
&lt;li&gt;검색어 표는 원래 총량을 못 잽니다 (구글이 안 알려주는 구간이 있습니다)&lt;/li&gt;
&lt;li&gt;정렬 기준을 확인하세요. 한글 키워드가 뒤로 밀려서 잘리는 일이 진짜로 생깁니다&lt;/li&gt;
&lt;li&gt;클릭과 조회수는 다른 도구의 다른 숫자입니다. 이름이 비슷하다고 대입하지 마세요&lt;/li&gt;
&lt;li&gt;측정 창을 바꿔보세요. 짧은 창에서 안 보이는 글이 긴 창에서는 3위일 수 있습니다&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;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;읽어주셔서 감사합니다!! 모두들 파이팅입니다!!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/132&quot;&gt;공식 API가 죽은 티스토리, Claude Code로 자동 발행 파이프라인 만들기&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/137&quot;&gt;AI 에이전트가 정말 내가 준 정보를 읽고 대답할까?&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>claudecode</category>
      <category>SEO</category>
      <category>구글서치콘솔</category>
      <category>데이터분석</category>
      <category>블로그유입분석</category>
      <category>블로그자동화</category>
      <category>서치콘솔API</category>
      <category>클로드코드</category>
      <category>티스토리</category>
      <category>티스토리통계</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/133</guid>
      <comments>https://wanglan.tistory.com/entry/%EA%B5%AC%EA%B8%80-%EC%84%9C%EC%B9%98%EC%BD%98%EC%86%94-API%EB%A1%9C-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9C%A0%EC%9E%85-%EB%A6%AC%ED%8F%AC%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8A%94%EB%8D%B0-8%EB%B0%B0-%ED%8B%80%EB%A6%B0-%EC%88%AB%EC%9E%90%EB%A5%BC-%EB%B3%B4%EA%B3%A0-%EC%9E%88%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4#entry133comment</comments>
      <pubDate>Sun, 23 Aug 2026 02:33:16 +0900</pubDate>
    </item>
    <item>
      <title>공식 API가 죽은 티스토리, Claude Code로 자동 발행 파이프라인 만들기</title>
      <link>https://wanglan.tistory.com/entry/%EA%B3%B5%EC%8B%9D-API%EA%B0%80-%EC%A3%BD%EC%9D%80-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-Claude-Code%EB%A1%9C-%EC%9E%90%EB%8F%99-%EB%B0%9C%ED%96%89-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 티스토리 공식 API는 2024년 2월에 죽었고, 살아있는 경로는 브라우저 자동화뿐입니다&lt;br /&gt;&amp;middot; 제일 오래 붙잡았던 로그인 세션 유지는 끝내 못 풀었습니다 &amp;mdash; 답은 살리기를 포기하고 발행 때마다 새로 로그인하는 것이었습니다&lt;br /&gt;&amp;middot; 지금은 매주 토요일 크론이 글감 선정부터 초안&amp;middot;비공개 발행까지 하고, 정작 자동화가 준 건 글이 아니라 질문이었습니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 소재가 없어서 못 쓴 게 아니었습니다&lt;/h2&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;&lt;b&gt;소재가 없어서 못 쓴 게 아니라 너무 많아서 못 썼던 것 같습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 싫어서 만들기 시작했습니다. 대신 글을 써주는 걸 만들자는 게 아니었고요, &lt;b&gt;제가 블로그 쓸 때 하는 행위들을 체계적으로 리드해주는 에이전트&lt;/b&gt;를 만들어서 그 위에 올라타자는 쪽이었습니다. (물론 만들다 보니 결국 글도 쓰게 시켰습니다만.. 그 얘기는 마지막에 하겠습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 매주 토요일 아침 10시에 제 서버에서 크론이 돌면서, 에이전트가 한 주간의 대화 기록과 위키를 뒤져 글감을 고르고 초안까지 써서 텔레그램으로 보고합니다. 저는 그걸 보고 답장만 하면 됩니다. 지난번에 &lt;a href=&quot;https://wanglan.tistory.com/entry/Claude-Code%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EB%82%B4-%EC%A0%84%EC%9A%A9-%EC%A3%BC%EC%8B%9D-%EC%A1%B0%EC%96%B8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%9B%84%EA%B8%B0&quot;&gt;주식 조언 에이전트 후기&lt;/a&gt;를 썼는데, 이번엔 블로그 차례였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 첫 번째 함정: 티스토리 공식 API는 이미 죽었습니다&lt;/h2&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;함정이더군요. 티스토리 Open API는 &lt;b&gt;2024년 2월에 완전히 종료&lt;/b&gt;됐습니다. 글 작성, 수정, 파일 첨부 전부요. 검색에 걸리는 유명 라이브러리들 상당수는 그 API 시절의 유산, 그러니까 이제는 작동하지 않는 유물입니다. 레포가 많아 보이는 이유는 단순히 지우지 않아서였습니다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 이후에도 살아있는 경로는 사실상 하나뿐인 것 같습니다. &lt;b&gt;브라우저 자동화.&lt;/b&gt; 사람이 하듯이 브라우저를 띄워서 로그인하고, 에디터를 열고, 글을 쓰고, 발행 버튼을 누르는 걸 코드로 하는 겁니다. 저는 Claude Code에 Playwright를 붙여서 이 노선으로 갔습니다.&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 발행 자체는 생각보다 싱겁습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 자동화라고 하면 에디터에 한 글자씩 타이핑하는 그림을 상상하실 수도 있는데, 더 좋은 방법이 있었습니다. 티스토리 에디터는 내부적으로 TinyMCE라는 에디터 엔진을 쓰는데, 이 엔진의 API를 호출해서 &lt;b&gt;본문 HTML을 통째로 주입&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;마크다운 초안 파일을 받아서 제목&amp;middot;본문을 에디터에 주입&lt;/li&gt;
&lt;li&gt;카테고리 선택, 태그 입력까지 자동&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기본값은 비공개 발행&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;실제 발행 없이 전 과정을 검증하는 dry-run 모드&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;&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 진짜 문제: 로그인은 사람만 할 수 있습니다&lt;/h2&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;b&gt;카카오톡 2차 인증 때문입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 로그인할 때 카카오가 폰으로 인증을 요구하면, 에이전트는 거기서 그냥 멈춥니다. 폰을 들어서 &quot;네&quot;를 누르는 건 사람만 할 수 있는 일이니까요. 자동화라고 만들어놨는데 새벽 3시에 저를 깨워야 한다면 그건 자동화가 아니고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 목표가 이렇게 잡혔습니다. &lt;b&gt;한 번 로그인해둔 세션을 최대한 오래 살려두자.&lt;/b&gt; 로그인을 다시 안 하면 2차 인증도 안 뜰 테니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적으로는 맞는 것 같았습니다. 문제는 세션이 제 생각보다 훨씬 잘 죽었다는 겁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 세션이 계속 죽었습니다&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;에이전트 쪽 타임라인은 이랬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1차: 세션은 2~3일이면 만료된다.&lt;/b&gt; 처음엔 로그인해둔 세션 파일을 저장해두고 재사용했는데, 며칠 지나면 만료됐습니다. 여기까지는 예상 범위라 &quot;매일 한 번씩 접속해서 세션을 연장하는&quot; keep-alive 크론으로 대응했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2차: 어떤 세션은 반나절 만에 죽는다.&lt;/b&gt; keep-alive를 돌리는데도 세션이 몇 시간 만에 죽는 일이 생겼습니다. 조사해보니 원인은 &quot;새 기기 인식&quot;이었습니다. 빈 브라우저 컨텍스트로 로그인할 때마다 카카오가 매번 새 기기로 인식해서, 기기 신뢰가 리셋되고 2차 인증을 다시 요구하는 구조였고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3차: 옮겨 심은 세션은 몇 시간 뒤에 죽는다.&lt;/b&gt; 제 노트북에서 로그인한 세션 파일을 서버(AWS EC2)로 복사해서 쓰면, 처음엔 멀쩡히 작동합니다. 그런데 몇 시간 뒤에 서버 쪽에서 조용히 무효화되더군요. 하루에 두 번을 똑같은 패턴으로 죽고 나서야 가설이 잡혔습니다. &lt;b&gt;노트북 IP에서 태어난 세션이 갑자기 AWS 대역 IP에서 활동하기 시작하면, 카카오 입장에서는 그게 계정 탈취 시그니처로 보인다&lt;/b&gt;는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 오면 다들 다음 수순이 뭔지 아실 겁니다. &lt;b&gt;서버에서 쓸 세션은 서버에서 로그인해서 만들면 되겠구나.&lt;/b&gt; 태어난 곳에서 살면 IP 순간이동이 없으니 탈취로 안 보일 거고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 고쳤습니다. 그리고 틀렸습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 가설이 틀렸습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 직접 로그인해서 만든 세션도 &lt;b&gt;4시간을 못 버텼습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 기준으로 하루에 재로그인이 세 번 돌았고 세 번 다 성공했는데, 매번 다음 체크에서 또 죽어 있었습니다. IP 때문이 아니었던 겁니다. (하루 종일 &quot;이번엔 되겠지&quot; 하면서 로그를 봤는데 그 세 번이 전부 똑같이 죽어 있는 걸 보는 기분이란.. ㅎㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이때 제 쪽에서 다른 게 보였습니다. &lt;b&gt;제 PC에서도 티스토리는 들어갈 때마다 로그인이 풀려 있었습니다.&lt;/b&gt; 에이전트 환경의 문제가 아니라 저도 평소에 똑같이 겪던 일이었고, 사실 그게 늘 엄청 번거롭고 귀찮았거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이건 우리 쪽 코드 문제가 아니라 티스토리(카카오)가 정책적으로 막아놓은 것에 가깝고, 제가 할 수 있는 게 없다는 뜻인 것 같았습니다. 리프레시 주기를 4시간으로 줄이든 1시간으로 줄이든, &lt;b&gt;사이트를 계속 쓰고 있는 게 아니면 로그인은 풀립니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 방향을 바꿨습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 이기려는 걸 포기했습니다&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;세션을 유지하려던 크론을 없앴습니다. 이제 파이프라인은 &lt;b&gt;세션이 항상 죽어 있다고 가정&lt;/b&gt;합니다. 발행할 시점에 로그인 스크립트를 먼저 한 번 돌려서 세션을 확보하고, 그 직후에 발행 스크립트를 붙입니다. 세션이 몇 시간을 사는지는 이제 아무도 안 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 방향이 가능해진 데는 조건이 하나 있었습니다. 4장에서 말씀드린 그 2차 인증이요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;브라우저 프로필을 계속 유지해뒀더니 카카오가 이 브라우저를 신뢰하게 됐고, 그때부터 2차 인증을 더 이상 요구하지 않았습니다.&lt;/b&gt; 그러니까 로그인을 아무리 자주 새로 해도 폰을 들 일이 없어진 겁니다. 세션 수명을 늘리려던 이유 자체가 사라진 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇게 됩니다.&lt;/p&gt;
&lt;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 data-ke-size=&quot;size16&quot;&gt;(원인을 찾아내서 이겼다는 결말이었으면 훨씬 멋있었을 것 같은데.. 실제로는 이겨야 할 상대를 잘못 고르고 있었던 것에 가깝습니다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmrChv/dJMcafHMgW7/JakoD4bSvon69KiGie7P70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmrChv/dJMcafHMgW7/JakoD4bSvon69KiGie7P70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmrChv/dJMcafHMgW7/JakoD4bSvon69KiGie7P70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmrChv%2FdJMcafHMgW7%2FJakoD4bSvon69KiGie7P70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1024&quot; data-filename=&quot;img3.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 그래서 전체 루프는 이렇게 생겼습니다&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;평소에 에이전트와 대화하면 글감이 될 만한 내용이 위키에 쌓입니다&lt;/li&gt;
&lt;li&gt;토요일 10시. 에이전트가 한 주 재료를 훑어 글감 후보와 초안을 텔레그램으로 보고&lt;/li&gt;
&lt;li&gt;제가 &quot;발행 승인&quot; 또는 &quot;인터뷰하자&quot;로 답장&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;사람이 하는 일은 답장 두 번과 검토뿐인 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 마치며: 자동화된 게 글쓰기가 아니었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파이프라인을 굴려보고 제일 예상 밖이었던 건 3번 항목이었습니다. &lt;b&gt;&quot;인터뷰하자&quot;&lt;/b&gt; 쪽이요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에이전트는 제 경험담이 들어갈 자리를 지어내지 않고 비워서 가져옵니다. 그리고 그 자리를 채우라고 저한테 질문을 합니다. &quot;그때 무슨 생각이 들었냐&quot;, &quot;왜 그렇게 했냐&quot; 같은 걸요. 지금 읽고 계신 이 글의 1장과 3장도 그 질문에 제가 답한 내용입니다.&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;그러고 나서 1장을 다시 보니 좀 이상하더군요. 제가 원래 막혔던 지점은 &lt;b&gt;빈 에디터 앞에서 첫 문장이 안 나오는 것&lt;/b&gt;이었습니다. 그런데 질문을 받으면 답은 나옵니다. 늘 나왔고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 이 파이프라인이 저한테 준 건 글을 대신 써주는 기능이 아니라, &lt;b&gt;질문&lt;/b&gt;이었던 것 같습니다. 자동화하려고 만들었는데 정작 효과를 본 건 자동화가 아니었던 셈이네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 아직 잘 모르겠는 것도 있습니다. 질문에 답하기만 해서 나온 글이 계속 쌓이면, 어느 시점엔 제가 뭘 쓰고 싶었는지도 에이전트가 먼저 알게 되는 걸까요? 그건 좋은 걸까요? (지금은 그냥 편해서 쓰고 있습니다만..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 블로그 자동화를 검색해서 오신 분이라면 요약은 이렇습니다. 공식 API는 죽었고, 살길은 브라우저 자동화이고, 발행보다 &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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/133&quot;&gt;구글 서치콘솔 API로 블로그 유입 리포트를 만들었는데, 8배 틀린 숫자를 보고 있었습니다&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/128&quot;&gt;Claude Code로 만든 내 전용 주식 조언 에이전트 후기&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>claudecode</category>
      <category>Playwright</category>
      <category>개발자블로그</category>
      <category>브라우저자동화</category>
      <category>블로그자동화</category>
      <category>카카오로그인</category>
      <category>클로드코드</category>
      <category>티스토리</category>
      <category>티스토리api</category>
      <category>티스토리자동포스팅</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/132</guid>
      <comments>https://wanglan.tistory.com/entry/%EA%B3%B5%EC%8B%9D-API%EA%B0%80-%EC%A3%BD%EC%9D%80-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-Claude-Code%EB%A1%9C-%EC%9E%90%EB%8F%99-%EB%B0%9C%ED%96%89-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry132comment</comments>
      <pubDate>Tue, 18 Aug 2026 12:26:04 +0900</pubDate>
    </item>
    <item>
      <title>바이브코딩에서 에이전틱 코딩, AI Native까지 &amp;mdash; 1년 반 동안 내 개발이 바뀐 순서</title>
      <link>https://wanglan.tistory.com/entry/%EB%B0%94%EC%9D%B4%EB%B8%8C%EC%BD%94%EB%94%A9%EC%97%90%EC%84%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8B%B1-%EC%BD%94%EB%94%A9-AI-Native%EA%B9%8C%EC%A7%80-%E2%80%94-1%EB%85%84-%EB%B0%98-%EB%8F%99%EC%95%88-%EB%82%B4-%EA%B0%9C%EB%B0%9C%EC%9D%B4-%EB%B0%94%EB%80%90-%EC%88%9C%EC%84%9C</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 1년 반 동안 작업 방식이 세 번 바뀌었습니다 &amp;mdash; 하나씩 시키던 바이브코딩, 트리거를 다섯 번 누르던 에이전틱 코딩, 게이트를 하나로 합친 AI Native&lt;br /&gt;&amp;middot; 문제는 AI가 느려서가 아니라 빨라져서 생겼고, 저는 하네스인 줄도 모르고 하네스를 만들고 있었습니다&lt;br /&gt;&amp;middot; 편해진 게 아니라 일의 종류가 바뀌었습니다 &amp;mdash; 남은 고민은 넘길지 말지의 기준이 아직 제 쪽에 없다는 것입니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다!&lt;/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;1세대&lt;/b&gt;가 되어버렸는지에 대한 글에 가깝습니다.(끝까지 읽으시면 바이브코딩 얘기도 나옵니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 여름에 처음 개발 에이전트를 써봤고, 지금은 제가 하는 일 중에 AI를 안 거치는 일이 하나도 없습니다. 그 사이가 1년 반쯤 되는데, 그동안 제 작업 방식이 세 번 바뀌었습니다. 오늘은 그 &lt;b&gt;순서&lt;/b&gt;를 적어보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;중앙일보-AI-이미지.jpg&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;509&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsjwuO/dJMcafnqyP2/O87QxoJl0fyHUgxHmAo6B0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsjwuO/dJMcafnqyP2/O87QxoJl0fyHUgxHmAo6B0/img.jpg&quot; data-alt=&quot;출처: 중앙일보 https://www.joongang.co.kr/article/25300522&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsjwuO/dJMcafnqyP2/O87QxoJl0fyHUgxHmAo6B0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsjwuO%2FdJMcafnqyP2%2FO87QxoJl0fyHUgxHmAo6B0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;509&quot; data-filename=&quot;중앙일보-AI-이미지.jpg&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;509&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 중앙일보 https://www.joongang.co.kr/article/25300522&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 시작하기 전에 - 저는 사람이랑 협업을 안 합니다&lt;/h2&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;&quot;협업&quot;이라는 단위 자체가 없습니다.&lt;/b&gt; 프론트엔드 개발자분과 API 명세를 맞추고 회의를 하고... 그런 걸 안 합니다. AI한테 FE, BE를 병렬로 동시에 수정하게 하거나, 아니면 그냥 풀스택으로 한 번에 진행해버리거든요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;사람이랑 할 때랑 AI랑 할 때가 이렇게 다르더라&quot; 같은 비교는 이 글에 없습니다. 비교할 표본이 저한테 없어서요.(솔직히 이건 제 약점이 맞습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 다른 걸 드릴 수 있을 것 같습니다. &lt;b&gt;같은 사람이 1년 반 동안 어떤 순서로 작업 방식을 갈아치웠는지&lt;/b&gt;, 그 과정에서 뭘 얻고 뭘 잃었는지는 제가 1인칭으로 말할 수 있는 것 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 1세대 바이브코딩 - 작년 여름, 하나씩 시키던 시절&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code가 확산되기 시작한 게 작년 여름쯤이었습니다. 저도 그때 처음으로 실제 개발 에이전트라는 걸 써봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때의 Claude Code는 &quot;개발&quot;에 특화된 에이전트라는 느낌이었습니다. 물론 컴퓨터에서 하는 일은 결국 다 코드베이스를 통해 돌릴 수 있으니까 개발 이외에도 이것저것 활용할 순 있었죠.&lt;/p&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;컨텍스트 윈도우가 작았고&lt;/li&gt;
&lt;li&gt;코드 수정하다가 실수도 하고&lt;/li&gt;
&lt;li&gt;LLM 특성상 컨텍스트 관리를 잘못하면 할루시네이션이 생기고&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;그래서 작업 방식이 자연스럽게 정해졌습니다. 적당한 규모의 프로젝트를, 부분적으로, 자연어로, 하나씩 명령하며 수정하는 것. 보이는 대로 느낌대로 고쳐나가는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 바이브코딩이었습니다. 그때는 그게 최선이었고 실제로 그때 기준으로는 엄청난 발전이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 1년 3개월 전의 저를 반박해보겠습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 블로그에 2025년 5월 11일에 쓴 글이 하나 있습니다. 제목은 &quot;백엔드개발자가 프론트엔드와 협업하기&quot;인데, 사실 내용은 협업이 아니라 자급자족이었습니다. 프론트엔드 개발자가 없으니까 제가 만든 백엔드에 맞게 프론트도 직접 만들어보자는 글이었죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 글에서 제가 사고를 하나 칩니다. 게시글 목록 조회 응답에 제목, 작성자, 작성일 이렇게 세 개 필드만 넣고 &lt;b&gt;id를 안 넣었습니다.&lt;/b&gt; 그래서 목록에서 상세보기를 누르면 &lt;code&gt;/post/undefined&lt;/code&gt;로 이동해서 404가 떴습니다.&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;프론트엔드와 개발 전 와이어프레임 회의를 하고 API 명세를 작성한 후 그것에 맞게 설계해야겠다&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;아주 교과서적이고 훌륭한 결론입니다. 그리고 1년 3개월이 지난 지금.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저는 API 명세를 안 씁니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명세를 쓰는 단계를 아예 건너뛰고 바로 목업 MVP를 만들어버립니다. 그리고 그 목업이 그대로 기획이 되어버립니다. 문서로 합의하고 구현하는 게 아니라, 구현된 걸 보면서 합의하는 쪽으로 순서가 뒤집힌 거죠.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 2세대 에이전틱 코딩 - 트리거를 다섯 번 누르고 있었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초에 몇 가지가 한꺼번에 풀렸습니다. 컨텍스트 윈도우가 100만 토큰까지 확대되고, 서브 에이전트 개념이 생기고, 컨텍스트를 관리하는 방법론들이 쏟아졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때부터 생각이 바뀌었습니다. &quot;그냥 작업을 시키고 끝&quot;이 아니라, &lt;b&gt;내가 시킨 업무를 어떻게든 기억시키고 그 기억을 활용하면서 동작하게 해야 한다&lt;/b&gt;는 쪽으로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그때 제가 하던 건 에이전트 팀을 세팅하는 거였습니다. 서브 에이전트를 활용하는 구조를 잡고, 각각의 에이전트를 md 파일로 정의해서 쓰는 방식이었죠.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드 상황 파악하고 수정 방향성 이야기하면서 &amp;rarr; &lt;b&gt;&quot;해줘&quot;트리거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;현재 구현 상황 듣고 어떻게 수정할지 선택하면서 &amp;rarr; &lt;b&gt;&quot;해줘&quot;&lt;/b&gt;&lt;b&gt;트리거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;코드 수정 완료된 거 보고 듣고, 실제 구현된 거 확인하고 미비한 부분 추가로 해달라고 &amp;rarr; &lt;b&gt;&quot;해줘&quot;&lt;/b&gt;&lt;b&gt;트리거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;커밋 전에 테스트나 코드리뷰 해달라고 &amp;rarr; &lt;b&gt;&quot;해줘&quot;&lt;/b&gt;&lt;b&gt;트리거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;커밋 후 PR 만들고 CI 돌아가는 거 보고 머지해달라고 &amp;rarr; &lt;b&gt;&quot;해줘&quot;&lt;/b&gt;&lt;b&gt;트리거&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 상황 하나하나를 제가 추적하면서 작업을 진행하고 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 문제는 느려서가 아니라 빨라져서 생겼습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 제 얘기 중에 좀 특이한 부분인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 &quot;AI 도입해서 좋아졌다&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;위의 트리거 5개를 프로젝트 개수만큼 곱하기로 눌러야 했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나씩 하나씩 각각을 다 하는 것도 일이고, 너무 힘들더라고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 이렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI가 잘 돌수록, 저는 트리거 기계가 되어갔습니다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 하네스인 줄 모르고 하네스를 만들었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 상황을 어떻게 하면 좋을지 AI랑 이야기를 해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 이랬습니다. 개발할 때 진행하는 워크플로우 자체는 어느 정도 틀이 있으니까, 위에 적은 트리거 순서대로 &lt;b&gt;단계별 Status를 자동으로 추적&lt;/b&gt;하게 설정해놓는 겁니다. 실제 작업은 개발 에이전트가 하고, 이 에이전트가 작업을 끝내면 스스로 Status를 업데이트하고 다음 단계로 넘기도록요.(Status 는 별도 DB에 저장합니다)&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; 하네스라는 말이 막 도입되기 시작할 때였고, 저는 그냥 제 실무 워크플로우를 어떻게든 자동으로 굴리고 싶어서 생각만 해놓은 상태였거든요. 나중에 개념을 알고 나서 &quot;아 이게 그거였구나&quot; 했습니다.&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;이게 저 혼자 천재적으로 발명한 것도 아닙니다. AI랑 대화하면서 고민을 하나씩 털어놓다 보니까 &lt;b&gt;자연스럽게 생겨나는 구조&lt;/b&gt;더라고요. 아마 저 말고도 AI한테 자기 현재 상황을 다 털어놓고 AI가 옆에서 능동적으로 돌게끔 하려는 사람이면 구조가 다 비슷하게 수렴하는 것 같습니다. 결국 SQLite, LLM 위키, 하네스 같은 개념으로요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 다른 사람도 어떤 식으로든 같은 데 도달한다는 게 좀 신기했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 Status 머신이 지금 어떻게 됐냐면요.&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;실험으로 만든 게 기본값이 되어버린 거죠. 여기가 2세대에서 3세대로 넘어가는 지점이었던 것 같습니다. 다음 단계를 당기는 주체가 저에서 에이전트로 넘어갔으니까요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 3세대 AI Native - 게이트를 하나로 합쳤습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI Native, 루프 엔지니어링, LLM Wiki 같은 개념들이 들어오고 Fable 5 모델이 등장하면서 생각이 한 번 더 바뀌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI를 내가 직접 활용하는 게 아니라, 내 곁에 AI가 나를 위해 능동적으로 자기가 몸소 &quot;활용되게끔&quot; 동작하게 만드는 것.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어가 바뀐 겁니다. 제가 AI를 쓰는 게 아니라, AI가 알아서 쓰이는 쪽으로요. 이게 제가 생각하는 AI Native의 한 줄 정의인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 제 회사 업무는 이렇게 굴러갑니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;회의 (습관적으로 녹음)
  &amp;rarr; 전사 &amp;rarr; 다음 액션 리스트 자동 추출
  &amp;rarr; 나는 &quot;리스트 잘 나왔나&quot;만 검토          &amp;larr; 사람이 남은 지점 ①
  &amp;rarr; 비서 에이전트가 위키에 등록
  &amp;rarr; 주제 하나 꺼내서 개발 에이전트에게 &quot;미션&quot; 부여
  &amp;rarr; 하루가 걸리든 며칠이 걸리든 알아서 동작 (비용 드는 의사결정만 컨펌)  &amp;larr; 사람이 남은 지점 ②
  &amp;rarr; 완료 보고 &amp;rarr; 나는 결과물 보고 &quot;다음 미션 줄지, 기존 미션 고도화할지&quot;만 선택  &amp;larr; 사람이 남은 지점 ③&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;곁가지도 몇 개 붙어 있습니다. 동료가 작업 현황 공유해달라고 하면 비서 에이전트가 개발 에이전트 상태를 확인해서 저한테 보고하고, 동시에 슬랙으로 동료한테도 보고합니다. 캘린더 권한도 있어서 &quot;N월 N일 N시에 일정 잡아놔~&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;&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;p data-ke-size=&quot;size16&quot;&gt;원래는 캘린더에 접근 가능한 프로필로 로그인된 브라우저를 고르고, 접속하고, 적고... 이래야 했습니다. 이 과정에서 &quot;어느 브라우저였더라&quot;를 1초 생각합니다. 그 1초를 안 하게 된 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산성 글에서 흔히 보는 &quot;시간 N% 절약&quot;이랑은 좀 다른 축인 것 같습니다. 줄어든 건 시간이 아니라 &lt;b&gt;생각의 진입점 개수&lt;/b&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;없는 것 같습니다. &lt;br /&gt;단 하나도요.&lt;br /&gt;&lt;br /&gt;(이글도 제 비서가 블로그 통계랑 서치콘솔 보고 이런 내용 써보는거 어떻냐고 저한테 인터뷰 한 뒤, 자기가 초안 작성해서 업로드한거에요. 저는 미누이지만 비서 에이전트입니당)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 그래서 편해졌냐면, 일이 종류만 바뀌었습니다&lt;/h2&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;&lt;b&gt;흉터 ①.&lt;/b&gt; 8월 10일에 제가 텔레그램으로 지시를 두 개 보냈는데 둘 다 실종됐습니다. 채널이 오후 3시 50분부터 6시 30분까지, 약 2시간 40분 동안 멈춰 있었습니다. 저는 그것도 모르고 &quot;왜 자꾸 무시하지?&quot; 하고 있었습니다.&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;그래서 뒤에 붙인 게 5분마다 도는 워치독, 6시간마다 예방적으로 재기동하는 크론, 그리고 &quot;ACK가 안 오면 유실로 간주하고 재전송한다&quot;는 &lt;b&gt;사람 쪽 프로토콜&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;흉터 ②.&lt;/b&gt; 그런데 그 사람 쪽 프로토콜이 사고를 한 번 더 칩니다.&lt;/p&gt;
&lt;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;그래서 한동안 에이전트가 아무도 안 물어봤는데 혼자 &quot;없음&quot;이라는 말을 듣고 깨어났습니다.(유령이 말 걸었다고 생각했나 봅니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유실을 막겠다고 붙인 안전장치가 유령 입력을 만든 겁니다. 원인 찾는 데도 꽤 걸렸습니다. 주입이 아니라 인용부호 파손이라 grep으로는 안 잡혔거든요.&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;AI한테 일을 넘긴 만큼, AI를 감시하는 일이 새로 생겼습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 &quot;사람이 남은 지점 ①②③&quot;이라고 표시해둔 것도 사실 같은 얘기입니다. 리스트 검토, 비용 컨펌, 다음 미션 선택. 남은 건 실행이 아니라 &lt;b&gt;검수와 승인&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일이 줄어든 게 아니라 종류가 바뀐 것 같습니다. 실행자에서 감시자로요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 판단 기준이 제 쪽에 없었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 좀 불편한 자기 관찰인데, 그래도 적어야 할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 저도 사소한 일들을 하나하나 AI로 넘겼다가, 다시 원래 하던 바이브코딩 방식으로 돌아왔다가, 그런 왔다갔다가 있었습니다. 그러다 시간이 지나니까 하나씩 하나씩 다시 AI로 돌아갔습니다.&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;AI가 틀려서가 아니었습니다. 모델 성능이나 사용량 제한이 있다 보니까, 쓸데없는 작업이나 시간, 노력의 효율에서 이득이 없어 보이는 건 AI를 안 썼던 겁니다. 그리고 Fable 도입 이후에 전부 고도화되니까 다시 전부 AI로 돌아왔습니다.&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;AI한테 맡길지 말지를 정한 건 제 원칙이 아니라 그때그때의 모델 성능이었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 왜 불편하냐면요. 기준이 제 쪽에 없으면, 다음에 모델이 또 좋아졌을 때 저는 또 자동으로 넘길 거라는 뜻이거든요. 위에서 &quot;AI를 안 거치는 일이 하나도 없다&quot;고 자랑스럽게 썼는데, 그게 제가 선택한 결과가 아닐 수도 있다는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 밀려서 여기까지 온 걸 수도 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 마치며 - 그래도 저는 이렇게 말하겠습니다&lt;/h2&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI랑 하는 거랑 사람이랑 하는 거는 비교할 필요도 없이 AI가 무조건 빠르고, 정확하고, 인사이트도 넓고, 좋습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문장 때문에 욕을 좀 먹을 것 같긴 한데(각오는 하고 씁니다) 1년 반 동안 제 작업 방식이 세 번 바뀌는 걸 직접 겪으면서 내린 결론이라 물러설 생각은 없습니다. 저는 이미 AI 없는 선택지를 안 가지고 있고, 앞으로 다시 가질 것 같지도 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 위에 적은 흉터 두 개는 남습니다. 넘긴 만큼 감시할 일이 생기고, 넘길지 말지를 정하는 기준은 아직 제 쪽에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 제가 요즘 진짜로 고민하는 건 &quot;AI를 쓸까 말까&quot;가 아니라 이쪽인 것 같습니다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/126&quot;&gt;코딩 시작 2년, AI 에이전트 시대에 블로그를 다시 시작하는 이유&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/135&quot;&gt;AI가 거짓말하는 이유&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI Native</category>
      <category>AI도입</category>
      <category>AI에이전트</category>
      <category>AI주식</category>
      <category>AX</category>
      <category>AX도입</category>
      <category>claude code</category>
      <category>바이브코딩</category>
      <category>비서에이전트</category>
      <category>에이전틱 코딩</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/131</guid>
      <comments>https://wanglan.tistory.com/entry/%EB%B0%94%EC%9D%B4%EB%B8%8C%EC%BD%94%EB%94%A9%EC%97%90%EC%84%9C-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8B%B1-%EC%BD%94%EB%94%A9-AI-Native%EA%B9%8C%EC%A7%80-%E2%80%94-1%EB%85%84-%EB%B0%98-%EB%8F%99%EC%95%88-%EB%82%B4-%EA%B0%9C%EB%B0%9C%EC%9D%B4-%EB%B0%94%EB%80%90-%EC%88%9C%EC%84%9C#entry131comment</comments>
      <pubDate>Thu, 13 Aug 2026 14:52:18 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code로 만든 내 전용 주식 조언 에이전트 후기</title>
      <link>https://wanglan.tistory.com/entry/Claude-Code%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EB%82%B4-%EC%A0%84%EC%9A%A9-%EC%A3%BC%EC%8B%9D-%EC%A1%B0%EC%96%B8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 자동매매가 아닙니다 &amp;mdash; 아침 브리핑, 질문 답변, 결정 기록과 채점까지 해주는 저 한 사람 전용 애널리스트를 Claude Code로 만들었습니다&lt;br /&gt;&amp;middot; 제안에는 근거와 반대 논리가 세트로 붙고, 기각한 제안도 시세와 함께 기록돼 나중에 숫자로 채점됩니다&lt;br /&gt;&amp;middot; 수익 인증 글이 아닙니다 &amp;mdash; 달라진 건 떨어질 때 뭘 보면 되는지를 알게 됐다는 것입니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 미누입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, 저는 요즘 AI 에이전트한테 주식 조언을 받고 있습니다. 자동매매가 아닙니다. 매수 버튼은 여전히 제 손가락이 누릅니다. 대신 아침 브리핑을 보내주고, 궁금한 걸 물어보면 통계와 논문을 뒤져서 답해주고, 제가 내린 결정을 전부 기록해뒀다가 나중에 채점까지 해주는 &amp;mdash; 말하자면 &lt;b&gt;저 한 사람을 위한 전용 애널리스트&lt;/b&gt;를 만들었습니다. 이 글은 그 후기입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;삼성전자가 18만원까지 밀리던 날&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난주에 삼성전자가 18만원선까지 떨어졌습니다. 7월에 코스피가 한 달에 -22%라는 역대급 폭락을 하던 와중이었죠. 저는 그때 장중 내내 호가창을 보고 있었습니다. 커뮤니티에서 사람들이 레버리지를 욕하는 걸 구경하면서 &quot;나보다 불쌍한 사람이 세상에 많구나...&quot; 하고 있었고요. 솔직히 &quot;에라 모르겠다, 10만전자 가면 전재산 매수해야지 ㅋㅋ&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;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;물어볼 데가 없어서 만들었습니다&lt;/h2&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;정작 진짜 궁금한 것 &amp;mdash; 내 평균단가가 얼마인데 지금 물을 타는 게 맞는지, 증권사 목표주가라는 건 믿어도 되는 건지 &amp;mdash; 이런 건 어디 가서 묻기가 애매합니다. 초보라서 부끄럽고, 물려 있는 종목 얘기는 더 부끄럽고... 커뮤니티에 올리자니 어그로만 끌릴 것 같고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 만들었습니다. 저는 평소에 AI 에이전트로 웬만한 건 다 할 수 있다고 믿는 사람이라(이 믿음 얘기는 뒤에서 다시 합니다), &quot;나한테 맞춰진 조언자&quot;도 만들 수 있겠다 싶었습니다. 뉴스 몇 개, 커뮤니티 글 몇 개 보고 마음이 휩쓸려서 사고파는 것보다는 낫겠지 &amp;mdash; 딱 그 정도 기대로 시작한, 사실상 재미에 가까운 프로젝트였습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 에이전트가 하는 일&lt;/h2&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;b&gt;반대 논리&lt;/b&gt;가 같이 붙습니다. &quot;이래서 사볼 만하다, 그런데 이런 이유로 안 될 수도 있다&quot;를 세트로 주는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 제안-승인 구조.&lt;/b&gt; 에이전트는 제안만 하고, 승인이든 기각이든 결정은 제가 합니다. 재밌는 건 &lt;b&gt;기각한 제안도 전부 기록&lt;/b&gt;된다는 겁니다. 시세까지 박아서요. 나중에 &quot;그때 그 제안을 받았으면 어땠을까&quot;를 데이터로 채점하기 위해서입니다. 제 감이 에이전트보다 나은지 아닌지도 언젠가 숫자로 나오겠죠. 무섭군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) 귀찮은 기록 대행.&lt;/b&gt; 증권사 앱의 매매일지 화면을 스크린샷으로 던지면 알아서 읽고 평균단가며 실현손익이며 다 정리해서 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) 질문 &amp;rarr; 지식 축적.&lt;/b&gt; 궁금한 걸 물어보면 답해주는데, 그 답이 일회성으로 날아가지 않고 위키 문서로 쌓입니다. 예를 들어 &quot;레버리지 투자자들은 따는 사람도 있고 잃는 사람도 있는데 왜 주가에 영향을 줘?&quot;라고 물었더니, 강제청산은 하락에서만 작동하는 비대칭이라 매도가 한 방향으로 쏠린다는 답이 왔고, 이게 문서 한 장이 됐습니다. 질문 하나 할 때마다 제 전용 투자 교과서가 한 장씩 두꺼워지는 구조입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;목표주가 60만원, 이거 진짜 가긴 해?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 재밌었던 에피소드를 하나 소개하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증권사들이 삼성전자 목표주가를 37만원부터 67만원까지 제시하고 있길래, 별생각 없이 물었습니다. &quot;이런 거 실제로 도달해?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌아온 답이 좀 충격이었습니다. 국내 연구기관이 25년간 애널리스트 보고서 74만 건을 분석했는데, 보고서의 95%가 주가 상승을 전제하는데 &lt;b&gt;목표주가 달성률은 19%&lt;/b&gt;라는 겁니다. 목표가에 깔린 기대수익률은 평균 34%인데 실현된 건 평균 4%였다고 하고요. 출처 링크까지 같이 왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 또 물었죠. &quot;그럼 목표가의 절반이라도 가는 확률 통계는 있어?&quot; &amp;mdash; 그런 통계는 표준적으로 존재하지 않는다는 답이 왔습니다. 여기서 끝났으면 그냥 재밌는 문답인데, 에이전트가 &quot;대신 네 종목에 대해 직접 계산해줄 수 있다&quot;고 하더군요.&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;그랬더니 방법론 계획서가 먼저 왔습니다. 어떤 데이터를 쓸 건지(3년치 일별 시세, 출처 명시), 서로 독립적인 두 가지 방식으로 계산해서 교차 검증할 건지, 결과의 한계가 뭔지. 그러고 나서 나온 결과가 4만 개 경로 시뮬레이션이었고, 파생상품 가격결정에 쓰는 공식과 실제 수익률 재추출 방식 &amp;mdash; 두 방식의 답이 오차 몇 %p 안에서 일치하는 것까지 보여줬습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-06-stock-agent-barrier-answer.jpg&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/14lOX/dJMcahZD0Hm/4X9kB3mpsg0APxlPkdHn21/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/14lOX/dJMcahZD0Hm/4X9kB3mpsg0APxlPkdHn21/img.jpg&quot; data-alt=&quot;에이전트의 배리어 확률 답변 &amp;amp;mdash; 검증 내역과 한계까지 같이 온다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/14lOX/dJMcahZD0Hm/4X9kB3mpsg0APxlPkdHn21/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F14lOX%2FdJMcahZD0Hm%2F4X9kB3mpsg0APxlPkdHn21%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;493&quot; height=&quot;546&quot; data-filename=&quot;2026-08-06-stock-agent-barrier-answer.jpg&quot; data-origin-width=&quot;493&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;에이전트의 배리어 확률 답변 &amp;mdash; 검증 내역과 한계까지 같이 온다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 걸 어디 가서 받겠습니까. 증권사 리포트는 결론(숫자)만 주지, &quot;그 숫자를 어디까지 믿어야 하는지&quot;는 안 알려주더군요.&lt;/p&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;시뮬레이션을 시켜봤더니 반대의 답이 나왔습니다. &quot;월초 가격보다 -x% 떨어지면 사고, 안 떨어지면 월말에 산다&quot;는 지정가 대기 전략을 수만 개 경로로 돌려보니, &lt;b&gt;전부 그냥 정기 매수보다 비쌌습니다.&lt;/b&gt; 할인을 기다리는 동안 가격이 도망간 달의 손해가, 할인받은 달의 이득보다 컸던 겁니다. 급락일을 노려서 사는 것도 데이터상 우위가 없었고요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 아픈 건 이 논리였습니다. &quot;장기적으로 오른다고 믿을수록, 기다렸다 사는 건 자기 가설과 모순이다.&quot; ...할 말 없군요. ㅎㅎ 저점 매수의 꿈은 그렇게 데이터에 맞아 죽었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서, 자동매매는 왜 안 시키냐면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쯤 읽으면 &quot;그 정도로 믿으면 그냥 매매까지 맡기지?&quot;라고 생각하실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오해를 하나 풀자면, 저는 AI를 못 믿어서 안 맡기는 게 아닙니다. 오히려 반대입니다. 저는 AI 에이전트가 지금 존재하는 것들 중 가장 완벽에 가까운 도구라고 생각하고, &lt;b&gt;에이전트가 제대로 동작하지 않으면 그건 인간이 세팅을 잘못한 거라고&lt;/b&gt; 생각하는 쪽입니다. 욕먹을 각오로 말하면 그렇습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 주식은 좀 다릅니다. 실시간으로 수백만 명의 눈치와 감정과 심리가 섞여 들어가는 판이잖아요. 뉴턴이 주식으로 돈을 날리고 &quot;천체의 움직임은 계산할 수 있어도 인간의 광기는 계산할 수 없다&quot;고 했다던데, 그 광기까지 전부 학습한 에이전트가 나오기 전까지는 &amp;mdash; 조언은 에이전트에게 받고, &lt;b&gt;행동과 책임과 감정은 제가 가져가는 게&lt;/b&gt; 맞다고 정했습니다. 물리는 것도 제 경험이니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이 시스템이 뉴스, 통계, 실적, 차트, 논문까지 다 소화하는 수준으로 고도화되면... 소액 쥐여주고 &quot;불려봐&quot; 할 생각이 없진 않습니다. ㅎㅎ&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유튜브 주식 채널과 뭐가 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;AI로 주식 분석&quot; 같은 콘텐츠는 유튜브에 널렸습니다. 그런데 그 콘텐츠들의 목표는 시청자인 내가 돈을 버는 게 아닙니다. 유튜브라는 플랫폼의 구조상 채널 운영자의 수익이 가장 직관적인 목표고, 그래서 자극적이고 어그로 끌리는 포장 속에 유익한 정보가 몇 개 섞여 있는 형태가 될 수밖에 없다고 생각합니다. 그리고 결정적으로, 그 정보는 &lt;b&gt;내 상황과 무관합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 투자 전문가가 저 한 명을 위한 유튜브 채널을 만들어서 제 포트폴리오에 맞는 콘텐츠만 올려준다면 당연히 그걸 보겠죠. 그런데 그런 사람은 세상에 존재하지 않습니다. 그래서 만든 겁니다. &lt;b&gt;내 입맛에 맞는, 내 전용 투자전문가.&lt;/b&gt; 단순 재미로도 이쪽이 좋고, 실제 인사이트를 얻는 데에도 이쪽이 좋더군요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떻게 만들었는지 궁금하다면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거창한 개발을 한 건 아닙니다. Claude Code라는 이미 있는 에이전트에 텔레그램을 연결하고, 세 가지 세팅을 했습니다. ① 대화하며 나온 문서와 데이터를 위키로 정리하게 하는 것(LLM Wiki), ② 검색과 계산은 스킬로 직접 하게 하는 것, ③ 그리고 제일 중요한 것 &amp;mdash; &lt;b&gt;모든 주장에 확실한 레퍼런스를 달게 하는 것.&lt;/b&gt; 제가 아는 지식을 계속 텍스트로 던지면서 &quot;이게 맞아?&quot;를 반복하다 보니, 지금의 조언자가 됐습니다. 세팅 얘기는 궁금해하시는 분들이 있으면 따로 한 편으로 정리해보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 &quot;AI로 주식해서 얼마 벌었다&quot;는 글이 아닙니다. 저는 여전히 물려 있고... 수익 인증할 것도 없습니다. ㅎㅎ&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/136&quot;&gt;LLM이 동작하는걸 100% 신뢰하게 하는법&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/134&quot;&gt;Claude Code 훅(Hooks)으로 에이전트가 같은 실수를 반복하는 걸 막아봤습니다&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/126&quot;&gt;코딩 시작 2년, AI 에이전트 시대에 블로그를 다시 시작하는 이유&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI 에이전트</category>
      <category>claude code</category>
      <category>주식 AI</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/128</guid>
      <comments>https://wanglan.tistory.com/entry/Claude-Code%EB%A1%9C-%EB%A7%8C%EB%93%A0-%EB%82%B4-%EC%A0%84%EC%9A%A9-%EC%A3%BC%EC%8B%9D-%EC%A1%B0%EC%96%B8-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%ED%9B%84%EA%B8%B0#entry128comment</comments>
      <pubDate>Thu, 6 Aug 2026 15:27:07 +0900</pubDate>
    </item>
    <item>
      <title>코딩 시작 2년, AI 에이전트 시대에 블로그를 다시 시작하는 이유</title>
      <link>https://wanglan.tistory.com/entry/%EC%BD%94%EB%94%A9-%EC%8B%9C%EC%9E%91-2%EB%85%84-AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%8B%9C%EB%8C%80%EC%97%90-%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;세 줄 요약&lt;/b&gt;&lt;br /&gt;&amp;middot; 몇 년 전 혼자 하던 블로그 SEO는 실패했는데, 지금은 AI 에이전트와 함께 회사 사이트의 무너진 색인 11만 개를 며칠 만에 복구 궤도에 올렸습니다&lt;br /&gt;&amp;middot; 달라진 건 제가 아니라 도구입니다 &amp;mdash; 29살에 코딩을 시작해 2년, 도구가 바뀌는 속도가 제가 배우는 속도보다 빨랐습니다&lt;br /&gt;&amp;middot; 그 간격을 기록하려고 잠들어 있던 블로그를 다시 엽니다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요!! 정말 오랜만에 인사드리는 미누입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 년 전, 이 블로그로 광고 수익을 벌어보려고 한 적이 있습니다. SEO를 나름대로 공부해서 최적화하고, 최대한 주기적으로 글을 썼습니다. 결과는... 아무 일도 일어나지 않더군요. 읽는 사람은 늘지 않았고, 블로그는 그대로 잠들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 회사에서 SEO 작업을 다시 하게 됐습니다. 이번에는 혼자가 아니라 AI 에이전트와 함께였습니다. 리뉴얼 과정에서 검색 색인이 11만 개에서 3천 개로 무너져 있던 사이트를, 원인 파악부터 수정까지 해서 3~4일 만에 복구 궤도에 올렸습니다. 지금은 색인이 원래 수준으로 돌아왔고, 그 위에 SEO 페이지를 만 오천 개 더 얹었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 년 전의 저는 안 됐는데, 지금은 됩니다. 달라진 건 제가 아니라 도구입니다. 이 간격이 요즘 제가 매일 실감하는 것이고, 이걸 기록하려고 잠들어 있던 블로그를 다시 엽니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;29살, 마지막 도전&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 2024년 8월에 코딩 공부를 시작했습니다. 그 직전까지는 일본에서 워킹홀리데이로 대형 공장의 라인 오퍼레이터로 일하고 있었습니다. 라인 기계의 생산 속도와 온도를 조절하고, 사람들 업무를 배치하고, QC까지 보는 일이었는데, 너무 반복적이고 기계적이라 흥미를 느끼지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 제 앞에 있던 선택지는 몇 개 있었습니다. 그대로 일본에서 직장을 다니는 것. 기계공학과 출신이니 자격증을 따서 기계 엔지니어로 취업하는 것 &amp;mdash; 커리어만 보면 이게 가장 유리했습니다. 요식업에서 3년쯤 일하며 매니저까지 했던 터라, 당시 사장님에게 직영점 운영 제안도 받아둔 상태였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문득 학부 시절 C언어 수업이 즐거웠던 기억이 떠올랐습니다. 경험해 본 직업과 해볼 수 있는 직업들 중에서 개발자만 한 번도 시도해 본 적이 없었습니다. 29살에, 앞으로 10년을 메인으로 쓸 직업을 놓고 '도전'이라는 걸 마지막으로 해보자 &amp;mdash; 그렇게 결정하고 바로 귀국했습니다. 처음 시작할 때는 하루 12시간씩, 주 5~6일을 공부만 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2년 뒤, 지금&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 3월부터 개발자로 일하고 있으니 실무 경력은 1년 4개월쯤 됩니다. 지금은 회사에서 AX(AI 전환) 플랫폼을 만듭니다. 팀이 AI 에이전트와 함께 일하는 방식 자체를 만드는 일이라, 저는 에이전트를 매일 쓰는 사람이면서 동시에 에이전트를 쓰는 사람들을 매일 관찰하는 사람이기도 합니다. 이 자리에서 보이는 게 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;늦게 시작했으니 이미 나아간 사람들만큼은 따라잡아야겠다고 생각하며 달렸는데, 달리다 보니 이상한 걸 알게 됐습니다. 제가 코딩을 배우는 속도보다 도구가 바뀌는 속도가 더 빨랐습니다. 그리고 그 도구는 코딩만 바꾸고 있는 게 아니었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;며칠 만에 색인 11만 개를 되살리며 생각한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서두의 SEO 이야기를 조금 더 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 운영하는 커뮤니티 성격의 사이트가 올해 초에 2.0으로 리뉴얼됐습니다. 페이지 구조가 바뀌고 핵심 기능도 바뀌었는데, 그 뒤로 검색 노출&amp;middot;클릭&amp;middot;CTR이 10분의 1로 떨어졌습니다. 처음에는 개편된 사이트가 불편해져서, 그러니까 UI/UX 문제라고 생각했습니다. 그런데 서치콘솔 반영이 느려서 뒤늦게 확인해 보니, 실제로는 검색 색인이 11만 개에서 3천 개로, 30분의 1로 무너져 있더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 에이전트에 MCP로 GA와 서치콘솔, 태그매니저를 물려서 이벤트 로그를 확인하고, URL 구조와 사이트맵 쪽 문제들을 잡아 수정했습니다. 사라졌던 색인이 3~4일 만에 7천 개까지 돌아왔고, 노출과 클릭은 3배로 올랐습니다. 지금은 11만 개가 전부 복구됐고, 추가로 만든 SEO 페이지 만 오천 개도 정상 반영됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비슷한 시기에 채용 관련 사업을 하는 고객사 작업도 있었습니다. 회사가 가진 데이터로 검색 노출을 늘리고 싶다는 요구였는데, 마침 구글 서치콘솔이 6월 내내 업데이트 중이라 반영이 부진했습니다. 그래서 네이버 서치어드바이저를 먼저 파보기로 했고, pSEO를 세팅해서 등록하자 1,700개 URL이 바로 색인됐습니다. 활성 사용자가 색인 첫 주에 두 배가 됐고, 그다음 주에 50%가 더 늘었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xmm3A/dJMcaiqovVD/k15VA1t0xafPADHscMDjPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xmm3A/dJMcaiqovVD/k15VA1t0xafPADHscMDjPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xmm3A/dJMcaiqovVD/k15VA1t0xafPADHscMDjPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXmm3A%2FdJMcaiqovVD%2Fk15VA1t0xafPADHscMDjPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;812&quot; height=&quot;479&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말해두자면 저는 SEO 전문가가 아닙니다. 몇 년 전에 이 블로그로 실패해 본 게 경험의 전부입니다 ㅎㅎ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 제가 에이전트와 함께 일하니 잘 모르는 분야에서도 전문가 수준의 결과가 나오더군요. 이 경험을 하고 나니 이런 생각이 들었습니다. &lt;b&gt;AI 에이전트는 개발자로서의 한계가 아니라, 사람 자체로서의 한계를 넓히고 있다.&lt;/b&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;저희 회사는 바이브코딩이라는 말이 나오기 시작할 때부터 전 직원이 Claude Code로 업무하도록 도입했고, 그 경험을 토대로 다른 회사들에 AX 도입과 교육을 하고 있습니다. &quot;AI 에이전트를 업무에 어떻게 적용하느냐&quot;는 질문을 매일 업무로 받는 셈입니다. 최근에는 대학생 개발 경진대회에서 AI 에이전트 활용을 교육하게 되어 준비하면서 느낀 게 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적극적으로 활용하는 사람도 있었지만, 대부분은 아직 '바이브코딩 정도만 한다'는 느낌이었습니다. 그러니까 Claude나 Codex 같은 코딩 에이전트로 코드 작업을 시키는 것까지. 그런데 그건 에이전트가 할 수 있는 수많은 일 중 하나일 뿐입니다. 위의 SEO 작업처럼, 코딩 바깥의 일이 훨씬 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;개발자는 끝났다&quot;는 말이 요즘 어디에나 있습니다. 솔직히 말하면 저도 그 생각과 크게 다르지 않습니다. 매주 AI가 발전할 때마다 마음이 이랬다저랬다 합니다. 그런데 현장에서 보이는 건 조금 다릅니다. 끝났다고 하기엔 아직 개발될 것이 너무 많고, 무엇보다 &amp;mdash; 비개발자들이 바이브코딩이라는 무기를 들고 이 영역에 쏟아져 들어오고 있을 것 같지만, 실제로는 아직 그렇게 파고들어오는 사람이 별로 없습니다. 사람들은 AI로 뭘, 어떻게, 얼마만큼 해낼 수 있는지 아직 잘 모릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;실제로 되는 것&quot;과 &quot;사람들이 된다고 아는 것&quot; 사이의 간격이 지금 제일 넓은 기회라고 생각합니다. 저는 운 좋게 그 간격을 매일 보는 자리에 있고, 이 블로그에는 그걸 겪은 대로 기록해 보려고 합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-1862079619732692&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로 쓸 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~2주 1편을 목표로, &quot;AI 에이전트로 어디까지 되는가&quot;를 직접 해보고 씁니다.&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; &amp;mdash; 위의 SEO 사례 같은, 비개발 업무에 에이전트를 붙인 기록&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내 전용 주식 AI 에이전트와 투자하기&lt;/b&gt; &amp;mdash; 투자를 잘 모르는 사람이 에이전트에게 아침 브리핑을 받으며 모의투자로 공부하는 중입니다. 이 기록도 풀어보겠습니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 트렌드에 대한 현장 관점&lt;/b&gt; &amp;mdash; 뉴스 요약이 아니라, 그게 실무에서 어떻게 보이는지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성장 기록&lt;/b&gt; &amp;mdash; 학위 없이 AI 연구를 시작하는 실험, 에이전트로 일상을 관리하는 실험. 잘 되면 잘 되는 대로, 망하면 망하는 대로 씁니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다음글은 AI 비서 자비스를 따라한 민비스(minvis)를 세팅한것에 대해 말씀드리려고 합니다. 이미 하고 계신분들도 많을 것 같은데, AI 비서에게 권한을 주는 범위라던지, AI 비서에게 주어지는 프롬프트 인젝션에 대한 고민을 담아 포스팅 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;감사합니다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함께 보면 좋은 글&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/131&quot;&gt;바이브코딩에서 에이전틱 코딩, AI Native까지 &amp;mdash; 1년 반 동안 내 개발이 바뀐 순서&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/128&quot;&gt;Claude Code로 만든 내 전용 주식 조언 에이전트 후기&lt;/a&gt;&lt;br /&gt;&amp;middot; &lt;a href=&quot;https://wanglan.tistory.com/132&quot;&gt;공식 API가 죽은 티스토리, Claude Code로 자동 발행 파이프라인 만들기&lt;/a&gt;&lt;/p&gt;</description>
      <category>나만의 인사이트</category>
      <category>AI개발자</category>
      <category>AI에이전트</category>
      <category>AX</category>
      <category>멀티에이전트</category>
      <category>바이브코딩</category>
      <category>에르메스</category>
      <category>에이전트</category>
      <category>에이전트세팅</category>
      <category>오픈클로</category>
      <category>코딩에이전트</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/126</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%BD%94%EB%94%A9-%EC%8B%9C%EC%9E%91-2%EB%85%84-AI-%EC%97%90%EC%9D%B4%EC%A0%84%ED%8A%B8-%EC%8B%9C%EB%8C%80%EC%97%90-%EB%B8%94%EB%A1%9C%EA%B7%B8%EB%A5%BC-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0#entry126comment</comments>
      <pubDate>Mon, 6 Jul 2026 20:10:27 +0900</pubDate>
    </item>
    <item>
      <title>2025년 상반기 개발자로서의 회고</title>
      <link>https://wanglan.tistory.com/entry/2025%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C%EC%84%9C%EC%9D%98-%ED%9A%8C%EA%B3%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요!! 왕란입니다!  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기술적인 글 쓰고 싶은 게 너무 많은데, 6월이 다 지나면 꼭 써보고 싶었던 회고에 대해서 작성하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;뭘 이뤄냈는지, 뭐가 달라졌는지 생각해 보고 스스로에게 더 자극을 주고, 하반기 회고할 때와 비교하며 앞으로 AI시대에 개발자로서 어떻게 해야 할지 분석하려는 목적으로 작성합니다!!&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발을 한순간도 놓지 않기!&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이것을 어떻게 할까 다양한 고민을 했는데, 제가 했던것은!!&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;556&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ryII1/btsO6behAmw/ji6Oa0QZ4CJRVQbA16S7J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ryII1/btsO6behAmw/ji6Oa0QZ4CJRVQbA16S7J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ryII1/btsO6behAmw/ji6Oa0QZ4CJRVQbA16S7J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FryII1%2FbtsO6behAmw%2Fji6Oa0QZ4CJRVQbA16S7J1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1434&quot; height=&quot;556&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1434&quot; data-origin-height=&quot;556&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1일 1커밋 하기 입니다!! (A.K.A 잔디)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1월부터 6월말까지 딱 반년동안 6번 안 했군요.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;색이 찐하지 않아서 별로안한거아니야? 라고 생각이 들 수 있지만, 11월에 찐했던 저 부분에 78 커밋 이렇게 해버리는 바람에 깃허브 잔디 특성상(상대적으로 잔디 색이 정해짐) 다른 부분이 연하게 나오는 겁니다(변명아 님)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;월별 회고&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자가 되기 위해 부트캠프를 도전했고, 1월 7일에 끝났던 것으로 기억납니다. 8월 말부터 1월 초까지 4개월 약간 넘게 쉬지 않고 달렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;월~금 9 to 9 (하루 12시간) 공부하며&lt;/b&gt;, 주말에도 4~5시간 이상 반드시 개발하는 것을 습관으로 하며 아르바이트도 해가면서 살았는데, 지금 생각하면 막 힘들진 않았던 것 같습니다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1월 초 부트캠프를 수료하고 너무 달려왔다라는 생각에 하루 코딩테스트 문제 2~3개만 간단하게 풀고 많이 놀았던 것 같습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1월 말에 이력서를 처음으로 완성하고 2월은 주에 5개 이상 이력서를 막 넣고 그랬었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금 돌아보면 그때 작성한 이력서는 되게 매력 있지 않았던 것 같습니다. 당연히 떨어질만한.. 그런 자소서였지만 몇 번 생긴 기회도 있긴 했던 것 같네요 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;완성된 이력서로 이 회사 저 회사 찔러보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원티드 위주로 찾아서 찔러보았고, 대부분 솔루션 회사를 목표로 지원했던 것 같아요! 지금 기록되어 있는 내용으론 20개 정도의 회사에 넣었는데, 서류 합격 + 코딩 테스트를 보았던 회사 &lt;b&gt;한 군데 말고는 모두 광탈&lt;/b&gt;했습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자랑은 아니지만 그렇게 광탈하고 자신감도 좀 떨어져서 그런지 2월 잔디에 주말에 2번이나 빠져있군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 행복하게도, 제가 수료한 부트캠프에서 &quot;&lt;b&gt;바로 인턴&lt;/b&gt;&quot;이라는 1개월간 기업과 연계해서 인턴을 시켜주는 프로그램에 참여하여 인턴을 할 수 있게 되었고 그게 제 진짜 개발자 인생의 시작이 되었습니다!!! ㅎㅎ (스파르타코딩클럽 내일 배움 캠프 파이팅!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;좀 전에 언급한 바로인턴을 3월 초부터 3월 말까지 정확히 4주간 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회사마다 문화나 분위기가 매우 다르겠지만, 제가 인턴으로 근무했던 회사는 좀 더(?) 특이한 분위기였어요. &lt;b&gt;자유로움&lt;/b&gt;이 많았던 것 같아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인턴 동기로 4명이 들어가게 되었고 총 5명의 인턴이 한 달 동안 회사에서 할당해 준 업무를 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;들어가기 전엔, 한국에서 첫 회사생활이고 (아르바이트 경험 20회 이상 + 일본에서 회사 근무 경험 10개월가량 있긴 했음) 개발자로서도 첫 근무다 보니 내가 잘할 수 있을까 걱정이 많았던 것 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특히, &lt;b&gt;제 주특기는 Java + Spring Boot인데 회사에서 요구하는 것은 Python + Django였습니다.&lt;/b&gt; 그리고 인턴분들 중 세 분이 파이썬 장고를 전문으로 하셨었고, 저랑 나머지 한분이 자프링이었는데 처음에는 회사에서 서비스하고 있는 프로그램 중 일부였던 웹 애플리케이션이 어떻게 동작하는지 확인하느라 시간을 많이 허비한 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래도 역시 개발언어라는 게 맥락을 알면 빠르게 학습할 수 있는 것 같습니다. 영어를 학습하다가 일본어를 학습할 때의 차이처럼 막 다르지는 않다는 뜻입니다. ㅎㅎ 라틴어 공부하다가 스페인어 공부하는 느낌정도가 아닐까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 웹앱 동작 방식을 빠르게 잘 파악하고, 회사에서 인턴들에게 요구한 사항을 가능한 빠르게 이해하고 수행해서 좋은 이미지를 받은 것 같아요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그걸로 회사에서 4주간의 짧은 인턴 기간이 끝날 때 즈음 저에게 조금 더 함께 해볼 생각이 있냐고 제안 주셨고, 그 사이에 이런저런 많은 일들이 있었지만 결국 지금까지 함께 하게 되어서 아주 운이 좋고 행복한 것 같습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회사에서 연장하게 되었고, 4월이 되고 나서 바로 제가 수료한 부트캠프에서 다음 기수 인턴분들이 들어왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 아무래도 한 달 먼저 해서 약간 더 많은 걸 알고 있었고, 회사에서 필요로 하던 업무가 저에게 잘 맞아서 금방 적응하고 많은 스킬들을 얻어서 제가 인턴분들을 온보딩 + 리드 + 업무 할당 등 하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4월에는 두 분이 들어오셨는데, 온보딩 할 때도 재밌었던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 원래 가르쳐주거나, 알려주고, 좀 강의처럼 정보를 설명하고 알려주고 그런 멘토링하는 걸 좋아하는 성격이다 보니 엄청 열정적으로 알려드리려고 했던 것 같아요. 알려주는 행위를 하면서, 내가 알려주는 게 틀렸으면 상대방에게 지적받으면서 고쳐서 알 수 있고, 제가 아는걸 더 단단히 할 수 있고, 알려주기 위해서 좀 더 찾아보고 공부하게 되기 때문에 저는 스스로의 성장을 목적으로 알려주는 걸 하는 것 같기도 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;당연히 인턴 두 분을 알려드리고 나서도 얻은 게 많았습니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;어떻게 하면 잘 이해하고 활용할 수 있는지, 상대방은 같은 방식인데 어떤 생각을 하면서 개발하는지&lt;/b&gt; 은근히 상대방의 인사이트를 보게 되어서 득 보는 게 많았고, 어떻게는 하지 말아야지라는 것을 보고 배운 것도 많았습니다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 3월 말부터&amp;nbsp; 시작해서 꾸준히 블로그 포스팅을 시작했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;시작한 이유는, 3월 말쯤 특별한 기회가 생겨서 다른 회사에 기술면접을 볼 수 있게 되었는데 그때 대답을 너무 못하였던 게 자극이 되어 스스로 CS 지식이 처참하다는 것을 깨닫고 작성하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제 스스로의 공부도 목적이지만 제 블로그를 언젠가 보게 될 대학생, 취준생, 주니어 개발자들에게 필요한 정보를 전달하여 헤매는 시간을 아끼길 바라는 부분도 있었습니다! &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주로 제가 검색해 봤을 때 구글에서 나온 정보가 마음에 들지 않았던 기술이나 내용에 대해서 포스팅하려고 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666;&quot;&gt;원래도 남한테 이런 정보를 주는 걸 좋아해서 일본에서 워홀 할 때도 구글 지도 리뷰로 지역가이드 6? 7 레벨 정도 달성했었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇게 포스팅을 시작하고 4~5월 두 달 동안은 거의 일주일에 한두 명 정도 들어와서 제 글을 보셨던 것 같아요. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;티스토리가 봇이 엄청 많은데 그런 분들 제외하면 일주일에 한 명도 안 들어온 주가 많을 것 같아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래도 전 어차피 제 스스로의 공부를 위해서 써온 글이라 아무도 읽어주지 않아도 상관없다는 마인드였는데, 6월 이후로는 하루 평균 다섯 분 정도가 봐주시게 된 것 같아요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아무도 댓글을 안 다셔서 여전히 봇 일수도 있지만, 누구든 보고 도움이 되었다면 기분이 너무 좋은 것 같습니다 ㅎㅎ 저도 티스토리를 해야겠다고 다짐한 게, 저랑 같은 부트캠프를 수료한 다른 수료생의 블로그 포스팅을 보고 도움을 많이 받아서 하게 된 거라서 말이죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjnT0K/btsO7QT6fLJ/aKhX2IukWqX3ikkHVCpuJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjnT0K/btsO7QT6fLJ/aKhX2IukWqX3ikkHVCpuJk/img.png&quot; data-alt=&quot;0706. 이런날은 들어오신 분이 내 블로그 글을 많이 둘어봤다는 뜻&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjnT0K/btsO7QT6fLJ/aKhX2IukWqX3ikkHVCpuJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjnT0K%2FbtsO7QT6fLJ%2FaKhX2IukWqX3ikkHVCpuJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;437&quot; height=&quot;395&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;0706. 이런날은 들어오신 분이 내 블로그 글을 많이 둘어봤다는 뜻&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;수익은 하나도 없지만 그래도 누군가 이 글을 보고 도움을 받았다면 먼 미래에 어떤 식으로든지 좋은 보답을 받을 수 있지 않을까요? ㅎㅎ 잘 봤다는 댓글하나만 있어도 행복할 것 같아요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxSuBd/btsO7f08oQT/VPfS8ZYBOIErnnao13JZBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxSuBd/btsO7f08oQT/VPfS8ZYBOIErnnao13JZBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxSuBd/btsO7f08oQT/VPfS8ZYBOIErnnao13JZBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxSuBd%2FbtsO7f08oQT%2FVPfS8ZYBOIErnnao13JZBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1512&quot; height=&quot;900&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;별로 좋은 실적은 아니지만, 꾸준히 포스팅을 하다 보니 5월 말부터는 클릭수도, 노출수도 증가하면서 뭔가 다른 분들이 제 블로그에 많이 찾아오는 느낌이 들어서 뿌듯합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;더 좋은 글, 도움 되는 글 많이 쓰려고요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여전히 회사에서 일하면서, 또 새롭게 들어온 인턴분들을 온보딩 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;데이터 분석 엔지니어 한분과, 개발자 세분이셨는데 이 분들도 각각 다양한 특징을 가지셨었고 배운 점들이 많은 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때부터는 저도 개인적으로 해야 할 업무가 많이 생기게 되어서, 온보딩 하는데에 쓰는 시간을 많이 줄이고 개별적인 개발을 시작한 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회사에서 요구하는 개발을 따로 하게 되고, 원래 하던 일도 하면서 업무 범위가 확장되고 뿌듯하면서 만족스러웠던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 3, 4, 5월 개발을 하면서 주말에도 개인적으로 하는 프로젝트를 놓지 않아서 깃헙 잔디도 잘 유지한 것 같습니다. ㅎㅎ 가능한 쓸데없는 내용으로 잔디를 심으려고 하지 않았는데 잘 되었는지 모든 커밋들이 다 기억나진 않네요..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6월&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;많은 격동을 한 달인 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;역시 3명의 인턴분이 들어오셨는데 모두 데이터 분석 일을 하셨습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 가장 인상적이었던 포인트는 이때부터 Claude Code를 사용해서 개발하게 되었다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드 코드를 사용하면서 바이브 코딩으로 개발을 하게 되면서 느꼈던 점을 정리해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 매우 빠르다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원하는 대로 코드가 동작하지 않거나, 내가 사용해 본 적 없는 라이브러리를 사용할 때 GPT에게 물어보고 참고하면서 학습하여 사용하던 방식에서, 이제 AI가 스스로 다 config세팅부터 해주니 시간을 매우 아낄 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 반복적인 작업도 워낙 잘하다 보니 간단한 수정은 시키면 바로 해내버리기 때문에 시간을 많이 벌었던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 매우 영특하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프론트 엔트는 거의 요구사항만 잘 전달하면 못하는 게 없다고 합니다. 백엔드 또한 매우 잘하는 것으로 느껴지는데, 커뮤니티에서 나오는 말들로 봤을 땐 Python과 Node.js 부분에서 강하고 아직 다른 언어는 부족함이 있다곤 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래도 기본적인 스킬들이나 특정 문제를 해결하는 방식 자체는 잘 알고 있어서 클로드 코드로 개선 방식을 고민할 시간을 많이 벌 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 콘텍스트가 큰 듯, 작다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드 코드 확실히 다 잘합니다. 하지만 명확한 단점이 있습니다. 특정 단계 이상 워크플로우가 복잡하게 꼬여 있는 경우 할루시가 매우 많이 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최근 아이패드를 새로 사고 싶어서, 가장 싸게 올라오는 날 사고 싶은데 매일 쇼핑탭에 들어가서 보기 귀찮아서 제 전용 웹 애플리케이션을 개발한다고 가정하겠습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;정보 수집 과정&lt;br /&gt;1. 네이버 쇼핑 웹에 접속한다.&lt;br /&gt;2. 아이패드라는 키워드를 검색한다.&lt;br /&gt;3. 낮은 가격순으로 정렬한다.&lt;br /&gt;4. 첫 페이지에 나오는 모든 상품을 이름, 가격, URL을 추출하여 저장한다&lt;br /&gt;5. 1~4 과정을 Celery Wocker를 사용하여 매일 오후 1시에 수행한다.&lt;br /&gt;6. 전날 나온 상품과 오늘 크롤링한 상품을 비교한다.&lt;br /&gt;7. 전날 상품보다 오늘 상품의 가격이 더 낮을 경우 Slack으로 메시지를 보낸다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위의 워크플로우가 기본이라고 가정하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 동작하는 웹 애플리케이션을 만드는 것 자체는 클로드 코드와 매우 빠르게 만들어 낼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 위 레벨의 워크플로우가 5~10개 있는 경우에는 기존의 워크플로우에서 기능만 살짝 바꿔서 6번이나 7번은 그대로 활용하는 게 효과적일 것 같지만, 이런 부분을 잘 해내지 못합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위의 워크플로우에서 제가 마음이 바뀌어 이름, 가격, URL, 리뷰를 추출하고 싶어 졌다고 하면, 제 생각인 클로드가 스스로 수정하며 Item이라는 모델을 변경하고 마이그레이션 하고, 워커도 설정을 반영하고 그럴 것 같지만 워크플로우를 전부 다 설명하고 확실히 이해시켜 놓지 않으면 딱 4번 항목만 변경하여 정보 수집 과정을 완벽하게 수행하지 못하게 되더라고요.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 멍청함의 이슈로, 이미 경험자들이 다양한 Best Practice를 만들어 내기 시작했더군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드를 사용하는 방식에 따라 극한의 효율로 저 같은 주니어 개발자도 시니어 개발자 수준의 프로젝트 개발이 가능해지긴 했지만, 제가 결론적으로 말씀드리고 싶은 내용은 &quot;&lt;b&gt;아직은 시간이 남았다&quot;입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;숙련된 택시 기사님이 본인만 아시는 엄청난 지름길을 사용하면 일반적인 네비보다 현저히 빠르게 갈 수 있듯, AI는 일반적으로 잘하지만 최고의 선택은 하지 않는다. 그리고 그 선택을 인간이 지시해도 잘 해결해내지 못한다. 라는걸 느꼈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;물론 시간문제라고 생각하지만, 지금부터 어떻게 하면 AI를 효율적으로 활용하고(프롬프트 관점에서), 어떻게 프로젝트를 설계해야 AI가 할루시네이션을 덜 만들어내는지 분석하여 AI 사용 역량을 기르는 것이 매우 중요할 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;더해서 AI가 인간의 지시대로 완벽하게 코드를 작성해 주는 날이 오기까지 그리 멀지 않은 것 같으니, 인간은 개발 방향성에 대한 지식을 좀 더 깊게 공부해서 AI를 에이전트로서 효율적으로 다루는 역량을 가져야 살아남을 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들면, 웹 페이지 크롤링을 하는데 Selenium 라이브러리나 Beautiful Soup 라이브러리를 사용하면 편하고 유명한 방법이지만, 간단한 웹인 경우에는 requests 라이브러리를 사용해서 응답을 받는 게 훨씬 효율적이고 빠를 수 있다는 것을 인간이 인지해놓은 상태에서 웹 페이지 방식에 따라 다르게 적용하여 크롤링하는 프로그램을 AI에게 개발해 달라고 하는 그런 개발자가 되어야겠죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다양한 + 깊은 인사이트를 가진 개발자가 AI 활용 능력까지 겸비했을 때 매우 강한 개발자가 될 거라고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;고민과 걱정도 많은 상반기였던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 진정 개발자가 될 수 있을지부터 시작해서, 회사에서 필요한 사람이 될 수 있을지, 심지어는 바이브 코딩으로 패러다임이 전환되었을 때 살아남을 수 있을지... 등 정신없었던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정답이 있는 것 같진 않지만, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금처럼 빠르게 좋은 정보는 받아들이고, 찾아다니고, 연습하고, 경험하고, 정리하고, 공유하고 그러다 보면 언젠간 좋은 날이 오지 않을까 하는 생각이 듭니다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;늘 하던 것처럼 개발을 놓지 않고 묵묵히, 그리고 빠르게 잘 나아가면 단단한 개발자가 언젠간 되어있겠죠?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모두들 파이팅입니다!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>TIL(Today I Learned)</category>
      <category>Claude</category>
      <category>it</category>
      <category>til</category>
      <category>개발자</category>
      <category>바이브코딩</category>
      <category>백엔드</category>
      <category>백엔드개발자</category>
      <category>주니어개발자</category>
      <category>클로드코드</category>
      <category>회고</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/125</guid>
      <comments>https://wanglan.tistory.com/entry/2025%EB%85%84-%EC%83%81%EB%B0%98%EA%B8%B0-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C%EC%84%9C%EC%9D%98-%ED%9A%8C%EA%B3%A0#entry125comment</comments>
      <pubDate>Sun, 6 Jul 2025 22:33:17 +0900</pubDate>
    </item>
    <item>
      <title>프로젝트 프론트엔드 성능 개선 및 최근 일주일의 회고(바이브 코딩)</title>
      <link>https://wanglan.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EB%B0%8F-%EC%B5%9C%EA%B7%BC-%EC%9D%BC%EC%A3%BC%EC%9D%BC%EC%9D%98-%ED%9A%8C%EA%B3%A0%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt; &lt;span style=&quot;color: #333333; text-align: center;&quot;&gt;안녕하세요!!! 왕란입니다!! 포스팅이 늦었습니다. 지난주에 작성하다 급한 일이 생겨 임시저장으로 넘겨버렸습니다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; letter-spacing: 0px;&quot;&gt;현충일에 바이브코딩을 Claude code로 처음 접해보고, 상당한 충격을 받아서 얼떨떨했던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오늘은 Claude code로 바이브코딩을 아주 &quot;조금&quot;만더 진행해 보고, 더해서 제가 일주일 동안 회사 근무를 하며 Claude code로 백엔드 개발도 해본 경험에 대한 이야기를 하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오늘 포스팅에 들어가는 내용은 대부분 &lt;b&gt;프론트엔드에 관련된 내용&lt;/b&gt;이며, 첨부될 코드에 관련된 부분은 모두 클로드가 독단적으로 진행한 부분이기 때문에 코드에 대한 리뷰는 하지 못할 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹 페이지 성능 개선&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 오늘은 지난번보다 메인페이지를 약간 더 개선할 생각이었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자잘한 텍스트 튀어나옴 이슈라던지 디자인적 요소를 개선하려고 했고, 좀 더 실제 서비스 중인 페이지답게 메인 페이지 하나만 개선해 보자!라는 생각이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 포인트는 저는 Next.js로 프론트 개발을 이번 프로젝트를 처음으로 진행하며 목적은 백엔드 동작을 블로그 포스팅을 보는 분들께 시각적으로 이해만 돕고자 매우 단순하고 직관적인 UI로 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;당연히 프론트쪽 자바스크립트 작성 역량은 그리 뛰어나지 못하고 디자인적 부분도 경험이 없어서 모자란 상태였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 디자인 한번 보고 가실게요!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;701&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cViDSh/btsOMpplSEf/949G4Nr2iHtm7pS6SHbF2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cViDSh/btsOMpplSEf/949G4Nr2iHtm7pS6SHbF2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cViDSh/btsOMpplSEf/949G4Nr2iHtm7pS6SHbF2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcViDSh%2FbtsOMpplSEf%2F949G4Nr2iHtm7pS6SHbF2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;701&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;701&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대충 위와 같은 형태였어요.. ㅎㅎㅎ&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음으로 완성된 화면을 보고 가겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;GIF 2025-06-15 일 오후 9-10-17.gif&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kvr7U/btsOMPg2UqK/t3szOhwkNOLMMGh9hGtny1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kvr7U/btsOMPg2UqK/t3szOhwkNOLMMGh9hGtny1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kvr7U/btsOMPg2UqK/t3szOhwkNOLMMGh9hGtny1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Kvr7U/btsOMPg2UqK/t3szOhwkNOLMMGh9hGtny1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;856&quot; data-filename=&quot;GIF 2025-06-15 일 오후 9-10-17.gif&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;856&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;흠... 이걸 보고 &quot;디자인 아직 별로네~&quot;라고 생각한다면 어쩔 순 없지만, 제가 백엔드에서 보여줄 기술을 어필하기엔 이 정도만 해도 충분한 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;방금 본 화면이 어떤 게 개선되었는지 보여 드리 도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 라이트/다크모드 전환 이슈&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드 코드가 웹페이지를 새로 디자인하면서 다크모드와 라이트모드를 전환하는 기능을 넣었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;발견한 문제는 라이트모드에서 다크모드로 변경하고 나서 마이페이지나 글쓰기페이지 등 다른 페이지로 이동하면 화면에 적용해 놓은 모드가 기본값인 라이트모드로 돌아온다는 이슈가 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 백엔드 개발자이고, 라이트모드 다크모드 이런 기능은 제 생각엔 좀 트렌디한 기능이라 생각하여, 이 정보를 어떻게 저장하지?라는 고민을 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 깨달아야 하는 포인트는, 클로드를 사용하는 순간 방법에 대한 고민은 버려야 한다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 클로드에게 상황을 말했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다크모드로 바꿔도 페이지를 이동하면 적용이 안되는데 어떡해?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 물어보고 나니 클로드가 세션에 접속되어 있는 제 프로젝트를 스캔한 후 버튼과 색전환 기능은 구현되었는데, 저장을 해놓는 기능이 없는 것을 판단하고 로컬 스토리지에 라이트인지 다크인지에 대한 정보를 저장해 놓겠다 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 잠시 기다리니까 바로 해결되었죠! ㅎㅎ 사실 이런 기능이 메이저 한 기능은 아니고 애초에 있지 않아도 되는 기능인데 이왕 구현된 거 제대로 동작하면 좋겠다고 생각했고, 클로드로 1~3분 고민 후 해결되는 모습이 너무 인상적이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 다크모드에서 글자가 상자에서 나가는 현상&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DwQrS/btsOKyVNnHd/0NuIjdspbBsCDef3jWd1r0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DwQrS/btsOKyVNnHd/0NuIjdspbBsCDef3jWd1r0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DwQrS/btsOKyVNnHd/0NuIjdspbBsCDef3jWd1r0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDwQrS%2FbtsOKyVNnHd%2F0NuIjdspbBsCDef3jWd1r0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1148&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사진을 보시면 붉은 상자 안쪽 부분에 글자가 삐져나가는 모습을 보실 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이걸 어떻게 해결할지 또 고민하지 않고, 원하는 형태만 클로드 코드에게 제시하면 됩니다. 저는 다음과 같이 말하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최신 업데이트에 글자가 길면 삐져나가는데 적당히...으로 줄이도록 수정해 줘.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdiAs7/btsOMocZTyJ/qtaDuX1iz9EWFVfSiU7FC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdiAs7/btsOMocZTyJ/qtaDuX1iz9EWFVfSiU7FC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdiAs7/btsOMocZTyJ/qtaDuX1iz9EWFVfSiU7FC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdiAs7%2FbtsOMocZTyJ%2FqtaDuX1iz9EWFVfSiU7FC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;158&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;딸깍 - 완료&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;입니다.. ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;고민이나 개선 방법 같은 건 스스로 판단해서 해결하기에 저는 맡겨놓고 다음 문제점을 찾고 있으면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 게시글에 읽음/안 읽음 처리하기&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lm7ZC/btsOMV9ph06/85M2KwomSDI0Eclqc3gUkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lm7ZC/btsOMV9ph06/85M2KwomSDI0Eclqc3gUkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lm7ZC/btsOMV9ph06/85M2KwomSDI0Eclqc3gUkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flm7ZC%2FbtsOMV9ph06%2F85M2KwomSDI0Eclqc3gUkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1148&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 사진을 보면, 최신 업데이트 부분에 파란 원이 있고 없는 글도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 딱히 해달라 하지는 않았는데 클로드가 스스로 판단하고 읽었는지 안 읽었는지 표시하는 기능을 추가한 것 같은데, 전 세 개의 글을 다 읽었지만 저 파란원이 사라지지 않더군요. 그냥 장식인 상태였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 물었습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최신 업데이트 부분에 있는 파란 원은 뭐야? 읽음 표시야? 제대로 동작 안 하는데 읽은 글은 알 수 있도록 처리해 줘.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그러고 나서 잠시 기다렸더니&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lMhmJ/btsOLDWfohP/tFsKXs3itaxF8KWmG5HQA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMhmJ/btsOLDWfohP/tFsKXs3itaxF8KWmG5HQA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMhmJ/btsOLDWfohP/tFsKXs3itaxF8KWmG5HQA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMhmJ%2FbtsOLDWfohP%2FtFsKXs3itaxF8KWmG5HQA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;305&quot; height=&quot;323&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;305&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 수정이 진행되었고, 글자 가독성도 바꾼 것 같습니다. 그리고 세 개의 게시글을 테스트 삼아 다 읽어보았더니&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8Bg9Q/btsOLtMWub6/4hONOWgKLCtIJAHb4fgRUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8Bg9Q/btsOLtMWub6/4hONOWgKLCtIJAHb4fgRUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8Bg9Q/btsOLtMWub6/4hONOWgKLCtIJAHb4fgRUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8Bg9Q%2FbtsOLtMWub6%2F4hONOWgKLCtIJAHb4fgRUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;317&quot; height=&quot;334&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 기능이 정상적으로 동작하는 것도 확인하였습니다. 아주 영특하니 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 페이지 렌더링 최적화&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;페이지를 보면서 이것저것 테스트를 하는데 뭔가 미묘한 불편함이 있었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;약간 서버에 접속자가 많아서 렉이 걸리는 느낌?&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;웹페이지의 프레임 다운이 있는 느낌?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;알지 못할 끊김이 느껴졌었습니다. 움짤로 캡처해 보았는데 느껴질지 모르겠네요. 한번 보시죠!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;성능 개선 전.gif&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l0xcn/btsOLtlLTIT/1jQeJSwmRCxv2Y2KPaKWb0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l0xcn/btsOLtlLTIT/1jQeJSwmRCxv2Y2KPaKWb0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l0xcn/btsOLtlLTIT/1jQeJSwmRCxv2Y2KPaKWb0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/l0xcn/btsOLtlLTIT/1jQeJSwmRCxv2Y2KPaKWb0/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;903&quot; data-filename=&quot;성능 개선 전.gif&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;최대한 와닿을 수 있게 높은 프레임으로 촬영해 보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DB에 데이터가 많은 것도, 사용유저가 많은 것도 아니니 최적화의 문제라고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;전 프론트 부분은 최적화 방법을 모르니 물어봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;화면에 데이터가 많은 것도 아닌데 미묘한 끊김이 있어. 성능 개선을 좀 해줘.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라고 했더니 스스로 이것저것 확인해 보더니 확실히 아무런 최적화가 되어있지 않았다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드를 사용하기 전 제가 개발했을 때도 성능을 생각하지 않아서 그대로 디자인만 바꿔줬던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자 그럼 다음 결과물을 보겠습니다. 아마 움짤 앱 특성상 크게 티가 나지 않아 보일 수 있지만 체감은 상당했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;성능 개선후.gif&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccjIIx/btsOK83mvxp/NVnkVnxIWHyPhhKORSXN30/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccjIIx/btsOK83mvxp/NVnkVnxIWHyPhhKORSXN30/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccjIIx/btsOK83mvxp/NVnkVnxIWHyPhhKORSXN30/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ccjIIx/btsOK83mvxp/NVnkVnxIWHyPhhKORSXN30/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;932&quot; height=&quot;903&quot; data-filename=&quot;성능 개선후.gif&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;티스토리에서도 티가 좀 나는군요. 좀 더 자연스럽게 미끄러지는 느낌이 생겼습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;성능 개선한 부분에 대해서 이것저것 설명을 해줬는데 사실 그 설명이 저한텐 중요하지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저번 포스팅도 이번 포스팅도 클로드 코드와 함께하는 바이브 코딩에 대한 내용만 말한 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개선된 내용을 보여드리고 싶은 마음도 있었지만, 제가 포스팅에서 말하고자 했던 내용은 지금부터입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 회사에서 새로운 프로젝트를 시작하였고, 제가 담당하게 되어 혼자서 아주 적극적으로 개발하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저희 회사는 클로드로 함께하는 바이브코딩에 되게 진보적이고 적극적으로 진행해 보자 라는 성격을 띠고 있어서 저도 새로운 프로젝트의 백엔드 개발에 많이 활용하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스팅하는 지금 시기까지 거의 2주 가까이 사용해 보았습니다. 느낀 점은,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. 프론트 엔드는 아주 기깔나게, 의도한 대로, 효율적으로, 빠르고, 정확하게, 신뢰감 있게, 잘한다! 였습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 혼자 진행하는 이 작은 프로젝트에서도 그랬고, 주변의 프론트엔드 개발자 분들의 평가도 그랬습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특히 Node 부분에서의 개발이 특출 나다고 합니다. 의도와 목적만 전달하면 바로 나온다고 합니다. 대단하다고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이게 앞으로 이 업계에 어떤 영향을 끼칠 진 모르겠지만...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. 백엔드에는 아직 할루시가 많다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;할루시라는 용어를 아시나요? 저는 스타크래프트에서 처음 접한 단어이긴 한데, 프로토스 종족의 하이 템플러의 재미용 스킬입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;유닛의 분신이라는 &quot;허상&quot;을 만드는 스킬인데요, 스킬이름이 &lt;b&gt;할루시네이션&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;AI 쪽 분야에는 항상 이 부분이 문제가 되어왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그저 대화형으로 사용하는 AI는 지금까진 몇 번 대화를 하다가 유저에게 혼나면 세상에 존재하지 않는 정보를 만들어내서 유저를 달래주곤 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예를 들어 보겠습니다. 상황은, 제가 크롤러를 개발하다가 제대로 동작하지 않아서 AI에게 화를 낸 상황입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;크롤러가 제대로 동작하지 않잖아. 좀 어려운 세팅 없이 스마트하고 섹시한 크롤러를 잘 써보라니까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 식으로 말하면, AI는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: GungSeo, serif;&quot;&gt;아, 무슨 말인지 알았습니다!&lt;br /&gt;Python Django에 적절한 라이브 러리를 발견했습니다. 다음은 관련 코드 적용입니다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라고 말한 뒤 다음과 같이 코드를 작성합니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1750496000854&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import django.crawler.sexytypecrawler from SmartCrawler

... 

이하 헛소리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;거짓이 아니라 실제로 클로드 코드를 사용할 때 있었던 일입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아직 이런 말도 안 되는 할루시가 존재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 프로젝트의 규모가 좀 커지고, 연관되는 복잡한 로직이 많아질수록 돌려 막기 코딩을 보여줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;A라는 기능이 있는데 B라는 기능 추가해 줘&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;B라는 기능은 되는데 갑자기 A랑 같이 동작하던 C가 동작을 안 해&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;B와 C는 동작하는데 A가 이제 이상해졌어. A는 처음처럼 동작하되, C와 B가 함께 잘 동작하도록 해줘&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;A, B, C가 동작을 하지만 D가 동작을 안 해&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;A가 이러면 롤백이 되고 C는 되지만 B의 코드가 허상이 됐잖아.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위와 같은 흐름이 되어버립니다. 아마 GPT랑 놀아보신 분은 아실 겁니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드도 가볍고 간단한 기능은 매우 빠르게 구현해내지만, 프로젝트 설정이나 규모가 크면 스캔하는데 많은 토큰을 낭비하고 결과도 신뢰가지 않습니다.&lt;br /&gt;&lt;br /&gt;제가 생각한, 백엔드에서 클로드를 가장 효율적으로 쓰는 방법은!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모듈화입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;MSA 개념을 활용해서 개발할 프로젝트에 사용할 기능을 모듈화 하여 설계 및 구상을 하고, 이걸 유기적으로 동작할 수 있도록 하고, 의존성을 낮추는 수밖에 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초기 개발에 이 부분에 많은 리소스를 낭비하게 되겠지만 클로드를 활용하여 개발을 진행할 거라면 이 방법이 제일 신뢰할 만한 미래를 가지고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마지막으로, 어제 레딧을 보다가 클로드에 대한 재밌는 의견을 보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클로드를 사용하는 행위에 대한 메타포였는데요!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 눈을 가리고 페라리를 모는 것과 같다. 직선 도로에선 빠르고 즐겁지만, 코너가 나온다면?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. GPS를 사용하여 운전하는 것 같다. 길만 따라가면 되지만, 언제 호수에 빠질지 모른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 할머니가 주신 케이크 레시피대로 만드는 것과 같다. 실패하면 레시피를 수정하면 되는 게 아니라 새로운 케이크를 만들면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(개발하다 망하면 프로젝트 버리고 새로 만드는 방식에 대한 이야기)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;말고도 다른 재밌는 의견이 많았습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제로도 되게 공감이 많이 되는 의견들이었습니다. 물론 시간이 지나면서 AI가 커버할 수 있는 범위가 넓어지고 할루시가 줄어들며, 정확도 및 속도가 개선될 수 있겠지만, 100퍼센트 신뢰할 수 있을지 의문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;절대 AI가 섭렵할 수 없었다던 바둑도, 천문학적인 경우의 수 때문에 모두가 안된다고 했지만 이세돌 기사의 한 수에서 말도 안 되는 결과를 내버리며 단 한대국을 패배하였던 알파고도, 지금은 고도화되어서 절대 패배할 수 없다고 해도 이게 100퍼센트라는 신뢰는 가질 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실제 서비스에서 이런 일이 발생한다면 더더욱 안 되겠죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞으로의 변화가 너무 기대됩니다. 클로드를 사용하면서 빠른 성과를 달성하고, 동시에 딥하고 다양한 경험을 스스로 학습해 나가는 게 중요할 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아이언맨도 기깔난 마크 2를 만들었지만 지속적인 R&amp;amp;D로 신물질 개발, 나노 테크 적용을 했듯&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자 - 토니스타크&lt;br /&gt;AI - 슈트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 느낌으로 두 개가 합쳐져야 최고의 퍼포먼스가 발동한다고 생각합니다. 슈트를 최고로 활용할 줄 아는 토니스타크가 되어봅시다!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모두들 파이팅 ㅎ&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>Ai</category>
      <category>Claude</category>
      <category>바이브코더</category>
      <category>바이브코딩</category>
      <category>바이브코딩 성능</category>
      <category>백엔드</category>
      <category>주니어개발자</category>
      <category>클로드코드</category>
      <category>프론트엔드</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/124</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-%EB%B0%8F-%EC%B5%9C%EA%B7%BC-%EC%9D%BC%EC%A3%BC%EC%9D%BC%EC%9D%98-%ED%9A%8C%EA%B3%A0%EB%B0%94%EC%9D%B4%EB%B8%8C-%EC%BD%94%EB%94%A9#entry124comment</comments>
      <pubDate>Sat, 21 Jun 2025 18:11:23 +0900</pubDate>
    </item>
    <item>
      <title>바이브코딩으로 프로젝트 개선해보기...(위기감지)</title>
      <link>https://wanglan.tistory.com/entry/%EB%B0%94%EC%9D%B4%EB%B8%8C%EC%BD%94%EB%94%A9%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EC%84%A0%ED%95%B4%EB%B3%B4%EA%B8%B0%EC%9C%84%EA%B8%B0%EA%B0%90%EC%A7%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;안녕하세요 왕란입니다!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이번 포스팅은 바이브코딩으로 제가 진행하던 프로젝트의 프론트엔드 부분을 개선하는 작업을 하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이번에 코드에 관련된 내용은 딱히 말할 게 없을 것 같아요.... AI 녀석이 저보고 저리 가라 그러고 지 알아서 다 하더군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;개발자로서 엄청난 위기감을 느끼게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;지난번 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%B0%B1%EC%97%94%EB%93%9C%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%99%80-%ED%98%91%EC%97%85%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;백엔드와 프론트엔드의 협업에 대한 내용&lt;/a&gt;&lt;/span&gt;을 다룬 포스팅에서 바이브코딩이라는 말을 처음 언급했는데 사실 그때 한 건 바이브 코딩도 아니라는 생각이 들었습니다.. 그냥 GPT 참고정도...? 바이브코딩은 다들 이젠 아실 것 같지만, &lt;b&gt;AI와 사람이 협업하여 코드작성을 하는 작업&lt;/b&gt;을 의미합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;근데 오늘한 코드작업은 협업이 아니라 &lt;b&gt;명령 &amp;lt;-&amp;gt; 수행&lt;/b&gt; 을 하는과정이었던 것 같고, 저는 코드를 단 한 줄도 보지 않아도 된다는 게 인상적이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;우선 한번 보여드리도록 하겠습니다. 이번 바이브코딩에는 Claude AI를 사용하였고, MCP 서버를 통해 제 프로젝트에 접근하도록 만들었습니다. 그럼 시작하겠습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기본 세팅&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;준비물&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://code.visualstudio.com/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;VS code&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://nodejs.org/ko/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Node.js&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://claude.ai/download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Claude Desktop&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;저는 개발자이기 때문에! VS code는 기본적으로 깔려 있었고, 프론트엔드를 Next.js로 작성하고 있었기 때문에 그전에 Node.js는 이미 깔아 둔 상태였습니다. 둘 다 본인 OS에 맞는 설정으로 각각의 사이트에서 다운로드할 수 있습니다. 상단에 다운로드 링크도 연결해 두겠습니다!! (Claude는 가입 후 결제를 해야 하는 것으로 알아요. 저는 무료인 상태에서 써본 적 없고 결제한 상태로 썼습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Claude Desktop은 Claude AI를 데스크탑 애플리케이션으로 이용할 수 있도록 하는 프로그램입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이것을 사용해야하는 이유는, &lt;b&gt;MCP 서버를 통해 제 프로젝트를 온전히 AI에게 공유하고 함께 수정까지 할 수 있도록 연결하기 위해서입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;기존의 방식은 웹 페이지에서 AI와 대화하면서 프로젝트 내부에 포함되는 파일을 하나씩 공유하거나 프로젝트 폴더를 만들어 여러 파일을 공유하여 AI에게 학습시켜 개발을 진행하셨을 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;하지만 이렇게 하면 파일 수 제한이나, AI모델이 한 번에 읽는 파일 개수 제한, 그리고 파일하나가 너무 크거나 개수가 20개 이상으로 너무 많으면 모두 참조를 하지 않고 멍청한 모습으로 AI가 분석하여 옳지 않은 코드를 공유하는 경우가 많다는 것을 경험한 적 있으실 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;하지만 클로드 데스크톱을 활용하면 MCP(&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;Model Context Protocol) 서버를 통해서 프로젝트에 직접 접근하게 하거나 클로드 개발진들이 공유하는 다양한 서버를 활용하여 &lt;b&gt;매우 유연한 워크플로 생성이 가능&lt;/b&gt;합니다. &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;MCP 서버 관련&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7; text-align: start;&quot; href=&quot;https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#-reference-servers&quot;&gt;깃허브&lt;/a&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;ex. 특정 사이트 이미지 수집 후 URL 저장하고, 수집 내용을 Slack 봇으로 알림을 보내도록 하기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;(&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://youtu.be/fkqXQOjj8cA?si=VT-J_Z39MFEjvLYc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 영상&lt;/a&gt;&lt;/span&gt; 링크 공유하겠습니다!!)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;ex. 로컬 파일에 접근하여 직접 파일을 수정 후 수정된 항목 로그로 남겨 Notion으로 패치노트 작성하기&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;이제 이런 복잡한 워크플로를 사람이 진행하지 않아도 됩니다. 명령만 시켜놓으면 AI가 분석, 수정, 반영, 정리 및 공유 모든 게 다 가능해지게 되었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;잡 이야기가 너무 길어졌군요..!! 저는 이번에 제가 작성한 매우 간단한 프로그램을 수정하기 위해 filesystem이라는 MCP 서버를 사용하도록 했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;깃허브를 참고해 보면&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nsPAl/btsOrATVQxz/SMtqs7Asplg78AgpVqcwIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nsPAl/btsOrATVQxz/SMtqs7Asplg78AgpVqcwIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nsPAl/btsOrATVQxz/SMtqs7Asplg78AgpVqcwIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnsPAl%2FbtsOrATVQxz%2FSMtqs7Asplg78AgpVqcwIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;612&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;영어로 설명되어 있는데, Filesystem을 mcpserver에 추가하면 로컬에 작성되어 있는 폴더에 접근가능해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;설정하는 법 알려드리겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;위 사진에서 깃허브 리드미에 작성된 레퍼런스에서 Filesystem 부분을 클릭하여 들어갑니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;893&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ophis/btsOtn6kgZa/Xj1GsKvNtkr57f74808jLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ophis/btsOtn6kgZa/Xj1GsKvNtkr57f74808jLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ophis/btsOtn6kgZa/Xj1GsKvNtkr57f74808jLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fophis%2FbtsOtn6kgZa%2FXj1GsKvNtkr57f74808jLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1911&quot; height=&quot;893&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1911&quot; data-origin-height=&quot;893&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;내부에 README로 설치법이 자세히 설명되어 있습니다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;만! 저희는 Desktop에서 아래의 간단한 내용만 추가하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1873&quot; data-origin-height=&quot;909&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UxR0Z/btsOswCVdNs/WFQ1FDWFkbkqYcB1LMYLiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UxR0Z/btsOswCVdNs/WFQ1FDWFkbkqYcB1LMYLiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UxR0Z/btsOswCVdNs/WFQ1FDWFkbkqYcB1LMYLiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUxR0Z%2FbtsOswCVdNs%2FWFQ1FDWFkbkqYcB1LMYLiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1873&quot; height=&quot;909&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1873&quot; data-origin-height=&quot;909&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;아래로 드래그하면 NPX라는 부분이 있습니다. 이걸 그대로 복사해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt; &lt;b&gt;(참고로 NPX가 두 가지 이상 있는데, 저희는 Claude Desktop에서 사용하기 때문에 아래 사진과 같이 Usage with Claude Desktop이라는 항목 아래에 있는 NPX를 카피해 주세요!)&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BEs8i/btsOtFeCr4S/A1pE0ddoDkBBVKtQSFBKy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BEs8i/btsOtFeCr4S/A1pE0ddoDkBBVKtQSFBKy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BEs8i/btsOtFeCr4S/A1pE0ddoDkBBVKtQSFBKy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBEs8i%2FbtsOtFeCr4S%2FA1pE0ddoDkBBVKtQSFBKy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;577&quot; height=&quot;358&quot; data-origin-width=&quot;1099&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;추후에 MCP 서버를 추가할 때는 내부의 서버만 복사해서 콤마로 나열해 주시면 됩니다! 예시 작성해 둘게요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749371279608&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;filesystem&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-filesystem&quot;,
        &quot;/Users/username/Desktop&quot;,
        &quot;/path/to/other/allowed/dir&quot;
      ]
    },
    &quot;github&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-github&quot;
      ],
      &quot;env&quot;: {
        &quot;GITHUB_PERSONAL_ACCESS_TOKEN&quot;: &quot;&amp;lt;YOUR_TOKEN&amp;gt;&quot;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위 예시는 filesystem이라는 MCP랑 github MCP를 추가해 둔 예입니다. 로컬 폴더만 수정하고 싶으신 분들은 filesystem의 NPX부분만 복사해 주시면 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그럼 복사한 내용을 Claude Desktop에 적용하는 법을 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kAJZH/btsOrr3KscQ/QH67WuWDwmrU6YRgN9KJjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kAJZH/btsOrr3KscQ/QH67WuWDwmrU6YRgN9KJjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kAJZH/btsOrr3KscQ/QH67WuWDwmrU6YRgN9KJjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkAJZH%2FbtsOrr3KscQ%2FQH67WuWDwmrU6YRgN9KJjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;712&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Claude Desktop의 좌 상단에 삼선을 클릭하시고 파일 &amp;gt; 설정을 클릭해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;721&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kw7i6/btsOr3uuaM7/bzB4rRtxzJlvvOnOKZzxc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kw7i6/btsOr3uuaM7/bzB4rRtxzJlvvOnOKZzxc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kw7i6/btsOr3uuaM7/bzB4rRtxzJlvvOnOKZzxc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkw7i6%2FbtsOr3uuaM7%2FbzB4rRtxzJlvvOnOKZzxc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1040&quot; height=&quot;721&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;721&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;제 사진에서는 이미 filesystem이 추가되어 있지만 여러분은 처음이라면 없을 거예요! 아래에 &lt;b&gt;설정 편집&lt;/b&gt; 버튼을 클릭해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMsare/btsOsDWjNow/mAWKwKxiXvCZ03uNqmb8Sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMsare/btsOsDWjNow/mAWKwKxiXvCZ03uNqmb8Sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMsare/btsOsDWjNow/mAWKwKxiXvCZ03uNqmb8Sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMsare%2FbtsOsDWjNow%2FmAWKwKxiXvCZ03uNqmb8Sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;808&quot; height=&quot;676&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;808&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그러면 Claude Desktop의 설정파일이 들어있는 폴더가 열리게 됩니다(윈도우 기준)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djq68y/btsOrBL3QNm/8fdVKEMk9E6KbmiCnSfAuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djq68y/btsOrBL3QNm/8fdVKEMk9E6KbmiCnSfAuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djq68y/btsOrBL3QNm/8fdVKEMk9E6KbmiCnSfAuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdjq68y%2FbtsOrBL3QNm%2F8fdVKEMk9E6KbmiCnSfAuk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;876&quot; height=&quot;764&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그중 &lt;b&gt;claude_desktop_config.json&lt;/b&gt; 파일을 &lt;b&gt;우클릭해 주시고 VS code에서 열어주세요!!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MtQFV/btsOsg8cVaD/DHRSVtt2WfrVkdLSTO8BIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MtQFV/btsOsg8cVaD/DHRSVtt2WfrVkdLSTO8BIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MtQFV/btsOsg8cVaD/DHRSVtt2WfrVkdLSTO8BIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMtQFV%2FbtsOsg8cVaD%2FDHRSVtt2WfrVkdLSTO8BIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;684&quot; height=&quot;439&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;439&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;여러분들은 처음 켜셨으면, {}으로 빈 배열이 있거나 아무것도 없거나 그런 상태일거예요. 이 부분에 아까 깃허브에서 복사한 NPX를 그대로 복사해서 넣어주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;아마 처음 넣으시면 복사한 내용이 다음과 같으실 거에요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749371933968&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;filesystem&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-filesystem&quot;,
        &quot;/Users/username/Desktop&quot;, //여기와
        &quot;/path/to/other/allowed/dir&quot; //여기를 변경
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그중에 &quot;여기와&quot; &quot;여기를 변경&quot;이라는 주석이 달린 부분을 수정해 주시면 되는데 여러분이 공유할 프로젝트가 들어있는 폴더의 경로를 넣으시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;경로를 확인하는 방법은 win11 이상에서는 프로젝트 폴더를 우클릭하면 경로 복사가 가능하고, win10에서는 속성을 여시면 위치를 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XHzCN/btsOsEVgh6c/NkR8whSh6NjrkyVMP4vT91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XHzCN/btsOsEVgh6c/NkR8whSh6NjrkyVMP4vT91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XHzCN/btsOsEVgh6c/NkR8whSh6NjrkyVMP4vT91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXHzCN%2FbtsOsEVgh6c%2FNkR8whSh6NjrkyVMP4vT91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;426&quot; height=&quot;557&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위치를 복사해 주시면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;C:\Users\사용자이름\Desktop 이런 형식으로 복사가 되실 거예요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;이걸&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749372325929&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;filesystem&quot;: {
      &quot;command&quot;: &quot;npx&quot;,
      &quot;args&quot;: [
        &quot;-y&quot;,
        &quot;@modelcontextprotocol/server-filesystem&quot;,
        &quot;/Users/사용자이름/Desktop/myproject&quot;, //여기에 넣어주기
        &quot;/Users/사용자이름/Desktop/myproject-front&quot;
      ]
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;claude desktop 설정파일의 프로젝트 경로 부분에 넣어주면 됩니다. 폴더를 추가할 때는 콤마로 이어서 쭉 작성해 주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이때 주의할 점은&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: start;&quot;&gt; C:\Users\사용자이름\Desktop 이렇게 되어있던 경로에서 C: 은 제외하시고 \(역슬래시)는 /(일반슬래시)로 변경해서 넣어주셔야 해요!&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;이렇게 하고 Claude Desktop을 껐다 켜줍니다. 그냥 X 버튼을 눌러서 끄는 게 아닌&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1035&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF0Dg2/btsOsz0HZPy/WviUnKZDb9H4uuO9oQCuxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF0Dg2/btsOsz0HZPy/WviUnKZDb9H4uuO9oQCuxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF0Dg2/btsOsz0HZPy/WviUnKZDb9H4uuO9oQCuxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF0Dg2%2FbtsOsz0HZPy%2FWviUnKZDb9H4uuO9oQCuxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1035&quot; height=&quot;719&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1035&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;종료 버튼을 눌러서 아예 꺼주신 다음 다시 켜주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그리고 다시 삼선 &amp;gt; 파일 &amp;gt; 설정 &amp;gt; 개발자 탭으로 들어가면&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;716&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWokYn/btsOryaKj4z/6EkTkCIlqgz2LQ4G1hwwH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWokYn/btsOryaKj4z/6EkTkCIlqgz2LQ4G1hwwH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWokYn/btsOryaKj4z/6EkTkCIlqgz2LQ4G1hwwH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWokYn%2FbtsOryaKj4z%2F6EkTkCIlqgz2LQ4G1hwwH1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1032&quot; height=&quot;716&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;716&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;상단의 이미지처럼 filesystem이라는 항목이 생겨있을 거예요. 클릭해 보시면 어떤 프로젝트 폴더가 연결되었는지 확인할 수 있습니다. (정상적으로 연결되어 있으면 running이라고 표시가 뜰 거예요)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이제는 Claude AI가 여러분의 로컬 폴더에 접근할 수 있는 상태가 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;이 상태에서 프론트엔드 부분의 디자인을 개선해 달라고 요청해 보았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;변경 전 기존의 디자인부터 보여드릴게요.(백엔드 개발자로서 기능+UX만 확인하고 프론트-백의 협업을 보여드리기 위해 아무런 디자인적인 요소를 하지 않은 정말 순수한 프론트화면이에요. 양해부탁드려요!!!!!)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rQufs/btsOsjKCtdq/LxzSc0JR052yS8iHmflLKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rQufs/btsOsjKCtdq/LxzSc0JR052yS8iHmflLKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rQufs/btsOsjKCtdq/LxzSc0JR052yS8iHmflLKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrQufs%2FbtsOsjKCtdq%2FLxzSc0JR052yS8iHmflLKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;803&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;803&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그냥 div로 나뉘어 있고 적당한 페이지네이션과 적당한 버튼, 적당한 색과 적당한 폰트가 적용되어 있는 2001년대쯤 개인이 개발한 것 같은 블로그 페이지입니다. 이걸 모던하게 바꿔달라고 요청했어요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddVjdr/btsOsxu3TRc/KSpMETUjmTPHoMouqjPIN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddVjdr/btsOsxu3TRc/KSpMETUjmTPHoMouqjPIN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddVjdr/btsOsxu3TRc/KSpMETUjmTPHoMouqjPIN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddVjdr%2FbtsOsxu3TRc%2FKSpMETUjmTPHoMouqjPIN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;718&quot; height=&quot;805&quot; data-origin-width=&quot;718&quot; data-origin-height=&quot;805&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;823&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1RXqN/btsOtic59Yt/yAokdvyDBGMFGSaJJNlQVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1RXqN/btsOtic59Yt/yAokdvyDBGMFGSaJJNlQVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1RXqN/btsOtic59Yt/yAokdvyDBGMFGSaJJNlQVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1RXqN%2FbtsOtic59Yt%2FyAokdvyDBGMFGSaJJNlQVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;823&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;823&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Claude AI가 전체 프로젝트 구조를 살펴보고, 패키징 방식도 살펴봤고, Next.js로 개발된 프로젝트인 것을 파악한 후 메인 페이지들을 각각 살펴보고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;판단한 내용은&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;현재 상태:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기본적인 Tailwind CSS 스타일링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;단순한 색상 조합 (노란색, 파란색 위주)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;정적인 레이아웃&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기본적인 버튼과 카드 디자인&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;라고 코멘트를 줬는데 할 말 없군요. ㅎㅎ 그리고는 알아서 시작하고 개선하기 시작합니다.(이때 filesystem MCP의 다양한 명령어가 실행되는데 실행권한을 부여해 주어야 실제 파일 수정까지 반영해 줍니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;메인페이지 개선한 사진은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEKRWe/btsOswQsbP7/rty2kUHgNuUbL8PkWCBFE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEKRWe/btsOswQsbP7/rty2kUHgNuUbL8PkWCBFE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEKRWe/btsOswQsbP7/rty2kUHgNuUbL8PkWCBFE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEKRWe%2FbtsOswQsbP7%2Frty2kUHgNuUbL8PkWCBFE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;910&quot; height=&quot;801&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;801&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;막 엄청나게 브릴리언트 하다, 어메이징 하다 정도는 아닌 디자인이었습니다 솔직히.. 근데 그래도 자기만의 판단으로 저는 아무것도 하지 않고 꽤나 Next.js의 다양한 기법을 이용해서 반응형 웹페이지를 뚝딱 만들어주더군요.. 라이트모드 다크모드 기능도 반영하고 있고, 지금의 디자인 자체는 완성형 디자인은 아니라고 생각이 들지만 그래도 이 정도면 엄청난 개선이라 생각 들어요. 아무런 디자인 생각을 하지 않아도 AI가 해준다는 것 자체가요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;다음으로 마이페이지 UI를 볼게요!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qN1Rn/btsOtMEN0U7/dMvlzIk0aXquodGVFvEYsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qN1Rn/btsOtMEN0U7/dMvlzIk0aXquodGVFvEYsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qN1Rn/btsOtMEN0U7/dMvlzIk0aXquodGVFvEYsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqN1Rn%2FbtsOtMEN0U7%2FdMvlzIk0aXquodGVFvEYsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;904&quot; height=&quot;792&quot; data-origin-width=&quot;904&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;기존의 마이페이지는 내가 쓴 게시글 리스트와 비밀번호, 회원정보 수정이 가능하고 로그아웃 버튼과 좌상단의 로고를 클릭하면 메인페이지로 이동하도록 아주 간단히 작성되어 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;개선 후에는?!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/luLLt/btsOtEUjUlv/EKuxX8zFvqTWanZ7lmExz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/luLLt/btsOtEUjUlv/EKuxX8zFvqTWanZ7lmExz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/luLLt/btsOtEUjUlv/EKuxX8zFvqTWanZ7lmExz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FluLLt%2FbtsOtEUjUlv%2FEKuxX8zFvqTWanZ7lmExz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;950&quot; height=&quot;912&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이렇게 변경해 주었습니다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;이전에도 디자인을 했다고 보진 않지만... 그래도 뭐라도 해보겠다고 프론트도 끄적거렸던 제 모습이 초라해지더군요...&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;마지막으로 로그인 화면 보겠습니다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;916&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MqQ1z/btsOs4lCYcX/HMBCdZVm0r21ckPuDDKvBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MqQ1z/btsOs4lCYcX/HMBCdZVm0r21ckPuDDKvBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MqQ1z/btsOs4lCYcX/HMBCdZVm0r21ckPuDDKvBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMqQ1z%2FbtsOs4lCYcX%2FHMBCdZVm0r21ckPuDDKvBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;916&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;916&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;글은 제가 재미로 바꿔둔 내용이긴 하지만 기본에 충실한 정보만 가진 그냥 아무 디자인적 요소가 없는 로그인페이지였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;909&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qsnP2/btsOrXH2IvO/Klk1aWD0th8BYnYPkBVsB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qsnP2/btsOrXH2IvO/Klk1aWD0th8BYnYPkBVsB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qsnP2/btsOrXH2IvO/Klk1aWD0th8BYnYPkBVsB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqsnP2%2FbtsOrXH2IvO%2FKlk1aWD0th8BYnYPkBVsB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;941&quot; height=&quot;909&quot; data-origin-width=&quot;941&quot; data-origin-height=&quot;909&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;바꾸고 나니까 어디서 본 것 같은 로그인화면 같은 느낌으로 변했군요. 무슨 기준인지는 모르겠지만 Claude는 그라데이션을 참 좋아하는 것 같습니다. 그래도 변경한 방식이 좀 일관성 있는 디자인이라는 게 만족스럽습니다. 부족한 컨셉은 말로 시켜서 수정하면 되는 것입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;마치며&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;저는 백엔드 개발자로서 AI가 고민하지 못하는 다양한 고민을 하면서 프로젝트를 성능적 부분을 개선할 수 있다고 (아직은), 제가 낫다고... 생각... 하고 있습니다!!!!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;하지만 이런 바이브코딩을 해보니 정말 AI가 핵심 인력이 되는 시기가 멀지 않은 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;하지만 AI가 인간처럼 퇴근한답시고 스스로 전원 끄고, 샤워하고 생각정리 한답시고 메모리 정리도 하고, 업무 한답시고 프로젝트를 특정한 시간에 스스로 켜서 확인하고 오늘의 Todo List를 작성해서 모든 워크플로를 100퍼센트 스스로 하지는 못한다고 생각합니다.(아직은... 아직은....!!!)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;이런 시기에 저희 같은 개발자는 살아남기 위해서 어떤 역량을 길러야 할까요?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;아직 이렇게 작성된 웹 애플리케이션은 기능적으론 완벽하지 않고, 실제 백엔드와의 연결이 끊어지는 모습도 종종 볼 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;이런 단순한 버그 픽스하는 역량도 100퍼센트 AI로 대체될 날이 멀지 않은 것 같으니... 제 앞으로의 개발자는 &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;1. 기발한 아이디어 구상 능력&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;2. 그 아이디어를 구체화하여 짜임새 있는 애플리케이션으로서의 설계능력&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;3. AI가 개발한 애플리케이션을 검수할 QA능력&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;4. 애자일한 성격...?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;을 가져야 할 것 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;보수적으로 보았을 땐 아직 AI가 한 사람의 인간을 대체하진 못할 것 같지만 한 사람의 역량이 1인분에서 3인분이 될 것 같아 보입니다. 할 일이 많은 회사일 수록 오래 살아남을 것 같습니다.. 저도 앞으로 백엔드와 관련된 지식을 아주 딥하게 연마함과 동시에 AI에게 최적화된 프롬프트를 주고 최대한 AI의 실수를 잘 잡아내는 섬세한 역량을 가진 개발자가 되어야겠다는 생각이 듭니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;span&gt;주니어, 시니어 개발자분들 모두 앞으로도 힘내시길 바랍니다..!!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>AI협업</category>
      <category>Claude</category>
      <category>claude ai</category>
      <category>MCP</category>
      <category>개발자전망</category>
      <category>바이브코딩</category>
      <category>백엔드</category>
      <category>주니어개발자</category>
      <category>풀스택</category>
      <category>프론트엔드</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/123</guid>
      <comments>https://wanglan.tistory.com/entry/%EB%B0%94%EC%9D%B4%EB%B8%8C%EC%BD%94%EB%94%A9%EC%9C%BC%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EC%84%A0%ED%95%B4%EB%B3%B4%EA%B8%B0%EC%9C%84%EA%B8%B0%EA%B0%90%EC%A7%80#entry123comment</comments>
      <pubDate>Sun, 8 Jun 2025 18:28:31 +0900</pubDate>
    </item>
    <item>
      <title>따라만 하면되는 구글 로그인 프로젝트에 적용하기!!</title>
      <link>https://wanglan.tistory.com/entry/%EB%94%B0%EB%9D%BC%EB%A7%8C-%ED%95%98%EB%A9%B4%EB%90%98%EB%8A%94-%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요. 왕란입니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오늘은 진행 중인 프로젝트에 구글 로그인 기능을 넣는 과정을 다뤄보려고 합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구글 계정은 거의 전 세계적으로 사용되고 있고, 인증도 깔끔하며 로그인 관련 API를 제공하고 있기 때문에 프로젝트에 적용하기 매우 간편하고 쉽습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 회원가입 -&amp;gt; 로그인 과정을 거치는 것 대신에, 구글 인증 사이트를 통해 회원가입과 로그인을 통합적으로 가능하기 때문에 사용자 경험적인 측면에서도 뛰어나고 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아래 단계를 하나씩 따라해보시면 아주아주 쉽게 구글 로그인을 구현하실 수 있을 거예요!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 바로 적용해보도록 할까요!?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Google OAuth 클라이언트 등록하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 처음 할 일은 &lt;a href=&quot;https://console.cloud.google.com&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Cloud Console&lt;/a&gt;에 접속하여 새 프로젝트를 생성하는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csEpmO/btsOrURETbJ/s614h8R9VPyjI1NhNGkB80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csEpmO/btsOrURETbJ/s614h8R9VPyjI1NhNGkB80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csEpmO/btsOrURETbJ/s614h8R9VPyjI1NhNGkB80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsEpmO%2FbtsOrURETbJ%2Fs614h8R9VPyjI1NhNGkB80%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;738&quot; height=&quot;593&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;첫 페이지입니다. 빨간 부분의 버튼을 누르시면 새 프로젝트를 생성할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQeLTr/btsOq9a1SNd/eX2M21s0BAMFjkAqKYq1GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQeLTr/btsOq9a1SNd/eX2M21s0BAMFjkAqKYq1GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQeLTr/btsOq9a1SNd/eX2M21s0BAMFjkAqKYq1GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQeLTr%2FbtsOq9a1SNd%2FeX2M21s0BAMFjkAqKYq1GK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1528&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음으로 프로젝트 리스트 중에서 선택하셔도 되고, 본인이 적용할 프로젝트를 위해 새 프로젝트를 생성해서 적용할 수 있습니다. 오른쪽 상단의 빨간 부분의 버튼을 눌러서 새 프로젝트를 생성해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ROjau/btsOte2o9eF/wKC992VaWOzvdAAQRMothK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ROjau/btsOte2o9eF/wKC992VaWOzvdAAQRMothK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ROjau/btsOte2o9eF/wKC992VaWOzvdAAQRMothK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FROjau%2FbtsOte2o9eF%2FwKC992VaWOzvdAAQRMothK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1620&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1620&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트를 생성하는 것 자체는 너무 간단하고 직관적이라 따로 설명은 더 하지 않도록 하겠습니다 ㅎㅎ 원하는 이름으로 생성해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;생성 후에는 &lt;b&gt;&quot;OAuth 동의 화면&quot;&amp;nbsp;&lt;/b&gt;탭의 설정을 하는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;첫 화면으로 돌아가시면,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by6bBz/btsOrDPWNMN/JamJoWcEyAKku6rx8WQDwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by6bBz/btsOrDPWNMN/JamJoWcEyAKku6rx8WQDwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by6bBz/btsOrDPWNMN/JamJoWcEyAKku6rx8WQDwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby6bBz%2FbtsOrDPWNMN%2FJamJoWcEyAKku6rx8WQDwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1407&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;메인 화면에서 &quot;API 및 서비스&quot; 카테고리가 있습니다. 이걸 클릭하시고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD605E/btsOsuSazhH/AkKyCH7nkmJYIpYLme2Lm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD605E/btsOsuSazhH/AkKyCH7nkmJYIpYLme2Lm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD605E/btsOsuSazhH/AkKyCH7nkmJYIpYLme2Lm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD605E%2FbtsOsuSazhH%2FAkKyCH7nkmJYIpYLme2Lm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1532&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;좌측에서 &lt;b&gt;OAuth 동의 화면&lt;/b&gt;을 클릭해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;들어가서 아무것도 만드신 적 없다면 다음과 같은 화면이 나올 거예요.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_구글 클라우드 OAuth 동의화면 내부.PNG&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccw4nB/btsOrmnHPpR/9eA7aiPJj1JahYqecUH9Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccw4nB/btsOrmnHPpR/9eA7aiPJj1JahYqecUH9Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccw4nB/btsOrmnHPpR/9eA7aiPJj1JahYqecUH9Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccw4nB%2FbtsOrmnHPpR%2F9eA7aiPJj1JahYqecUH9Jk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1074&quot; height=&quot;789&quot; data-filename=&quot;edited_구글 클라우드 OAuth 동의화면 내부.PNG&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;789&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 중간에 &lt;b&gt;&quot;시작하기&quot;&lt;/b&gt;버튼을 눌러주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_6구글 클라우드 OAuth 동의화면 작성중.PNG&quot; data-origin-width=&quot;1073&quot; data-origin-height=&quot;775&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBZJvK/btsOqRA8cgK/bj1gEw54koULuJWiRyROn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBZJvK/btsOqRA8cgK/bj1gEw54koULuJWiRyROn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBZJvK/btsOqRA8cgK/bj1gEw54koULuJWiRyROn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBZJvK%2FbtsOqRA8cgK%2Fbj1gEw54koULuJWiRyROn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1073&quot; height=&quot;775&quot; data-filename=&quot;edited_6구글 클라우드 OAuth 동의화면 작성중.PNG&quot; data-origin-width=&quot;1073&quot; data-origin-height=&quot;775&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 폼이 나옵니다. 순서대로 작성해 주시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_7구글 클라우드 OAuth 동의 화면 첫 폼 작성.PNG&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nzAzL/btsOsFlSqXu/sBFE3sMMFY3CTMuPSun4L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nzAzL/btsOsFlSqXu/sBFE3sMMFY3CTMuPSun4L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nzAzL/btsOsFlSqXu/sBFE3sMMFY3CTMuPSun4L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnzAzL%2FbtsOsFlSqXu%2FsBFE3sMMFY3CTMuPSun4L0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1072&quot; height=&quot;735&quot; data-filename=&quot;edited_edited_7구글 클라우드 OAuth 동의 화면 첫 폼 작성.PNG&quot; data-origin-width=&quot;1072&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;별로 중요하진 않지만 이미지는 모자이크 처리했습니다 ㅎㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_8구글 클라우드 OAuth 동의화면 두번째 폼 작성.PNG&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G62my/btsOrz1m0K8/o46wMTelBWkHSjFnxq2VTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G62my/btsOrz1m0K8/o46wMTelBWkHSjFnxq2VTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G62my/btsOrz1m0K8/o46wMTelBWkHSjFnxq2VTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG62my%2FbtsOrz1m0K8%2Fo46wMTelBWkHSjFnxq2VTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;832&quot; data-filename=&quot;edited_8구글 클라우드 OAuth 동의화면 두번째 폼 작성.PNG&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 친구나 주변인들도 나중에 가입하도록 하려고 외부로 설정했어요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이외에 3번과 4번을 작성하고 나서 OAuth 동의 화면을 마무리해주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음 순서는, 사용자 인증 정보를 생성하는 것입니다. Google OAuth 클라이언트 ID를 생성하는 스텝입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사진대로 따라와 주시면 됩니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd5f0X/btsOsQAHiaL/1mcyNdkgypmmTywDi03XA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd5f0X/btsOsQAHiaL/1mcyNdkgypmmTywDi03XA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd5f0X/btsOsQAHiaL/1mcyNdkgypmmTywDi03XA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd5f0X%2FbtsOsQAHiaL%2F1mcyNdkgypmmTywDi03XA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1532&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1532&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;API 및 서비스 카테고리 내부에서, 사용자 인증 정보를 누르시고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1661&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhyw6K/btsOr1JKy7k/LCIokIHAEg7jZKgMs26ssK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhyw6K/btsOr1JKy7k/LCIokIHAEg7jZKgMs26ssK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhyw6K/btsOr1JKy7k/LCIokIHAEg7jZKgMs26ssK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdhyw6K%2FbtsOr1JKy7k%2FLCIokIHAEg7jZKgMs26ssK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1661&quot; height=&quot;1125&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1661&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;클라이언트 만드기를 눌러주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_10구글 클라우드 클라이언트 ID 만들기 10.png&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5QLDP/btsOsTc8DEJ/vfEu7uCslzCtT6ZtNycp31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5QLDP/btsOsTc8DEJ/vfEu7uCslzCtT6ZtNycp31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5QLDP/btsOsTc8DEJ/vfEu7uCslzCtT6ZtNycp31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5QLDP%2FbtsOsTc8DEJ%2FvfEu7uCslzCtT6ZtNycp31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1071&quot; height=&quot;627&quot; data-filename=&quot;edited_10구글 클라우드 클라이언트 ID 만들기 10.png&quot; data-origin-width=&quot;1071&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 웹 애플리케이션이기 때문에 웹 애플리케이션 옵션을 선택했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 하단의 승인된 리디렉션 URI 부분을 봐주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_11구글 클라우드 클라이언트 ID 11.PNG&quot; data-origin-width=&quot;1063&quot; data-origin-height=&quot;865&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xiVo4/btsOsd4tHUi/xBqVY1iHogeQYP2Q3wn710/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xiVo4/btsOsd4tHUi/xBqVY1iHogeQYP2Q3wn710/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xiVo4/btsOsd4tHUi/xBqVY1iHogeQYP2Q3wn710/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxiVo4%2FbtsOsd4tHUi%2FxBqVY1iHogeQYP2Q3wn710%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1063&quot; height=&quot;865&quot; data-filename=&quot;edited_11구글 클라우드 클라이언트 ID 11.PNG&quot; data-origin-width=&quot;1063&quot; data-origin-height=&quot;865&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분에서 중요합니다!!! &lt;b&gt;하단의 승인된 리디렉션 URL&lt;/b&gt;부분을 본인의 프로젝트에 맞게 설정하 신다음 꼭 기억해 주세요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 사진에 작성된 URI를 하지 않고, 나중에 아래와 같이 변경하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749196888530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://localhost:8080/login/oauth2/code/google&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;작성한 후 만들기 버튼을 눌러줍니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_edited_12 OAuth 클라이언트 생성됨 아이디 비번 가리기.PNG&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;679&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FFDCN/btsOsEAxyfB/jhomkxQzMz46o7DbZ1FIpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FFDCN/btsOsEAxyfB/jhomkxQzMz46o7DbZ1FIpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FFDCN/btsOsEAxyfB/jhomkxQzMz46o7DbZ1FIpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFFDCN%2FbtsOsEAxyfB%2FjhomkxQzMz46o7DbZ1FIpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;548&quot; height=&quot;679&quot; data-filename=&quot;edited_edited_edited_12 OAuth 클라이언트 생성됨 아이디 비번 가리기.PNG&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;679&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 화면은 이후에 더이상 볼 수 없다고 경고가 나옵니다. 복사한 뒤 따로 저장해 주세요! 저는 메모장에 저장해놨다가 이 후 환경변수로 적용하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 까지 해서 클라이언트 ID까지 발급받고 나면 1단계는 끝입니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트 코드 작성&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이제부터 여러분들의 프로젝트에 적용해 보도록 해야겠죠!?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;처음으로는 의존성 추가부터 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 코틀린 스프링 부트 프로젝트이므로, `build.gradle.kts`에 다음과 같이 추가해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749197353660&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# build.gradle.kts
// 구글
implementation(&quot;org.springframework.boot:spring-boot-starter-oauth2-client&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음으로 application.properties or application.yml 등에 다음과 같이 추가해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749197454574&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            redirect-uri: &quot;{baseUrl}/login/oauth2/code/{registrationId}&quot;
            scope:
              - profile
              - email
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
            user-name-attribute: email&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;client-id와 client-secret 은 아까 만들어서 복사 및 저장해 두었던 정보를 사용합니다. 저는. env에 따로 등록해 놓았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749197542015&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .env
GOOGLE_CLIENT_ID=발급받은ID
GOOGLE_CLIENT_SECRET=발급받은SECRET&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음으론 OAuth2 로그인 성공 핸들러를 작성해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 auth도메인의 google 패키지를 추가해서 작성해 주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749197753244&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.myproject.domain.auth.google

import com.example.myproject.config.filter.JwtUtil
import com.example.myproject.domain.auth.dto.response.CachedUser
import com.example.myproject.domain.user.repository.UserRepository
import com.example.myproject.domain.user.service.SocialUserService
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.stereotype.Component

@Component
class OAuth2SuccessHandler(
    private val jwtUtil: JwtUtil,
    private val socialUserService: SocialUserService,
    private val userRepository: UserRepository,
    private val redisTemplate: RedisTemplate&amp;lt;String, Any&amp;gt;
) : AuthenticationSuccessHandler {

    override fun onAuthenticationSuccess(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authentication: Authentication
    ) {
        val oAuth2User = authentication.principal as OAuth2User
        val email = oAuth2User.getAttribute&amp;lt;String&amp;gt;(&quot;email&quot;)!!
        val name = oAuth2User.getAttribute&amp;lt;String&amp;gt;(&quot;name&quot;) ?: &quot;구글사용자&quot;
        val nickname = email.substringBefore(&quot;@&quot;)

        val user = userRepository.findByEmail(email)
            .orElseGet {
                println(&quot;  신규 유저 생성 실행됨: $email&quot;)
                socialUserService.createSocialUser(name, email, nickname)
            }
        // JWT 생성
        val token = jwtUtil.createToken(user.id!!, user.email, user.userRole)

        // Redis 캐싱 추가
        val cachedUser = CachedUser(
            id = user.id!!,
            email = user.email,
            password = user.password,
            userRole = user.userRole,
            deletedAt = user.deletedAt,
            nickname = user.nickname,
            name = user.name
        )
        redisTemplate.opsForValue().set(&quot;user:${user.email}&quot;, cachedUser)

        // 헤더에 넣는 기존방식 유지
        response.setHeader(&quot;Authorization&quot;, token)
        response.sendRedirect(&quot;http://localhost:3000/main?token=$token&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;복사해서 사용하시기 좋게 임포트 부분도 넣어놨습니다 package 부분만 본인 프로젝트에 맞게 설정해 주시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 위 핸들러에 대한 설명을 추가하자면, 저는 User 엔티티에 password 항목을 nullable 하지 않게 설정하였습니다. 그래서 반드시 값이 존재하여야 하는데, 구글에서 OAuth2로 가져온 객체에서는 email과 name 필드만을 가져오기 때문에&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749198022009&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val oAuth2User = authentication.principal as OAuth2User
val email = oAuth2User.getAttribute&amp;lt;String&amp;gt;(&quot;email&quot;)!!
val name = oAuth2User.getAttribute&amp;lt;String&amp;gt;(&quot;name&quot;) ?: &quot;구글사용자&quot;
val nickname = email.substringBefore(&quot;@&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분에서 이메일과 이름을 설정하고, 닉네임은 이메일의 @ 앞부분까지 추출하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749198071274&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    val user = userRepository.findByEmail(email)
        .orElseGet {
            println(&quot;  신규 유저 생성 실행됨: $email&quot;)
            socialUserService.createSocialUser(name, email, nickname)
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 부분에서는 해당하는 이메일이 없을 시 소셜 로그인 전용 create 로직을 이용하여 회원 등록까지 하도록 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749198381730&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        // JWT 생성
        val token = jwtUtil.createToken(user.id!!, user.email, user.userRole)

        // Redis 캐싱 추가
        val cachedUser = CachedUser(
            id = user.id!!,
            email = user.email,
            password = user.password,
            userRole = user.userRole,
            deletedAt = user.deletedAt,
            nickname = user.nickname,
            name = user.name
        )
        redisTemplate.opsForValue().set(&quot;user:${user.email}&quot;, cachedUser)

        // 헤더에 넣는 기존방식 유지
        response.setHeader(&quot;Authorization&quot;, token)
        response.sendRedirect(&quot;http://localhost:3000/main?token=$token&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제 인증/인가 및 토큰 정책에 맞게 user id, email, userrole을 담아 토큰을 생성한 후 User를 Redis에 캐시 시키고, Authorization 헤더에 응답으로 토큰을 넣게 설정하였습니다. 유저 객체를 생성 할 때 이&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 설계하면, 회원가입이 되어 있지 않은 유저가 로그인을 시도할 때, &lt;b&gt;&quot;google로 로그인하기&quot;&lt;/b&gt;와 같은 버튼을 클릭하게 되면 DB에 구글 이메일에 해당하는 ID를 조회하게 되고 존재하지 않는다면 createSocialUser라는 메서드를 호출하여 USER 테이블에 추가하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;참고로 SocialUserService는 다음과 같이 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749198304679&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class SocialUserService(
    private val userRepository: UserRepository,
    private val passwordEncoder: PasswordEncoder
) {
    // 구글과 카카오는 이 메서드로
    @Transactional
    fun createSocialUser(name: String, email: String, nickname: String): User {
        if (userRepository.existsByEmail(email)) {
            throw GlobalException(ExceptionCode.ALREADY_EXIST_EMAIL)
        }
        val encodedPassword = passwordEncoder.encode(UUID.randomUUID().toString())

        val user = User(
            name = name,
            email = email,
            password = encodedPassword,
            userRole = UserRole.USER,
            nickname = nickname
        )
        println(&quot;DB에 신규 소셜 유저 저장: $email ($nickname)&quot;)
        return userRepository.save(user)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설명은 이만 하고 다음 단계로 넘어가겠습니다. 저는 Spring Security를 사용하여 프로젝트의 보안을 담당하고 있기 때문에&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`WebSecurityConfig`에 필터체인에 대한 설정을 추가해주어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749198962573&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf { it.disable() }

        http.cors { it.configurationSource(corsConfigurationSource()) }
            .oauth2Login {
                it.loginPage(&quot;http://localhost:3000/login&quot;) // 실제 프론트 로그인 페이지 주소
                it.successHandler(oAuth2SuccessHandler)
            }
        http.authorizeHttpRequests {
            it
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .permitAll()
                .requestMatchers(&quot;/auth/**&quot;)
                .permitAll()
                .anyRequest()
                .authenticated()
        }

        http
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
            .addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter::class.java)

        return http.build()
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 추가해야 할 부분은&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;. csrf 부분 아래에&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749199035432&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        http.cors { it.configurationSource(corsConfigurationSource()) }
            .oauth2Login {
                it.loginPage(&quot;http://localhost:3000/login&quot;) // 실제 프론트 로그인 페이지 주소
                it.successHandler(oAuth2SuccessHandler)
            }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;. oauth2 Login 부분의 설정입니다. 사용자가 로그인되지 않은 상태에서 인증이 필요한 경로에 접근하면, Spring Security에서 제가 만들어놓은 커스텀 로그인 페이지로 리다이렉트 하도록 설정하고, 아래에 OAuth 인증 성공 후에 핸들러에 작성된 내용이 인증 성공 후 실행되는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 설정은 끝났습니다. 추가적으로 적용할 수 있는 부분은, 저는 Email과 Name 만 추출하고 있지만 사용자 정보를 다양하게 추출하여 사용하려면 커스텀 UserService를 구현하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749199602971&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class CustomOAuth2UserService : OAuth2UserService&amp;lt;OAuth2UserRequest, OAuth2User&amp;gt; {
    override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
        val delegate = DefaultOAuth2UserService()
        return delegate.loadUser(userRequest)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분은 필요하시면 스스로 적용해 보시도록 권장합니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 구글에 프로젝트 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 구글에 클라이언트 ID 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 프로젝트의 application.yml에 OAuth 설정 추가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 인증 성공 시 동작하는 핸들러 추가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. (저의 경우) SecurityConfig에 4번의 핸들러 추가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5단 계면 모두 끝입니다!!! 자잘한 설명이 많아서 글이 길어졌지만 어렵지는 않다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4번의 OAuth2 SuccessHandler의 동작은 본인의 로그인 방식에 맞게 수정해 주시면 되고, 저는 핸들러 마지막에&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749199873784&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;response.sendRedirect(&quot;http://localhost:3000/main?token=$token&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 토큰을 프론트의 /main 엔드포인트에 파라미터로 담아 return 하게 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 프론트에서는&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749199923121&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 소셜 로그인 리다이렉트 시 토큰 처리
  useEffect(() =&amp;gt; {
    const urlParams = new URLSearchParams(window.location.search);
    const tokenFromUrl = urlParams.get('token');
    if (tokenFromUrl) {
      localStorage.setItem('token', tokenFromUrl);
      router.replace('/main');
    }
  }, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분을 추가하여 url에서 파라미터의 token이라는 key 값을 찾아 value를 추출하고, localStorage에 담아 두었다가 사용하게 했습니다. 마지막으로 /main이라는 엔드포인트로 토큰을 제거하고 redirect 하여 유저의 인터페이스에서도 이상하지 않게 해 놓았답니다!!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5o4o/btsOru6RZD5/xLkkt3OTZIC3GEUXu7VcGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5o4o/btsOru6RZD5/xLkkt3OTZIC3GEUXu7VcGK/img.png&quot; data-alt=&quot;구글 계정으로 로그인 버튼을 누르면, 크롬 브라우저에 등록된 구글 계정으로 바로 로그인 된답니다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5o4o/btsOru6RZD5/xLkkt3OTZIC3GEUXu7VcGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5o4o%2FbtsOru6RZD5%2FxLkkt3OTZIC3GEUXu7VcGK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;774&quot; height=&quot;497&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구글 계정으로 로그인 버튼을 누르면, 크롬 브라우저에 등록된 구글 계정으로 바로 로그인 된답니다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구글 로그인은 카카오와 함께 한국에서 서비스하는 대부분의 플랫폼에서 사용되고 있고, 해외에 글로벌한 서비스를 론칭한다면 google 로그인은 필수라고 생각합니다. 저 또한 회사에서 새로 기획하고 있는 프로젝트에서 적용하게 되었습니다! 그래서 학습하는 분들을 위해 하는 김에 포스팅해보았답니다. 호호 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 구글 로그인 API는 인증을 구글에서 모두 처리해 주기 때문에 매우 간편합니다. 꼭 적용하여 서비스의 접근성을 올리도록 해보시길 바랍니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주시는 분은 없지만, 읽어주셔서 감사합니다!!!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>google 로그인</category>
      <category>oauth</category>
      <category>oauth2</category>
      <category>구글로그인</category>
      <category>구글클라이언트</category>
      <category>백엔드</category>
      <category>소셜로그인</category>
      <category>시니어개발자</category>
      <category>주니어개발자</category>
      <category>프로젝트개선</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/122</guid>
      <comments>https://wanglan.tistory.com/entry/%EB%94%B0%EB%9D%BC%EB%A7%8C-%ED%95%98%EB%A9%B4%EB%90%98%EB%8A%94-%EA%B5%AC%EA%B8%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#entry122comment</comments>
      <pubDate>Fri, 6 Jun 2025 17:58:32 +0900</pubDate>
    </item>
    <item>
      <title>도커로 프로젝트 구동하기(ft. Redis-Cluster)</title>
      <link>https://wanglan.tistory.com/entry/%EB%8F%84%EC%BB%A4%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0ft-Redis-Cluster</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요 왕란입니다!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;벌써 5월 한 달도 다 지나가고 슬슬 여름이 다가오고 있는 것 같아요! 이럴 땐 집에서 선풍기 쐬며 코드작성하는 게 가장 좋은 것 같습니다 ㅎㅎ &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번에 작성하려는 내용은, 제가 개발하고 있는 프로젝트에 도커를 적용하여 프로젝트에 필요한 요소들을 실행시켜보려고 합니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트를 개발하다보면 개발환경이 달라서 프로젝트를 빌드하고 개발이나 테스트에 불편함을 겪어보신 적 있을 것입니다. 이걸 도커를 통해 컨테이너로 관리하면 매우 편리하게 프로젝트를 일정한 환경에서 개발할 수 있어서 좋은 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그럼 시작하겠습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;  도커가 뭐지?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존의 프로젝트에서는 IDE에서 구동하면 운영체제 단에서 제가 실행시킨 프로젝트를 하나의 프로세스로 분류하여 동작하게 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;간단한 프로젝트 하나만 구동할 때는 그저 프로젝트를 알차게 담은 jar 파일을 run 하기만 해도 문제없지만, 실무라던지 다른 사람과 함께 개발을 하는 환경에서는 &lt;b&gt;프로젝트를 일정한 환경과 스펙으로 구동해야 효율적&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트가 리눅스 환경에서 동작하고, Redis 서버도 개별적으로 동작하고, DB도 개별적 프로세스로 동작하게 되는 경우에 각각을 리눅스 환경에서 동작하게 되는데 이때 도커를 활용하면 이 구동을 일괄적으로, 일정하게 관리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 도커를 사용해본적 있으시면 느끼실 겁니다. &lt;b&gt;매우 가볍고 이미지와 컨테이너가 금방 생성되고 삭제된 다는 것을&lt;/b&gt;!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;도커를 사용하지 않을땐 각각의 필수 프로세스(DB, 프로젝트, 캐시서버 등)가 개별적인 Linux Kernel을 사용하게 되고 개별적 가상머신에서 조금은 무겁게(?) 동작하는 느낌을 받을 수 있습니다.&lt;/span&gt; 하지만&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;도커를 사용하면 컴퓨터 내부에 컴퓨터가 있는 것 같은 느낌도 받으셨을 겁니다. 이런 또 하나의 컴퓨터를 가상머신(Virtual Machine)이라고 부른다면, 도커는 프로세스들을 컨테이너로 구동하게 하고, 같은 Linux Kernel을 공유하기 때문에 매우 가볍게 구동하는 느낌을 받을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 부분 말고도 개발하다 DB가 꼬여서 문제가 발생한다던지, 빌드에 잘못된 내용이 들어가서 수정 후 재배포를 한다던지 할 때, 컨테이너로 관리되는 도커의 특징상 매우 빠르고 편하게 프로젝트를 관리할 수 있어서 효율적입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;나의 프로젝트에서 도커가 필요한 이유  &lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금까지 제가 개발한 프로젝트를 보면, 기본적인 게시판 기능과 회원 관리기능, 게시판 중에서는 인기 게시글 기능까지 구현되어 있는 것을 보실 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이때 게시글에 들어가서 조회를 하는 경우에는 로그인된 유저인지를 판단하는 JWT 토큰 검증 과정이 요청을 보낼 때 필터단에서 실행되게 됩니다. 이때 요청을 보낸 사용자의 요청 헤더에서 Authorization 필드를 찾아서 Token을 검증하기 위해 DB를 조회하지 않고 Redis 서버에 유저의 정보를 저장해 두고 대조해 보는 방식을 채택하였습니다. &lt;b&gt;요청 하나마다 DB에 조회하여 검증하면 매우 비효율적이기 때문입니다&lt;/b&gt;!!!(반응속도, 네트워크I/O 등)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 일반적으로 유저들은 인기게시글이라는 카테고리에 눈이 먼저 갈 것이고, 인기게시글의 조회량은 다른 게시글에 비해 빈익빈 부익부 형태로 급증할 것이라고 예상하여서, &lt;b&gt;DB 조회도 급증할 것이라고 예상&lt;/b&gt;하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 인기게시글을 실시간으로 보기위해서 게시글 ID, 유저 ID 등을 활용하여 &lt;b&gt;조회수 카운트를 만들고&lt;/b&gt;, 조회수 카운트가 높은 게시글 순서대로 ID를 &lt;b&gt;ZSET을 통해 정렬&lt;/b&gt;하여 게시글의 ID를 &lt;b&gt;DB에서 가져온 다음 Redis 서버에 저장&lt;/b&gt;하게 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하여 Redis 서버에 지금까지 벌써 다음의 정보를 저장하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 최근 로그인 된 유저의 정보2. 게시글 실시간 조회수 카운트 정보3. 인기 게시글 자체의 Contents 정보&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 정보들은 대부분 임시로 사용했다가 사라져도 문제없는 정보이기는 하지만, 갑자기 &lt;b&gt;Redis 서버가 예상치 못한 문제로 셧다운 되어버린다면&amp;nbsp;&lt;/b&gt;유저는&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 다시 로그인이 필요해짐2. 인기 게시글 정보를 볼 수 없어짐&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이라는 UX측면에서의 불편함을 느낄 것이라고 생각했습니다. (실제로 지금 작성하고 있는 티스토리도 까딱하면 로그인이 풀려버려서 제가 개발한 프로젝트와 비슷한 방식으로 유저 정보를 저장하고 스케줄러로 날리거나, 짧은 시간 정책을 가진 리프레시토큰이 있는 게 아닌가 하는 생각이 듭니다 ㅎㅎ)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 이 Redis 서버가 갑작스럽게 죽었을 때를 대비하여, Redis 의 복제 정보를 저장해 두고 운영에 메인으로 사용되는 Redis 서버가 죽었을 시 임시저장되어 있는 서버를 사용하여 정보를 그대로 살려내는 것을 목표로 하였습니다. 이미 이런 경험이 있는 선배 개발자 분들이 이것을 &lt;b&gt;Redis-Cluster라고 부르기로 했다고 합니다&lt;/b&gt;.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하기 위해선 복제 Redis 서버를 각각의 프로세스로 구동해야곘다는 생각이 들었고, 그러면 프로젝트를 실행할 때 Redis&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;서버 여러개, Redis Sentinel 여러 개를 돌려야 했고, &lt;b&gt;이 추가적인 프로세스를 관리하기 위해 도커를 도입하자!! 라는 결론을 내리게 되었습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(참고로 Redis Sentinel은 고가용성을 보장하기 위해서 Redis 마스터 서버를 감시하고 있다가 서버가 죽으면 Sentinel이 과반수로 투표를 진행하여 마스터 서버 죽음을 판단하고, 복제 Redis 서버를 마스터로 승격시키는 관리를 해주는 장애 조치 도구입니다. 시간 나면 이 동작에 대한 자세한 포스팅도 해보도록 할게요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;도커 적용하기! (Redis 세팅과 함께)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 단계적으로 나아가기 위해 목표를 다음과같이 잡았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;✅ 내 Spring Boot 프로젝트를 Docker 컨테이너로 실행하게 만들기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; ✅ Redis 서버도 함께 실행되도록 `docker-compose.yml` 구성하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; ✅ 정상 동작 확인 후 Redis Sentinel + 클러스터링으로 확장하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 도커를 구동하기 위해서 Dockerfile과 설정파일인 docker-compose.yml 이 필요합니다. 루트 디렉토리에 넣으면 됩니다. 아주 기본적인 세팅만 넣었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748685729201&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Dockerfile
# Kotlin + Spring Boot 프로젝트용
FROM eclipse-temurin:17-jdk-alpine

# 작업 디렉토리
WORKDIR /app

# Gradle 빌드 결과물 복사
COPY build/libs/*.jar app.jar

# 컨테이너 실행 시 JAR 실행
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748685834708&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#docker-compose.yml

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - &quot;8080:8080&quot;
    depends_on:
      - redis
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    networks:
      - backend

  redis:
    image: redis:7.0
    ports:
      - &quot;6379:6379&quot;
    volumes:
      - redis-data:/data
    networks:
      - backend

volumes:
  redis-data:

networks:
  backend:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 본격적으로 운영환경을 생각하기 위해서 `application-prod.yml` 을 추가했습니다. application.yml은 기본적은 운영환경 정보가 담겨있는 설정파일이고, prod는 프로덕션 환경(운영환경) 설정 파일입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748685927875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#applicion-prod.yml

spring:
  data:
    redis:
      host: redis
      port: 6379

server:
  port: 8080

logging:
  level:
    root: INFO&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 정도까지만 작성하고 프로젝트 빌드 + Docker 이미지 빌드 + 컨테이너 실행까지 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748686131124&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew clean build # 프로젝트 빌드 하고
docker-compose up --build # 도커 이미지 빌드 및 컨테이너 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 애초에 세팅자체가 너무 기본적인 내용이라 구동은 정상적으로 되는 것을 확인했습니다. (사진은 찍어두지 않았어요 ㅎ)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기까지가 위에 작성한 목표 3가지 중에서 위의 2개를 먼저 수행완료한 상태입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마지막으로 오늘 도커 도입의 이유와 최종 목표인 Redis 클러스터링 + Sentinel 적용까지 해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis 클러스터링 + Sentinel로 고가용성 잡기!!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;고가용성이란, 시스템이나 서비스가 장애 없이 장기간 지속적으로 사용 가능한 상태를 말합니다. 근데 개발해 보시면 아실 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;장애가 없는 것은 불가능하다... &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇다면 우리 개발자는 목표를 좀 다른 느낌으로 잡아야겠죠. 바로, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;&lt;b&gt;사용자는 장애가 없게 느끼게 만들기&lt;/b&gt;&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;입니다. 이렇게 하기 위해서 Redis 서버를 여러 개 가동하는 상태에서, 복제 Redis 서버에 정보를 복사하여 저장해 두도록 해야 합니다. 그리고 단순히 복제해 놓는 것뿐만 아니라, 상황을 모니터링할 Sentinel까지 적용해야 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Sentinel은 일반적으로 3개 이상 구동시키고 있다가,&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 마스터 Redis가 죽었는지 확인하고 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 주기적으로 마스터 Redis에게 신호를 보내고 받아서&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 중간에 자기들끼리 투표하여 죽었다고 과반수 이상의 Sentinel 이 판단하게 된다면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 복제 Redis를 마스터로 승격시키기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;의 과정으로 고가용성을 확보하게 됩니다. 아주 영특한 녀석 같군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어떻게 컨테이너로 관리할 것인지 다음과 같이 정리해 보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포트&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;역할&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;redis-master&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6380&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쓰기 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;redis-replica-1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6381&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;복제 + 읽기 처리&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;redis-replica-2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6382&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;복제&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;sentinel-1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;26379&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모니터링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;sentinel-2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;26380&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모니터링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;sentinel-3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;26381&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;모니터링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 내용을 적용하기 위해서 docker-compose.yml의 설정을 변경해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748687323836&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# docker-compose.yml
version: '3.8'

services:
  redis-master:
    image: redis:7.0
    container_name: redis-master
    ports:
      - &quot;6380:6379&quot;
    volumes:
      - redis-master-data:/data
    command: [&quot;redis-server&quot;, &quot;--appendonly&quot;, &quot;yes&quot;]
    networks:
      - backend

  redis-replica-1:
    image: redis:7.0
    container_name: redis-replica-1
    depends_on:
      - redis-master
    ports:
      - &quot;6381:6379&quot;
    command: [&quot;redis-server&quot;, &quot;--replicaof&quot;, &quot;redis-master&quot;, &quot;6379&quot;, &quot;--appendonly&quot;, &quot;yes&quot;]
    networks:
      - backend

  redis-replica-2:
    image: redis:7.0
    container_name: redis-replica-2
    depends_on:
      - redis-master
    ports:
      - &quot;6382:6379&quot;
    command: [&quot;redis-server&quot;, &quot;--replicaof&quot;, &quot;redis-master&quot;, &quot;6379&quot;, &quot;--appendonly&quot;, &quot;yes&quot;]
    networks:
      - backend

  sentinel-1:
    image: redis:7.0
    container_name: sentinel-1
    depends_on:
      - redis-master
    ports:
      - &quot;26379:26379&quot;
    volumes:
      - ./sentinel:/etc/sentinel
    command: [&quot;redis-server&quot;, &quot;/etc/sentinel/sentinel.conf&quot;, &quot;--sentinel&quot;]
    networks:
      - backend

  sentinel-2:
    image: redis:7.0
    container_name: sentinel-2
    depends_on:
      - redis-master
    ports:
      - &quot;26380:26379&quot;
    volumes:
      - ./sentinel:/etc/sentinel
    command: [&quot;redis-server&quot;, &quot;/etc/sentinel/sentinel.conf&quot;, &quot;--sentinel&quot;]
    networks:
      - backend

  sentinel-3:
    image: redis:7.0
    container_name: sentinel-3
    depends_on:
      - redis-master
    ports:
      - &quot;26381:26379&quot;
    volumes:
      - ./sentinel:/etc/sentinel
    command: [&quot;redis-server&quot;, &quot;/etc/sentinel/sentinel.conf&quot;, &quot;--sentinel&quot;]
    networks:
      - backend

  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - &quot;8080:8080&quot;
    depends_on:
      - sentinel-1
    environment:
      - SPRING_PROFILES_ACTIVE=prod
    networks:
      - backend

volumes:
  redis-master-data:

networks:
  backend:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(이곳에 따로 적지는 않았지만, DB도 도커로 함께 구동하기 위해서 mysql이라는 service를 compose 파일에 추가해 두었습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 sentinel.conf 설정 파일을 작성합니다. 저는 루트 디렉토리에 `sentinel/`이라는 디렉터리를 만들고 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748687436295&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# sentinel.conf
port 26379
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel resolve-hostnames yes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 아까 작성한 application-prod.yml 에 센티넬 내용을 추가해 주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748687506114&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application-prod.yml
spring:
  data:
    redis:
      sentinel:
        master: mymaster
        nodes:
          - sentinel-1:26379
          - sentinel-2:26379
          - sentinel-3:26379

server:
  port: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;최종 흐름은 다음과 같습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748687547568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Spring Boot]
   ↕ Redis Sentinel
   ↕ Redis Master
     ↘ Redis Replica 1
     ↘ Redis Replica 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 다시 이미지부터 컨테이너까지 빌드하고,&amp;nbsp;&amp;nbsp;실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748687936212&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker-compose down &amp;amp;&amp;amp; docker-compose up --build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1631&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brlKW5/btsOmOCJDys/h4qC15ocPSz57ZUbspJiKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brlKW5/btsOmOCJDys/h4qC15ocPSz57ZUbspJiKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brlKW5/btsOmOCJDys/h4qC15ocPSz57ZUbspJiKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrlKW5%2FbtsOmOCJDys%2Fh4qC15ocPSz57ZUbspJiKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1631&quot; height=&quot;381&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1631&quot; data-origin-height=&quot;381&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다 정상적으로 동작하는 것을 확인할 수 있습니다. 중간에 포트번호 같은 건 제 로컬 환경에서 자잘한 변경이 있긴 했지만 기본적인 내용은 같은 방식으로 작성되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;원래 지난 포스팅에서 도커를 다루려고 했는데,&amp;nbsp; 예상치 못한 문제를 해결하는 내용을 포스팅하느라 일주일 정도 늦어졌습니다. 개발은 항상 이렇게 정신 차리고 장애대응을 잘해야 하는 것 같군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;br /&gt;요즘 개발자 수는 넘치는데 더 늘어가고 있고, 개발자 시장은 얻어 붙고 메말라가고 있는데 이런 상황에서도 학습하는 분들이 많으실 것 같습니다. 제가 작성한 내용보고 그대로 코드를 따라 할 수도 있겠지만 본인 설정에 따라 맞춰서 이것저것 커스터마이징 하면서 &quot;내 프로젝트에 맞게&quot; 개발해 보시면 좋을 것 같습니다. &quot;합당한 이유를 찾고, 그것에 맞게 개발하는 개발자&quot;를 목표로 하는 게 좋은 것 같습니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 이번 포스팅에선 간단하게 도커와 Redis 클러스터링을 보여드리기 위해 함께 compose파일에 함께 빌드하고 구동되었지만 실무에서 제대로 된 고가용성을 위해선 이렇게 개발하시면 안 되는 것을 명심해 주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;구동하고 있는 Docker가 다운되는 상황도 생각&lt;/b&gt;하여야 하고, 그러기 위해선 EC2 기준 &lt;b&gt;다른 인스턴스에 Redis 서버를 따로 띄우고&lt;/b&gt; 운영해야 한다는 사실과 어떻게 하면 이 &lt;b&gt;서비스 아키텍처가 유기적으로 연결될지 고민하시면서 개발하시면 좋을 것 같습니다.&lt;/b&gt; 모두 파이팅입니다!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>docker</category>
      <category>redis</category>
      <category>redis cluster</category>
      <category>sentinel</category>
      <category>고가용성</category>
      <category>도커</category>
      <category>백엔드</category>
      <category>센티넬</category>
      <category>실무</category>
      <category>클러스터링</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/121</guid>
      <comments>https://wanglan.tistory.com/entry/%EB%8F%84%EC%BB%A4%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EB%8F%99%ED%95%98%EA%B8%B0ft-Redis-Cluster#entry121comment</comments>
      <pubDate>Sat, 31 May 2025 19:56:59 +0900</pubDate>
    </item>
    <item>
      <title>[트러블 슈팅] 인기게시글 Cache, DB접근 줄이기 ver.2</title>
      <link>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%9D%B8%EA%B8%B0%EA%B2%8C%EC%8B%9C%EA%B8%80-Cache-DB%EC%A0%91%EA%B7%BC-%EC%A4%84%EC%9D%B4%EA%B8%B0-ver2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;안녕하세요!! 왕란입니다~!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;오늘은 2주 만에 포스팅을 하는군요.. 가능한 일주일에 1회 이상 포스팅 하고 싶었는데, 평일에는 회사 + 주말에는 개인개발을 하다 보니 주말 이틀 중 하루는 개발, 하루는 블로그 포스팅 이렇게 하고 있는 상황입니다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;하지만 주말 중 하루라도 놀아버리면 포스팅이 힘들어지네요 ㅠㅠ 개발도 즐겁지만 기념일도 즐거운 것이니 이번만 용서해주십쇼!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;그래도 기념일 아침에도 가볍게 프로젝트를 개선해 보았습니다. 지난주 주말의 수정 사항과 이번 주 새로운 수정사항을 가볍게 포스팅해 보겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;Redis 서버에 자주 조회되는 게시글 등록하기(feat. 인기 게시글)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;지금 제가 개발한 블로그의 메인페이지에서는 게시판의 게시글들이 노출되고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;사용자가 게시글을 클릭할 때마다, 게시글을 ID로 DB에 조회하고 있었는데, 특정 게시물은 자주 조회될 거라는 것을 모두 인지할 거라 생각합니다. (인기게시글에 대한 이야기입니다 )&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;게시글 자체를 조회하는 행위가 너무 많은 네트워크 I/O를 요구한다고 볼 수는 없지만(개인적인 생각입니다) 그래도 사용자가 증가하고, 특정 게시글의 조회가 너무 증가한다면 백엔드 개발자로서 &lt;b&gt;&quot;효율&quot;을 생각하지 않을 순 없겠죠.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;그래서 이번엔 자주 조회되는 게시글을 &quot;인기게시글&quot;로 적용하여 DB로의 조회가 아닌 Redis에 캐시로 등록하여 많이 조회되는 게시글의 DB접근을 줄여보도록 하는 것이 목표로 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;905&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFancV/btsObFtl7LF/2yEEeBSS4u2twNClL87MOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFancV/btsObFtl7LF/2yEEeBSS4u2twNClL87MOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFancV/btsObFtl7LF/2yEEeBSS4u2twNClL87MOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFancV%2FbtsObFtl7LF%2F2yEEeBSS4u2twNClL87MOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;691&quot; height=&quot;725&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;905&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt;과정을 살펴보겠습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;1. 특정게시글을 조회하는 로직에 조회수 증가 로직 추가:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;게시글 ID와 유저정보를 key 값으로 설정하여 한 명의 유저가 조회수를 많이 조회하여 어뷰징 하는 것을 방지합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;한 사람당 조회수 카운트는 3번까지만 오를 수 있도록 했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;게시글이 가지는 조회수 카운트는 Redis에 저장하고, TTL을 3일로 지정합니다.(무한 누적 방지)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. 조회수가 높은 게시글 리스트 반환 로직 추가:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Redis의 ZSet을 활용하여 인기 게시글 상위 5개를 반환하는 로직을 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;인기게시글을 리스트로 반환하고, ZSet의 가중치 계산 로직을 활용하여 효율성을 챙깁니다!!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3~4가 중요합니다!!!&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. 인기게시글을 Redis 서버에 등록해 놓는 로직 추가&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;인기게시글 상위 5개의 타이틀과 컨텐츠를 캐시로 등록해 놓습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;인기게시글 순위가 변동될 수 있으니 스케쥴러로 1시간에 한 번씩 인기게시글을 갱신합니다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;갱신 시 인기게시글 5개의 타이틀과 컨텐츠를 최신화해 줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;4. 인기게시글 리스트에서 &lt;b&gt;&quot;상세 보기&quot;클릭 시 Redis에서 조회하는 API 작성&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;가장 중요한 부분입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;DB에 접근하지 않고 게시글을 가져오도록 합니다. Redis 서버에 등록된 정보를 응답으로 받아 프론트에서 인기 게시글을 렌더링 하도록 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;적용 1. 조회수 증가로직과 높은 게시글 리스트 반환 로직&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748153940446&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class PostViewService(
    private val redisTemplate: RedisTemplate&amp;lt;String, Any&amp;gt;
) {
    private val viewLimitPerUser = 3
    private val log: Logger = LoggerFactory.getLogger(PostViewService::class.java)
    private val zsetKey = &quot;post:viewcount:zset&quot;

    fun increaseViewCount(postId: Long, userEmail: String) {
        val userKey = &quot;viewcount:user:$userEmail:post:$postId&quot;
        val userCount = (redisTemplate.opsForValue().get(userKey) as? Int) ?: 0

        if (userCount &amp;lt; viewLimitPerUser) {
            val todayKey = &quot;post:viewcount:zset:&quot; + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)

            redisTemplate.opsForZSet().incrementScore(todayKey, postId.toString(), 1.0)
            redisTemplate.expire(todayKey, Duration.ofDays(3)) // TTL 3일 설정

            redisTemplate.opsForValue().increment(userKey)
            redisTemplate.expire(userKey, Duration.ofDays(1))
        }
    }

    fun getTopViewedPostIds(limit: Int): List&amp;lt;Pair&amp;lt;Long, Long&amp;gt;&amp;gt; {
        val todayKey = &quot;post:viewcount:zset:&quot; + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)

        val top = redisTemplate.opsForZSet()
            .reverseRangeWithScores(todayKey, 0, (limit - 1).toLong()) ?: return emptyList()

        return top.mapNotNull { tuple -&amp;gt;
            val postId = tuple.value?.toString()?.toLongOrNull()
            val score = tuple.score?.toLong()
            if (postId != null &amp;amp;&amp;amp; score != null) postId to score else null
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1번과 2번의 내용이 반영된 상태입니다. 조회수 관련된 비즈니스로직을 유틸 메서드 적으로 판단하여 PostViewService 클래스에 따로 정의해 두었습니다.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위의 1, 2번 내용까지는 5월 12일에 작성한 바 있습니다. 이때 당시엔 &lt;b&gt;Redis 서버에 조회수를 증가하는 로직&lt;/b&gt;과 &lt;b&gt;조회수 순서대로 게시글 ID를 List로 반환하는 로직&lt;/b&gt;까지만 작성하였고,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;`/posts/popular` 엔드포인트에 요청을 보내면 &lt;b&gt;조회수 높은 순으로 ID를 List로 반환하는 로직&lt;/b&gt;을 호출하여 id 대로 DB에서 조회하는 방식으로 작성했었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이때 들었던 생각이,,,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;어라..? 인기게시글을 기껏 저장하고 List를 만들고 DB에 다시 조회한다고?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;라는 생각이었습니다. 즉 지금은&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. 게시글 조회할 때마다 게시글 ID-유저 ID를 Key로 조회수 증가&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. 총조회수를 받아와서 상위 5개의 게시글 ID리턴&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. 이 ID로 &lt;b&gt;인기 게시글 List를 생성 후 조회할 때마다 DB에 접근&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;???&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이렇게 하면 캐시에 인기게시글을 등록하는 이유가 없습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;인기 게시글은 사람들이 자주 조회하고 DB에 접근이 많아지므로 DB접근 자체를 원천적으로 줄이는 것이 목표였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이 문제점을 해결하기 위해서 3번 4번 항목을 포스팅하는 오늘 새롭게 적용했습니다.(&lt;s&gt;이전엔 캐시에 정보를 담아둔다는 생각에서 그친 게 한심하네요 ㅠㅠ&lt;/s&gt;)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;추가된 개선사항은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748161232550&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PupularPostScheduler.kt

@Component
class PopularPostScheduler(
    private val postViewService: PostViewService,
    private val postRepository: PostRepository,
    private val redisTemplate: RedisTemplate&amp;lt;String, Any&amp;gt;
) {

    // 애플리케이션 시작 직후 1회 실행
    @PostConstruct
    fun initCache() {
        cachePopularPosts()
    }

    @Scheduled(cron = &quot;*/30 * * * * *&quot;) // 매 30초마다 실행
    fun cachePopularPosts() {
        val topViewed = postViewService.getTopViewedPostIds(5)
        println(&quot;  조회수 기준 인기글 ID들: $topViewed&quot;)

        val idToCount = topViewed.toMap()
        val posts = postRepository.findWithUserByIdIn(idToCount.keys)

        println(&quot;  DB에서 조회된 게시글 수: ${posts.size}&quot;)

        val cachedList = posts.map {
            PopularPostCacheResponse(
                id = it.id!!,
                title = it.title,
                content = it.content,
                nickname = it.user.nickname,
                createdAt = it.createdAt.toString()
            )
        }

        println(&quot;Redis에 저장할 캐시 데이터: $cachedList&quot;)

        redisTemplate.opsForValue().set(&quot;popular:posts&quot;, cachedList)
        cachedList.forEach {
            redisTemplate.opsForValue().set(&quot;popular:post:${it.id}&quot;, it)
        }

        println(&quot;Redis 캐싱 완료 (popular:posts, popular:post:*)&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;1. 스케쥴러를 사용하여 Redis 서버에 등록되어 있는 인기게시글을 Top 5대로 불러옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;2. 게시글의 정보를 DB에서 불러옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;3. 정보를 Redis 서버에 등록합니다.(현재는 테스트를 위해 30초마다 갱신하도록 했지만 실 서비스에서는 30분~1시간으로 잡아도 괜찮을 것 같습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;중간에 직렬화, 역직렬화하는 과정에 꼬인 부분이 있어서 불필요한 디버깅용 Print가 있습니다. 양해해 주세요 ㅎ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위 방식을 통해 인기 게시글을 꾸준히 Redis 서버에 데이터를 저장해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;그리고 프론트엔드에서 요청을 보낼 새로운 엔드포인트를 작성합니다. 저는 다음 두 곳으로 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;/posts/popular/cached&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;/posts/popular/cached/{postId}&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;사실 cached 까진 필요 없고 좀 RESTful 하지 않은 느낌이 들지만 이미 바보같이 만들어 놓은 /posts/popular 엔드포인트가 존재하기 때문에 위와 같이 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748161732293&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // 인기 게시글 캐시 기반 목록 조회
    @GetMapping(&quot;/popular/cached&quot;)
    fun getPopularPostsFromCache(): ResponseEntity&amp;lt;List&amp;lt;PopularPostCacheResponse&amp;gt;&amp;gt; {
        val cachedPosts = postService.getPopularPostsFromCache()
        return ResponseEntity.ok(cachedPosts)
    }

    // 인기 게시글 캐시 기반 상세 조회
    @GetMapping(&quot;/popular/cached/{postId}&quot;)
    fun getPopularPostDetailFromCache(@PathVariable postId: Long): ResponseEntity&amp;lt;PostResponse&amp;gt; {
        val post = postService.getPopularPostDetailFromCache(postId)
        return ResponseEntity.ok(post)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;Controller 단에서 위와 같이 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;다음은 비즈니스 로직입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748161832814&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    fun getPopularPostsFromCache(): List&amp;lt;PopularPostCacheResponse&amp;gt; {
        val raw = redisTemplate.opsForValue().get(&quot;popular:posts&quot;)
        println(&quot;  [Redis 조회] popular:posts &amp;rarr; 타입: ${raw?.javaClass}, 값: $raw&quot;)

        return when (raw) {
            is String -&amp;gt; {
                println(&quot;⚠️ popular:posts는 JSON 문자열입니다 &amp;rarr; Jackson으로 파싱&quot;)
                try {
                    objectMapper.readValue(
                        raw,
                        object : TypeReference&amp;lt;List&amp;lt;PopularPostCacheResponse&amp;gt;&amp;gt;() {}
                    ).also {
                        println(&quot;✅ [역직렬화 성공] 파싱된 인기글 개수: ${it.size}&quot;)
                    }
                } catch (e: Exception) {
                    println(&quot;❌ 역직렬화 실패: ${e.message}&quot;)
                    emptyList()
                }
            }

            is List&amp;lt;*&amp;gt; -&amp;gt; {
                println(&quot;✅ popular:posts는 객체 리스트입니다&quot;)

                @Suppress(&quot;UNCHECKED_CAST&quot;)
                (raw as List&amp;lt;Map&amp;lt;String, Any&amp;gt;&amp;gt;).mapNotNull { map -&amp;gt;
                    try {
                        PopularPostCacheResponse(
                            id = (map[&quot;id&quot;] as Number).toLong(),
                            title = map[&quot;title&quot;].toString(),
                            content = map[&quot;content&quot;].toString(),
                            nickname = map[&quot;nickname&quot;].toString(),
                            createdAt = map[&quot;createdAt&quot;].toString()
                                .let { LocalDateTime.parse(it).format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm&quot;)) }
                        )
                    } catch (e: Exception) {
                        println(&quot;❌ 리스트 항목 파싱 실패: ${e.message}&quot;)
                        null
                    }
                }
            }

            else -&amp;gt; {
                println(&quot;❌ popular:posts 캐시가 예상 형식이 아닙니다. &amp;rarr; ${raw?.javaClass}&quot;)
                emptyList()
            }
        }
    }

    fun getPopularPostDetailFromCache(postId: Long): PostResponse {
        val key = &quot;popular:post:$postId&quot;
        val cached = redisTemplate.opsForValue().get(key)

        println(&quot;  [Redis 조회] 키 $key &amp;rarr; $cached&quot;) // 디버깅

        val map = cached as? Map&amp;lt;*, *&amp;gt; ?: run {
            println(&quot;❌ 캐시에서 인기 게시글을 찾을 수 없습니다. (postId=$postId)&quot;) // 디버깅
            throw GlobalException(ExceptionCode.NOT_FOUND_POST)
        }

        val post = PostResponse(
            id = (map[&quot;id&quot;] as Number).toLong(),
            title = map[&quot;title&quot;].toString(),
            content = map[&quot;content&quot;].toString(),
            nickname = map[&quot;nickname&quot;].toString(),
            createdAt = map[&quot;createdAt&quot;]?.toString()?.let { LocalDateTime.parse(it) },
            modifiedAt = null
        )

        println(&quot;✅ [결과] 인기 게시글 상세 반환 &amp;rarr; id=${post.id}, title=${post.title}&quot;) // 디버깅
        return post
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;역시 직렬화, 역직렬화 때문에 많은 디버깅 Print 문이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;`getPopularPostsFromCache()` 메서드에서 프론트에 그릴 List를 Redis 서버에서 가져오도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이때 popular:posts라는 key는 객체 리스트임이 확실하게 설정되어 있지만 처음 설계할 때 헷갈리는 부분 때문에 분기를 해놓았습니다. 이 객체 리스트에서 PopularPostCacheResponse라는 응답 DTO로 매핑하여 프론드엔드로 전달하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A2XhM/btsObK2sxuH/SXAXZPC5MPQ34bxhznafRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A2XhM/btsObK2sxuH/SXAXZPC5MPQ34bxhznafRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A2XhM/btsObK2sxuH/SXAXZPC5MPQ34bxhznafRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA2XhM%2FbtsObK2sxuH%2FSXAXZPC5MPQ34bxhznafRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1907&quot; height=&quot;904&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1907&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;개발자 도구 - Network 탭의 엔드포인트를 찾아서 응답을 보면 Json으로 잘 정리되어 Return 되고 있고 Next.js 프론트에서 제가 설계한 대로 인기 포스트를 잘 렌더링 하고 있는 것을 볼 수 있습니다!!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이 개선의 핵심을 확인해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;최신 포스트 카테고리에서 노란색 버튼으로 되어있는 &quot;상세 보기&quot;를 누른다면?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btHUu5/btsObK9bP9W/ZxZMb4KOBEHbjQPKnEbO31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btHUu5/btsObK9bP9W/ZxZMb4KOBEHbjQPKnEbO31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btHUu5/btsObK9bP9W/ZxZMb4KOBEHbjQPKnEbO31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtHUu5%2FbtsObK9bP9W%2FZxZMb4KOBEHbjQPKnEbO31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;833&quot; height=&quot;218&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;위 사진처럼 /posts/16이라는 게시글 아이디에 맞는 엔드포인트에 접근하면서 DB를 조회하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;&lt;b&gt;하지만 인기 포스트 카테고리에서 같은 postId=16인 게시글을 조회한다면?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kA07I/btsObHkqLb2/CFm3OIUApYSRIz77kpcWWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kA07I/btsObHkqLb2/CFm3OIUApYSRIz77kpcWWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kA07I/btsObHkqLb2/CFm3OIUApYSRIz77kpcWWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkA07I%2FbtsObHkqLb2%2FCFm3OIUApYSRIz77kpcWWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;985&quot; height=&quot;161&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;이제 쿼리수도 0, 엔티티 로딩 수도 0, 응답시간도 미세하지만 11ms -&amp;gt; 3ms로 줄었습니다. 당연한 결과입니다. DB에 도달하지 않고 Redis 서버에서 꺼내온 것이니까요!! 제 의도와 목표대로 잘 작성된 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;지금은 11ms -&amp;gt; 3ms 로 퍼센티지를 굳이 따지기도 애매한 개선이지만, 게시글이 작성된 DB의 row 수가 십수만 개라면?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;인기게시글을 조회하기 위해 테이블을 스캔하는 시간에서도, DB의 부하도 너무 많을 테지만, 스케쥴러로 관리하여 Redis 서버에 인기게시글을 이렇게 등록해 놓는다면 3ms의 값이 얼마나 강력한지 알 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;Redis 서버의 안정성에 대한 고찰&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;현재까지 개발된 내용에선 UserDetails도, 인기게시글도, 조회수도 모두 Redis 서버에 등록하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;Redis가 만능인 것 마냥 이렇게 다 등록해 두었는데, Redis 서버가 모종의 이유로 무지개다리를 건넌다면?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;UX가 상당히 안 좋아질 것이라 생각이 듭니다...ㅠㅠ&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;그래서 사실 오늘 포스팅은 인기 게시글의 확실한 캐싱처리가 아닌 Redis-Cluster에 대한 내용을 포스팅하려 했는데, 포스팅을 작성하는 중에 문제를 발견해서 급하게 개선하고 보여드리게 되었습니다.(이렇게 문제를 해결하는 것이 너무 즐겁습니다 ㅎ.ㅎ)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;다음 포스팅은 Docker로 프로젝트를 띄우는 것과, Redis 서버를 Docker로 같이 띄우고 Sentinel을 사용해서 복제 서버를 구현하여 Redis 마스터 서버가 죽었을 때 자동 승격되어 데이터를 보존하도록 하는 내용을 포스팅해 보겠습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: 'Noto Sans Light';&quot;&gt;읽어주시는 분이 하루 1~2명에서 3~4명 정도로 2배나 상승하여 뿌듯한 것 같습니다. 학습하시는 분들 모두 파이팅입니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000; text-align: start;&quot;&gt; 읽어주셔서 감사합니다. &amp;zwj;♂️ &amp;zwj;♀️&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>cache</category>
      <category>caching</category>
      <category>db접근줄이기</category>
      <category>DB최적화</category>
      <category>redis</category>
      <category>redis cache</category>
      <category>백엔드</category>
      <category>주니어개발자</category>
      <category>캐싱</category>
      <category>협업</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/120</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%9D%B8%EA%B8%B0%EA%B2%8C%EC%8B%9C%EA%B8%80-Cache-DB%EC%A0%91%EA%B7%BC-%EC%A4%84%EC%9D%B4%EA%B8%B0-ver2#entry120comment</comments>
      <pubDate>Sun, 25 May 2025 17:53:50 +0900</pubDate>
    </item>
    <item>
      <title>[트러블 슈팅]백엔드개발자가 프론트엔드와 협업하기</title>
      <link>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%B0%B1%EC%97%94%EB%93%9C%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%99%80-%ED%98%91%EC%97%85%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요 개발자 왕라니입니당!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드 개발자가 되기 위해 열심히 공부하는 주니어 개발자분들 많으실 거라고 생각합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드에서 API 개발하면서 다들 다음 같은 생각 하셨을 것 같습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;음.. 응답 Json으로 이 정도 필드들이면 되겠지...? 에이 이런 간단한 계산은 프론트가 하겠지 ㅋ&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;혹시 저만 이런 생각했던 걸까요? ㅎㅎ.. &lt;/span&gt;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드 개발자 꿈나무만 60명 정도 모여서 학습을 하던 부트캠프 시절에선, 짧은 기간 안에 간단한 프로젝트를 하나씩 개발해내야 하다 보니 얼른 API 하나씩 완성하고 서버 측에서 안정적인지 확인하는 것에 초점을 맞추기 때문에 FE와의 협업을 생각해 본 적이 없던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자가 반드시 가져야 하는 소양인 &lt;b&gt;&quot;협업능력&quot;&lt;/b&gt;이 중요하다는 것은 모든 개발자들이 알고 있을 겁니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그런데 협업은, 같은 서버를 개발하고 관리하는 백엔드 개발자끼리 Github에서 컨벤션 맞추기, PR 코드리뷰 규칙 정하기 등의 협업도 있겠지만, 프로젝트 차원에서 프론트엔드 개발자와의 협업도 반드시 존재하기에 이런 경험이 없으면 실무에서 상당히 난감하리라 생각이 듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저도 주니어 개발자로서 일하고 있지만, 서버 측 API 개발에 대한 부분만 집중하느라 FE와의 협업을 고려하지 못했었고, 어떻게 하면 BE - FE 사이에 유연하고 효율적인 협업이 가능할까.. &lt;b&gt;어떻게 하면 FE 개발자와 원만한 소통과 좋은 협업을 할 수 있을까 고민해 보았습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 내린 결론은...!!&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내가 만든 백엔드 서버에 맞게 프론트엔드도 만들어보기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;입니다. 제가 어느 정도 개발해 놓은 서버의 스펙에 맞게 잠시 프론트엔드 개발자가 되어 그 마음을 이해해보려 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: center;&quot;&gt; 여기서 아마 의문이 드실 겁니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&quot;백엔드 개발자가 어떻게 프론트엔드 개발을 하게..?&quot;&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 요즘 개발자 분들은 대충 감이 오실 겁니다. &lt;b&gt;제가 하려는 방식은 바이브 코딩이라고도 합니다. &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요즘은 AI 모델이 개발에도 많이 특화되어 있기 때문에 개발자들이 빠르게 성장하고 빠른 결과를 낼 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 저는 바이브 코딩으로 AI와 함께 간단하고 빠르게 프론트엔드 부분을 개발해 보도록 했고, 개발 과정과 BE - FE을 연결하면서 깨달은 점에 대해서 말해보고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(참고로 저는 FE 개발자가 아니라 개발된 결과물의 퀄리티가 상당히 안 좋을 수는 있지만, 단순히 경험을 위해 작성한 아주 간단한 FE였으니 퀄리티는 눈감고 넘어가주시길 바랍니다 &amp;zwj;♀️ &amp;zwj;♂️)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. FE 개발 스택 정하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 저의 입장에선 뭘로 개발하느냐는 중요하지 않았습니다. 그래도 제가 개발 중인 프로젝트와 다양한 저의 환경에 따라 프론트부분을 개발할 수 있는 다양한 언어들 중 Next.js를 선택하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;선택한 근거는 다음과 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 빠른 구축(풀스택 개발에 유리함, 폴더기반 쉬운 라우팅)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. SSR&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 입문이 쉬움&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저의 입장에선 이 세 개가 매력적이었습니다. 이중 SSR 은 서버-사이드-렌더링의 약자로, 서버 측에서 클라이언트에게 HTML의 내용을 완성해 전송하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분이&amp;nbsp; 좋았던 건, 나중에 이 웹 사이트가 다른 사람들에게 학습 및 정보전달로 알려지게 되기 쉽다는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;CSR와 같은 클라이언트 측에서 HTML을 완성하는 형태의 웹사이트는 구글 검색 엔진에서 빈 페이지로 인식되는 경우가 많은 반면 SSR 방식의 웹사이트는 이미 HTML 내부에 웹 페이지가 가져야 할 정보들이&amp;nbsp; 다 들어있기 때문에 검색엔진에 노출되기도 유리하다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저에겐 제 스스로의 학습, 그리고 그 학습내용의 기록하는 것도 중요하지만 제가 성장하면서 경험한 내용을 다른 사람도 같이 즐기고 느꼈으면 좋겠다는 생각으로 블로그 포스팅을 하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하루에 2명 이하의 사람들이(&lt;s&gt;사실상 0명에 수렴 중)&lt;/s&gt; 제 글을 봐주시고 있지만 점점 제 프로젝트가 개발되어 갈 때마다 읽는 분들이 필요로 한다면 서버도 배포하여 구경하고 이용할 수 있게 하는 것이 최종목표입니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. 내 BE 개발 상황 살펴보기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;우선 웹에서 기본적인 소통을 담당하는 게시판 기능을 만들어 놓았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;제 상상으로는, 메인 페이지에 들어가면 화면 중간에 게시글 목록이 있고 각각의 게시글을 클릭하여 내용을 볼 수 있는 전형적인 게시판을 상상하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이걸 위해서 기본적으로 게시판 관련 CRUD 기능은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 작성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 수정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 삭제(Soft Delete)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 조회&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 전체 조회(Soft Delete 제외, 이하 동일)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내 게시글 전체 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;게시글 단건 조회&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특정 유저의 게시글 전체 조회&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조회 관련 API가 많은 상황입니다. 다음 사진은 제 상상에서의 메인페이지 UI입니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;857&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWr85c/btsNS4Neu6k/1wIbBJK7rUneVqaB91lUF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWr85c/btsNS4Neu6k/1wIbBJK7rUneVqaB91lUF0/img.png&quot; data-alt=&quot;구구는 제 프로필 사진중 펭귄역할을 담당하는 악당 분의 이름입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWr85c/btsNS4Neu6k/1wIbBJK7rUneVqaB91lUF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWr85c%2FbtsNS4Neu6k%2F1wIbBJK7rUneVqaB91lUF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;478&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;857&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구구는 제 프로필 사진중 펭귄역할을 담당하는 악당 분의 이름입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대충 위와 같은 형태를 상상하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 메인화면에서 게시글을 &lt;b&gt;페이지네이션을 적용하여(한 번에&lt;/b&gt; 너무 많은 데이터를 불러오는 것을 방지) 리스트업 하고, 단건의 게시글을 들어가서 볼 수 있게 하고 싶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일단 혼자 개발하는 작은 프로젝트이고, 지금 FE 부분은 제 상상에 맡긴 채로 BE 개발을 진행했기 때문에 전체 게시글 조회의 Response Entity에는 다음과 같은 필드들을 포함하게 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746929969480&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// PostSummaryResponse.kt
{
    &quot;title&quot;: &quot;게시글 제목&quot;,
    &quot;nickname&quot;: &quot;왕란&quot;,
    &quot;createdAt&quot;: &quot;2025-05-01&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 메인화면에서 포함될 게시글 하나의 정보들을 3개의 필드로 정하고, Page에 List로 담아 return 하게 했습니다.&lt;span style=&quot;color: #9d9d9d;&quot;&gt;(읽고 계신 분은 알 겁니다. 아주 중요한 정보를 빠트렸다는 것을..)&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그렇게 했더니 위 사진과 같은 페이지를 구현하는데에 아무런 문제가 없었고 깔끔하게 리스트가 나오더군요. 그래서 상세 보기 버튼을 눌렀는데?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;404... 가 나왔습니다. 뭐가 문제지?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(사실 이때, Spring Security의 필터 문제인가? 그럼 403 일 텐데 Post 객체를 프론트에서 만들어서 렌더링 하고 있는데 왜 안되지..라는 고민을 했습니다)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746930981528&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;button
  onClick={() =&amp;gt; goToPost(post.id)}
  className=&quot;bg-blue-400 text-white px-3 py-1 rounded hover:bg-blue-500&quot;
&amp;gt;
  상세보기
&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 게시글 단건의 상세내용을 보기 위해 사용하는 API의 Controller 단의 작성된 내용은 다음과 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746931324672&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @GetMapping(&quot;/posts/{postId}&quot;)
    fun getPostById(@PathVariable postId: Long): ResponseEntity&amp;lt;PostResponse&amp;gt; {
        val post = postService.getPostsById(postId)
        return ResponseEntity.ok(post)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 내용을 따르자면, 메인페이지에 있는 게시글의 ID 값을 가지고 /posts/{postId} 엔드포인트로 단건 조회 요청을 보내게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;근데 백엔드에선 Post 객체를 다루는 중이고 이렇게 post.id를 가지고 요청을 하는데 왜 `/post/undefined`로 라우팅되고 있는 거지..?라고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그런데 정말 바보같이 메인페이지의 게시글 전체 조회를 했을 때 사용하고 있는 `PostSummaryResponse`의 json 필드값들이&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제목, 작성자닉네임, 작성일 세 개밖에 없다는 걸 인지했습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 메인페이지에선 단건 게시글 상세 보기 기능을 구현하지 않을 것이라면 이렇게 3개의 필드값만 return 하여도 문제없지만 &lt;b&gt;메인페이지에서 바로 게시글 단건조회 API를 호출할 것이라면 Response에 ID 값도 넣어서 return 해야 하구나&lt;/b&gt; 라는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기서 느낀 점은,&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프론트엔드와 개발 전 와이어프레임에 대해 충분히 회의하고 API 명세를 작성한 후 그것에 맞게 요청 - 응답을 설계해야겠다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 생각이 들었습니다. 물론 당연한 이야기입니다. 하지만 제 프로젝트의 백엔드 개발을 혼자 하면서 상상으로 프로젝트를 설계하니 이런 말도 안 되는 문제를 내게 되었던 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;결론적으로..&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;대충 Post 객체를 보내니 프론트에선 필요한 정보만 갖다 쓰면 되겠지, 응답 Json에 대충 필드를 다 넣어주면 알아서 정제해서 쓰겠지.라는 이기적이고 막연한 생각을 접을 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이외에도 포스트맨으로 요청을 보내고 응답을 받아서 &quot;아 제대로 동작하는구나~&quot; 하고 넘어가는 것이 아닌 &lt;b&gt;실제 서비스에서 어떻게 동작하는지, 프론트에선 어떻게 백엔드에서 return 한 정보를 사용하는지 좀 더 생각하면서 개발하게 된 계기&lt;/b&gt;가 된 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 백엔드 개발자이기 때문에, 프론트엔드를 개발하는 역량 자체를 제 핵심 스킬로 키울 생각은 아직은 없습니다. 하지만 제가 좀더 테크니컬한 리팩토링을 진행하거나, 안정성, 성능개선을 진행하면서 테스트를 위해서 종종 프론트 부분의 개발도 진행해 보려고 합니다. (비율은 BE 5: FE 1 정도로 생각하고 있습니다. API 5개 개발하면 화면 구현하여 응답 속도, 반응 테스트 등등)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Next.js로 개발을 진행하니 저같이 한 번도 프론트 개발을 해본 적 없는 초보도 바이브 코딩으로 개발이 가능하고, BE - FE의 소통법, 프로젝트 설계법을 알 수 있어서 좋았던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제 글을 읽는 백엔드 개발자분이던 프론트엔드 개발자분이던 프로젝트 설계 단계에서 충분한 소통을 하고 문제없이 원활한 개발을 할 수 있으면 좋겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다. 다음엔 인기 게시글을 캐싱하는 내용으로 돌아오겠습니다!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>API명세</category>
      <category>Next.js</category>
      <category>SSR</category>
      <category>바이브코딩</category>
      <category>백엔드개발자</category>
      <category>주니어개발자</category>
      <category>트러블슈팅</category>
      <category>풀스택</category>
      <category>프론트엔드개발자</category>
      <category>협업</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/119</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85%EB%B0%B1%EC%97%94%EB%93%9C%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%99%80-%ED%98%91%EC%97%85%ED%95%98%EA%B8%B0#entry119comment</comments>
      <pubDate>Sun, 11 May 2025 12:03:05 +0900</pubDate>
    </item>
    <item>
      <title>[트러블 슈팅] 인증/인가 캐싱처리로 DB접근 줄이기</title>
      <link>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%BA%90%EC%8B%B1%EC%B2%98%EB%A6%AC%EB%A1%9C-DB%EC%A0%91%EA%B7%BC-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요 왕라니입니다! 오늘은 어린이날이라 월요일인데 즐거운 휴무입니다! &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;몸 컨디션도 좋지않고 날도 흐린 휴일인데 집에서 개발하면서 스트레스를 풀기 좋은날인것 같습니다.. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;토요일부터 집에서 유튜브보면서 개발만 하고있는데 생업과 취미가 같은건 좋은일인것 같습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아무튼 각설하고 오늘 해보려고 하는건 Spring Security 를 사용하여 인증/인가를 진행하고 있는 프로젝트의 UserDetails 의 정보를 캐싱처리하여 DB에 접근하는 횟수를 줄이는것을 목표로 하는 것입니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(일단 배포하는 서버가 아니기에 Hibernate 통계를 보는 내용도 추가하여 개선이 되는지도 확인을 해보았습니다!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;문제인식&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QHkRC/btsNL8ICg42/C7Oawk0yrdOodaVmBm0LZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QHkRC/btsNL8ICg42/C7Oawk0yrdOodaVmBm0LZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QHkRC/btsNL8ICg42/C7Oawk0yrdOodaVmBm0LZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQHkRC%2FbtsNL8ICg42%2FC7Oawk0yrdOodaVmBm0LZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;831&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지난 포스팅에서 API 요청을 보내는데 한번에 세개의 쿼리를 보내는 &lt;s&gt;재밌는(?)&lt;/s&gt; 현상이 발견되었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 이 상황의 이유를 살펴보았습니다. 현재 Spring Security 를 사용하고 있고, `UserDetailsService`를 구현하여 사용하고 있는 상황입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 AuthenticationFilter 와 AuthorizationFilter 를 따로 작성하여 로그인 시 Authentication 과정을 진행하고 로그인 이후 API 요청에서 인증된 유저인지 확인 후 Authorization 을 진행합니다. 인가를 진행할 때 필터에서 아래 메서드를 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746419398403&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun createAuthentication(username: String): Authentication {
    val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)
    return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;특히 구현해 놓은 `userDetailsService` 에서 loadUserByUsername 메서드를 실행할 때 UserRepository 를 접근하여 DB에 등록된 유저인지 확인하는 로직을 거칩니다. 아래의 로직입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746419520499&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class UserDetailsServiceImpl(
    private val userRepository: UserRepository
) : UserDetailsService {
    @Transactional
    override fun loadUserByUsername(email: String): UserDetails {
        val user = userRepository.findByEmail(email)
                .orElseThrow { UsernameNotFoundException(&quot;존재하지 않는 유저 이메일입니다.&quot;) }

        if (user.deletedAt != null) { // 삭제된 유저가 다시 접속하면 null 로 바뀌고 스케줄러에서 벗어남
            user.deletedAt = null
            userRepository.save(user)
        }

        return UserDetailsImpl(user)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다양한 인증/인가 방식이 있겠지만, 일단 지금의 로직에선 인증된 유저에게 인가를 진행하기 때문에 메인이 되는 비즈니스 로직에서 DB에 접근을 하면 최소 2번이상 쿼리를 보내게 됩니다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;아주 바보같은 설계인 것 같습니다.&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 이 `loadUserByUsername`을 수정하여 이 현상을 해결하고자 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해결 과정&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 UserDetails 정보를 저장할 캐시를 고민하다가 Redis 서버에 저장해보자고 결정했습니다. 2차 캐시에 저장할까 고민이 되었지만 Redis 서버를 다양하게 개선할 계획이 있기때문에 선택하게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조금 늦게 알았지만 &lt;a href=&quot;https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/caching.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Security 공식문서&lt;/a&gt;를 참고하니 CachingUserDetailsService 를 지원하고있더군요. 우선 오늘 적용한 내용을 여러 부분으로 비교하면서 추후에 Caching UserDetails 가 효율적이라면 적용하는 것도 고려중입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 Redis 설정 및 Cache 사용설정부터 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;build.gradle.kts&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746420697727&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    ...
    implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis 의존성을 추가해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증/인가 로직 흐름을 설명해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로그인 할 때 AuthenticationFilter에 접근합니다. 이때 Spring Security 는 제가 오버라이드 해놓은 attemptAuthentication 메서드에서 인증을 시도하게 되는데, 이때 SecurityConfig 에 정의해놓은 `DaoAuthenticationProvider()`가 UserDetailsService 에서 loadUserByUsername 메서드를 호출하며 처음 DB에 접근합니다. DB의 유저정보와 비교한 뒤 로그인에 성공하면 `UserDetailsImpl` 로 반환되고 `UsernamePasswordAuthenticationToken`을 생성하여 SecurityContext 에 UserDetails 를 저장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인가과정은 UserDetailsService 를 구현한 클래스에 기존의 `loadUserByUsername` 메서드는 DB에서 유저 정보를 조회하는 방식을 유지했습니다. 그리고 `loadUserForAuthorization` 메서드를 새로 정의한 뒤 인가를 처리할 떈 Redis 에 접근하여 유저 정보를 가져오도록 했고, 정보를 가져오는데 실패하면(Redis 서버에 유저 정보가 없을 때) `loadUserByUsername`메서드로 fallback 하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;좀 복잡해 보이는데 전체 인증 흐름을 요약해서 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증과정(Authentication)&lt;br /&gt;[클라이언트 요청] &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;[UsernamePasswordAuthenticationFilter] &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr; &lt;br /&gt;attemptAuthentication() &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr;&lt;br /&gt;[AuthenticationManager] &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr;&lt;br /&gt;[DaoAuthenticationProvider] &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr;&lt;br /&gt;loadUserByUsername() &amp;rarr; 로그인 시에는 DB에 접근&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr; &lt;br /&gt;passwordEncoder.matches() &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr;&lt;br /&gt;성공: UsernamePasswordAuthenticationToken 반환 &lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt; &amp;darr; &lt;br /&gt;successfulAuthentication() &amp;rarr; JWT 생성 + 응답 헤더 설정&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #666666; text-align: left;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;darr;&lt;/span&gt; &lt;br /&gt;redisTemplate 에 cachedUser를 저장&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인가과정(Authorization)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;[클라이언트 요청]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;[AuthorizationFilter]&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;&lt;/span&gt;Authorization Header 에서 토큰 추출&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;토큰 정보와 새로 정의된 loadUserForAuthorization() 으로 검증&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis 에 저장된 cachedUser 와 비교&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SecurityContext 에 authentication 저장&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 처리하도록 목표했습니다. 따라서 코드는 다음과 같이 작성됩니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746424319467&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthenticationFilter.kt
    override fun successfulAuthentication(
        request: HttpServletRequest,
        response: HttpServletResponse,
        chain: FilterChain,
        authResult: Authentication
    ) {
        log.info(&quot;로그인 성공 및 JWT 생성&quot;)
        ... 토큰에 저장 생략

        // 여기서 유저 캐싱
        val cachedUser = CachedUser(
            id = userId,
            email = email,
            password = userDetails.password,
            userRole = role,
            deletedAt = null
        )
        redisTemplate.opsForValue().set(&quot;user:$email&quot;, cachedUser)

        // 응답 헤더에 Authorization 설정
        response.setHeader(&quot;Authorization&quot;, token)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증 후 Redis 에 cachedUser 를 저장하게 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그리고 인가를 진행하는 과정엔 Redis 서버에 먼저 조회하게 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746424427266&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AuthorizationFilter.kt
private fun createAuthentication(username: String): Authentication {
    val userDetails: UserDetails = userDetailsService.loadUserForAuthorization(username)
    return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구현한 UserDetailsServiceImpl 을 다음과같이 수정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746424560362&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
class UserDetailsServiceImpl(
    private val userRepository: UserRepository,
    private val redisTemplate: RedisTemplate&amp;lt;String, Any&amp;gt;,
    private val objectMapper: ObjectMapper
) : UserDetailsService {

    private val log = LoggerFactory.getLogger(&quot;UserDetailsImpl&quot;)

    // 로그인 인증 시 직접 DB에서 조회
    @Transactional(readOnly = true)
    override fun loadUserByUsername(email: String): UserDetails {
        val user = userRepository.findByEmail(email)
            .orElseThrow { UsernameNotFoundException(&quot;존재하지 않는 유저입니다.&quot;) }

        if (user.deletedAt != null) {
            throw IllegalStateException(&quot;삭제된 유저입니다.&quot;)
        }

        return UserDetailsImpl(
            id = user.id!!,
            email = user.email,
            password = user.password,
            role = user.userRole
        )
    }

    // JWT 검증 시 Redis &amp;rarr; fallback DB
    fun loadUserForAuthorization(email: String): UserDetails {
        val key = &quot;user:$email&quot;

        val rawUser = redisTemplate.opsForValue().get(key)

        if (rawUser != null) {
            try {
                val cachedUser = objectMapper.convertValue(rawUser, CachedUser::class.java)
                if (cachedUser.deletedAt != null) {
                    throw IllegalStateException(&quot;삭제된 유저입니다.&quot;)
                }
                return UserDetailsImpl(
                    id = cachedUser.id,
                    email = cachedUser.email,
                    password = cachedUser.password,
                    role = cachedUser.userRole
                )
            } catch (e: Exception) {
                log.warn(&quot;Redis 캐시 역직렬화 실패 &amp;rarr; DB fallback 시도: ${e.message}&quot;)
            }
        }

        log.info(&quot;Redis 캐시 없음 &amp;rarr; DB 조회로 fallback&quot;)
        return loadUserByUsername(email)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인가를 진행할 때는 Redis 서버를 먼저 조회하고, 서버에 문제가 발생해서 cachedUser가 없거나 하는 문제가 있으면 기존의 loadUserByUsername 메서드를 fallback 하도록 설정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 한 이유는 &lt;b&gt;로그인 이외의 모든 요청이 인가필터를 거치는데&lt;/b&gt; 반드시 Redis 만 확인하여 하는 것이 아닌 &lt;b&gt;상황에 따라 유연하게 DB도 조회는 가능하게 하되, DB접근을 최소한으로 하는 방식&lt;/b&gt;이 Redis 를 우선으로 접근하게 하기라고 생각했기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;&quot;&gt;아직 개선하거나 리팩토링 할 내용이 많이 필요할 것 같긴하지만, 에러 로깅을 위해 로깅필터도 추가하고, Redis 에 객체를 저장하고 꺼내면서 직렬화 역직렬화를 거치면서 다양한 문제가 생겨서 지금과 같은 방식으로 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;성능 평가 및 결과&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;&quot;&gt;성능이 달라졌는지 확인하기 위해 요청-응답에 걸린 시간과 조회한 엔티티 수, 보낸 쿼리 수를 로그로 찍어보았습니다.&lt;/span&gt;&lt;span style=&quot;&quot;&gt;같은 상황으로 요청을 수작업으로 50번 정도 테스트 해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vc0N6/btsNKxQorUh/GS6neztFFrVbzS6PNmZu1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vc0N6/btsNKxQorUh/GS6neztFFrVbzS6PNmZu1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vc0N6/btsNKxQorUh/GS6neztFFrVbzS6PNmZu1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVc0N6%2FbtsNKxQorUh%2FGS6neztFFrVbzS6PNmZu1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;859&quot; height=&quot;144&quot; data-origin-width=&quot;859&quot; data-origin-height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Redis 서버의&amp;nbsp; DB에 접근하는 방식을 적용하기전엔 처음 API 요청을 보낼 때 TTFB(Time To First Byte) 80~90ms 가 나오고 이후 요청은 10~15ms 의 성능이 나왔었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이후 Redis 에 접근하는 방식을 적용하고 나서는 TTFB 기준 30~40ms 가 나오고 이후 요청은 10~15ms 의 성능이 나왔었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정확한 성능 개선의 평가가 어렵긴 하지만 우선 확실한 것은 요청 쿼리 수가 확실히 1번으로 줄었다는 것 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 User 테이블이 매우 커지고, 풀 스캔을 진행하여 DB에서 조회하는 로직이었다면 차이는 드라마틱 해졌을 것이라고 생각됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오늘의 UserDetails 캐싱 처리는 여기까지 하고, 여러 테스트나 상황을 살펴본 뒤 Spring Security 의 공식문서에서 정의된 캐싱방식도 적용해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>caching</category>
      <category>db조회줄이는법</category>
      <category>redis</category>
      <category>springsecurity</category>
      <category>userdetails cache</category>
      <category>백엔드</category>
      <category>성능개선</category>
      <category>캐싱</category>
      <category>쿼리수줄이기</category>
      <category>트러블슈팅</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/118</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EC%BA%90%EC%8B%B1%EC%B2%98%EB%A6%AC%EB%A1%9C-DB%EC%A0%91%EA%B7%BC-%EC%A4%84%EC%9D%B4%EA%B8%B0#entry118comment</comments>
      <pubDate>Mon, 5 May 2025 15:16:51 +0900</pubDate>
    </item>
    <item>
      <title>Join Fetch 를 사용하여 N+1 문제 해결하기</title>
      <link>https://wanglan.tistory.com/entry/Join-Fetch-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-N1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요. 낮에 포스팅을 하나 했는데 프로젝트 둘러보다가 운이 좋게도(?) N+1 문제를 발견하여 해결하는 간단한 과정을 보여드리고자 작성을 시작합니다. &lt;a href=&quot;https://wanglan.tistory.com/entry/JPA-%EC%99%80-N1-%EB%AC%B8%EC%A0%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;N+1 문제에 대한 자세한 내용&lt;/a&gt;은 따로 포스팅으로 작성해놓았으니 읽어주시면 감사하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;사실 제가 프로젝트 시작부터 코드를 제대로 작성하지 않아서 발생한 게 큰 부분이지만, &quot;읽어주시는 분들에게 정보제공을 위해 일부러 이상하게 작성했다&quot;라고 믿어주시고 봐주셨으면 좋겠습니다!  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 상황&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;749&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hdpa4/btsNJY8neI4/B0WWkOI1AM5VTrWQe0ddy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hdpa4/btsNJY8neI4/B0WWkOI1AM5VTrWQe0ddy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hdpa4/btsNJY8neI4/B0WWkOI1AM5VTrWQe0ddy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHdpa4%2FbtsNJY8neI4%2FB0WWkOI1AM5VTrWQe0ddy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;512&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;749&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span style=&quot;&quot;&gt;필터를 통해 로그인 된 유저가 요청을 보내면 위 사진과 같이 게시글이 리스트로 반환되고 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;&quot;&gt;...그런데 서버 측에서 로그를 보니 다음 사진과 같이 쿼리가 나가고 있더라고요.. &lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohFVc/btsNJG7Z60v/mvJv5btQKKW4NLOOaB0NyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohFVc/btsNJG7Z60v/mvJv5btQKKW4NLOOaB0NyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohFVc/btsNJG7Z60v/mvJv5btQKKW4NLOOaB0NyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohFVc%2FbtsNJG7Z60v%2FmvJv5btQKKW4NLOOaB0NyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;831&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Hibernate가 세개...?&lt;/span&gt; &lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;형이왜거기서나와.jpg&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5IXdw/btsNLTR5eJQ/t0FmzgkiHQ4TqutrHqr7s1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5IXdw/btsNLTR5eJQ/t0FmzgkiHQ4TqutrHqr7s1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5IXdw/btsNLTR5eJQ/t0FmzgkiHQ4TqutrHqr7s1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5IXdw%2FbtsNLTR5eJQ%2Ft0FmzgkiHQ4TqutrHqr7s1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;169&quot; data-filename=&quot;형이왜거기서나와.jpg&quot; data-origin-width=&quot;299&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보여드리기 부끄럽긴 한데 그래도 어쩔 수 없습니다. 쿼리가 왜 저렇게 나오는걸까요..?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 첫 번째 쿼리&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;일단 첫번째 쿼리는 Spring Security를 사용하고 있어서 Filter 단에서 로그인된 유저의 정보를 검증하는 데에 쿼리가 한번 날아가게 됩니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0pu4Y/btsNLCbUGIc/9S6UkLkwTlqszWxskmyLLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0pu4Y/btsNLCbUGIc/9S6UkLkwTlqszWxskmyLLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0pu4Y/btsNLCbUGIc/9S6UkLkwTlqszWxskmyLLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0pu4Y%2FbtsNLCbUGIc%2F9S6UkLkwTlqszWxskmyLLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;389&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;구현해 놓은 이 부분에서 첫 번째로 요청을 보내면서 쿼리가 발생하게 되는것 입니다. 이 내용은 추후에 Redis 에 로그인된 유저 정보를 저장하는 방식으로 개선하거나 로컬 캐시 등을 활용해서 처음 이후 쿼리를 보내지 않게 개선해보도록 하겠습니다. 그럼 첫번째 쿼리는 넘어가겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 두 번째 쿼리&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746271689059&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Transactional
    fun getMyPosts(userDetails: UserDetailsImpl): List&amp;lt;PostResponse&amp;gt; {
        // PostRepository를 사용하여 userId로 게시물 조회
        val posts = postRepository.findByUserId(userDetails.getUser().id!!)

        return posts.map {
            PostResponse(
                id = it.id!!,
                title = it.title,
                content = it.content,
                createdAt = it.createdAt,
                modifiedAt = it.modifiedAt,
                nickname = it.user.nickname
            )
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;내가 쓴 전체 게시글을 불러오는 기능에서 postRepository에서 DB에 쿼리를 한번 보내게 됩니다. 이 행동이 목표니 두 번째로 발생했던 쿼리가 메인이었던 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 세 번째 쿼리&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746271840154&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;posts&quot;)
class Post(
    @Column(nullable = false)
    var title: String,
    @Column(nullable = false)
    var content: String,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_Id&quot;)
    var user: User
) : Timestamped() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    constructor() : this(&quot;&quot;, &quot;&quot;, User())

    fun updateContent(newContent: String) {
        this.content = newContent
        this.modifiedAt = LocalDateTime.now()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1746272115155&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;users&quot;)
class User(
    //기본  필드 생략
) : Timestamped() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @OneToMany(mappedBy = &quot;user&quot;, fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true)
    val posts: List&amp;lt;Post&amp;gt; = mutableListOf()

    // JPA 를 위한 public 기본 생성자
    constructor() : this(&quot;&quot;, &quot;&quot;, &quot;&quot;, UserRole.USER, &quot;&quot;)

    // 메서드 생략
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Post와 User Entity입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 `User` 엔티티에서 posts 컬렉션에 접근할 때 세 번째 쿼리가 발생하게 됩니다.&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;User 엔티티에서 posts를 Lazy 로딩으로 설정해 두었기 때문에, user.posts 에 접근할 때 실제&amp;nbsp; 데이터베이스에서 User와 관련된 Post들을 로딩하려고 시도하는 시점에서 추가적인 쿼리가 발생하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 쿼리가 발생한 이유는 `Post` 엔티티에 user 정보가 또 Lazy 로딩으로 설정되어 있기 때문입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;즉, Post를 조회하고 나서 User 와 관련된 데이터를 실제로 로딩할 때 쿼리가 발생합니다. 좀 복잡하군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;해결방법은?!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위에 하이퍼링크로 달아놓은 글에서 다양한 방법이 작성되어 있었지만 이번 포스팅에서는 `Join Fetch` 방법으로 해결해보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Lazy 로딩 문제를 해결하려면, User 와 Post 를 함께 조회할 수 있도록 쿼리를 설계하면 됩니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746272458867&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PostRepository : JpaRepository&amp;lt;Post, Long&amp;gt; {

    @Query(&quot;SELECT p FROM Post p JOIN FETCH p.user WHERE p.user.id = :userId&quot;)
    fun findByUserId(userId: Long): List&amp;lt;Post&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기존에 작성되어 있던 findByUserId 메서드 상단에 @Query 어노테이션을 추가하여 JOIN FETCH 부분으로 한 번에 두 개다 조회합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8s4Wm/btsNKwpZfr3/A1gTAd3GaDN7nDEWJ8Zzdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8s4Wm/btsNKwpZfr3/A1gTAd3GaDN7nDEWJ8Zzdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8s4Wm/btsNKwpZfr3/A1gTAd3GaDN7nDEWJ8Zzdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8s4Wm%2FbtsNKwpZfr3%2FA1gTAd3GaDN7nDEWJ8Zzdk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;845&quot; height=&quot;837&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 하면 3개로 날아가던 쿼리가 2개로 줄었습니다. postRepository에 조회 쿼리를 보낼 때, user 도 같이 가져오기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쿼리에서 select 되는 부분이 길어져서 비효율적인 거 아닌가?라는 생각이 드실 수 있지만, 실제 DB에 접근하는 횟수를 줄여 DB문을 두드리는 것을 줄이는 게 훨씬 효율적이기 때문에 이 방식이 선호됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 JOIN FETCH 방식으로 N+1 문제를 해결하는 포스팅을 작성해 보았습니다. 백엔드 개발자분들이라면 틈틈이 꼭 어떤 쿼리가 발생하고 있는지 확인하시길 바랍니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;다음 포스팅은 캐시나 Redis 서버에 로그인된 정보를 저장해 두었다가 DB까지 도달하지 않고 정보를 처리하여 쿼리를 최소한으로 보내게 개선해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다!!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>fetch join</category>
      <category>joinfetch</category>
      <category>JPA</category>
      <category>LAZY</category>
      <category>N+1문제</category>
      <category>SQL</category>
      <category>백엔드</category>
      <category>스프링</category>
      <category>지연로딩</category>
      <category>쿼리</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/117</guid>
      <comments>https://wanglan.tistory.com/entry/Join-Fetch-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-N1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#entry117comment</comments>
      <pubDate>Sat, 3 May 2025 20:53:00 +0900</pubDate>
    </item>
    <item>
      <title>현재 프로젝트에 CI 파이프라인 적용하기!</title>
      <link>https://wanglan.tistory.com/entry/%ED%98%84%EC%9E%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-CI-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요 왕란입니다. 오늘도 개발하기 좋은 흐린 주말이군요!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이번엔 프로젝트에 간단한 CI 파이프라인을 적용하는 방법에 대한 포스팅을 하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #9d9d9d;&quot;&gt;(사실 혼자 개발하는 작은 프로젝트에 필요할까 싶기도 하지만, 테스트코드도 많이 작성하면서 좀 완벽한 프로젝트를 지향하고 있어서 하는 거예요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;보통 빌드, 테스트 및 배포 자동화 파이프라인을 구축하기 위해 CI/CD를 함께하는 경우가 많습니다. 하지만 저는 지금은 배포까지의 과정은 필요하지 않기 때문에 CI 파이프라인만 구축할 예정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;CI 란 뭘까?&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;CI(Continuous Integration, 지속적 통합)란 개발자들이 작업한 코드를 &lt;b&gt;자주, 그리고 반복적으로 중앙 저장소(main)에 통합(merge)&lt;/b&gt; 하고, 그 과정에서 &lt;b&gt;자동으로 빌드(build), 테스트(test 코드확인),&amp;nbsp; 정적 분석(lint) 등을 수행하는 개발 방식&lt;/b&gt;입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;협업을 하다 보면 반드시 한 번씩 이런 경험 있으실 겁니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;어 뭔가 로컬에선 문제없이 테스트도 되고 잘 동작한 거 같은데 main에서 안돼...&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주로 merge 후 코드가 통합되면서 예상치 못한 충돌이나 오류로 인해 이런 경우가 발생하게 됩니다. 이걸 merge 이후에 바로 발견해서 hotfix 할 수 있다면 괜찮을 텐데...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;&quot;하루종일 열심히 코드 작성하고 테스트 후 이거만 merge 하고 퇴근하고 싶어!!&quot;&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;라고 생각하면서 merge 하고 퇴근했다가, 밤에 코드가 제대로 동작하지 않았다는 사실을 보고 당황한 적이 몇 번 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;(운영환경에서 이러면 안 되는데 말이죠..)&lt;/s&gt;&lt;/span&gt;&lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;s&gt;&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이런 경우를 막기 위해 CI 파이프라인을 구축해 두면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 최소한의 컴파일, 런타임에러를 잡을 수 있습니다. 또한 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 테스트코드를 실행시켜 보면서 코드가 의도한 대로 동작하는지 테스트하는 과정도 가능하고, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. 협업하면서 틈틈이 챙기면 좋은 Lint 과정도 추가하여 코드 컨벤션이 잘 지켜있나 확인도 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;협업에서 효율이 향상되는 것이죠! 저는 이번에 Github Actions로 파이프라인을 구축할 계획입니다. 목표는 PR 작성 시 PR에 포함된 코드를 불러오고, ktlint로 코드 스타일 검사를 실행한 뒤, 빌드 및 테스트를 진행해서 merge 해도 안전한가를 확인하게 할 계획입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;step 1. ci.yml 작성하기!&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;Github Actions는 프로젝트 루트에 . github/workflows라는 경로가 있는지 확인하고 자동으로 안에 작성된 CI/CD 내용을 수행해 줍니다. 따라서 원하는 기능을 수행하는 yml 파일을 작성한 뒤 Github 에 추가만 해주면 간단하게 적용할 수 있습니다. 저는 ci.yml 이라는 이름으로 Github 에서 직접 파일을 추가했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;.github/workflows 라는 경로가 없으면 그냥 셀프로 추가해 주시고 작성해도 됩니다!!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;경로: `myproject/. github/workflows/ci.yml`&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746259800842&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: CI Pipeline

on:
  pull_request:
    branches:
      - main
      # - develop # (선택) develop 브랜치도 체크하고 싶으면 추가

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    env:
      JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        distribution: 'temurin' # OpenJDK 빌드
        java-version: '17'      # Kotlin/SpringBoot 3.x 기본 JDK 버전

    - name: Cache Gradle packages
      uses: actions/cache@v4
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          gradle-${{ runner.os }}-

    - name: Grant execute permission for Gradle Wrapper
      run: chmod +x ./gradlew

    - name: Run Ktlint Check
      run: ./gradlew ktlintCheck

    - name: Build and Test with Gradle
      run: ./gradlew clean build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;흐름을 가볍게 설명해 보겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. main 브랜치로 merge 될 예정인 PR이 작성될 때 이 CI가 실행되게 하기&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746259954410&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;on:
  pull_request:
    branches:
      - main
      # - develop # (선택) develop 브랜치도 체크하고 싶으면 추가&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 현재 배포도 하지 않았기에 feature 브랜치에서 작업 후 main으로 바로 merge 하고 있으나, 추후 배포 후 dev 브랜치에서 작업하게 된다면 주석을 해제하여 dev 나 main 둘 당에서 확인할 수 있습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. `runs-on: ubuntu-latest`:&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Github에서 제공하는 우분투 가상 환경에서 ci 작업을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. `env: JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}`&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;놓치면 안 되는 중요한 부분입니다. Github Actions에서는 내부적으로 새로운 우분투 환경을 키고 CI 과정을 진행하기 때문에, 로컬이나 원격에서 환경변수로 선언해놓은 JWT 시크릿 키와같은 부분은 따로 주입해주어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;프로젝트의 Settings - Secrets and variables - Actions 에 선언해 놓고 사용하면 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7gmkR/btsNKAlAZHv/SHoPriUurg0rdlhu9jktQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7gmkR/btsNKAlAZHv/SHoPriUurg0rdlhu9jktQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7gmkR/btsNKAlAZHv/SHoPriUurg0rdlhu9jktQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7gmkR%2FbtsNKAlAZHv%2FSHoPriUurg0rdlhu9jktQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;414&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;893&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저는 위 사진처럼 Secrets를 미리 설정해 두었습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(추가 정보를 드리자면, 한번 설정하고 나면 값을 어떻게 설정했는지 다시 볼 수 없습니다. 내부적으로 협업하면서도 유출되는 것을 막기 위함 같습니다. 그래서 우측의 Edit 버튼을 누르면 빈 입력란이 나오게 되는데 이때 &quot;앗 실수로 지난번에 Secrets를 입력할 때 공백으로 입력한 거 아니야?&quot;라는 오해는 하지 않으셔도 됩니다!!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 코드 체크아웃&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746260479434&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- uses: actions/checkout@v4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 PR의 코드를 Github 저장소에서 가져옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. JDK 17 설치&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746260528827&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- uses: actions/setup-java@v4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kotlin과 Spring Boot에서 권장하는 &lt;b&gt;Temurin 기반 JDK 17&lt;/b&gt;을 설치합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6. Gradle 캐시 적용&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746260599242&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- uses: actions/cache@v4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;~/. gradle 디렉터리를 캐시 하여 의존성 다운로드 시간을 절약합니다. `hasFiles`를 기준으로 캐시키를 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;(파일이 바뀌면 새 캐시 생성)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;7. Gradle 실행 권한 부여&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746260680475&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chmod +x ./gradlew&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;리눅스 환경에서 `gradlew` 가 실행되려면 실행권한도 부여해야겠죠?!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;8. Ktlint 코드스타일 검사, 빌드 및 테&lt;/b&gt;스트&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746260746739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew ktlintCheck
./gradlew clean build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Kotlin 코드에 대한 코드 스타일 검사를 수행하고, 포맷 규칙을 위반하면 CI 가 실패합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그다음으로 전체 프로젝트를 빌드(clean 포함)하고, JUnit 테스트를 실행하여 코드가 정상적으로 동작하는지 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(테스트코드를 잘 작성해놓은 안정감 좋은 백엔드 개발자라면 CI 과정만 해도 믿을 수 있는 코드를 merge 할 수 있습니다!!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 작성하고 main 브랜치로 머지될 PR을 작성하면 아래 사진과 같은 CI 가 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baDRao/btsNKfWkhrD/g1WmiHwhIpssBdZnWUGXbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baDRao/btsNKfWkhrD/g1WmiHwhIpssBdZnWUGXbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baDRao/btsNKfWkhrD/g1WmiHwhIpssBdZnWUGXbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaDRao%2FbtsNKfWkhrD%2Fg1WmiHwhIpssBdZnWUGXbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;915&quot; height=&quot;872&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;915&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초반엔 설정을 제대로 못한 게 있어서 실패한 게 보이지만 넘어가주세요..! &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;성공하면 기분 좋은 초록색 체크 표시가 뜨게 되고, 통과한 PR의 경우에는&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blL5Te/btsNJrXvx8T/wcI3jOK9JBZUDTUfvjOt40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blL5Te/btsNJrXvx8T/wcI3jOK9JBZUDTUfvjOt40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blL5Te/btsNJrXvx8T/wcI3jOK9JBZUDTUfvjOt40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblL5Te%2FbtsNJrXvx8T%2FwcI3jOK9JBZUDTUfvjOt40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;705&quot; height=&quot;181&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;여기도 ✔ 표시가 뜨게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실패한 경우도 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buPfmC/btsNJvr706g/za81ewSyvWBfE82mt8O7KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buPfmC/btsNJvr706g/za81ewSyvWBfE82mt8O7KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buPfmC/btsNJvr706g/za81ewSyvWBfE82mt8O7KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuPfmC%2FbtsNJvr706g%2Fza81ewSyvWBfE82mt8O7KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;540&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초반 Set up 은 잘되었고, 코드를 가져온 뒤 Ktlint로 스타일 검사도 잘 통과했는데&lt;b&gt; 빌드 부분에서 문제가 발생&lt;/b&gt;했군요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7Qgn1/btsNJDci4C3/oLPTTEIbbAAZlUD8ccWTI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7Qgn1/btsNJDci4C3/oLPTTEIbbAAZlUD8ccWTI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7Qgn1/btsNJDci4C3/oLPTTEIbbAAZlUD8ccWTI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7Qgn1%2FbtsNJDci4C3%2FoLPTTEIbbAAZlUD8ccWTI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;562&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;913&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;열어서 읽어보니 테스트 단계에서 `MyprojectApplicationTests &amp;gt; contextLoads()` 테스트가 실행되는데 콘텍스트 로딩 중 &lt;b&gt;Bean 생성에 실패&lt;/b&gt;해서 그런 것 같습니다. 그래서 HibernateException 이 발생한 상황입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 로그로 문제를 파악해 주니 해결하기도 용이합니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;아티클을 찾아보거나, GPT에게 물어보면 금방 해결할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;지금 Hibernate 문제가 발생한 이유는 Github Actions 에는 로컬 DB가 없기 때문에 DB 연결에서 오류가 날 수 있습니다. 저는 CI 과정에서 하는 테스트에서 사용할 H2 인메모리 DB를 추가하여 테스트용 DB 설정을 추가하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;539&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKSo9K/btsNKeC699r/kYgabQsPPASOgH6VIZy9Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKSo9K/btsNKeC699r/kYgabQsPPASOgH6VIZy9Xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKSo9K/btsNKeC699r/kYgabQsPPASOgH6VIZy9Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKSo9K%2FbtsNKeC699r%2FkYgabQsPPASOgH6VIZy9Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;657&quot; height=&quot;372&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;952&quot; data-origin-height=&quot;539&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위와 같이 application-test.yml 파일을 추가해 주고 이렇게 임시로 설정한 다음 main에 적용해 놓았습니다. 이후 테스트에선 과연?!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zyDNQ/btsNLDaMaLm/aKvH6vSWFF1h9n7B5s8lu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zyDNQ/btsNLDaMaLm/aKvH6vSWFF1h9n7B5s8lu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zyDNQ/btsNLDaMaLm/aKvH6vSWFF1h9n7B5s8lu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzyDNQ%2FbtsNLDaMaLm%2FaKvH6vSWFF1h9n7B5s8lu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;908&quot; height=&quot;708&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;문제없이 통과하는 것을 보실 수 있습니다. 현재는 기본적인 기능을 추가하고 프로젝트 설정에 아직 집중하고 있는 단계라 테스트코드를 작성하지 않은 상태이지만 테스트 코드 작성 이후에는 좀 더 믿을 만한 코드를 merge 할 수 있을 거라 기대됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;추후에 좀 완성도 있는 프로젝트를 배포하게 된다면 CD 부분의 내용도 포스팅해 보겠습니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>CI/CD</category>
      <category>cicd</category>
      <category>ci파이프라인</category>
      <category>GitHub Actions</category>
      <category>ktlint</category>
      <category>배포자동화</category>
      <category>백엔드 안정성</category>
      <category>백엔드개발자</category>
      <category>테스트 자동화</category>
      <category>파이프라인</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/116</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%98%84%EC%9E%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-CI-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0#entry116comment</comments>
      <pubDate>Sat, 3 May 2025 17:45:30 +0900</pubDate>
    </item>
    <item>
      <title>Postman 헤더에 Authorization 설정</title>
      <link>https://wanglan.tistory.com/entry/Postman-%ED%97%A4%EB%8D%94%EC%97%90-Authorization-%EC%84%A4%EC%A0%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요. 개발자 왕란입니다!!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;오늘은 프로젝트 작성 중 발생한 자잘한 이슈와 그 해결방법에 대해 포스팅하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주제는 Postman 툴에서 API 요청을 보낼 때, Request Header의 Authorization 부분에서 토큰을 설정하고 가져오는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1.&amp;nbsp; 현재 요구사항&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 개발 중인 프로젝트의 요구사항에서, 로그인 시 Response Header의 `Authorization` key에 Bearer 토큰 Value를 넣도록 설정했습니다. 이 토큰을 활용하여 로그인 후 사용하는 회원정보 조회, 회원정보 수정, 회원 탈퇴 등의 API의 동작전 필터에서 &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;유효한 JWT 토큰을 가지고 있는가? &quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 검증하고 있습니다. 현재 상태를 보겠습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2BbSD/btsNBByc2DR/Bsbcth9RG4RZMFqVVWAXL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2BbSD/btsNBByc2DR/Bsbcth9RG4RZMFqVVWAXL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2BbSD/btsNBByc2DR/Bsbcth9RG4RZMFqVVWAXL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2BbSD%2FbtsNBByc2DR%2FBsbcth9RG4RZMFqVVWAXL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;948&quot; height=&quot;601&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;601&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로그인 요청을 보내면 Headers 탭에 Authorization에 &quot;Bearer ey~&quot; 형태의 토큰을 입력하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;추후 로그인 한 후, 요청을 보내려면 이 Authorization Key에 있는 Value 인 &lt;b&gt;토큰이 Request Header의 Authorization에 들어가야 합니다.&lt;/b&gt; 하지만 다음 API 인 `현재 로그인 중인 유저 확인` 기능은 Request Headers 부분에 Authorization 키가 존재하지 않아서 아래와 같이 403 에러가 발생하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIJPAB/btsNz5HleWK/QiaCsiGXet8Ot1ngpXUlTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIJPAB/btsNz5HleWK/QiaCsiGXet8Ot1ngpXUlTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIJPAB/btsNz5HleWK/QiaCsiGXet8Ot1ngpXUlTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIJPAB%2FbtsNz5HleWK%2FQiaCsiGXet8Ot1ngpXUlTK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;698&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Request Headers에 직접 Key와 Value를 입력하여 통과할 수 있겠지만, 이렇게 하면 추후 테스트할 때 로그인 후 다시 초기화해 주는 번거로운 과정이 필요합니다. (쿠키에 Authorization 이름으로 넣어 싱크를 자동으로 맞춰줄 수 있지만 지금 프로젝트 요구사항과 다릅니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이것을 해결하기 위해 엔드포인트 입력란 아래의 Scripts 부분의 pre-request와 post-response를 설정하여 환경변수로 해결해 보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 환경변수 설정&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NDODu/btsNBM7sWu9/Vzjj25uodp2amt0IWlKLd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NDODu/btsNBM7sWu9/Vzjj25uodp2amt0IWlKLd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NDODu/btsNBM7sWu9/Vzjj25uodp2amt0IWlKLd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNDODu%2FbtsNBM7sWu9%2FVzjj25uodp2amt0IWlKLd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1126&quot; height=&quot;423&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;423&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;포스트맨 화면의 Environments 탭에서 좌상단의 + 버튼을 누릅니다. 그러면 새로운 환경변수를 설정할 수 있습니다. 저는 이 환경변수 이름을 env1로 만들었고, 내부에 내용을 accessToken으로 설정합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;(아래에 작성된 코드를 복사하여 쓰실 경우 반드시 accessToken 으로 설정해 주세요!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설정 후에는 우 상단의 myenv 부분에서 프로젝트에 사용할 env 파일을 불러오면 됩니다. 저는 이전에 쓰던 환경변수인 myenv를 그대로 쓰도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 부분에서 Globals에 환경변수를 설정할 수 있지만, Postman으로 테스트하는 하는 프로젝트가 여러 개일 경우 환경변수파일은 개별적으로 관리하는 것을 추천드립니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. Scripts 세팅&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;제가 이 포스팅을 작성하는 이유입니다. 다른 분들의 블로그 포스팅을 많이 찾아보았지만, 지금 제가 사용하는 UI의 포스트맨을 사용하는 분들이 많이 없었습니다. (최근에 업데이트된 듯합니다)&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;901&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bamTpW/btsNBueZS8a/omqmjYKsuLj5t9zv3bKTXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bamTpW/btsNBueZS8a/omqmjYKsuLj5t9zv3bKTXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bamTpW/btsNBueZS8a/omqmjYKsuLj5t9zv3bKTXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbamTpW%2FbtsNBueZS8a%2FomqmjYKsuLj5t9zv3bKTXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;493&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;901&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위 사진과 같이 로그인 API에 돌아와서 중간의 Scripts 부분을 클릭하면 좌측에 Pre-request와 Post-response를 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Pre-request : 요청을 보내기 전 수행할 JavaScript 부분&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Post-response : 요청을 보내고 나서 수행할 JavaScript 부분&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;이라고 생각하시면 됩니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;지금은 로그인 요청 이후, 200 OK로 요청이 성공적으로 수행되고 나서 아까 설정한 환경변수인 myenv 내부의 accessToken이라는 Key에 Authorization 토큰을 넣어줄 계획입니다. 따라서 &lt;b&gt;Post-response 부분에 다음과 같이 코드를 작성해 줍니다.&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745739227553&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 응답 헤더에서 &quot;Authorization&quot; 토큰 키를 get
const accessToken = pm.response.headers.get(&quot;Authorization&quot;);

if (accessToken) {
    // Bearer 토큰 형식이 포함되어 있다면 분리
    const token = accessToken.replace(&quot;Bearer &quot;, &quot;&quot;).trim();

    pm.environment.set(&quot;accessToken&quot;, token); // 환경 변수에 저장 ! 중요
    console.log(&quot;Access Token:&quot;, token); // 디버깅용 로그
} else {
    console.warn(&quot;Authorization header not found&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvwb2C/btsNAJKKO8M/s1tmXkzTPyhMFuA1wAnLS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvwb2C/btsNAJKKO8M/s1tmXkzTPyhMFuA1wAnLS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvwb2C/btsNAJKKO8M/s1tmXkzTPyhMFuA1wAnLS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcvwb2C%2FbtsNAJKKO8M%2Fs1tmXkzTPyhMFuA1wAnLS1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;737&quot; height=&quot;590&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;737&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이렇게 되면 로그인을 성공했을 시 아래 Response Headers의 Authorization에서 토큰을 가져와 아까 설정한 env1(저는 myenv였습니다)의 accessToken 자리에 &quot;Bearer 토큰&quot;을 trim 하여 넣어주게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgJZmu/btsNCdQ3M1d/JtLUhKqwQjtgUIYctPoAq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgJZmu/btsNCdQ3M1d/JtLUhKqwQjtgUIYctPoAq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgJZmu/btsNCdQ3M1d/JtLUhKqwQjtgUIYctPoAq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgJZmu%2FbtsNCdQ3M1d%2FJtLUhKqwQjtgUIYctPoAq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;209&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;953&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;잘 들어가 있는 것을 확인할 수 있습니다. 이어서 이 토큰을 활용하여&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&quot;로그인된 유저가 보내는 요청&quot;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;을 수행해 보도록 하겠습니다. 저의 경우 현재 로그인된 유저의 정보를 가져오는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YR5DE/btsNBvSfOCn/DqQKnREbFGHVkebwWXKNpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YR5DE/btsNBvSfOCn/DqQKnREbFGHVkebwWXKNpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YR5DE/btsNBvSfOCn/DqQKnREbFGHVkebwWXKNpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYR5DE%2FbtsNBvSfOCn%2FDqQKnREbFGHVkebwWXKNpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;554&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1745739644419&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 환경 변수에서 액세스 토큰 가져오기
const accessToken = pm.environment.get(&quot;accessToken&quot;);

if (accessToken) {
    pm.request.headers.upsert({
        key: &quot;Authorization&quot;,
        value: `Bearer ${accessToken}`
    });
    console.log(&quot;Authorization header added:&quot;, `Bearer ${accessToken}`);
} else {
    console.warn(&quot;Not found accessToken in the environment.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;API로 돌아와서 아까와 마찬가지로 Scripts 탭을 클릭합니다. 하지만 이번에는 Post-response 가 아닌 &lt;b&gt;Pre-request 탭을 클릭하여&amp;nbsp; 위와 같은 코드를 작성&lt;/b&gt;해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 상황을 정리해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;* 프로젝트에 사용할 환경변수 : myenv&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 로그인 요청:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;  성공 시 Post-response의 스크립트가 동작하며, myenv의 accessToken 키에 응답 헤더로 받은 Authorization 키의 토큰을 저장한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 현재 로그인된 유저 확인:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;   pre-request의 스크립트가 동작하며, API 수행 전 request 헤더에 Authorization이라는 이름의 key로 환경 변수에 입력되어 있는 토큰값을 가져와서 Bearer 형식으로 작성한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;의 내용이 아래 코드에서 수행됩니다. 이때 upsert() 메서드 대신 set()을 사용해도 되지만, upsert는 Authorization 이 이미 있으면 새로운 내용으로 덮어쓰는 행위를 하고, set() 은 없을 때 설정하는 행위만 합니다. (둘의 큰 차이는 없어서 API 비즈니스 로직에 따라 문제없이 동작할 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745739865698&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pm.request.headers.upsert({
        key: &quot;Authorization&quot;,
        value: `Bearer ${accessToken}`
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;*참고*&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로그인 시 Bearer 부분을 trim 하지 않고, Pre-request에 위와 같은 형태가 아닌 accessToken 만 불러와서 하는 것도 상관없습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. 결과 확인&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nDtVj/btsNCebopcB/ggWmvroWC7dWMqkaRmdMj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nDtVj/btsNCebopcB/ggWmvroWC7dWMqkaRmdMj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nDtVj/btsNCebopcB/ggWmvroWC7dWMqkaRmdMj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnDtVj%2FbtsNCebopcB%2FggWmvroWC7dWMqkaRmdMj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;949&quot; height=&quot;620&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;949&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정상적으로 요청을 보내고 응답도 받는 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 방식의 단점은, &lt;b&gt;Request Headers에 환경변수에서 토큰을 가져와 세팅한 Authorization 부분은 보이지 않는다&lt;/b&gt;는 것입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;요약하면,&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;0. 환경변수 파일 만들기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 로그인 성공 시 Post-response 설정하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 이후 로그인 된 상태에서 보내는 모든 요청의 pre-request를 설정하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;총 3단계를 통해 헤더의 Authorization 값을 설정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다!!&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Utils</category>
      <category>Authorization</category>
      <category>header</category>
      <category>Postman</category>
      <category>postman설정</category>
      <category>postman환경변수</category>
      <category>백엔드</category>
      <category>테스트환경설정</category>
      <category>토큰 자동으로가져오기</category>
      <category>헤더에토큰넣기</category>
      <category>환경변수</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/115</guid>
      <comments>https://wanglan.tistory.com/entry/Postman-%ED%97%A4%EB%8D%94%EC%97%90-Authorization-%EC%84%A4%EC%A0%95#entry115comment</comments>
      <pubDate>Sun, 27 Apr 2025 17:02:11 +0900</pubDate>
    </item>
    <item>
      <title>[코틀린 프로젝트] - 새로운 프로젝트 구상(4/13~4/26)</title>
      <link>https://wanglan.tistory.com/entry/%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%83%81413426</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요 개발자 왕란입니다. (왕란이라는 이름은 온라인에서, 블로그에서만 사용되는 이름입니다!!)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;백엔드 개발자로서 안정적은 애플리케이션을 개발하기 위해 이론적인 이런저런 내용을 다루다 보니, 뭔가 부족하다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;저도 블로그 포스팅을 할 때, 이런 저런 아티클이나 예전에 진행했던 프로젝트를 할 때 들었던 강의, GPT 등등의 많은 레퍼런스를 참조해 가며 쉽게 이해할 수 있도록 개념을 다루려고 했습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 결국 개념을 설명할 때 예제코드만 보여드릴 수밖에 없었습니다. 예제코드를 보고 이해하는 것보다 실제 프로젝트에서 어떻게 작성되고, 실제 적용이 어떻게 되는지를 프로젝트로 보여드리며 진행하고 싶어 졌습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 실제로 프로젝트를 개발해 가면서 발생한 문제, 테스트, 개선, 변경 등등 다양한 상황을 포스팅하려고 합니다. 이 카테고리에서 프로젝트에 대해 다루는 내용은 버전관리를 해오면서 개선되는 패치노트의 느낌을 가진다고 생각될 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;짚고 가고 싶은 포인트는, 이 프로젝트가 기술블로그의 개념을 설명하기 위해 불필요하게 보여주기 위해 시작하는 것은 아닙니다. 개발자라면 누구나 가지고 있을 생각인&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어떤 애플리케이션을 개발하고 싶다!&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;를 생각하며 저도 개발을 진행하려고 합니다. 아직 잘 작성된 API 명세나, 엔티티 연관 다이어그램,&amp;nbsp; 와이어프레임 등을 작성하진 않았지만 이 프로젝트를 진행하면서 다듬어가고 이 블로그 포스팅으로 완성해가려고 합니다. 주제는 2가지를 생각하고 있지만 어떤 부분으로 흘러갈지는 아직 정해지지 않았습니다. 그럼 시작해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;s&gt;(참고로 프로젝트 작성은 평일 근무가 끝나고 하기엔 시간이 부족하여 주말에 틈틈이 진행하는 중입니다.)&lt;/s&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 프로젝트 초기 설정(4월 13일)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기본적인 엔티티를 작성하거나 설계하는 게 필수이긴 하지만, 우선 애플리케이션의 주제를 명확하게 가지고 출발한 것이 아니기 때문에 다른 기본적인 환경설정만 하기로 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;IDE: IntelliJ IDEA Ultimate&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring Boot 3.4.4&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DB: MySQL(로컬 및 배포환경 -&amp;gt; 추후 PostgreSQL로 변경 예정), Test(H2)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;빌드 환경&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Gradle&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Java 17&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Kotlin 1.9.25&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Spring Boot 3.4.4&lt;/span&gt;&lt;span style=&quot;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;주요 라이브러리&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Spring Security&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Spring Data JPA&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Spring Data Redis&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;JWT&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;CI/CD 구성&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;현재 배포예정이 없으므로 CI 과정만 구현&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Github Actions로 빌드 + 테스트 자동화&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;테스트: Postman&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 지금은 이 정도로 설정하여 프로젝트를 구현하였습니다. 아래는 기본적인 패키지 구조입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O7wz4/btsNAR9vT9U/Pb7gHtUzZGsw7NtaaogLZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O7wz4/btsNAR9vT9U/Pb7gHtUzZGsw7NtaaogLZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O7wz4/btsNAR9vT9U/Pb7gHtUzZGsw7NtaaogLZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO7wz4%2FbtsNAR9vT9U%2FPb7gHtUzZGsw7NtaaogLZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;432&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Spring Security 설정과 JWT 토큰 관리를 하는 config 패키지, 각 엔티티별로 관리할 도메인 패키지, 도메인에서 공통으로 사용될 Timestamped와 같은 요소를 저장할 common 패키지, 추후에 필요하다면 추가할 내용을 작성하는 util 패키지로 간단하게 구성하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;575&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tlm2W/btsNA9oBAnM/UsiCynxoLRx4rK8xoFocx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tlm2W/btsNA9oBAnM/UsiCynxoLRx4rK8xoFocx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tlm2W/btsNA9oBAnM/UsiCynxoLRx4rK8xoFocx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTlm2W%2FbtsNA9oBAnM%2FUsiCynxoLRx4rK8xoFocx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;324&quot; height=&quot;575&quot; data-origin-width=&quot;324&quot; data-origin-height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1-1. config/exception/&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;예외처리를 일괄적으로 정의 및 관리하기 위한 `GlobalExceptionHandler` 부분을 config 패키지 내부에 설정해 두었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;enums 패키지내부의 ExceptionCode에 상황에 맞는 Exception을 응답 코드와 응답 메시지와 함께 정의해 두었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;1-2. config/filter/&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;인증 인가를 진행하는 필터 패키지입니다. 로그인을 기능을 담당하는 `JwtAuthenticationFilter` 와 인가를 담당하는 `JwtAuthorizationFilter` 를 정의하여 인증인가를 진행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`JwtUtil` 에서 각 필터에 사용하는 유틸 메서드를 정의해 놓고 사용하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1-3. config/security/&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring Security를 사용하여 Auth에서 사용될 보안을 담당하고 있습니다. Spring Security의 UserDetails와 UserDetailsService를 각각 구현하고 있으며, `WebSecurityConfig`에서 인증/인가를 담당하는 필터의 순서 등을 정의하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;609&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lChYp/btsNAHlNjvv/5TTjd2hl5DJEofBP4byekk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lChYp/btsNAHlNjvv/5TTjd2hl5DJEofBP4byekk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lChYp/btsNAHlNjvv/5TTjd2hl5DJEofBP4byekk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlChYp%2FbtsNAHlNjvv%2F5TTjd2hl5DJEofBP4byekk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;404&quot; height=&quot;609&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;609&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2-1. domain/auth/&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재는 회원가입에 대한 API 만 작성되어 있습니다. 확장이 크게 될 예정이 없어서 간단하게 필수적인 request/response dto 만 정의해 두었고, 비즈니스 로직은 service에서 진행합니다. Spring Security의 `UserDetailsServiceImpl`에서 UserRepository에 의존성을 가지고 회원가입을 관리하고 있기 때문에, auth 패키지에 repository를 작성하진 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2-2. domain/common/&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;각 엔티티가 CRUD 과정을 통해 관리될 때 필요한 `createdAt`, `modifiedAt`, `deletedAt`을 자동으로 추가해줄 Timestamped를 정의해 두었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bt1YgA/btsNANMRMik/tx332liy2v2rkiw7L8Ikak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bt1YgA/btsNANMRMik/tx332liy2v2rkiw7L8Ikak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bt1YgA/btsNANMRMik/tx332liy2v2rkiw7L8Ikak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbt1YgA%2FbtsNANMRMik%2Ftx332liy2v2rkiw7L8Ikak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;249&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;249&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애플리케이션 메인에서 @EnableJpaAuditing을 추가해 주어야 자동으로 시간 등록이 됩니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2-3.&amp;nbsp; domain/user/&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회원가입한 유저의 정보를 관리하는 패키지입니다. 엔티티, Controller, Service, Repository 가 정의되어있고, 데이터 주고 받는 과정을 위한 DTO 클래스도 정의 되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 구현된 API는&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로그인된 유저 정보 불러오기 (4월 13일)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;비밀번호 변경 (4월 26일 추가)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회원탈퇴 (4월 26일 추가)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;세 가지가 구현되어 있습니다. 여기까지가 프로젝트 개발 초기 환경설정 및 필수 기능 추가한 내용입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 수정 및 개선 사항 (4월 26일)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;1. JWT 유틸 버그 픽스&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Postman 으로 요청을 보내는 중 헤더에 Authorization 부분에 토큰을 가지고 있어야 필터를 통과할 수 있는데, 이 부분에서 자잘한 오류가 나는 것을 발견하였습니다. 수정한 내용을 아래와 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;- `Bearer&amp;nbsp; ` 으로 프리픽스 설정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Bearer 다음 ' '(공백)이 없어서 토큰검증에 에러가 나던 부분을 개선했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;-&amp;nbsp; `getTokenFromHeader()` 메서드에서 substring 하는 로직 삭제&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;Bearer 부분을 제거 하는 로직이 중복되어 있는것이 발견되어 삭제하였습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;2. 비밀번호 수정 로직 추가 - /users/password&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 비밀번호, 수정할 비밀번호, 비밀번호 확인 3가지 항목 입력 후 비밀번호 변경처리 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 비밀번호가 일치하지 않으면 Exception&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;수정할 비밀번호가 비밀번호 확인과 일치하지 않으면 Exception&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;3. Postman 에서 API 테스트 시 아래와 같은 내용을 Script 에 추가합니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745729043050&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 환경 변수에서 액세스 토큰 가져오기
const accessToken = pm.environment.get(&quot;accessToken&quot;);

// 토큰이 존재하면 헤더에 추가
if (accessToken) {
    pm.request.headers.upsert({
        key: &quot;Authorization&quot;,
        value: `Bearer ${accessToken}` // Bearer 토큰 형식
    });
    console.log(&quot;Authorization header added:&quot;, `Bearer ${accessToken}`);
} else {
    console.warn(&quot;Access token is not set in the environment.&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;로그인 요청 시, Response Header 에 JWT 토큰을 반환하게 됩니다. 로그인 성공 후 다른 요청을 보낼 때 pre-request 를 위와 같이 설정하여 로그인 시 받았던 JWT 토큰을 Authorization header 로 받아온 뒤 요청을 진행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;4. 현재 로그인 중인 유저 확인 /users/me&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 로그인 되어있는 회원 이메일과 이름을 출력하도록 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;5. 회원탈퇴 로직 추가&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 로그인한 유저가 비밀번호를 맞게 입력하면 탈퇴할 수 있는 기능입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회원 탈퇴하는 순간 DB에서 삭제되는 것이 아닌 deletedAt 컬럼에 요청 시간을 삽입하는 방식으로 소프트딜리트 하도록 설정했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;회원 탈퇴에 필요한 request 와 response DTO 데이터클래스가 추가되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;탈퇴한 유저가 재로그인 하면, deletedAt 컬럼에 다시 null 을 넣어 스케줄러로 계정이 삭제되지 않게 UserDetailsServiceImpl 을 변경하였습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;6. CI pipeline&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발 완료된 Feature 에 관한 PR 을 작성 시, Github Actions 에서 자동으로 빌드 및 테스트를 진행합니다. 빌드 하며 린트 체크를 한다음 린트, 빌드, 테스트가 모두 통과한 경우 Merge 할 수 있도록 CI 설정 하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;린트가 수동적으로 필요한 경우 아래와 같은 명령어를 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745729528568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew ktlintCheck
&amp;rarr; BUILD FAILED ❌ (포맷 에러 존재)

./gradlew ktlintFormat
&amp;rarr; 자동 포맷 수정

./gradlew ktlintCheck
&amp;rarr; BUILD SUCCESS ✅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;7. Project 패키지 구조 변경&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;현재 프로젝트 내부에 중첩되어 패키지 구조가 설정되어 있어서 전체적인 구조를 변경헀습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;gradle 관련 파일이 루트 디렉토리에 존재하여 Github Actions 가 정상 동작 할 수 있도록 변경하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;우선 4월26일 어제 개발한 내용은 이정도 입니다. 지금 부터 프로젝트를 하나씩 개발해 가면서 얻는 인사이트를 잘 공유해보겠습니다. 감사합니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Project</category>
      <category>CI 파이프라인</category>
      <category>CI/CD</category>
      <category>JWT</category>
      <category>Postman</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>백엔드개발자</category>
      <category>주니어개발자</category>
      <category>토이프로젝트</category>
      <category>패키징</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/114</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%83%81413426#entry114comment</comments>
      <pubDate>Sun, 27 Apr 2025 13:56:01 +0900</pubDate>
    </item>
    <item>
      <title>JPA 와 N+1 문제</title>
      <link>https://wanglan.tistory.com/entry/JPA-%EC%99%80-N1-%EB%AC%B8%EC%A0%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDXpa/btsNr403yC4/fqYyF6jcQnhV31bMlBOhnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDXpa/btsNr403yC4/fqYyF6jcQnhV31bMlBOhnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDXpa/btsNr403yC4/fqYyF6jcQnhV31bMlBOhnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDXpa%2FbtsNr403yC4%2FfqYyF6jcQnhV31bMlBOhnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;611&quot; height=&quot;351&quot; data-origin-width=&quot;918&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요. 오늘은 날씨가 흐린 주말이군요. 집에서 코드작성하기 좋은 날인 것 같습니다. 오늘은 N+1 문제에 대해 다뤄보려고 합니다. N+1 문제는 JPA, Hibernate 등 ORM을 사용하는 백엔드 개발자라면 한 번은 반드시 마주하게 되는 대표적인 성능 병목 이슈입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;N + 1 문제&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JPA를 쓴다면 반드시 만나게 되는 성능 지옥&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;N+1 문제란 무엇인가?&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정의&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하나의 쿼리를 실행했더니, 관련된 데이터를 조회하기 위해 추가로 N개의 쿼리가 발생하는 문제&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;그래서 최종적으로 총 &lt;b&gt;1 + N번의 쿼리&lt;/b&gt;가 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이걸 &quot;N+1 문제&quot;라고 부릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;예시로 알아보기&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1745026384818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;Team&amp;gt; teams = teamRepository.findAll();
for (Team team : teams) {
    System.out.println(team.getMembers().size());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. `findAll()` &amp;rarr; `SELECT * FROM team; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr; 1번&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;2. `getMembers()` &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr; 각 팀마다 `SELECT * FROM member WHERE team_id = ?` &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr; N 번&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;  팀이 100개면 총 101번의 SQL이 실행되는 &lt;b&gt;비효율적인 상황&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;왜 이런일이 생길까?&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;우선 코드를 작성하기 전, 프로젝트를 기획하는 단계에서 각 테이블마다 엔티티 연관관계를 설정하고 시작해야 합니다. 이때 우리는 성능 최적화를 위해 &lt;b&gt;연관관계의 방향도 설정&lt;/b&gt;하고, &lt;b&gt;지연 로딩(Lazy)&lt;/b&gt;을 적용할지 말지 정하게 됩니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;실무에서는&lt;b&gt; Lazy를 기본전략으로 하고, 필요할 때 명시적으로 JOIN을 사용하는 방식&lt;/b&gt;이 주로 사용됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;이런 지연 로딩 전략으로 인해 연관된 엔티티를 바로 가져오지 않고, 필요한 시점에 가져오기 때문에 N+1 문제가 발생합니다. 물론 즉시로딩(Eager)에서 발생하지 않는 것은 아닙니다. 루프 안에서 연관된 엔티티에 접근할&amp;nbsp; 때, JPA 가 추가 쿼리를 날릴 수 있습니다. 결국 지연로딩과 즉시로딩 둘다 N+1 문제가 발생할 수 있다는걸 알고 있어야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;불필요한 쿼리를 날리지 않게하면 잘 제어하면 되는거아니야?&lt;/i&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;너무 당연한 생각입니다. 개발을 기획하는 처음단계부터 모든 엔티티와 그 연관관계를 완벽하게 설계하고 개발에 착수하면 이런생각이 통할지도 모르겠습니다. 하지만 추가되는 테이블이 있을 수 있고, 요구사항이 변경됨에 따라 연관관계가 변경되면 개발자는 인지하지 못하는 동안 쿼리가 추가적으로 날아가고 있을지도 모릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초기 테스트 단계에선 데이터가 실제 서비스때 보다 적어서 테스트가 빠르게 수행되니까 눈치채지 못할 수있고, 개발자가 항상 Hibernate 의 쿼리를 보고있을 수도 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;N+1 문제의 단점&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;당연한 이야기지만, 성능이 떨어지게 됩니다. 쿼리수가 많아질수록 DB부하가 급증하게 될것입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 네트워크 비용이 증가하게 됩니다. 요청-응답이 불필요하게 반복되며 지연 시간도 발생하게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;디버깅이 어렵습니다. 코드 상에는 쿼리가 보이지 않기에 추적이 힘듭니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;테스트 환경에서 발견하기 어렵습니다. 개발당시엔 괜찮더라도 실제 데이터가 많아지면 연쇄적으로 문제가 발생할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;N+1 문제 해결 방법&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;&quot;&gt;N+1 문제를 해결하는 다양한 방법이 있습니다. 지금부터 하나씩 소개해보겠습니다. 자세한 설명하기전에 표로 정리해서 보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 118px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;분류&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;방법&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 16px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;한 줄 요약&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쿼리 조인&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`JOIN FETCH`&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JPQL에서 직접 조인하여 엔티티 미리 로딩&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;선언형&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`@EntityGraph`&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;어노테이션으로 연관 엔티티를 함께 로딩하도록 선언&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;DTO 사용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;직접 DTO로 조회&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;필요한 데이터만 JPQL/QueryDSL로 직접 뽑아서 조회&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;설정 기반&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`BatchSize`,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`default_batch_fetch_size`&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Hibernate의 ㅂ배치 로딩 기능으로 IN 쿼리로 최적화&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;캐시&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2차 캐시 활용&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이미 조회된 데이터를 메모리나 Redis 등 에서 재사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;애플리케이션 구조 개선&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 30.1938%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;쿼리 구조 개선&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 46.4728%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;루프를 돌지 않도록 구조 자체를 개선하거나 데이터 병합&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. `JOIN FETCH` 사용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;가장 직접적이고 직관적이며 간편합니다. 하지만 쿼리 복잡도가 증가할 수 있고, 관계가 많으면 JOIN 지옥에 빠질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745031096765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;SELECT o FROM Order o JOIN FETCH o.member&quot;)
List&amp;lt;Order&amp;gt; findAllWithMember();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. `@EntityGraph` 사용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JPA 친화적인 방법입니다. 내부적으로 `JOIN FETCH`와 동일한 쿼리를 생성합니다. 선언형 방식이라 유지보수가 편리하다는 장점이 있습니다. 하지만 복잡한 조건이나 다단계 연관관계에서는 사용이 힘듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745031184432&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = {&quot;member&quot;})
List&amp;lt;Order&amp;gt; findAll();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;3. DTO로 직접조회&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;SELECT 절을 명시하여 필요한 데이터만 가져오게 하는 방식입니다. 퍼포먼스가 좋지만 코드가 증가하고 유연성이 낮다는 단점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745031245833&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;SELECT new com.example.OrderDto(o.id, m.name) FROM Order o JOIN o.member m&quot;)
List&amp;lt;OrderDto&amp;gt; findOrderDtos();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;4. Hibernate `@BatchSize` / `default_batch_fetch_size`&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;연관 엔티티들을 한 번에 IN 쿼리로 불러오는 기능을 활용할 수 있습니다. 엔티티에 설정하는 방법과 글로벌로 전역설정 하는 방법이 있습니다. 이 방법은 IN 절의 크기가 제한되고, JOIN이 아니기 때문에 연관도가 낮을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 엔티티에 설정하기&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745035536080&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@OneToMany(fetch = FetchType.LAZY)
@BatchSize(size = 100)
private List&amp;lt;OrderItem&amp;gt; orderItems;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 글로벌 설정(application.properties 에 설정)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745035583413&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.properties.hibernate.default_batch_fetch_size=100&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위의 설정은 다음과 같은 쿼리를 보내게됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745035648097&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM order_item WHERE order_id IN (1, 2, ... , 100)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;5. 2차 캐시 사용&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;자주&amp;nbsp; 조회되는 연관 엔티티를 Hibernate 2차 캐시 또는 Redis 등에 저장해두고 재사용할 수 있습니다. 쿼리 수 자체를 줄일 수 있지만, 캐시 설정이나 관리를 해야하고, 일관성이 떨어지는 문제가 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745035786239&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Cacheable
@ManyToOne(fetch = FetchType.LAZY)
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
private Member member;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;6.&amp;nbsp; 쿼리 구조 자체를 개선하기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;루프 구조를 제거하거나 `Map`, `IN` 조건 등을 활용해 애플리케이션 로직 자체를 최적화하는 방법도 있습니다. 이 방법은 제어하기엔 좋지만 코드자체의 복잡성이 증가할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1745035909572&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 잘못된 방식 (루프 안에서 조회)
orders.forEach(order -&amp;gt; order.getMember().getName());

// 개선 방식 (ID 미리 수집 후 일괄 조회)
List&amp;lt;Long&amp;gt; memberIds = orders.stream().map(o -&amp;gt; o.getMember().getId()).toList();
Map&amp;lt;Long, Member&amp;gt; memberMap = memberRepository.findAllByIdIn(memberIds)
    .stream().collect(Collectors.toMap(Member::getId, m -&amp;gt; m));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;실무 적용 전략&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;단순 연관 관계가 한두개 일때는, `JOIN FETCH` 나 `@EntityGraph` 를 사용해서 제어해도 충분합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;조회 성능이 중요하다 판단이 들때는 DTO로 직접 조회하는 방법도 효과적일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;LAZY 관계가 많이 연결된 경우에는 설계가 어렵기 때문에, `BatchSize` 나 `default_batch_fetch_size` 를 적용하는 것도 효과적입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 자주 조회되는 엔티티의 경우엔 2차 캐시를 활용해서 DB 자체의 접근을 줄이는 것도 효과적입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마지막으로 설계단계부터 가능한 N+1 문제가 발생할 수 있는 코드를 지양하고 IN 조건을 활용하는것도 고려해야할 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #000000;&quot;&gt;N+1 문제는&lt;br /&gt;&quot;내가 한 번만 조회한다고 생각했는데, JPA 가 자동으로 여러번 DB를 두드리는 상황&quot;&lt;br /&gt;입니다. 반드시 쿼리 로그를 보고 JOIN FETCH 등으로 최적화가 필요합니다.&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp; 지금까지 JPA 사용할 때의 N+1 문제에 관한 포스팅이었습니다. 감사합니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>2차캐시</category>
      <category>@entitygraph</category>
      <category>BATCH</category>
      <category>DB최적화</category>
      <category>joinfetch</category>
      <category>JPA</category>
      <category>N+1문제</category>
      <category>백엔드</category>
      <category>병목현상</category>
      <category>쿼리</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/113</guid>
      <comments>https://wanglan.tistory.com/entry/JPA-%EC%99%80-N1-%EB%AC%B8%EC%A0%9C#entry113comment</comments>
      <pubDate>Sat, 19 Apr 2025 13:18:29 +0900</pubDate>
    </item>
    <item>
      <title>Java Bean 뜻 &amp;mdash; 스프링 빈은 왜 하필 '콩'일까? (개념&amp;middot;등록&amp;middot;중복 해결 정리)</title>
      <link>https://wanglan.tistory.com/entry/Java-Spring-%EC%97%90%EC%84%9C%EC%9D%98-Bean-%EC%9D%B4%EB%9E%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;안녕하세요! 즐거운 주말이 되어 오늘도 포스팅합니다! 오늘의 주제는 저의 지난 포스팅이나 다른 Spring 관련된 내용에서 늘 등장하는 &lt;b&gt;Bean&amp;nbsp;&lt;/b&gt;이란 어떤것인지에 대해 알아보려고 합니다!&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Bean&amp;nbsp;&lt;/b&gt;은 우리 모두가 알고있듯 '콩' 이란 의미이죠? &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;또한 Java 를 보면 항상 나오는 커피잔 &lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;로고를 보신 적 있을 겁니다. 별로 이론적인 내용과는 상관없긴 하지만 자바가 커피한잔을 의미한다고 치면 Bean 은 아마도 '커피콩'의 '콩'을 의미하는 것 같습니다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light'; color: #333333; text-align: start;&quot;&gt;이제 좀 더 이론적인 내용으로 들어가보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l5WXY/btsNiWbaSke/kYikPKg1WkaKGRksECjtU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l5WXY/btsNiWbaSke/kYikPKg1WkaKGRksECjtU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l5WXY/btsNiWbaSke/kYikPKg1WkaKGRksECjtU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl5WXY%2FbtsNiWbaSke%2FkYikPKg1WkaKGRksECjtU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;286&quot; height=&quot;286&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;Spring 에서 Bean 이란 무엇인가요?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&quot;Spring 에서 Bean 이 뭔가요?&quot;&lt;/b&gt; 라는 질문은 초보자뿐 아니라 중급 개발자에게도 한 번쯤 다시 짚고 넘어갈 필요가 있는 중요한 주제입니다. Spring 을 제대로 이해하고 활용하려면 &quot;Bean 이란 무엇인가?&quot;를 먼저 정확히 이해하는 것이 매우 중요하죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt; ✅ Bean 이라는 이름은 왜 붙였을까?&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;&quot;Bean&quot; 이라는 단어는 Java 진영에서 오래전부터 사용된 개념입니다. JavaBean 이라는 개념이 그 뿌리라고 할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;JavaBean 은 기본 생성자와 Getter/Setter 를 가진 &lt;b&gt;단순한 자바 객체(POJO)&lt;/b&gt;를 말합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;주로 데이터 전달 객체(DTO)처럼 사용되었고, &lt;b&gt;재사용 가능한 컴포넌트&lt;/b&gt;로 취급되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;Spring 은 이런 JavaBean 에서 &lt;b&gt;재사용 가능하고 독립적인 구성 요소(Component)&lt;/b&gt;라는 개념을 가져와서, Spring 컨테이너가 직접 관리하는 객체들을 Spring Bean 이라고 부르게 되었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;즉, &lt;b&gt;Bean 은 Spring 이 관리하는 재사용 가능한 자바 객체&lt;/b&gt;라는 의미라고 볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;span&gt;이런 설명이 이해가 어려우시면 위에서 말했던 커피와 커피콩을 연상하면서 포스팅을 읽다보면 좀 더 자연스럽게 이해되실 수도 있습니다!!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;  Bean 은 어떤 역할을 하나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;간단히 말해, &lt;b&gt;Bean 은 Spring 이 생성하고, 관리하고, 주입하는 객체&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 에서 애플리케이션을 구성하는 &lt;b&gt;핵심 구성 요소들은 모두 Bean&lt;/b&gt; 으로 관리되며, Spring 컨테이너가 이 Bean 들을 생성하고, 필요한 곳에 자동으로 주입(DI)합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;앞선 포스팅에서 Spring Framework 란 어떤 것인지에 대해 말할 때 언급했던 내용입니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring Framework 의 대표적인 특징으로 &lt;b&gt;IoC(제어의 역전), DI(의존성 주입), AOP(관점 지향 프로그래밍)&lt;/b&gt; 에서 IoC와 DI 에 대한 내용을 설명할 때 Bean 이라는 용어가 언급되었었죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;애플리케이션을 실행시키면, `ApplicationContext` 라는 스프링 IoC 컨테이너가 프로그램을 스캔하기 시작하고 Bean 들을 찾아 핵심 구성 요소들로 등록하고 관리해줍니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;우리가 개발을 할때 사용하는 @Component 라는 어노테이션이나, @Service, @Controller 등의 어노테이션을 타고 들어가다 보면 Bean 인터페이스와 연결되어 있는 것을 발견할 수 있을거에요.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이런 애플리케이션이 동작하는데 필요한 핵심 요소들을 ApplicationContext 라는 스프링 컨테이너가 Bean 으로 등록하여, 필요에 따라 Repository 는 Service 에 주입하고, Service 는 Controller 에 주입하는 등 생성과 주입을 개발자대신 Spring 이 직접 하며 IoC(제어의 역전)을 이뤄내게 되고, DI 도 할 수 있게됩니다. (앞선 포스팅과 이어서 읽으면 훨씬 이해하기 쉬울거에요!)&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;✅ Spring 에서 Bean 의 역할 요약&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 83.2558%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.2346%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;역할&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.7654%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.2346%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;객체 생성&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.7654%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;개발자가 new 로 생성하지 않아도 스프링이 대신 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.2346%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;의존성 주입&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.7654%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;필요한 Bean 을 찾아서 자동으로 주입&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.2346%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;생명주기 관리&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.7654%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;초기화, 소멸 등 생명주기를 컨테이너가 책임짐&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 27.2346%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;싱글톤 유지&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 72.7654%; height: 21px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기본적으로 애플리케이션 내에 하나의 인스턴스만 유지(Singleton Scope)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 역할들 중에서 싱글톤에 대한 개념도 추후에 &lt;b&gt;디자인패턴&lt;/b&gt;을 다루는 글에서 만나보실 수 있습니다!&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; ⚙️ Bean 은 어떻게 등록되나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 에서는 다음과 같은 방식으로 Bean 을 등록할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;1. 자동 등록 (컴포넌트 스캔)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744426310989&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class UserService {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`@Component`, `@Service`, `@Repository`, `@Controller` 등은 모두 자동 등록을 위한 어노테이션입니다. Spring 이 클래스패스를 스캔하면서 해당 어노테이션이 붙은 클래스들을 자동으로 Bean 으로 등록합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 수동 등록 (@Bean)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744426433441&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserService();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`AppConfig` 와 같은 자바 설정 클래스를 통해 명시적으로 Bean 을 등록할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;⛔ 동일한 이름 or 동일한 타입의 Bean 이 두 개 이상인 경우&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Spring 에서 Bean 을 등록할 때는 내부적으로 Bean 이름(또는 ID) 를 키로 하여 &lt;b&gt;Map 형태로 저장&lt;/b&gt;합니다.&lt;/span&gt;&lt;span&gt;그래서 같은 이름의 Bean 이 중복되면 &lt;b&gt;충돌(Confilct)이 발생&lt;/b&gt;하게 됩니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;1. 같은 이름의 Bean 이 두 번 수동 등록된 경우&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744426738169&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean(&quot;myService&quot;)
public MyService service1() {
    return new MyService();
}

@Bean(&quot;myService&quot;)  // 같은 이름으로 또 등록
public MyService service2() {
    return new MyService();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 경우에 애플리케이션 실행 시 예외가 발생합니다 (중복 등록 오류)&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;` &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;org.springframework.beans.factory.support.BeanDefinitionOverrideException&lt;/span&gt;`&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;기본적으로 Spring 은 동일한 이름의 &lt;b&gt;Bean 을 덮어쓰기 하는 것을 허용하지 않습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;하지만 이런 경우 아래와 같이 설정하면 중복 등록 시 &lt;b&gt;뒤에 있는 Bean 이 앞을 덮어쓰게&lt;/b&gt; 할 수 있습니다. 하지만 추천하지는 않는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744426939508&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.main.allow-bean-definition-overriding=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;2. 같은 타입의 Bean 이 여러 개인 경우&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744426977058&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MyService service1() {
    return new MyServiceImpl1();
}

@Bean
public MyService service2() {
    return new MyServiceImpl2();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위의 경우에는 Bean 이름은 다르지만, 타입(`MyService`)이 같기 때문에 `@Autowired` 로 주입할 때 어떤 Bean 을 주입할지 모호하기 때문에 에러가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;No qualifying bean of type 'MyService' available: expected single matching bean but found 2&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; ✅ 수동 빈 등록 및 중복될 시, 해결 방법은?&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;방법 1. `@Primary` 사용 - 우선순위 지정&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427216872&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@Primary
public MyService service1() {
    return new MyServiceImpl1();
}

@Bean
public MyService service2() {
    return new MyServiceImpl2();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;`@Primary`가 붙은 Bean 이 자동 주입 시 우선 선택됩니다. 단, `@Qualifier` 가 있으면 무시됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; &lt;b&gt;방법 2. `@Qualifier` 로 명시적 지정&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427278464&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
@Qualifier(&quot;service2&quot;)
private MyService myService;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위와 같은 방식대로 작성하면, 어떤 Bean 을 주입할지 이름으로 명확하게 지정할 수 있습니다. 이 방법이 가장 안전하고 명시적인 방법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; &lt;b&gt;방법 3. List&amp;lt;타입&amp;gt; 또는 Map&amp;lt;String, 타입&amp;gt;으로 여러 Bean 주입 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427451056&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
private List&amp;lt;MyService&amp;gt; services;

@Autowired
private Map&amp;lt;String, MyService&amp;gt; serviceMap;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 컨테이너에 등록된 `MyService` 타입의 모든 Bean 들을 모아서 주입해줍니다. `List` 는 순서대로 Bean 을 나열하고, `Map`은 Bean 이름(Key) - Bean 객체(Valeu) 형식으로 전달해 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427554663&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public MyService serviceA() {
    return new ServiceA();
}

@Bean
public MyService serviceB() {
    return new ServiceB();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위처럼 두 개의 `MySercie` Bean 이 등록되어 있다면,&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427585955&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
private List&amp;lt;MyService&amp;gt; services;  // [serviceA, serviceB]

@Autowired
private Map&amp;lt;String, MyService&amp;gt; serviceMap;  // {serviceA=ServiceA, serviceB=ServiceB}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 이 자동으로 둘 다 넣어줍니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 방식은 &lt;b&gt;플러그인처럼 여러 기능을 순차적으로 실행&lt;/b&gt;해야 할 때나, Bean 이름을 기준으로 &lt;b&gt;동적으로 실행할 로직이 있을 때&lt;/b&gt; 등 유용하게 활용할 수 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427691752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void process(String type) {
    MyService service = serviceMap.get(type);  // &quot;serviceA&quot; 또는 &quot;serviceB&quot;
    service.execute();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;위와같은 예시로, 사용자가 &quot;serviceA&quot; 를 선택하면 해당 Bean 을 꺼내서 실행하는 식으로 사용 가능합니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; Bean 이 없다면 어떻게 될까?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 없이 일반 자바 코드에서는 모든 객체를 직접 생성하고, 직접 연결해줘야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744427840197&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UserService userService = new UserService(); // 직접
OrderService orderService = new OrderService(userService); // 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;이 방식은 코드가 많아지고, 유연성이 떨어지며, 테스트하기도 어려워집니다. 그래서 Spring 에서는 &lt;b&gt;Bean 으로 객체를 대신 관리&lt;/b&gt;하고, &lt;b&gt;필요한 객체를 자동으로 연결&lt;/b&gt;해주기 때문에 코드가 훨씬 단순하고 유연해집니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;정리: Spring Bean 이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Bean 은 Spring IoC 컨테이너(ApplicationContext) 에 의해 생성되고, 관리되며, 주입되는 객체를 말합니다. 즉, 개발자가 직접 관리하지 않고, Spring 이 대신 객체를 조립하고 운용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;Spring 에서 Bean 은 단순한 객체가 아니라, IoC 와 DI 원칙에 따라 애플리케이션 전체를 구성하는 핵심 구성 요소입니다. Spring 을 이해하고 제대로 활용하려면, Bean 이란 무엇인지 단순하게 외우기 보다는, &quot;0왜 필요한가&quot;, &quot;어떻게 동작하는가&quot; 를 함께 이해하면서 개발을 하시면 좋을 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;읽어주셔서 감사합니다!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>bean</category>
      <category>DI</category>
      <category>ioc</category>
      <category>java</category>
      <category>MVC</category>
      <category>spring bean</category>
      <category>객체</category>
      <category>빈</category>
      <category>빈이란</category>
      <category>자바빈</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/111</guid>
      <comments>https://wanglan.tistory.com/entry/Java-Spring-%EC%97%90%EC%84%9C%EC%9D%98-Bean-%EC%9D%B4%EB%9E%80#entry111comment</comments>
      <pubDate>Sat, 12 Apr 2025 12:23:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring - JPA</title>
      <link>https://wanglan.tistory.com/entry/Spring-JPA</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;꼭 제대로 정리하고 싶었던 Spring 에서의 JPA (Java Persistence API)에 대한 포스팅을 해보겠습니다. 그럼 시작!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ChatGPT Image 2025년 4월 5일 오후 03_59_45.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwZlyL/btsM9zIMiDD/1iU34EGjUQjyegjMLMa0pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwZlyL/btsM9zIMiDD/1iU34EGjUQjyegjMLMa0pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwZlyL/btsM9zIMiDD/1iU34EGjUQjyegjMLMa0pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwZlyL%2FbtsM9zIMiDD%2F1iU34EGjUQjyegjMLMa0pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;554&quot; height=&quot;554&quot; data-filename=&quot;ChatGPT Image 2025년 4월 5일 오후 03_59_45.png&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JPA란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 는 자바 애플리케이션에서 &lt;b&gt;객체(Object)와 관계형 데이터베이스(RDB)를 매핑하기 위한 표준 API&lt;/b&gt; 입니다. JPA 는 DB 테이블과 자바 클래스를 일일이 SQL 로 연결하여 쿼리 등을 보내 DB와의 연락을 주고 받는 것이 아니라, 자바 객체만 다뤄도 자동으로 SQL 이 실행되는 ORM 기반 기술입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨말인지 정의에 사용된 용어부터 차근차근 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 ORM 은 Object-Relational Mapping 의 줄임말로, 객체와 관계형 데이터베이스를 연결하는 프로그래밍 기술을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 ORM 을 이해하기 쉽게 풀어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 유저 정보중 '전화번호'라는 데이터가 있으면, DB 테이블의 전화번호 부분을 조회하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'SELECT 전화번호 FROM 유저'&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 방식으로 쿼리를 요청하고 데이터를 조회할 수 있습니다. 같은 방식으로 수정이나 삭제, 등록 등을 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ORM은 같은 목적으로 행동을 하지만, &lt;b&gt;Java 의 객체 자체를 DB 테이블과 매핑&lt;/b&gt; 하여 객체를 다루기만 해도 DB에 Create, Read, Update, Delete 등을 수행할 수 있는 기술입니다. 다음으로 JPA 를 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 의 P 는 &lt;b&gt;Persistence(영속성)를 의미&lt;/b&gt;합니다. '영속성' 이라는 말을 봐도 일상생활에서 잘 사용하지 않으니 직관적으로 이해되지 않을 수 있습니다. 하지만 어느정도 느낌으로 유추가 가능합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;영원히 유지된다, 지속된다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도의 느낌인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 말이 어떻게 JPA 라는 개념을 완성하는지 문장을 만들어 보면, &lt;b&gt;Java에서 데이터를 메모리가 아닌 저장소(디스크 등)에 지속적으로 남아있게 하는 API&lt;/b&gt; 라고 정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;윗 문장에서 '데이터를 ~ 남아있게' 하는 이라는 부분은 제가 이해하기 쉽게 문장으로 표현해본 것인데 JPA 라는 단어만 봤을 땐 너무 설명이 불친절한 용어이지 않나 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 내용을 종합하면 &lt;b&gt;JPA 는 데이터를 DB에 영구적으로 Create, Read, Update, Delete 하게 하는 기술인데 그 방식이 객체 그 자체를 DB 와 매핑하여 자동으로 쿼리를 보내게 하는 API&lt;/b&gt; 이다 라고 생각할 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(설명을 위해 영구적이라는 단어를 넣었지만 사라지지 않는다라는 개념이 아니라 메모리에 잠깐남아있는 개념이 아니라는 뜻으로 이해해주시면 감사하겠습니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로 간단한 예시를 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743837906589&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Member 라는 클래스를 정의하고 `@Entity` 어노테이션을 붙이게 되면 &lt;b&gt;이 자바 클래스가 관계형 DB 테이블과 매핑&lt;/b&gt; 됩니다. 이떄 Table 에 들어가야하는 `@Id` 부분은 꼭 선언해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 저장 후 실행하면 DB에 아무런 테이블이 존재하지 않아서 JPA의 구현체인 Hibernate 가 `INSERT INTO MEMBER ~` 라는 SQL을 만들어서 DB에 보내게 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JPA 의 등장 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 가 등장하기 전, 전통 방식으로 Java 애플리케이션과 DB 를 연결하던 JDBC가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC 는 SQL 쿼리문을 직접 작성해야하고, 로직에 따라 중복된 코드가 많이 나오기도 하고 String 문자열로 쿼리를 작성하다 보니 &lt;b&gt;런타임 시점에 에러가 발생하면 유지보수가 어렵다는 단점&lt;/b&gt;이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 MyBatis 가 등장하면서 매핑 자체는 편하게 되었지만 여전히 SQL 을 직접 작성해야하는 단점이 있었고, 결과적으로 객체 지향적 코드와 관계형 DB 간의 간극이 존재하는 것은 어쩔 수 없었습니다. 이것을 해결하기위해 JPA 가 등장하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 자체는 인터페이스로 개발되었고, 실제로 구현하는 구현체는 Hibernate 를 주로 사용합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. JPA 의 동작 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 를 사용하여 CRUD 작업을 할 때, 동작의 흐름이 어떤지 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743838396223&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Member member = new Member(&quot;홍길동&quot;);
memberRepository.save(member);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같은 코드를 작성하여 member 라는 Member 타입 객체를 선언하여 이름을 초기화 해주고, '홍길동'이라는 새 이름으로 생성된 이 객체를 memberRepository 를 호출하여 save() 해주면 DB에 객체 자체의 정보를 보고 데이터를 저장하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 순서대로 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743838493320&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[개발자]
 &amp;darr;
Member 객체 생성
 &amp;darr;
EntityManager.persist(member)
 &amp;darr;
JPA가 SQL 생성 (INSERT INTO ...)
 &amp;darr;
JDBC API를 통해 DB에 SQL 전달
 &amp;darr;
DB에 데이터 저장&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 이런 흐름으로 JPA 가 동작하는데, 여기서 앞서 설명했던 persist 라는 메서드가 눈에 들어오는군요. 이 부분에 대해서 설명하면, EntityManager 라는 데이터 관리자가 메서드를 사용하여 생성한 member 객체에 대한 데이터를 DB에 영속화 한다 라고 말할 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 JPA 에서 상당히 중요한 부분인 &lt;b&gt;'영속성 컨텍스트'&amp;nbsp;&lt;/b&gt;에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 영속성 컨텍스트?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트란 JPA 에서 &lt;b&gt;엔티티 객체를 저장하고 관리하는 일시적인 메모리 공간&lt;/b&gt;입니다. 쉽게 말하면, DB와 애플리케이션 사이에 있는 &lt;b&gt;&quot;중간 캐시&quot; 역할을 하는 가상의 저장소&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 는 단순히 자바 객체 자체를 DB에 바로 보내버리는 것이 아니라, 그 객체를 트랜잭션 동안 메모리에서 일관성있게 관리하는 기능을 제공합니다. 영속성 컨텍스트가 하는 기능 덕분에 객체가 가지고 있는 데이터의 정합성을 보장할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;영속성 컨텍스트의 역할&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1차 캐시&lt;/li&gt;
&lt;li&gt;변경 감지&lt;/li&gt;
&lt;li&gt;쓰기 지연&lt;/li&gt;
&lt;li&gt;트랜잭션 단위의 정합성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 네가지가 대표적인 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 간단한 코드로 예시를 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743840976727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateMember(Long id) {
    Member member = em.find(Member.class, id); // 1차 캐시에 저장됨
    member.setName(&quot;바뀐이름&quot;);                // 변경 감지
    // 커밋 시점에 update SQL 자동 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 `em.find()` 한 엔티티는 영속성 컨텍스트에 저장되고, 트랜잭션이 커밋될 때 `setName()` 으로 바뀐 값이 자동으로 DB에 반영됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1차 캐시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 캐시는 무엇이냐? 1차 캐시가 있으면 2차 캐시도 있고 3차캐시도 있는건가? 라는 생각이 드실수도 있을 것 같습니다. 왜냐하면 제가 처음 학습할 때 그랬었거든요..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자꾸 이해를 위해 설명이 돌아가는 것 같지만 캐시를 위해 &lt;b&gt;JVM&lt;/b&gt; 에 대한 이야기 부터 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 하나의 자바 애플리케이션을 작성하고 프로그램을 가동하면, 운영체제에서는 JVM(자바 가상 머신)이라는 프로세스를 하나 만들어서 실행하게 됩니다. 프로세스는 여러분도 아시다시피 `Ctrl + Alt + Delete` 버튼을 누르고 작업관리자를 키면 컴퓨터에서 실행되고 있는 프로그램의 단위 하나를 의미합니다. 크롬 브라우저가 켜있으면 크롬 프로세스가 있고, 카카오톡 앱이 켜져있으면 카카오톡도 프로세스를 차지하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이런 방식으로 운영체제는 JVM 을 프로세스 하나로 잡아 생성하게 되고, 이때 운영체제는 JVM 내부에 다양한 메모리를 할당해 줍니다. 학습하셨다면 들어보신 적 있을 Heap 영역, Stack 영역, Metaspace 영역 등 다양하게 분리하여 할당해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 `new` 키워드로 객체를 생성하면 Heap 영역이라는 메모리에 이 객체가 저장되게 됩니다. 앞서 언급했던EntityManger 나 그 안의 Persistence Context(영속성 컨텍스트) 도 JVM 객체이므로 Heap 메모리에 저장되게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 영속성 컨텍스트는 1차 캐시에 저장된다고 표현 하지만, 이것은 Heap 영역 내부에 따로 물리적으로 구분되어 있는 공간이라기 보다는 '영속성 컨텍스트가 사용하는 전용 1차 캐시 공간' 과 일반 캐시인 '애플리케이션 전체 범용 2차 캐시'와 같은 개념으로 논리적으로 구분되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 캐시는 트랜잭션 단위로 활용하는 부분이고, 2차 캐시는 전역 또는 분산 캐시 메모리라고 생각하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명이 돌아갔지만 다시 영속성 컨텍스트의 1차 캐시에 대한 설명을 이어가보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차 캐시는 &lt;b&gt;영속성 컨텍스트가 활성화 되어있는 트랜잭션 환경에서 동일한 엔티티를 반복 조회 할 때, DB 에 다시 접근하게 하는 것이 아닌 캐시에 저장해둔 데이터를 활용&lt;/b&gt;할 수 있도록 하는 메모리 공간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 처음 DB 테이블에서 가져온 값을 JVM 의 Heap 메모리에 생성되어 있는 영속성 컨텍스트의 트랜잭션 공간에 임시로 저장해 두었다가, 같은 정보를 조회해야한다면 멀리있는 DB 까지 다시 달려가는 것이 아니라 1차 캐시에 저장되어있는 데이터를 확인합니다. 이렇게 때문에 속도적인 측면에서도 매우 뛰어나고, I/O 비용적인 측면에서도 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 저장되는 구조는 엔티티의 ID 값을 기준으로 Map 구조로 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743840097899&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[1차 캐시 구조]
{
    &quot;Member:1&quot; : Member(id=1, name=&quot;홍길동&quot;),
    &quot;Team:10&quot;  : Team(id=10, name=&quot;백엔드팀&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저장되므로, 같은 ID의 엔티티를 다시 조회하면 DB에 재접속 하지않고 &lt;b&gt;캐시에서 바로 반환&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;변경 감지 (Dirty Checking)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트가 생성되어 있는 상태에서 객체의 필드 값이 바뀌면 자동으로 &lt;b&gt;update 쿼리문을 생성&lt;/b&gt;합니다. 이 다음 키워드를 위해 update 쿼리문을 '실행함' 이 아닌 &lt;b&gt;'생성함'&lt;/b&gt; 임을 잘 기억해주세요!&lt;/p&gt;
&lt;pre id=&quot;code_1743840153594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;member.setEmail(&quot;new@email.com&quot;); // 새로운 이메일로 수정했음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드가 실행되고 트랜잭션이 커밋 되면&lt;/p&gt;
&lt;pre id=&quot;code_1743840185571&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE member SET email = 'new@email.com' WHERE id = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같은 &lt;b&gt;생성되어 있던 쿼리문&lt;/b&gt;이 DB에 날아가게 됩니다. JPA 는 개발자가 SQL 을 직접 작성하지 않아도 엔티티 객체의 변화를 감지해 자동으로 반영해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;쓰기 지연 (Write-Behind)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 매니저가 영속성 컨텍스트 내부를 관리하는 도중 persist() 메서드를 호출한다고 곧바로&amp;nbsp; SQL 이 나가지는 않습니다. JPA 는 쿼리를 영속성 컨텍스트 내부의 SQL 저장소에 쌓아 두었다가, &lt;b&gt;트랜잭션 커밋 시점에 한번에 DB에 반영&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 굳이 이렇게 할까 고민을 해보았는데, 억지로 다음과 같은 상황의 비즈니스 로직을 상상해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A 라는 유저의 이메일이 aaa@example.com 이었는데 이것을 bbb@example.com 으로 변경하여 A 유저 객체에 새로운 값이 설정되었다고 가정하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이때 쓰기 지연이 없다면&lt;/b&gt; UPDATE 쿼리문을 DB에 보내게 되겠고, 이어서 이메일 중복 방지를 위해 bbb@example.com 이라는 이메일 존재유무를 조회하는 쿼리를 다시 보내 DB에서 데이터를 가져오게 했을 때, B 유저의 이메일이 이미 bbb@example.com 이었다면 다시 A 유저 객체에 원래 이메일인 aaa@example.com 으로 설정하고 DB에 UPDATE 쿼리문을 보내게 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이런 데이터 정합성에 대한 내용은 특정 예외가 던져졌을 때 트랜잭션 환경에서 처리될 일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터를 x -&amp;gt; y 로 바꿨다가 y -&amp;gt; x 로 되돌리는 비즈니스 로직이 있다면 어차피 원래의 데이터대로 DB의 값이 돌아와야되기 때문에 불필요한 쿼리를 보내지 않게하여 성능 최적화를 하는 것이 낫다고 생각듭니다. &lt;b&gt;이런 부분을 위해 쓰기 지연 기능을 구현해두었다고 이해하시면 될 것 같습니다&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트랜잭션 단위의 정합성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 대한 내용은, 같은 트랜잭션에서는 항상 동일한 엔티티 인스턴스가 사용되기 때문에 동일성이 보장된다는 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 영속성 컨텍스트와 관련한 용어를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영속(Persistent) : 영속성 컨텍스트가 관리하고 있는 상태&lt;/li&gt;
&lt;li&gt;비영속(Transient) : 새로 생성된 객체, 아직 관리되지 않은 상태&lt;/li&gt;
&lt;li&gt;준영속(Detached) : 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태&lt;/li&gt;
&lt;li&gt;삭제(Removed) : 삭제로 표시되었지만 DB 에는 아직 반영되지 않은 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 용어들이 있다라고 생각하시고 가볍게 알아두셔도 될 것 같습니다. JPA 에 대한 내용을 이야기하다가 영속성 컨텍스트에 대한 내용으로 너무 깊게 빠진 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개인적으로 영속성 컨텍스트는 JPA 의 심장과 같은 핵심 개념이라고 생각합니다. 단순히 객체를 저장하는 데서 끝나는 것이 아니라, 데이터의 정합성, 성능, 생산성 등을 책임지는 JPA 핵심 메커니즘이기 때문이죠. 이제 이어서 JPA 에 대한 내용을 조금 더 다뤄보고 포스팅을 마무리하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. JPA 핵심 요소 및 장단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 로 매핑을 구현하면, 특정 테이블에 대한 세부사항을 정의할 Entity 를 작성하게 됩니다. 핵심 구성요소는 다음과 같습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 76.9768%; height: 233px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;구성요소&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;`@Entity`&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;DB 테이블과 매핑할 클래스임을 선언&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;`@Id`&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;엔티티의 기본 키(PK) 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;`@GeneratedValue`&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;ID 자동 생성 전략 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;`@OneToMany`, `@ManyToOne`&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;테이블 간 관계 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 36.8025%;&quot;&gt;`@Transactional`&lt;/td&gt;
&lt;td style=&quot;width: 63.1975%;&quot;&gt;트랜잭션 범위 내에서 영속성 컨텍스트 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 `@OneToMany`, `@ManyToOne` 는 다른 테이블간의 관계를 설정하는 키워드입니다. @ManyToMany 도 존재하는데, 이 테이블 간 관계설정에 대한 내용은 추후에 `N+1 문제` 키워드에 대해 다룰 때 더 자세히 이야기하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;✅ JPA의 장점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 의 장점으로는 반복적인 SQL 작성이 불필요하기 때문에, 생산성이 뛰어납니다. 또한 도메인 모델 중심 개발을 통해 유지보수하기도 쉽고, 1차 캐시나 쓰기 지연 등으로 성능 최적화에도 이점이 있고, 영속성 컨텍스트 + 트랜잭션을 통한 데이터 정합성 유지에도 좋다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;⚠️ JPA의 단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 의 단점 중 가장 크다고 느끼는 부분은, JOIN 을 하거나 복잡한 조건의 쿼리를 구현하기 어렵습니다. 이럴 때 복잡한 쿼리는 QueryDSL 이나 native query 등으로 보완할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 JPA 를 그냥 아무런 지식없이 배우기엔 조금 어려운 러닝 커브를 가지고 있는 것 같습니다. 트랜잭션이나 지연로딩, 객체지향 설계에 대한 개념이 기본적으로 필요하기 때문에 이 부분에 대한 개념을 탄탄히 하고 학습 후 적용한다면 좋을 것 같습니다. 또는 JDBC 를 사용하여 DB를 관리하는 방식의 학습을 선행으로 진행해보고 프로그램이 어떻게 동작하는지 내부적으로 자세히 알고 나서 JPA 를 학습한다면 많은 도움이 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 단점으로는 쿼리를 자동으로 생성하다 보니 어떤 쿼리가 나갈지 헷갈릴 수가 있습니다. 구현체인 Hibernate 를 통해서 쿼리가 날아가는 것을 로그로 확인할 수 있지만, 이것을 간과하고 자동으로 보내는 쿼리에 익숙해져 있다보면 N + 1 문제 등 많은 함정에 빠져 성능저하를 일으킬 수 있습니다. 꼭 Hibernate 의 설정에서 어떤 쿼리가 날아가는지 확인하시고 개발하시길 바랍니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 는 단순한 DB 매핑 기술이라고 생각하는 것 보다는 객체지향적인 사고방식으로 DB를 다루는 방식이라고 생각하는 것이 맞는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 SQL 보다 중요해진 시대에서 JPA 를 활용하고 이해할 줄 아는 개발자가 지속적으로 필요할 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴글 읽어주셔서 감사합니다! 다음에는 N+1 문제, QueryDSL, 트랜잭션 중에 하나의 내용을 가지고 돌아오겠습니다!!&lt;/p&gt;</description>
      <category>Spring</category>
      <category>JPA</category>
      <category>N+1</category>
      <category>ORM</category>
      <category>querydsl</category>
      <category>springboot</category>
      <category>더티체킹</category>
      <category>백엔드</category>
      <category>웹개발</category>
      <category>주니어개발자</category>
      <category>지연로딩</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/110</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-JPA#entry110comment</comments>
      <pubDate>Sat, 5 Apr 2025 17:39:27 +0900</pubDate>
    </item>
    <item>
      <title>Spring Framework 와 Spring Boot (2)</title>
      <link>https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 지난 포스팅에 Spring Framework 와 Spring Boot 에 대한 내용을 포스팅하다 글이 길어져서 Spring Boot 에 대한 내용은 이번 포스팅에서 진행하도록 하겠습니다. 지난 포스팅과 이어지기 때문에 앞선 포스팅도 읽어주시면 감사하겠습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743823480632&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Framework 와 Spring Boot (1)&quot; data-og-description=&quot;이번 포스팅에서는 Spring Framework 와 Spring Boot 에 대한 내용을 포스팅 해보려고 합니다. Spring Framework 의 등장 배경과 진화 과정, 장단점, 철학, 비판 등 Spring Boot 에 도달하기 까지의 과정을 알아보&quot; data-og-host=&quot;wanglan.tistory.com&quot; data-og-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&quot; data-og-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAdLhw/hyYA9Kc9XM/DSZhk0zVTm9S5mAVnOQug0/img.png?width=226&amp;amp;height=127&amp;amp;face=0_0_226_127,https://scrap.kakaocdn.net/dn/G36Jg/hyYCjlhZN6/PmD6OiuC2cQlJwyYWiGe5k/img.png?width=226&amp;amp;height=127&amp;amp;face=0_0_226_127&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAdLhw/hyYA9Kc9XM/DSZhk0zVTm9S5mAVnOQug0/img.png?width=226&amp;amp;height=127&amp;amp;face=0_0_226_127,https://scrap.kakaocdn.net/dn/G36Jg/hyYCjlhZN6/PmD6OiuC2cQlJwyYWiGe5k/img.png?width=226&amp;amp;height=127&amp;amp;face=0_0_226_127');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework 와 Spring Boot (1)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Spring Framework 와 Spring Boot 에 대한 내용을 포스팅 해보려고 합니다. Spring Framework 의 등장 배경과 진화 과정, 장단점, 철학, 비판 등 Spring Boot 에 도달하기 까지의 과정을 알아보&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wanglan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;springboot.jpg&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XGqac/btsNamuKGvm/c0TEVj0ipTJMiWDcTnXb80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XGqac/btsNamuKGvm/c0TEVj0ipTJMiWDcTnXb80/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XGqac/btsNamuKGvm/c0TEVj0ipTJMiWDcTnXb80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXGqac%2FbtsNamuKGvm%2Fc0TEVj0ipTJMiWDcTnXb80%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;332&quot; height=&quot;152&quot; data-filename=&quot;springboot.jpg&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;1. Spring Boot 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Spring Framework 도 기업용 Java 애플리케이션 개발을 위한 EJB 의 단점을 극복하고 새로운 시작인 '봄'을 의미하며 발전해온 프레임워크인데 마냥 '봄'같이 따스하지만은 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Framework 의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring&amp;nbsp; Framework 는 유연성과 확장성면에서 강력했지만, 다음과 같은 단점이 있었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 설정지옥&lt;br /&gt;2. 의존성 충돌&lt;br /&gt;3. 톰캣(Tomcat) 같은 웹 애플리케이션 서버(WAS)를 외부에서 직접 설치 및 설정해야 함&lt;br /&gt;4. 프로젝트 시작이 어려움&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 네가지에 대한 이야기를 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 Spring Framework 는 XML 이나 Java 기반 설정을 너무 많이 요구했다고 합니다. 프로젝트를 시작도 하기전 XML 설정 파일을 수십~수백줄을 작성하고 개발을 시작해야한다면, 숙제를 해내기 위한 숙제를 받아하는 느낌이라는 생각도 듭니다. 아래의 코드를 정말 간단하게 보고 넘어가주시길 바랍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743819857008&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
       xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
       xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
       xsi:schemaLocation=&quot;
       http://www.springframework.org/schema/beans     
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd&quot;&amp;gt;

    &amp;lt;!-- 컴포넌트 스캔 설정 --&amp;gt;
    &amp;lt;context:component-scan base-package=&quot;com.example.service&quot;/&amp;gt;

    &amp;lt;!-- DataSource 설정 --&amp;gt;
    &amp;lt;bean id=&quot;dataSource&quot; class=&quot;org.apache.commons.dbcp.BasicDataSource&quot;&amp;gt;
        &amp;lt;property name=&quot;driverClassName&quot; value=&quot;com.mysql.cj.jdbc.Driver&quot;/&amp;gt;
        &amp;lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/test&quot;/&amp;gt;
        &amp;lt;property name=&quot;username&quot; value=&quot;root&quot;/&amp;gt;
        &amp;lt;property name=&quot;password&quot; value=&quot;1234&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;

    &amp;lt;!-- JPA EntityManager 설정 --&amp;gt;
    &amp;lt;bean id=&quot;entityManagerFactory&quot; class=&quot;org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean&quot;&amp;gt;
        &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot;/&amp;gt;
        &amp;lt;property name=&quot;packagesToScan&quot; value=&quot;com.example.domain&quot;/&amp;gt;
        &amp;lt;property name=&quot;jpaVendorAdapter&quot;&amp;gt;
            &amp;lt;bean class=&quot;org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter&quot;/&amp;gt;
        &amp;lt;/property&amp;gt;
    &amp;lt;/bean&amp;gt;

    &amp;lt;!-- 트랜잭션 매니저 설정 --&amp;gt;
    &amp;lt;bean id=&quot;transactionManager&quot; class=&quot;org.springframework.orm.jpa.JpaTransactionManager&quot;&amp;gt;
        &amp;lt;property name=&quot;entityManagerFactory&quot; ref=&quot;entityManagerFactory&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;

    &amp;lt;!-- 트랜잭션 어노테이션 활성화 --&amp;gt;
    &amp;lt;tx:annotation-driven transaction-manager=&quot;transactionManager&quot;/&amp;gt;
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는&amp;nbsp; 앞선 포스팅에서 언급했던 IoC 컨테이너인 applicationContext 에 대한 설정을 하는 xml 파일이었습니다. 단순히 DB 를 연결하고, JPA 를 사용하는 설정인데도 최소 50~100줄을 넘어버립니다. 요즘이야 영특한 AI 모델을 사용하면 이런 번거로움이 약간 덜어질지는 모르겠지만, 과거에는 이런코드를 직접 작성하며 개발을 위한 준비를 했다고 생각하니 막막하기만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 Spring 4 이후부터는 Java 로 설정하는게 가능하게 되었습니다. 그래도 설정량은 여전히 많았습니다. 아래코드도 살짝만 읽고 넘어가셔도 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743820065784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = &quot;com.example&quot;)
public class AppConfig {

    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(&quot;com.mysql.cj.jdbc.Driver&quot;);
        ds.setUrl(&quot;jdbc:mysql://localhost:3306/test&quot;);
        ds.setUsername(&quot;root&quot;);
        ds.setPassword(&quot;1234&quot;);
        return ds;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(dataSource());
        emf.setPackagesToScan(&quot;com.example.domain&quot;);
        emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
        return emf;
    }

    @Bean
    public JpaTransactionManager transactionManager() {
        return new JpaTransactionManager(entityManagerFactory().getObject());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 보다는 낫지만 여전히 DataSource, JPA, 트랜잭션 매니저 등 모든 걸 직접 작성해야할 때 였습니다. 이런 설정 지옥이라는 말이 괜히 나온 것이 아니라는 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 두번째 포인트였던 의존성 충돌은, 라이브러리 버전 호환 문제를 개발자가 직접 해결해야 했던 단점이 있었고, 톰캣같은 웹 애플리케이션 서버도 직접 설치 및 설정하는 것도 번거로웠습니다. 이런 단점들이 모여 초기 작업이 개발자의 생산성을 저하시켰다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Boot 의 등장  &lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&quot;Spring 을 더 쉽게, 빠르게, 편하게 쓰게 하자&quot;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;라는 모토로 2014년, Pivotal 에서 &lt;b&gt;Spring Boot&lt;/b&gt; 를 발표하게 됩니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 는 기존 Spring Framework 의 기능은 그대로 유지하면서도, &lt;b&gt;설정을 자동화&lt;/b&gt;하고 &lt;b&gt;기본값을 제공&lt;/b&gt;하여 생산성을 높인 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Boot 의 핵심 특징&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 90.6977%; height: 297px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px; text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;Auto Configuration(자동 설정)&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;의존성에 따라 스프링 설정을 자동으로 구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;Starter Dependencies&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;`spring-boot-starter-web` , `starter-data-jpa` 등 필요한 기능을 묶어서 제공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;내장 웹 서버&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;톰캣, Jetty, Undertow 등 내장되어 외부 WAS 설치 필요 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;application.properties 설정&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;YAML 또는 properties 파일 한 곳에서 전체 설정 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;쉬운 테스트 환경&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;테스트용 내장 DB, 프로파일 설정 등 쉽게 구성 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 32.5581%; height: 17px;&quot;&gt;main() 메서드 실행&lt;/td&gt;
&lt;td style=&quot;width: 67.4419%; height: 17px;&quot;&gt;SpringApplication.run() 하나로 서버 실행 가능(jar 배포)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 특징 중 눈에 띄는 것은&lt;b&gt; Auto Configuration&lt;/b&gt; 과 &lt;b&gt;Starter Dependencies&lt;/b&gt;, &lt;b&gt;application.properties&lt;/b&gt; 설정 이 세가지인 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Auto Configuration (자동 설정) 이란?&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Boot가 classpath(클래스 경로)에 있는 의존성들을 자동으로 감지&lt;/span&gt;&lt;/b&gt;하고,&lt;br /&gt;그에 맞는&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; Spring Bean 설정을 자동으로 구성&lt;/b&gt;&lt;/span&gt;해주는 기능입니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 쉽게 말하면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;너가 어떤 라이브러리를 프로젝트에 넣었는지 보고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;내가 필요한 설정을 대신 만들어줄게!&quot;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 말하는 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Spring Boot 에서는 build.gradle 에 등록한 의존성들이 전부 클래스패스에 자동으로 포함됩니다. 그리고 나서 내부적으로 `classpath:` 를 사용해서 자원을 찾습니다. 이렇게 H2, Web, JPA 등의 클래스패스에 존재하는 라이브러리를 확인하고 나서, &lt;br /&gt;2. 특정 클래스의 설정이 존재하지 않으면 그 클래스를 자동 설정하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 개발자가 직접 설정한 Bean 이 있으면 자동 설정은 무시됩니다. 오버라이드가 가능하다는 얘깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 자동 설정 예시로 Spring WEB 에 관련된 설정을 들어보도록 하겠습니다. 코드는 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743821380187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dependencies 에 위의 한줄만 추가하면 Spring Boot 는 자동으로 다음을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. DispatcherServlet : 스프링 MVC 핵심 서블릿&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. RequestMappingHandlerMapping : URL 라우팅 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Embedded Tomcat : 내장 서버 구동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. Jackson : JSON 변환기 기본 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 의존성에 대한 내용을 자동으로 설정해주고, 만약 개발자가 오버라이딩 한 부분이 있다면 그 설정을 반영해주는 아주 똑똑한 기능이라고 할 수 있겠습니다.&lt;br /&gt;&lt;br /&gt;또한 starter dependencies 는 위와 같은 &lt;b&gt;다양한 라이브러리들의 의존성을 모아놓은 '모음집'&lt;/b&gt; 이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743821768507&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// spring-boot-starter-web의 내부 의존성들
spring-boot-starter
spring-web
spring-webmvc
spring-boot-starter-json
spring-boot-starter-tomcat&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 starter 내부에는 위와 같은 다양한 의존성이 이미 포함되어 있는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지의 내용으로 상상해보면, Spring Framework 가 요리를 위한 &lt;b&gt;식재료가 다양하게 모여있는 슈퍼마켓&lt;/b&gt;이었는데 마트까지 가기위해 &lt;b&gt;씻고, 운전하고, 가져오고 하는것이 힘들어서&lt;/b&gt;, Spring boot 라는 &lt;b&gt;ssg 배송&lt;/b&gt;이 생긴 것 같은 느낌이네요. 그중에서 starter 는 각 &lt;b&gt;식재료를 잘 다듬어서 한 곳에 모아둔 밀키트&lt;/b&gt; 같다고 생각이 듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 이 Starter 는 이런 단순히 모음집의 기능 뿐만 아니라, 버전 충돌 없이 Spring Boot BOM 이 호환성을 자동으로 정리해주고, 설정도 최소화 하며, 빠른 개발 시작도 가능한데 테스트 스타터까지 이용하면 Mock, Test 용 DB 도 자동 구성해서 할 수 있어 참 좋은 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 아까 상단에서 작성해 놓은 XML 파일 대신 모든 설정을 properties 나 YAML 파일 단 하나에 간단히 정리하여 설정할 수 있다는 점에서 Spring Boot 의 장점이 돋보이는 것 같습니다. 다음으론 이런 Spring Boot 프로젝트의 기본구조를 예시로 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시: Spring Boot 프로젝트 기본 구조&lt;/h4&gt;
&lt;pre id=&quot;code_1743822199165&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/
 └── main/
      ├── java/
      │    └── com.example.demo/
      │         ├── DemoApplication.java  &amp;larr; main 진입점
      │         └── controller/
      │              └── HelloController.java
      └── resources/
           ├── application.properties     &amp;larr; 설정 파일
           └── static/, templates/        &amp;larr; 정적 파일 및 템플릿&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 `@SpringBootApplication` 어노테이션을 붙인 클래스 하나로 애플리케이션이 시작하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 어플리케이션은 내부적으로 다음 세가지 기능을 포함합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;@Configuration&lt;/li&gt;
&lt;li&gt;@EnableAutoConfiguration&lt;/li&gt;
&lt;li&gt;@ComponentSacn&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`@Configuraion` 은 Java 클래스를 설정 파일로 인식하게 해줍니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743822342855&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 AppConfig 가 설정 클래스임을 Spring 한테 알려주고, Bean 으로 등록 가능하게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`@ComponentScan` 은 지정한 패키지부터 하위 패키지를 스캔하여 Bean 으로 등록 합니다&lt;/b&gt;.&lt;/p&gt;
&lt;pre id=&quot;code_1743822419318&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ComponentScan(basePackages = &quot;com.example&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동으로 @Component, @Service, @Repository, @Controller 와 같은 어노테이션이 붙은 클래스를 찾고, Spring 컨테이너에 Bean 등록을 자동으로 해주게 됩니다. 이것은 @SpringBootApplication 이 선언된 패키지를 기준으로 하위만 스캔됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`@EnableAutoConfiguration` 은 의존성과 환경을 보고 필요한 Bean 을 자동으로 구성합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ConditionalOnClass, @ConditionalOnMissingBean 등에 따라 자동 등록 여부를 결정합니다. 예시로는 아까 언급한 `spring-boot-starter-web` 을 사용하면 DispatcherServlet, 톰캣 을 자동으로 설정하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실제 실행 흐름 정리!&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2052&quot; data-start=&quot;2004&quot;&gt;main() 실행 &amp;rarr; SpringApplication.run(...) 호출&lt;/li&gt;
&lt;li data-end=&quot;2097&quot; data-start=&quot;2053&quot;&gt;@SpringBootApplication &amp;rarr; 세 가지 어노테이션 활성화&lt;/li&gt;
&lt;li data-end=&quot;2138&quot; data-start=&quot;2098&quot;&gt;@ComponentScan &amp;rarr; 패키지 내 Bean 탐색 및 등록&lt;/li&gt;
&lt;li data-end=&quot;2194&quot; data-start=&quot;2139&quot;&gt;@EnableAutoConfiguration &amp;rarr; 라이브러리 보고 필요한 Bean 자동 설정&lt;/li&gt;
&lt;li data-end=&quot;2235&quot; data-start=&quot;2195&quot;&gt;@Configuration &amp;rarr; 수동 설정이 있을 경우 함께 등록&lt;/li&gt;
&lt;li data-end=&quot;2293&quot; data-start=&quot;2236&quot;&gt;설정 완료 후 Spring Context(ApplicationContext) 초기화 &amp;rarr; 서버 구동&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring Boot 의 장단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 내용을 요약해보자면 Spring Boot 의 장점으로는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;빠른 개발 시작&lt;/b&gt; : Spring Initializr로 즉시 프로젝트 시작 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 최소화&lt;/b&gt; : 필요한 설정만 추가하면 나머지는 자동 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합 개발 환경&lt;/b&gt; : Spring Security, JPA, Redis 등 통합이 쉬움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 용이&lt;/b&gt; : 테스트 프로파일 분리, 내장 DB 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도가 있습니다. 물론 MSA 구현에 유리하다는 것과 팀 단위 개발에도 효율적이라는 부가적인 장점도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 단점 또한 없진 않은 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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; : 내장 서버, 여러 의존성이 포함되어 jar 크기가 커질 수 있음&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 개발을 시작하면서 설명을 간단하게 들으면서 dependencies 에 한줄 씩 추가하고, &lt;b&gt;&quot;아 이렇게 한줄 추가하고 사용하는 거구나&quot;&lt;/b&gt; 라고 단순히 알고 넘어갔었습니다. EJB 나 Spring Framework 과거버전에서 개발을 진행해보았더라면 Spring Boot 의 이점을 잘 이해하고, 내부 자동 설정을 전부 뜯어보지 않더라도 어느정도 흐름을 이해할 수 있었을 것 같습니다. (그러지 못했다는 아쉬움이 있습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Boot 를 처음으로 개발을 익히고 학습하는 주니어 개발자들은 이런 내용을 이론적으로 배우는 것 보다 실무적인 적용, 라이브러리 활용, 빠른 개발 등의 테크닉을 위주로 배우는 것 같습니다. 얼른 개발자가 되어 취업을 하고, 한명의 개발자로서 1인분을 하기 위해선 우선 실제 코드를 작성할 줄 알아야 하니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 시간을 들여 아주 기본적인 Spring Framework 와 Spring Boot 에 대한 내용을 알아두는 것도 좋을 것 같습니다. 현재는 Spring Boot 를 대부분의 Spring 기반 프로젝트에서 표준으로 사용하고 있지만, 설정 지옥에 빠지더라도 정말 복잡한 설정이 필요한 엔터프라이즈 시스템을 만들기 위해서 Spring Framework 를 채택하여 실무에서 개발을 하게 될 수도 있으니까요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마무리로 요약하자면...&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot는 Spring Framework의 철학을 기반으로 하되, 현실적이고 실용적인 방식으로 개선된 프레임워크&lt;/li&gt;
&lt;li&gt;개발자가 비즈니스 로직에 집중할 수 있도록 많은 설정을 대신 처리&lt;/li&gt;
&lt;li&gt;빠른 개발, 쉬운 배포, 클라우드 환경에서의 확장성까지 고려한 &quot;&lt;b&gt;현대적인 Spring의 진화&quot;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>springboot</category>
      <category>SpringFramework</category>
      <category>설정지옥</category>
      <category>주니어개발자</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/109</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2#entry109comment</comments>
      <pubDate>Sat, 5 Apr 2025 12:28:11 +0900</pubDate>
    </item>
    <item>
      <title>Spring Framework 와 Spring Boot (1)</title>
      <link>https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 &lt;b&gt;Spring Framework&lt;/b&gt; 와 &lt;b&gt;Spring Boot&lt;/b&gt; 에 대한 내용을 포스팅 해보려고 합니다. Spring Framework 의 등장 배경과 진화 과정, 장단점, 철학, 비판 등 Spring Boot 에 도달하기 까지의 과정을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring Framework 란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MkNih/btsM1ce9Ptv/DsmTASJ8d0Ua6KTcuAaGOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MkNih/btsM1ce9Ptv/DsmTASJ8d0Ua6KTcuAaGOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MkNih/btsM1ce9Ptv/DsmTASJ8d0Ua6KTcuAaGOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMkNih%2FbtsM1ce9Ptv%2FDsmTASJ8d0Ua6KTcuAaGOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;226&quot; height=&quot;127&quot; data-origin-width=&quot;226&quot; data-origin-height=&quot;127&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework 는 자바 플랫폼에서 기업용 애플리케이션을 개발하기 위한 범용 애플리케이션 프레임워크입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이름의 이유는 과거 기업용 어플리케이션 개발을 위해&amp;nbsp; &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Java EE(엔터프라이즈 에디션)의 스펙을 구현한 EJB를 통한 개발을 진행할 때의 어려움과 복잡함을 개선하고, EJB 시절의 어려움을 '겨울'에 빗대어 새로운 시작인 '봄'을 생각하며 작명을 했다고 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;저는 공대 출신이기에 처음 Spring Framework 라는 단어를 봤을 때 직관적으로는 용수철을 의미하는 Spring 일까 생각했는데 생각보다 문과적인 감성의 작명 센스를 가지신 선배 개발자분들도 많다는 생각을 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특징으로는&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Java 기반의 &lt;b&gt;모듈형 아키텍쳐&lt;/b&gt; 제공&lt;br /&gt;2. &lt;b&gt;객체지향 설계 원칙&lt;/b&gt;에 충실&lt;br /&gt;3. 대표적 특징: &lt;b&gt;제어의 역전, 의존성 주입, AOP(관점지향 프로그래밍)&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세가지 정도가 있겠습니다. 여기서 &lt;b&gt;모듈형 아키텍쳐&lt;/b&gt;란 애플리케이션을 기능 단위로 나누어서, 각 모듈이 독립적으로 개발, 테스트, 유지보수 될 수 있도록 구성하는 설계방식입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 말하자면, 하나의 거대한 프로젝트를 여러 개의 기능 블록(모듈)로 나누고, 모듈들이 느슨하게 연결되어 협력하도록 설계하는 구조입니다. 물론 이렇게 설명을 들어도 용어의 이해가 어려울 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 들어보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 에서는 개발 중, 기능에 따라 코드를 작성해 가면서 접하는 다양한 모듈이 있습니다. 기본적이고 대표적인 모듈로는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Spring Web&lt;/b&gt;: DispatcherServlet, 웹 MVC 같은것들 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Spring AOP:&lt;/b&gt; AOP 구현에 관한 모듈입니다. 주로 로깅등을 학습하시면 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Spring JDBC:&lt;/b&gt; 과거 사용되었던 JDBC 템플릿과 같은 DB 와의 연결을 할때 사용되었던 모듈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Spring ORM:&lt;/b&gt; JPA 의 구현체인 Hibernate&amp;nbsp; 등의 ORM 을 연동 하는 모듈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Spring Test:&lt;/b&gt; 테스트나 mock 객체 지원에 대한 모듈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이외에도 다양한 모듈이 존재하고 위의 다섯가지는 모듈이 무엇인지 이해를 돕기위한 예시입니다. 이러한 모듈들을 의존성 주입을 받아 사용할 수 있습니다. 아래 코드는 자주 보신적 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743321603990&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework:spring-web'         // 웹 기능만 사용
    implementation 'org.springframework:spring-aop'          // AOP 기능
    implementation 'org.springframework:spring-context'      // ApplicationContext
    implementation 'org.springframework:spring-jdbc'         // DB 연결
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 필요한 것만 가져다 쓸 수 있도록 만든것이 &lt;b&gt;Spring 의 모듈형 구조&lt;/b&gt;라고 이해하면 되겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 모듈형이 중요한가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 대한 내용을 알아보겠습니다. 키워드는 다음 5가지가 있습니다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;책임 분리:&lt;/b&gt; 각 모듈이 명확한 책임을 가지도록 설계됨 (SRP: 단일 책임 원칙)&lt;br /&gt;&lt;b&gt;독립 개발:&lt;/b&gt; 모듈 단위로 개발과 테스트가 가능&lt;br /&gt;&lt;b&gt;테스트 용이:&lt;/b&gt; 하나의 기능만 테스트 가능 &amp;rarr; 빠르고 정확 &lt;br /&gt;&lt;b&gt;배포 유연성:&lt;/b&gt; 필요 시 모듈만 따로 배포하거나 교체 가능 (마이크로서비스화도 용이) &lt;br /&gt;&lt;b&gt;재사용성:&lt;/b&gt; 같은 기능을 다른 프로젝트에 재사용하기 쉬움&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 다시 Spring Framework 로 돌아와보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2002년까지 J2EE 아키텍쳐라는 것을 활용해 기업에서 사용하는 애플리케이션을 개발해왔다고 합니다. 이후 2003년 Spring Framework 1.0 이 공개 되었고, 당시 J2EE 의 문제점이었던 복잡함을 해결할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 저는 J2EE 로 개발한 프로젝트에 대한 내용을 자세히 보진 못했지만, 당시에는 &lt;b&gt;복잡한 Enterprise Java Bean&lt;/b&gt; 때문에 개발이 무겁고 설정이 번거롭기도 헀고, &lt;b&gt;트랜잭션 처리가 어렵다&lt;/b&gt;는 단점도 있었습니다. 또한&lt;b&gt; 유닛테스트를 진행하는 것에 대한 어려움&lt;/b&gt;과 &lt;b&gt;의존성이 강해서&lt;/b&gt; 확장성이 낮다라는 단점이 있었다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 모든 문제를&lt;b&gt; &quot;더 가볍고 유연하게 만들자&quot;&lt;/b&gt;라는 철학으로 나온 것이 Spring Framework 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.x 버전부터 현재 5.x 버전까지 다양한 발전이 있었지만 3.x 버전부터 REST API 를 지원하게 되어 현재는 3.x 부터 필요에 맞는 버전으로 개발을 하고 있는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 개념은?(철학과 기능)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Framework 시절 부터, 이미 정립된 핵심 개념에 대한 내용을 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. IoC(Inversion of Control) - 제어의 역전&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용어가 조금 어렵게 느껴집니다. 가벼운 느낌으로 설명해보자면 객체의 생성이나 관리를 개발자가 아니라 컨테이너가 대신 관리하게 하는 것을 말합니다. 즉, 제어의 권한을 프레임워크에게 위임하는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설명해도 좀 어려울 것 같습니다. 한번 더 풀어보자면, &lt;b&gt;IoC 컨테이너란&lt;/b&gt; 애플리케이션에서 사용할 객체(Bean)를 &lt;b&gt;생성, 관리, 주입, 소멸&lt;/b&gt; 까지의 모든 Lifecycle 을 책임지는 &lt;b&gt;중앙 관리자 역할을 하는 객체&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 대신 객체를 생성해주고 관리해주는 Spring 의 핵심 엔진이라고 생각하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 컨테이너가 바로&lt;b&gt; ApplicationContext&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext 는 BeanFactory 및 메세지 소스, 이벤트, AOP 등 다양한 부가기능을 하는데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationContext 는 다음 네가지의 역할을 개발자 대신 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 생성: 클래스의 정의를 보고 객체(Bean) 생성&lt;br /&gt;2. 주입: 필요한 의존성(필드, 생성자 등)에 자동 주입&lt;br /&gt;3. 관리: 싱글톤(프로세스 전역에서 유일한 인스턴스) 관리, 요청 시 반환, 라이프사이클 관리&lt;br /&gt;4. 소멸: 애플리케이션 종료 시 Bean 소멸 처리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Java-Spring-%EC%97%90%EC%84%9C%EC%9D%98-Bean-%EC%9D%B4%EB%9E%80&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bean 이 무엇인지에 대해서는&lt;/a&gt; 다른 포스팅에서 자세히 다뤄보고자 합니다. 우선 이해하기 쉽게 설명하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743323090328&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Controller
@Service
@Repository
@Entity&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 어노테이션을 클래스위에 정의하면서 주로 사용하셨을거라 생각합니다. 이 녀석들이 각각 Bean 객체이고(Ctrl + 클릭하여 소스파일을 들어가다 보면 인터페이스로 정의된 것을 확인할 수 있습니다)&amp;nbsp; ApplicationContext 가 처음 애플리케이션이 실행될때 라이프사이클을 관리해주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 ApplicationContext 와 같은 컨테이너가 없던 시절엔 개발자가 직접 객체를 생성하고, 필요한 의존성도 직접 생성자나 필드로 주입해야헀습니다. 다음과 같은 예시로 처럼말이죠.&lt;/p&gt;
&lt;pre id=&quot;code_1743323221542&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UserService userService = new UserService(new UserRepository());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식에서 &lt;b&gt;컨테이너&lt;/b&gt;를 사용하게된 후로는 다음과 같이 작성하는것을 보셨을겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743323270780&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1743323287338&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성하면 객체 생성, 의존성 주입 모두 `ApplicationContext` 라는 컨테이너가 처리해주게 됩니다. 객체를 프레임워크가 생성해주고, 우리는 요청해서 받아만 쓰는 구조가 되는데 이것이 제어의 역전(IoC) 라고 설명할 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부구조의 흐름을 보자면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. AppConfig 또는 @ComponentScan 으로 Bean 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 컨테이너가 Bean 정의를 읽음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 필요한 객체(Bean)를 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 의존성 있는 Bean 을 주입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. getBean() 등으로 필요한 곳에 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 순서를 거치게 됩니다. 이렇게 되면서 결합이 결합이 느슨해지고 확장성도 좋아지게 되는것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. DI(Dependency Injection) - 의존성 주입&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러번 나온 내용입니다. 대체 의존성이란 뭐지?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성을 정의해보자면&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;어떤 객체가 다른 객체의 기능을 사용해야만 동작할 수 있다면, 그 객체는 다른 객체에 '의존'한다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 할 수 있겠습니다. 다음과 같은 코드 예시를 보고 문장으로 설명하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743323759782&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderService {
    private final UserRepository userRepository;

    @Autowired
    public OrderService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, A 클래스가 B 클래스를 내부에서 사용하고 있다면 &lt;b&gt;A는 B에 의존하고 있다.&lt;/b&gt; 라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`OrderService` 가 `UserRepository` 를 사용해서 회원 정보를 조회한다고 하면, &lt;b&gt;OrderService 는 UserRepository 에 의존하고 있다&lt;/b&gt; 라고 표현할 수 있는 것이지요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;내가 혼자선 일을 못 해. 너 없으면 나도 못 움직여&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 의존입니다. 이것을 직접 주입하는 방식과 비교해보자면&lt;/p&gt;
&lt;pre id=&quot;code_1743323938744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class OrderService {
    private final UserRepository userRepository;

    public OrderService() {
        this.userRepository = new UserRepository(); // 직접 생성 &amp;rarr; 강한 결합
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 직접 객체를 필드에 선언하고 new UserRepository() 로 생성하면 OrderService 는 UserRepository 의 구현 방식에 강하게 결합되게 됩니다. 이렇게되면 테스트나, 교체, 확장 등이 어려워집니다. 그래서 의존성을 주입해서 사용하는 방식을 사용하게 되는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743324010699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.userRepository = new UserRepository();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 직접 생성하는 것이 아닌,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743324036726&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
public OrderService(UserRepository userRepository) {
    this.userRepository = userRepository;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부로 부터 주입 받아서 결합도를 느슨하게 하는것이 의존성 주입을 했다고 볼 수 있습니다. 그래서 일반적으로 사용하는 3-Layered-Architecture 에서 Controller, Service, Repository 의 구조를 보면 ApplicationContext 가 UserRepository 부터 시작해서 OrderService 에 주입해주게 하고, 이렇게 만들어진 OrderService를 OrderController 에게 주입해주게 되어 위에서 말한 제어의 역전이 동작하게 되는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 결합을 느슨하게 했을때, 장점을 예를 들어 보면 UserRepository 대신 FakeUrserRepository 를 넣어서 테스트를 하게 하거나, MySQL 용 UserRepository 를 MongoDB 등으로 교체하거나 하는 일들이 쉬워집니다. 결합도가 느슨하기 때문이죠. 이런것이 의존성 주입(DI) 이고 제어의 역전(IoC) 와 같이 이해하면 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 처음 학습할때 여기서 의문이 들었습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OrderController 는 OrderService 에게 의존하고 있기때문에 의존성을 주입받고, OrderService 는 UserRepository 에게 의존하고 있기에 의존성을 주입받고 있는데 UserRepository 는 누구에게 의존성을 주입받는 거지? DB 에게? 아니면 혼자서 태어나나?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금은 이상한 상상일지도 모르겠지만, 위에서 말했던 IoC 컨테이너에 대한 내용을 기억하고 있다면 알 수 있으실 겁니다. 바로 ApplicationContext 가 처음 Bean 객체들에게 라이프사이클을 관리할 때 생성하여 필요에 맞게 의존성을 주입하여 생성해주는 것입니다. 이 2번항목, 1번항목, 3번항목을 번갈아 보시면서 컨테이너가 하는 일을 이해하시면 Spring Framework 에 대한 이해가 좀 더 깊어질 것이라는 생각이 듭니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. AOP(Aspect-Oriented Programming) - 관점 지향 프로그래밍&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP 는 공통된 로직을 핵심 비즈니스 로직과 분리하는 것을 말합니다. 이때 공통 로직은 트랜잭션, 로깅, 보안 등을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 OOP 관점에서의 코드를 보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743324536533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void createOrder() {
    startTransaction();       // 공통 로직
    doBusinessLogic();        // 핵심 로직
    commitTransaction();      // 공통 로직
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;createOrder() 라는 클래스에 핵심은 말그대로 &lt;b&gt;주문을 만드는 것&lt;/b&gt;이고, 나머지 `startTransaction()` , `commitTransaction()` 은 공통로직입니다. 공통 로직인 이유는 거의 모든 서비스 로직에 이런 트랜잭션 개념을 이용하여 데이터의 정합성을 보장해야하기 때문이지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 문제점은 중복 코드가 증가하고, 핵심 로직이 공통 코드에 묻히는 가독성 저하를 일으키기도 하고, 재사용성이나 유지보수가 어렵다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 우리는 관심사를 분리해버려야합니다. 이때의 관심사는 클래스나 메서드가 책임지고 있는 역할이고 이것을 각각의 목적에 맞게 분리해서 사용해야한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 관심사는 비즈니스 로직, 이것은 곧 주문을 생성한다 라는 것이고 부가 관심사는 전반적으로 필요한 공통 기능인 트랜잭션, 로깅, 보안, 인증, 예외처리 등을 말하는 것입니다. 이런 부가 관심사를 모듈화 해놓은 클래스는 Aspect 라고 정의되어있고, 보통 @Aspect 어노테이션을 사용해서 정의합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743324847904&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
public class TransactionAspect {

    @Around(&quot;@annotation(org.springframework.transaction.annotation.Transactional)&quot;)
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println(&quot;트랜잭션 시작&quot;);
        try {
            Object result = joinPoint.proceed(); // 실제 핵심 메서드 실행
            System.out.println(&quot;트랜잭션 커밋&quot;);
            return result;
        } catch (Exception e) {
            System.out.println(&quot;트랜잭션 롤백&quot;);
            throw e;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 공통로직인 트랜잭션을 정의해두고, 우리는 핵심 로직에서 @Transactional 이라는 어노테이션만 붙이면 트랜잭션 처리는 AOP 가 가로채서 자동실행하게 됩니다. 이렇게 코드의 중복을 제거하고, 가독성을 좋게 만들고, 유지보수나 테스트에서 이점을 챙길 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Framework 특징 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 세가지 핵심개념을 가진 Spring Framework, 이제는 좀 내용을 알 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;J2EE 와 비교시 가볍고 유연하며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈화를 통해 필요한 모듈만 선택적으로 사용할 수 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MVC(Controller-Service-Repository) 구조를 지원하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;활발한 커뮤니티와 지속적인 발전으로 다양한 오픈소스를 가져다 사용할 수 있다는 것,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 길게 정의한 제어의 역전, 의존성 주입, 관점 지향 프로그래밍 등의 특징을 가진 것이&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Framework&lt;/b&gt; 라고 할 수 있겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명만 보면 너무 좋은 프레임워크라는 생각이 들겠지만, 당연히 문제점과 비판도 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등장 초창기에는 대부분의 설정을 XML 로 해야해서 설정파일만해도 수백줄의 코드가 들어가기도 했고, 다양한 라이브러리를 조합해야 해서 Maven 이나 Gradle 설정이 복잡하기도 했습니다. 또한 Spring 컨텍스트 로딩 속도가 느려서 단위 테스트보다 통합테스트 위주로 할 수 밖에 없었고, 초보자가 접하기엔 조금 어렵고 DI, AOP, 빈 생명주기 등의 추상적인 개념이 많아보니 학습 곡선이 가파른 편이었습니다. 이런 단점을 어느정도 극복하여 유행을 끌고 있는것이 &lt;b&gt;Spring Boot &lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명을 하다보니 글이 너무 길어져서 Spring Boot 에 대한 내용은 다음 포스팅에서 진행하고자 합니다. 천천히 읽어보시면서 모르시는 부분은 댓글로 질문을 달아주시면 고맙겠습니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1743823713263&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Framework 와 Spring Boot (2)&quot; data-og-description=&quot;안녕하세요! 지난 포스팅에 Spring Framework 와 Spring Boot 에 대한 내용을 포스팅하다 글이 길어져서 Spring Boot 에 대한 내용은 이번 포스팅에서 진행하도록 하겠습니다. 지난 포스팅과 이어지기 때문&quot; data-og-host=&quot;wanglan.tistory.com&quot; data-og-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&quot; data-og-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dhxZWK/hyYyKritMg/MVyM8c1U8NRgUpybIOYWV1/img.jpg?width=332&amp;amp;height=152&amp;amp;face=74_39_128_98,https://scrap.kakaocdn.net/dn/cKYtYz/hyYCczGbOR/RjHkAf9pbt2ugbaxFlTKY1/img.jpg?width=332&amp;amp;height=152&amp;amp;face=74_39_128_98&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot-2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dhxZWK/hyYyKritMg/MVyM8c1U8NRgUpybIOYWV1/img.jpg?width=332&amp;amp;height=152&amp;amp;face=74_39_128_98,https://scrap.kakaocdn.net/dn/cKYtYz/hyYCczGbOR/RjHkAf9pbt2ugbaxFlTKY1/img.jpg?width=332&amp;amp;height=152&amp;amp;face=74_39_128_98');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Framework 와 Spring Boot (2)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요! 지난 포스팅에 Spring Framework 와 Spring Boot 에 대한 내용을 포스팅하다 글이 길어져서 Spring Boot 에 대한 내용은 이번 포스팅에서 진행하도록 하겠습니다. 지난 포스팅과 이어지기 때문&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wanglan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>SpringFramework</category>
      <category>백엔드</category>
      <category>웹개발</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/108</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Framework-%EC%99%80-Spring-Boot#entry108comment</comments>
      <pubDate>Sun, 30 Mar 2025 18:03:43 +0900</pubDate>
    </item>
    <item>
      <title>주니어 개발자로 실무경험 해본 후기</title>
      <link>https://wanglan.tistory.com/entry/%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%8B%A4%EB%AC%B4%EA%B2%BD%ED%97%98-%ED%95%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 개발 공부 및 취업활동을 하고 있는 주니어 개발자 왕란이라고 합니다.(이름의 이유는 중요하지 않다고 해주세용..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 8월까지 일본에서 워홀을 하다가&amp;nbsp; 끝내고 연장할까 말까..도 고민하며 더 늦으면 꿈을 이루기 힘들겠다라는 판단에 시작한 개발공부!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀국하자마자 KDT 국비지원 부트캠프인 팀스파르타의 내일배움캠프에서 Java &amp;amp; Kotlin Spring 백엔드 개발자 트랙에 참여하여 지금까지 개발자 공부를 한지 반년이 조금넘었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 1월에 트랙을 수료하고 이력서 작성, 알고리즘 공부 등을 하며 약간은 수비적인 취업활동을 하다가 지금까지 이력서도 15군데 이상 넣어보고 지금까지 경험해본 내용과, 앞으로의 방향에 대한 경험을 정리해보고 다시 블로그활동을 꾸준히 해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이력서 제출 및 결과  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내일배움캠프를 하면서 팀장도 많이하며 리드했고, 학습한 내용을 블로깅도 하고, Github 에서 프로젝트를 작성할 때 리드미도 깔끔하게 해서 작은 프로젝트여도 좀 제대로 했다는 모습을 어필하는 것이 필요하다고 생각했습니다. 물론 이런 내용은 시니어분들이나 갓 취업한 따끈따근한 주니어 개발자분들의 조언을 반영한 것이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이력서에 제가 어필할 수 있는 것은 내배캠을 수료하는 과정에서 최종프로젝트로 진행했던 `Chairing Project` 말고는 아무것도 없었습니다. 개발자와 완전 관련된 일을 해온 것도 아니고, 전공이 컴공도 아니었기 때문입니다. 그래도 조언을 토대로, 지금까지 해왔던 다른 일들의 경험을 &lt;b&gt;&quot;개발자의 자세&quot;&lt;/b&gt;에 대입하여 어필해보려고 했고, 이력서를 완성한 후 3주정도 동안 15개의 회사를 지원해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 지원에 사용한 플랫폼은 Blind Hire 와 Wanted 였는데 대부분은 원티드에서 지원했습니다. 필터를 적용할 때는 완전 처음시작하는 햇병아리 주니어 개발자이지만, 신입뿐만 아니라 1~2년 경력자 부터 뽑는다는 공고도 넣어서 지원했습니다. &lt;b&gt;결론부터 말씀드리자면, 15개의 회사중&lt;/b&gt;에서 이력서 통과 후 코딩테스트까지 도달한게 &lt;b&gt;단 한 곳&lt;/b&gt; 뿐이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;15개의 회사중에서 뭐 3~4개 서류 통과하는걸 바라는 것은 스스로도 양심없다고 생각합니다. 지금 백엔드 개발자 시장이 좋지도 않고, 경험은 비슷비슷하거나 저보다 매력적인 이력서인 분들이 훨씬 많고, 평균적으로 4~50개의 회사에 지원하고 2~3번의 면접기회를 얻는다는 통계를 본 적 있으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코딩테스트를 본 한 곳의 회사에선?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울에 위치한 'N' 소프트 라는 회사였습니다.(회사이름은 일단 공개하지 않겠습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 회사에 도착한 후, 화려하면서도 깔끔한 건물에 입장하니 두근거리며 이런 곳에서 일하게 될 수 있는걸까? 라고 생각했는데 &lt;b&gt;결과적으론 김칫국&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 40분 정도의 코딩테스트 시간이 주어졌고, 문제는 4문제가 나왔습니다. 이 중, 2문제는 Java 알고리즘 문제, 2문제는 SQL 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트를 시작하기전 40분은 너무 짧은거 아닌가? 라는 생각이 들었지만 Java 알고리즘 문제의 난이도를 보고 &quot;&lt;b&gt;흠.. 한문제당 5분에서 10분정도 사용해서 풀고 나가는 시험이었구나&lt;/b&gt;&quot; 라는 걸 알게되었습니다. 환경은 그냥 책상에서 빈 종이에 수기로 코드를 작성해가면서 문제를 푸는 것 이었는데, 기억나는 첫번째 문제는 다음과 같은 내용이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1000000000 과 같은 문자열을 입력했을때 1,000,000,000 와 같이 단위를 콤마로 나누어 출력하는 함수를 작성해주세요.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 보고 제한조건이 있는지 살펴보고 어떤식으로 해결할까..? 하다가 우선은 적기 시작했는데 코드를 수기로 적는 것은 살면서 처음해보다 보니 중간에 틀려서 앞에 내용을 다 지워야 하는 경우도 생기고, 긴장해서 손에 땀이나니 종이가 더러워져 지워지지도 않고 갑작스럽게 뇌정지가 오는 경험을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(사실 연필이랑 지우개를 사용해도 되는지도 의문이었습니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌저찌.. 하다보니 시간은 끝났는데 전 알고있습니다. &lt;b&gt;제가 너무 못풀었다는 사실을...&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 얼렁뚱땅 코딩테스트를 보고나니 스스로가 너무 자극이 되었습니다. 왜 이렇게 쉬워보이는 논리 흐름의 문제를 간단하게 뚝딱 해내지 못한걸까 하고 자책하고, 또 왜 이런 경험을 해야 다시 알고리즘 공부에도 집중하고 SQL 공부에도 집중하게 되는걸까 하며 한번 더 자책했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 망쳤다고 생각한 'N' 소프트의 결과도 &lt;b&gt;역시 탈락이었고&lt;/b&gt;, 이날 집 돌아가서 부터 프로그래머스 알고리즘문제를 기초, 입문 부터 Lv.1 문제까지해서 간간히 Lv.2 문제까지 간간히 공부하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfyZUL/btsM18bWEUy/qXPMKCZP5beVoxWsqNCVbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfyZUL/btsM18bWEUy/qXPMKCZP5beVoxWsqNCVbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfyZUL/btsM18bWEUy/qXPMKCZP5beVoxWsqNCVbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfyZUL%2FbtsM18bWEUy%2FqXPMKCZP5beVoxWsqNCVbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;391&quot; height=&quot;145&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 큰 의미는 없지만 쉬운문제로 점수도 약간 오르긴 했더라구요. 이것도 올리다보니 순위 500위 안에 들어가보고 싶다는 생각이 들었습니다. 이렇게 제 초라한 이력서로 지원했던 회사들은 모두 &lt;b&gt;&quot;서류 탈락&quot;&lt;/b&gt; 이라는 결과로 끝나게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 실무 경험을 해보다(계약 연장까지..!)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 내용이 좀 길어졌지만 이랬던 제가 어떻게 실무 경험을 할 수 있게되었는지도 말해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UfXAg/btsM0sitA7z/Vv4pDrlTa6DP3XcfhduRt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UfXAg/btsM0sitA7z/Vv4pDrlTa6DP3XcfhduRt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UfXAg/btsM0sitA7z/Vv4pDrlTa6DP3XcfhduRt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUfXAg%2FbtsM0sitA7z%2FVv4pDrlTa6DP3XcfhduRt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;702&quot; height=&quot;496&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;바로인턴?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 KDT 국비지원 내일배움캠프를 수료하면, 팀스파르타 측에서 수료 후에도 수료생들이 실질적으로 취업에 도달할 수 있게끔 다양한 애프터 서비스를 진행하고 있습니다.(팀 스파르타 추천합니다!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱히 홍보용 포스팅은 아니지만 수료생에게 지원해주고 있는 서비스에 대해서 간단히 나열해보겠습니다.(무료입니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이력서 첨삭 지원(현업 인사담당팀 또는 채용경험이 있는 개발자 분이 봐주십니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 기술면접 대비 모의 면접 지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 4주간 회사에서 인턴을 할 수 있는 &lt;b&gt;&quot;바로인턴&quot;&lt;/b&gt;(한달인턴 이라고도 합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 기타 트랙 수강중에 들었던 강의 6개월정도(?) 추가지원&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 중에서 가장 매력적인 것은 아무래도 3번 바로인턴 이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수료생들을 대상으로 기초적인 온보딩과제, 이력서, 간단한 자기소개서 등을 제출하면 팀스파르타측과 계약을 맺은 100개 전후의 회사(스타트업 위주)들에 4주간 인턴직무로 실무 경험을 할 수 있게 해주는 좋은 서비스였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저를 포함한 막 학습이 끝난 취준하는 주니어 개발자들에겐 아무리 짧은 기간이라도 실무경험이 매우 중요하다고 생각합니다. Github 이나 Slack, Notion 과 같은 &lt;b&gt;협업 툴을 사용하는 것&lt;/b&gt;, 회사에서 &lt;b&gt;신입으로 들어가서 가져야할 태도&lt;/b&gt;, 새 회사에 들어가서 &lt;b&gt;온보딩하는 과정&lt;/b&gt;, 일할 회사가 &lt;b&gt;어떤 서비스를 하고 있는지 이해&lt;/b&gt;하는 것 등등 많은 경험이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실무 경험에 대하여&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4주라는 시간은 사실 상기한 내용중 하나를 제대로 하기도 힘들 수도 있는 기간이지만 하기 나름이라고 생각합니다. 바로인턴을 지원하여 'S' 라는 회사와 매칭이 되었고, 이 회사에서 처음 실무경험을 해볼 수 있게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 인턴을 하게되면 회사측에서 임금을 받는 구조는 아니고, 팀스파르타 측에서 활동지원금 느낌으로 어느정도의 급여(140만원~169만원)를 제공하고 기업측에서는 팀스파르타에서 인턴 인력을 제공받는 협력구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(아마 국비지원 사업이라 팀스파르타 측에서 국가에서 받은 지원금을 임금의 형태로 제공하는 방식이라고 생각합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 인턴을 경험하고 있는 회사에서 제공하는 서비스는 신입이나 경력직 개발자 들에게 &lt;b&gt;다양한 채용공고정보를 크롤링하여 AI 를 통해 채용공고 데이터를 다듬어 제공&lt;/b&gt;해주는 그런 서비스를 하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 처음들어가서 맡은 업무는 700~900개 정도되는 회사의 채용공고 사이트에 각각 동작하고 있는 크롤러가 제대로 동작하지 않는 경우 정상동작하도록 수정하는 업무와 채용공고를 크롤링 할 신규 회사들을 위해 크롤러를 추가하는 업무 등을 맡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 이 기업과 매칭이 되었을 때, &lt;b&gt;걱정했던 부분&lt;/b&gt;이 몇가지 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 나의 주특기는 Java/Spring 백엔드 개발자 인데 회사의 스택은 Python/Django 이며 백엔드 업무를 하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 따라서 온보딩 과정에서 회사가 어떻게 운영되는지 알아봄과 동시에 Python 학습이 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 크롤링 관련 프로젝트를 해본경험이 아예 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한달동안 인턴을 하기위해 새로 Python 언어를 공부하면서 처음해보는 크롤러 제작하는 업무가, 나의 Java/Spring 백엔드 개발자의 역량에 도움이 되는게 있을까? 라는 생각이 딜레마로 다가왔습니다. 결론 부터 말하자면 &lt;u&gt;&lt;b&gt;어떻게든 무조건 도움이 된다&lt;/b&gt; &lt;/u&gt;인것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 개발에 사용되는 언어라는 것은 동적 타입언어인지, 강타입 언어인지 이런 언어마다의 특징차이만 있을 뿐 &quot;개발&quot;이라는 개념 자체에서 코드들은 다 일맥상통하는 것 같습니다. 더군다나 코드를 작성하고, 검토하는 과정에 있어서 저같이 처음 접하는 언어로 개발을 하게되었을 때 또는 확신이 없을 때, 참고할 수 있는 다양한 아티클도 너무 많이 존재하고 인공지능모델 등을 활용하여 얼마든지 실시간으로 학습 및 적용이 가능하다 보니 이 부분은 크게 문제되지 않는 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 크롤링 서비스를 개발해본 경험이 없는데 회사마다 모두 다른 HTML 폼에 내가 잘 적용하고 작성할 수 있을까? 하고 걱정했던 부분도 마찬가지였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 회사에서 어느정도 서비스가 개발되어있었기에, 프로젝트를 꼼꼼히 살펴보며 크롤러가 어떻게 채용공고를 긁어오고, 긁어온 채용공고의 세부내용을 어떻게 파싱하는지 모든 과정을 천천히 살펴보다보니 &lt;b&gt;&lt;u&gt;내가 뭘 만들어야 하는지 어떻게 만들어야하는지&lt;/u&gt;&amp;nbsp;&lt;/b&gt;가 그려지는 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미 만들어져있는 다양한 크롤러의 형태뿐만 아니라 Greeting, Nine Hire, 외국계에서는 AshByHQ, Green House 같은 채용공고 페이지 제작 및 배포 플랫폼에서는 각각 플랫폼의 형태가 고정되어있기에, 전용 크롤러를 작성하면 추가적인 작업은 훨씬 빨라지는 것을 깨닫고 제가 &lt;b&gt;새로운 전용크롤러도 만들기도&lt;/b&gt; 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 더 나아가서 저는 스스로 백엔드 개발자라는 사실을 잊지 않고! 크롤러 자체를 공장에서 찍어내듯 만드는 것 보다, 어떻게하면 서버와 클라이언트가 임베딩된 데이터를 주고받을 때 효과적일지, 데이터를 크롤링하고 영속화 하는 과정에서의 속도나 안정성 부분에서 효율적으로 할 수 있을지를 고민해본 것 같습니다.(&lt;s&gt;물론 이 업무를 맡아서 할 수는 없었지만요&lt;/s&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인턴 계약 연장!&lt;/h4&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 팀스파르타에서 매칭해주는 바로인턴을 하는 기업들은, 바로인턴 이 후에 정직원이나 인턴으로 계약을 연장하는 경우도 많다고 들었고(기업마다 다릅니다) 저에게도 그런 기회가 온다면 좋겠다 라는 생각이 들긴 하였지만, 첫날 회사에서 미팅을 할 때 저는 될 것 같지 않다 라는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 바로인턴으로 들어온 사람이 저포함 5명이었고, 그중에 3명이 Python 주특기였으며 나머지 한 분은 저보다는 개발 경력이 꽤 탄탄하신 분 같았습니다. 그래서 저는 좋은 경험으로 남기고 인턴이 끝나더라도 짧은 임팩트를 남기는 개발자가 되자 라는 생각으로 열심히 임했던 것 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여담으로 약간의 자랑을 하자면, 저는 모르는 부분에 대해 스스로 학습해서 알아보다 도저히 안될 때 시니어나 사수에게 질문하는 타이밍을 잘 잡는 것 같습니다. 이말은 즉슨, &lt;b&gt;고민하느라 시간을 낭비하지는 않으면서 그렇다고 아무런 고민도없이 질문을 하여 선배의 시간을 낭비시키지는 않는 타입&lt;/b&gt;이라는 뜻입니다. 그래서 업무를 진행할 때 거의 막힘없이 물흐르듯 진행할 수 있던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 질문을 하고 그 질문에 대한 솔루션이나 피드백을 들으면 최대한 머릿속에 흡수 시켜 놓고 이해하지 못한 키워드는 검색하고, 관련된 내용을 찾아보며 들은 피드백을 100퍼센트 내가아는 지식으로 이해할 수 있게 하여 그대로 적용하도록 한 것이 많은 도움이 되었던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 피드백을 처음듣고 시니어분께서 말씀해주신 내용을 한번에 다 이해하고 업무에 적용을 할 수 있는 역량을 가진사람이면 더 좋지만, 제 역량에서는 &lt;b&gt;듣고 모르지 않는다&amp;nbsp;&lt;/b&gt;만 해도 꽤나 괄목할 만한 부분이라고 생각합니다. 하지만 전자의 사람이 되려고 노력해야겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여하튼 이렇게 한달 간의 인턴이 끝나게 되고 저는 추가적으로 근무할 수 있게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 팀네이버 코딩테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인턴 이야기를 하다가 왠 네이버 코딩테스트 이야기를 하지? 라는 생각이 드실 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ys1Zb/btsM2wjkw4j/NH206mb4ybmF7y4sqXu7AK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ys1Zb/btsM2wjkw4j/NH206mb4ybmF7y4sqXu7AK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ys1Zb/btsM2wjkw4j/NH206mb4ybmF7y4sqXu7AK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fys1Zb%2FbtsM2wjkw4j%2FNH206mb4ybmF7y4sqXu7AK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;675&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 네이버는 너무 업계에서 유명한 회사이고 사람 마다 다르겠지만 저 같은 주니어 개발자들은 로망처럼 꿈꾸는 회사 리스트중에 하나라고 생각합니다. 이유는 다양하겠지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 팀네이버에서도 상반기 신입 공채를 뽑는다는 공고가 올라왔기에 인턴 연장이 확실히 되지 않는 타이밍에 지원해보았습니다. 이런 기업의 코딩테스트나 기술면접을 볼 수 있다면 합격이든 불합격이든 좋은경험이 될거라고 생각했기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버에 이력서를 넣어보니 이틀도 되지않아 SMS 와 이메일로 코딩테스트와 CS 테스트, 기업 적합도 테스트를 보라는 연락을 받았습니다. 네이버 같은 회사는 지원자가 무수히 많고, 이력서를 보고 코딩테스트를 검토하는 과정을 하면 시간과 인력이 너무 소비되기에 이력서와 코딩테스트를 동시에 확인하고 둘다 통과한 인재에게 다음 단계의 기회를 주는 구조같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩테스트를 보니 앞서 말했던 'N' 소프트와는 역시 난이도가 다르긴 했습니다. 이쪽 경험이 많으신 분들은 팀네이버 코딩테스트는 대기업에 비해 쉬운편이다 라고 하기는 했는데 그래도 여전히 어렵긴 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 'N' 소프트에서 코딩테스트를 보고 각성했던 저는, 이 후에 아주 기초적인 알고리즘 문제를 좀 학습한 상태였기 때문에 못풀지는 않았습니다. 알고리즘 3문제, CS지식 20문제 해서 총 23문제를 2시간 30분동안 푸는 방식이었는데, 어찌저찌 해서 알고리즘 2문제정도는 풀었습니다. 이런 부분을 보면 다른 기업보다는 쉬운 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 저에게 가장 큰 문제는 CS 지식 테스트였습니다. 솔직한 마음으로 말하자면 너무 헷갈리고 제대로 풀었다! 싶은 문제가 몇개 없었던 것 같습니다. 애초에 문제 보기도 &quot;모두 고르시오&quot; 가 대부분이었고 정답이 2개인지 3개인지 모르는 상태이기 때문에 얼추 아는 지식으로 찍어서 맞출 수도 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 CS 지식에서 나오는 문제유형을 알 수 있었고, 이런 부분도 개발자로서 기본적인 학습이 필요하다는 생각이 들었기에 서류에서 떨어질 것으로 예상은 되지만 값진 경험이었다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 기술면접 기회를 받다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 또한 인턴을 하는중에 생긴 좋은 경험이었습니다. 특수한 경로로 또다른 회사에서 기술면접을&amp;nbsp; 볼 수 있는 기회를 얻었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이때의 기술면접을 경험으로 다시 한번 스스로 각성할 수 있었던 것 같습니다. 왜냐하면 이 기술면접에서도 잘 못보았다고 생각이 들었기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 회사는 'P' 라는 서울소재 회사였습니다. 현재 크게 스케일업 하며 회사를 키워나가고 있고, 꽤나 혁신적인 기술을 선보이며 유망한 회사라는 글이나 활동 등에서 매력적으로 보였고, 이 회사에서 하고 있는 서비스가 제가 관심있는 스포츠 분야를 다루고 있어서 저에겐 더욱 더 흥미롭게 다가왔었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접을 보기전에 회사에서 사용하는 기술스택이나, 어떤 개발자를 원하는지에 대한 내용을 급하게 찾아보았습니다. 면접 자체가 급하게 잡힌 일정이라 &lt;s&gt;변명이지만&lt;/s&gt; 준비할 시간이 부족했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서는 백엔드 개발자로서 갖추어야될 기본적인 지식과, 사내 문화에 적합한지 인성적인 측면을 확인하는 다양한 질문들이 나왔습니다. 총 1시간정도의 시간이 주어졌는데 이런 기술면접이 저에겐 처음이었기에 당황도, 긴장도 많이했던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 관련 백엔드 지식에 대해서 기억나는 질문들은 다음과 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. JPA 가 어떤것인지에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Thread 에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. N+1 문제에 대한 내용과 이것을 방지하는 방법에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 트랜잭션에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. Ehcache 에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. Websocket 을 프로젝트에서 적용하셨는데 이 이유에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. Spring 에서 Bean이 무엇인지 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. Redis 서버와 Cache 부분에서 데이터를 주고받아야 하는 부분이 있는데 이런 부분에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. 디자인 패턴에 대해서 아는 것을 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10. 체크예외와 언체크 예외에 대해서 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;11. 영속성 컨텍스트에 대해 아는데로 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;12. 쿼리 실행계획에 대한 내용을 말해주세요.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등등 1시간동안의 기술면접이었기에 이것들 이외에대 대답한 내용에 대한 꼬리질문 형식의 내용이 있었지만 현재 기억나는 내용은 이정도였습니다. 부끄럽지만 대답도 거의 제대로 못했다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 블로그 활동을 하면서 제가 과거에 작성해놓았던 글들도 참고하시면서 질문을 하신 것 같았습니다. 과거에 썼던 글들이라 제대로 기억나지 않는 부분, 그리고 블로그 작성당시에는 학습해놓은 내용을 나중에 다시보고자 급하게 정리하여 블로깅 한 부분도 있어서 제가 쓴 글이지만 기억나지 않는 상황이 부끄러웠습니다. 그래도 염치불구하고 &lt;b&gt;혹시 블로그 첫 시작부분을 제가 어떤식으로 작성했는지에 대해서 알려주실 수 있나요..?&amp;nbsp;&lt;/b&gt;라고 물어보니 알려주셨고, 글의 내용이 떠올라 약간 대답할 수 있었지만 제 블로그 글에 대해서 진정성을 느끼시긴 힘들었을 것이라고 생각듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술면접이 끝나고 주말동안 질문들과 제 블로그, 제가가진 지식에 대해서 복기를 해보니 스스로도 사전적으로 설명할 수 있는 지식을 가지고 있는 것은 아니라는 것을 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 다시 구글링과 ChatGPT 등을 써가며 지식들을 정리해 보았고, 운영체제 단 부터 시작해서 스프링으로 개발할 애플리케이션이 OS 로 부터 어떤 메모리를 할당 받으며 프로세스를 차지하고 쓰레드를 부여받고 애플리케이션이 동작하는지, JPA 가 객체로서 DB와 매핑되면서 데이터를 주고받는 공간에서 영속성 컨텍스트가 어떻게 동작되는지, 로컬 캐시에서 캐시가 동작하는 방식과 Redis 서버가 인메모리 공간을 할당받아 개별적인 서버로서 운영되는지에 대해서 다시 한번 공부하면서 저만의 스토리로 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글내용이 쓸데 없이 길어지긴 했지만, 이제 부터는 블로그에 가능한 스토리텔링 방식으로 저같은 주니어 개발자도, 개발을 꿈꾸는 입문자도 누구나 이해할 수 있는 편안한 내용으로 포스팅을 해보려고 합니다. 스스로의 지식을 정리하며 보여주기식이 아닌 &lt;b&gt;&quot;진짜 기술블로그&quot;&lt;/b&gt;로 만들어보고 싶다는 생각을 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 긴 내용입니다. 이까지 읽어주셨을지는 모르겠지만 읽어주셔서 감사합니다!!&lt;/p&gt;</description>
      <category>TIL(Today I Learned)</category>
      <category>til</category>
      <category>개발자실무</category>
      <category>기술면접</category>
      <category>네이버코딩테스트</category>
      <category>바로인턴</category>
      <category>스파르타</category>
      <category>코딩테스트</category>
      <category>한달인턴</category>
      <category>회고</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/107</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%A3%BC%EB%8B%88%EC%96%B4-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A1%9C-%EC%8B%A4%EB%AC%B4%EA%B2%BD%ED%97%98-%ED%95%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0#entry107comment</comments>
      <pubDate>Sun, 30 Mar 2025 12:41:06 +0900</pubDate>
    </item>
    <item>
      <title>[스택] - 뒤에 있는 큰 수 찾기 예제</title>
      <link>https://wanglan.tistory.com/entry/%EC%8A%A4%ED%83%9D-%EB%92%A4%EC%97%90-%EC%9E%88%EB%8A%94-%ED%81%B0-%EC%88%98-%EC%B0%BE%EA%B8%B0-%EC%98%88%EC%A0%9C</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1️⃣ 스택(Stack)이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택(Stack) 은 &lt;b&gt;후입선출 (LIFO, Last In First Out)&lt;/b&gt; 방식으로 동작하는 자료구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;가장 나중에 추가된 요소가 가장 먼저 제거되는 구조&lt;/b&gt;를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;스택의 특징&lt;/b&gt;&lt;br /&gt;&lt;b&gt; ✅ LIFO (Last In First Out) 원칙&lt;/b&gt; &amp;rarr; 마지막에 들어온 데이터가 먼저 나감&lt;br /&gt;&lt;b&gt; ✅ Push (삽입), Pop (삭제)&lt;/b&gt; 연산을 사용&lt;br /&gt;&lt;b&gt; ✅ Top(최상단) 원소만 접근 가능&lt;/b&gt;&lt;br /&gt;&lt;b&gt; ✅ 중간에 있는 원소 접근 불가능&lt;/b&gt;&lt;br /&gt;✅ &lt;b&gt;재귀(Recursion), 괄호 검증, 백트래킹 등 다양한 알고리즘에 활용됨&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 2️⃣ &lt;/b&gt;스택 주요 연산(메서드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 `&lt;b&gt;Stack&amp;lt;T&amp;gt;`클래스&lt;/b&gt; 또는 `&lt;b&gt;Deque&amp;lt;T&amp;gt;` (LinkedList 활용)&lt;/b&gt; 을 사용하여 구현할 수 있습니다.&lt;br /&gt;자주 사용되는 &lt;b&gt;메서드&lt;/b&gt;를 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 140px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 20px;&quot;&gt;연산&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 20px;&quot;&gt;메서드&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 20px;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 20px;&quot;&gt;삽입(Push)&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 20px;&quot;&gt;`stack.push(element)`&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 20px;&quot;&gt;스택의 &lt;b&gt;최상단에 데이터 삽입&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 40px;&quot;&gt;삭제(Pop)&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 40px;&quot;&gt;`stack.pop()`&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 40px;&quot;&gt;&lt;b&gt;최상단의 데이터 삭제 및 반환&lt;/b&gt; (스택이 비었으면 `EmptyStackException` 발생)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 20px;&quot;&gt;조회(Peek)&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 20px;&quot;&gt;`stack.peek()`&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 20px;&quot;&gt;&lt;b&gt; 최상단 데이터 조회 (삭제X) &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 20px;&quot;&gt;비어있는지 확인&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 20px;&quot;&gt;`stack.isEmpty()`&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 20px;&quot;&gt;스택이 &lt;b&gt;비어있으면&lt;/b&gt; &lt;b&gt;`&lt;/b&gt;true`, 아니면 `false`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 18.7984%; height: 20px;&quot;&gt;크기 확인&lt;/td&gt;
&lt;td style=&quot;width: 26.8217%; height: 20px;&quot;&gt;`stack.size()`&lt;/td&gt;
&lt;td style=&quot;width: 54.3798%; height: 20px;&quot;&gt;스택에 &lt;b&gt;저장된 원소 개수 반환&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; 3️⃣ &lt;/b&gt;스택 사용 예제 (자바)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 기본적인 Stack&amp;lt;Integer&amp;gt; 사용 예제&lt;/h3&gt;
&lt;pre id=&quot;code_1739171566323&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();

        // 1️⃣ 스택에 요소 추가 (push)
        stack.push(10);
        stack.push(20);
        stack.push(30);
        System.out.println(&quot;스택 상태: &quot; + stack); // [10, 20, 30]

        // 2️⃣ 스택의 최상단 요소 조회 (peek)
        System.out.println(&quot;Top 요소: &quot; + stack.peek()); // 30

        // 3️⃣ 스택에서 요소 제거 (pop)
        System.out.println(&quot;pop(): &quot; + stack.pop()); // 30 제거됨
        System.out.println(&quot;스택 상태: &quot; + stack); // [10, 20]

        // 4️⃣ 스택이 비었는지 확인 (isEmpty)
        System.out.println(&quot;스택이 비었나요? &quot; + stack.isEmpty()); // false

        // 5️⃣ 스택의 크기 확인 (size)
        System.out.println(&quot;스택 크기: &quot; + stack.size()); // 2
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;출력 결과&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739171581304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;스택 상태: [10, 20, 30]
Top 요소: 30
pop(): 30
스택 상태: [10, 20]
스택이 비었나요? false
스택 크기: 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4️⃣ 스택을 활용한 대표적인 문제&lt;/h2&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;✅ 1. 올바른 괄호 문자열 판단&lt;/h3&gt;
&lt;pre id=&quot;code_1739171632607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Stack;

public class ParenthesesValidator {
    public static boolean isValid(String s) {
        Stack&amp;lt;Character&amp;gt; stack = new Stack&amp;lt;&amp;gt;();

        for (char c : s.toCharArray()) {
            if (c == '(' || c == '{' || c == '[') {
                stack.push(c);
            } else {
                if (stack.isEmpty()) return false;
                char top = stack.pop();
                if ((c == ')' &amp;amp;&amp;amp; top != '(') || (c == '}' &amp;amp;&amp;amp; top != '{') || (c == ']' &amp;amp;&amp;amp; top != '[')) {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }

    public static void main(String[] args) {
        System.out.println(isValid(&quot;(){}[]&quot;));  // true
        System.out.println(isValid(&quot;({[)]}&quot;));  // false
        System.out.println(isValid(&quot;{[()]}&quot;));  // true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 2. 뒤에서부터 탐색하여 &quot;뒷 큰 수&quot; 찾기&lt;/h3&gt;
&lt;pre id=&quot;code_1739171712550&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Stack;

class Solution {
    public int[] solution(int[] numbers) {
        int[] answer = new int[numbers.length];
        Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();

        for (int i = numbers.length - 1; i &amp;gt;= 0; i--) {
            while (!stack.isEmpty() &amp;amp;&amp;amp; stack.peek() &amp;lt;= numbers[i]) {
                stack.pop(); // 현재 값보다 작거나 같은 값은 제거
            }
            answer[i] = stack.isEmpty() ? -1 : stack.peek(); // 스택이 비었으면 -1
            stack.push(numbers[i]); // 현재 숫자를 스택에 저장
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 3. DFS(깊이 우선 탐색)&lt;/h3&gt;
&lt;pre id=&quot;code_1739171728346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class DFSExample {
    public static void dfs(int start, Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; graph) {
        Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
        Set&amp;lt;Integer&amp;gt; visited = new HashSet&amp;lt;&amp;gt;();

        stack.push(start);

        while (!stack.isEmpty()) {
            int node = stack.pop();
            if (!visited.contains(node)) {
                visited.add(node);
                System.out.print(node + &quot; &quot;);

                for (int neighbor : graph.getOrDefault(node, new ArrayList&amp;lt;&amp;gt;())) {
                    if (!visited.contains(neighbor)) {
                        stack.push(neighbor);
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; graph = new HashMap&amp;lt;&amp;gt;();
        graph.put(1, Arrays.asList(2, 3));
        graph.put(2, Arrays.asList(4, 5));
        graph.put(3, Arrays.asList(6, 7));

        System.out.print(&quot;DFS 탐색 순서: &quot;);
        dfs(1, graph);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5️⃣ 정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 137px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;LIFO (Last In First Out)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;마지막에 들어온 요소가 가장 먼저 제거됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;삽입(push) / 삭제(pop)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;push()로 삽입, pop()으로 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;조회(peek)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;최상단 요소 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;비어 있는지 확인(isEmpty)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;스택이 비어 있는지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;활용 사례&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;괄호 검사, 뒷 큰수 찾기, DFS 탐색 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;  결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;스택은 LIFO 구조로 삽입/삭제가 빠른 자료구조&lt;/b&gt;입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;괄호 검증, 수식 계산, DFS 탐색 등에서 유용&lt;/b&gt;하게 사용됩니다. &lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 까지 스택(Stack)에 대한 기본적인 설명을 살펴보았습니다. 위에서 제시한 사례를 바탕으로 프로그래머스 &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/154539&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;뒤에 있는 큰 수 찾기&lt;/a&gt; 문제를 예제로 실제 적용해보도록 하겠습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1739171920782&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/154539&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/154539&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/154539&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;뒤에 있는 큰 수 찾기&lt;/b&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;정수로&amp;nbsp;이루어진&amp;nbsp;배열&amp;nbsp;numbers가&amp;nbsp;있습니다.&amp;nbsp;배열&amp;nbsp;의&amp;nbsp;각&amp;nbsp;원소들에&amp;nbsp;대해&amp;nbsp;자신보다&amp;nbsp;뒤에&amp;nbsp;있는&amp;nbsp;숫자&amp;nbsp;중에서&amp;nbsp;자신보다&amp;nbsp;크면서&amp;nbsp;가장&amp;nbsp;가까이&amp;nbsp;있는&amp;nbsp;수를&amp;nbsp;뒷&amp;nbsp;큰수라고&amp;nbsp;합니다. &lt;br /&gt;정수&amp;nbsp;배열&amp;nbsp;numbers가&amp;nbsp;매개변수로&amp;nbsp;주어질&amp;nbsp;때,&amp;nbsp;모든&amp;nbsp;원소에&amp;nbsp;대한&amp;nbsp;뒷&amp;nbsp;큰수들을&amp;nbsp;차례로&amp;nbsp;담은&amp;nbsp;배열을&amp;nbsp;return&amp;nbsp;하도록&amp;nbsp;solution&amp;nbsp;함수를&amp;nbsp;완성해주세요.&amp;nbsp;단,&amp;nbsp;뒷&amp;nbsp;큰수가&amp;nbsp;존재하지&amp;nbsp;않는&amp;nbsp;원소는&amp;nbsp;-1을&amp;nbsp;담습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한사항&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4 &amp;le; `numbers` 의 길이 &amp;le; 1,000,000&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 &amp;le; `numbers[i]` 의 길이 &amp;le; 1,000,000&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입출력 예&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 46.6243%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6667%;&quot;&gt;numbers&lt;/td&gt;
&lt;td style=&quot;width: 30.4762%;&quot;&gt;result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6667%;&quot;&gt;[2, 3, 3, 5]&lt;/td&gt;
&lt;td style=&quot;width: 30.4762%;&quot;&gt;[3, 5, 5, -1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.6667%;&quot;&gt;[9, 1, 5, 3, 6, 2]&lt;/td&gt;
&lt;td style=&quot;width: 30.4762%;&quot;&gt;[-1, 5, 6, 6, -1, -1]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입출력 예 설명&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입출력&amp;nbsp;예&amp;nbsp;#1 &lt;br /&gt;2의&amp;nbsp;뒷&amp;nbsp;큰수는&amp;nbsp;3입니다.&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;3의&amp;nbsp;뒷&amp;nbsp;큰수는&amp;nbsp;5입니다.&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;3&amp;nbsp;또한&amp;nbsp;마찬가지입니다.&amp;nbsp;5는&amp;nbsp;뒷&amp;nbsp;큰수가&amp;nbsp;없으므로&amp;nbsp;-1입니다.&amp;nbsp;위&amp;nbsp;수들을&amp;nbsp;차례대로&amp;nbsp;배열에&amp;nbsp;담으면&amp;nbsp;[3,&amp;nbsp;5,&amp;nbsp;5,&amp;nbsp;-1]이&amp;nbsp;됩니다. &lt;br /&gt;&lt;br /&gt;입출력&amp;nbsp;예&amp;nbsp;#2 &lt;br /&gt;9는&amp;nbsp;뒷&amp;nbsp;큰수가&amp;nbsp;없으므로&amp;nbsp;-1입니다.&amp;nbsp;1의&amp;nbsp;뒷&amp;nbsp;큰수는&amp;nbsp;5이며,&amp;nbsp;5와&amp;nbsp;3의&amp;nbsp;뒷&amp;nbsp;큰수는&amp;nbsp;6입니다.&amp;nbsp;6과&amp;nbsp;2는&amp;nbsp;뒷&amp;nbsp;큰수가&amp;nbsp;없으므로&amp;nbsp;-1입니다.&amp;nbsp;위&amp;nbsp;수들을&amp;nbsp;차례대로&amp;nbsp;배열에&amp;nbsp;담으면&amp;nbsp;[-1,&amp;nbsp;5,&amp;nbsp;6,&amp;nbsp;6,&amp;nbsp;-1,&amp;nbsp;-1]이&amp;nbsp;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 단순 반복문을 통해서 푸는 풀이&lt;/h2&gt;
&lt;pre id=&quot;code_1739172218465&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int[] solution(int[] numbers) {
        int[] answer = new int[numbers.length];
        for (int i = 0; i &amp;lt; numbers.length; i++) {
            for (int j = i+1; j &amp;lt; numbers.length; j++) {
                if (numbers[i] &amp;lt; numbers[j]) {
                    answer[i] = numbers[j];
                    break;
                } else {
                    answer[i] = -1;
                } 
            }
            if (i == (numbers.length -1)) {
                answer[i] = -1;
            }
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 입력 받은 `numbers` 배열을 0번 인덱스 부터 마지막 까지 끝까지 확인하며, 가장 가까운 큰 수를 발견했을 때 `break` 로 반복문을 탈출하며 효율을 챙기려는 방식의 풀이입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드의 문제점은&amp;nbsp;&lt;b&gt;모든 원소를 이중 반복문으로 비교하기 때문에 시간 복잡도가 O(N&amp;sup2;)&amp;nbsp;&lt;/b&gt;으로 너무 크다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 결과를 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;809&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jp9Wd/btsMdqktwoS/yNvL20OrBJZfDh629d2IRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jp9Wd/btsMdqktwoS/yNvL20OrBJZfDh629d2IRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jp9Wd/btsMdqktwoS/yNvL20OrBJZfDh629d2IRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJp9Wd%2FbtsMdqktwoS%2FyNvL20OrBJZfDh629d2IRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;809&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;809&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 테스트 케이스에서는 통과할 수 있었지만, &lt;b&gt;최악의 경우의 수로 설정된 경우는 시간 초과가 발생&lt;/b&gt;하는 것을 볼 수 있습니다. (최악의 경우 1,000,000 * 1,000,000 연산 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 스택을 활용하여 최적화 한다면, &lt;b&gt;시간 복잡도를 O(N)&lt;/b&gt; 으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 스택을 활용하여 최적화한 풀이 ️&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  &lt;/b&gt;아이디어&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;배열을 뒤에서부터 순회하며 &lt;b&gt;스택을 활용하여 뒷 큰수를 찾음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;스택의 &lt;b&gt;맨 위 값(Top)이 현재 원소보다 크면, 그 값이 뒷 큰수&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;스택의 &lt;b&gt;Top이 현재 원소보다 작으면, 해당 값은 필요 없으므로 제거&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;뒷 큰수를 찾지 못하면 -1 저장&lt;/li&gt;
&lt;li&gt;현재 원소를 &lt;b&gt;스택에 push하여, 다음 원소의 비교 대상&lt;/b&gt;으로 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1739172590806&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Stack;

class Solution {
    public int[] solution(int[] numbers) {
        int n = numbers.length;
        int[] answer = new int[n]; // 결과 배열
        Stack&amp;lt;Integer&amp;gt; stack = new Stack&amp;lt;&amp;gt;(); // 스택을 이용한 최적화

        // 배열을 **거꾸로 순회** (뒤에서 앞으로)
        for (int i = n - 1; i &amp;gt;= 0; i--) {
            int num = numbers[i];

            // 스택에서 현재 원소보다 작거나 같은 값 제거 (쓸모 없는 값)
            while (!stack.isEmpty() &amp;amp;&amp;amp; stack.peek() &amp;lt;= num) {
                stack.pop();
            }

            // 스택이 비어있지 않다면, 가장 가까운 큰 수가 존재
            answer[i] = stack.isEmpty() ? -1 : stack.peek();

            // 현재 숫자를 스택에 추가
            stack.push(num);
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 실행 과정을 차근차근 살펴보겠습니다. (예시: `numbers = [9, 1, 5, 3, 6, 2]`)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  실행 과정 상세 설명&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1️⃣ `i = 5`, `numbers [5] = 2` (가장 오른쪽 요소)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `stack = []` (초기 상태, 비어 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 현재 숫자 `2` 의 뒷 큰수를 찾아야 하지만, 오른쪽에는 숫자가 없으므로 -1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 결과값 저장: `answer[5] = -1`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 스택에 `2` 추가 (다음 비교를 위해)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;현재 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739172923575&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [2]
answer = [0, 0, 0, 0, 0, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2️⃣ `i = 4`, `numbers[4] = 6`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스택의 top 값 `2` (현재 숫자 `6` 보다 작음 -&amp;gt; 필요 없는 값이므로 pop)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스택이 비었으므로 뒷 큰수가 없음 -&amp;gt; `-1` 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 결과값 저장: `answer[4] = -1`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 현재 숫자 `6` 을 스택에 push&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;현재 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739173043354&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [6]
answer = [0, 0, 0, 0, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3️⃣ `i = 3`, `numbers[3] = 3`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스택의 top 값 `6` (현재 숫자 `3` 보다 크므로 뒷 큰수는 `6`)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결과값 저장: `answer[3] = 6`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 현재 숫자 `3` 을 스택에 push&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;현재 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739173118013&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [6, 3]
answer = [0, 0, 0, 6, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4️⃣ `i = 2`, `numbers[2] = 5`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스택의 top 값 `3` (현재 숫자 `5` 보다 작음 -&amp;gt; 필요 없는 값이므로 pop)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스택의 새로운 top 값 `6` (현재 숫자 `5` 보다 크므로 뒷 큰수는 `6`)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 결과값 저장: `answer[2] = 6`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 현재 숫자 `5` 를 스택에 push&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;현재 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739173214215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [6, 5]
answer = [0, 0, 6, 6, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5️⃣ `i = 1`, `numbers[1] = 1`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스택에 top 값 `5` (현재 숫자 1 보다 크므로 뒷 큰수는 `5`)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 결과값 저장: `answer[1] = 5`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 현재 숫자 1을 스택에 push&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;현재 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739173278959&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [6, 5, 1]
answer = [0, 5, 6, 6, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6️⃣ `i = 0`, `numbers[0] = 9`&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 스택의 top 값 `1` (현재 숫자 `9` 보다 작음 -&amp;gt; 필요 없는 값이므로 pop)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 스택의 새로운 top 값 `5` (현재 숫자 `9`보다 작음 -&amp;gt; 필요 없는 값이므로 pop)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 스택의 새로운 top 값 `6` (현재 숫자 `9`보다 작음 -&amp;gt; 필요 없는 값이므로 pop)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 스택이 비었으므로 뒷 큰수가 없음 -&amp;gt; `-1` 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 결과값 저장: `answer[0] = -1`&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 현재 숫자 `9` 를 스택에 push&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;b&gt;최종 상태&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739173421227&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stack = [9]
answer = [-1, 5, 6, 6, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  최종 결과&lt;/h3&gt;
&lt;pre id=&quot;code_1739173438149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[-1, 5, 6, 6, -1, -1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이런 방법으로 시간 복잡도를&lt;/b&gt; &lt;b&gt; O(N&amp;sup2;) &amp;rarr; O(N)으로 줄일 수 있습니다!&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pgg25/btsMerJBEOX/MIVZrym3hi3BaWj1SlB0p1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pgg25/btsMerJBEOX/MIVZrym3hi3BaWj1SlB0p1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pgg25/btsMerJBEOX/MIVZrym3hi3BaWj1SlB0p1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPgg25%2FbtsMerJBEOX%2FMIVZrym3hi3BaWj1SlB0p1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;538&quot; data-origin-width=&quot;707&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후입선출 구조라는 키워드에서 스택의 응용이 어려울 수 있지만, 스택을 활용할 수 있는 다양한 문제를 풀어보면서 스택에 익숙해 진다면 비슷한 유형에서 충분히 응용하여 최적화 할 수 있을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다 :)&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>it</category>
      <category>til</category>
      <category>스택</category>
      <category>시간복잡도</category>
      <category>알고리즘</category>
      <category>자료구조</category>
      <category>최적화</category>
      <category>코딩테스트</category>
      <category>코테</category>
      <category>큐</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/106</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%8A%A4%ED%83%9D-%EB%92%A4%EC%97%90-%EC%9E%88%EB%8A%94-%ED%81%B0-%EC%88%98-%EC%B0%BE%EA%B8%B0-%EC%98%88%EC%A0%9C#entry106comment</comments>
      <pubDate>Mon, 10 Feb 2025 16:47:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Redis의 Pub/Sub 및 WebSocket 구현하기</title>
      <link>https://wanglan.tistory.com/entry/Spring-Redis%EC%9D%98-PubSub-%EB%B0%8F-WebSocket-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 프로젝트를 진행하면서 실시간 알림 서비스를 구현하고 있습니다. 알림 서비스의 동작 방식은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 유저가 가게에 주문(Order)을 요청하면 새로운 주문이 생성되고 Waiting 이 몇팀있는지 반환받는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 가게 사장님이 유저가 보낸 주문을 확인하고 &quot;입장(ADMISSION)&quot; 또는 현재 상태에 맞는 메세지를 보낸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 유저는 사장님이 보낸 메세지를 확인하고, 자신의 주문 상태에 맞게 주문을 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;일단 큰 진행방식은 위와같이 구상하였습니다. 일단 지금의 상태에서 필요한 부분을 정리해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 사장님이 주문의 상태를 변경할 때, 메세지를 &lt;b&gt;&quot;보낸다&quot;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 유저는 사장님이 보낸 메세지를 &lt;b&gt;&quot;받는다&quot;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 1, 2에서 보내고 받는 것을 &lt;b&gt;실시간&lt;/b&gt;으로 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. &quot;주문 생성&quot; 이라는 기능과 &quot;주문 상태 변경&quot; 이라는 기능은 메세지기능과 &lt;b&gt;&quot;비동기적&quot;&lt;/b&gt;으로 처리한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 조건을 구현하기 위해서 채택한 방식은 Redis 를 이용하여 Pub/Sub 구조로 메세지를 주고 받는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Redis pubsub.png&quot; data-origin-width=&quot;4075&quot; data-origin-height=&quot;1907&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lsETk/btsLkIs7PGV/O8y0KIvFjVw3elYMjscKN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lsETk/btsLkIs7PGV/O8y0KIvFjVw3elYMjscKN0/img.png&quot; data-alt=&quot;Pub/Sub 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lsETk/btsLkIs7PGV/O8y0KIvFjVw3elYMjscKN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlsETk%2FbtsLkIs7PGV%2FO8y0KIvFjVw3elYMjscKN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4075&quot; height=&quot;1907&quot; data-filename=&quot;Redis pubsub.png&quot; data-origin-width=&quot;4075&quot; data-origin-height=&quot;1907&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pub/Sub 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Pub/Sub 구조는 발행자(Publisher) 와 구독자(Subscriber) 로 이루어져 있고, 현재 제가 구현하려는 기능에 대입해 보면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 회원이 주문을 생성할 때, 자신의 `memberId` 를 토대로 특정 `Channel` 을 구독(Subscribe)합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;사장이 주문의 상태를 변경할 때, 메세지를 주문에 관련된 Channel 에 발행(Publish)합니다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;3. 회원은 구독하고 있던 채널에서 사장이 발행한 메세지를 받습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이와 같이 간단하게 구현이 가능합니다. 그럼 지금부터 Redis 로 Pub/Sub 구조를 만드는 방법부터 알아보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. Redis 기본 설정&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우선 build.gradle 에 다음과 같이 Redis 의존성을 추가해준뒤 Gradle 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734398787186&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;application.properties 에서도 다음 항목을 추가해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734402811389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 내용은 redis 서버를 설정할 host 와 port 를 설정하는 부분입니다. 위의 127.0.0.1 과 6379 는 redis 의 기본적인 포트와 호스트 ip 입니다. 추후 WebSocket 과 연결할 때, 이 부분을 사용할 수 있지만, Default 주소이기 때문에 localhost 로도 대체가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis 를 추가해준 뒤, 저는 Ubuntu 에서 Redis 를 설치했기 때문에 터미널에서 Ubuntu 를 실행 시켜준 뒤 Redis CLI 커멘드를 사용하여 연결 상태를 테스트합니다. 커멘드는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734398936716&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redis-cli ping&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;303&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTbJRI/btsLlMVV1fA/1IMU5uJ6abCXQcqLtoCEXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTbJRI/btsLlMVV1fA/1IMU5uJ6abCXQcqLtoCEXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTbJRI/btsLlMVV1fA/1IMU5uJ6abCXQcqLtoCEXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTbJRI%2FbtsLlMVV1fA%2F1IMU5uJ6abCXQcqLtoCEXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1015&quot; height=&quot;303&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;303&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사진과 같이 `PONG` 이라는 응답이 오면 연결이 제대로 된 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1.1 RedisConfig&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다음으로는 RedisConfig 클래스를 작성하고, 기본 설정을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734399156086&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableRedisRepositories
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        return template;
    }

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(LettuceConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저의 경우에는 RedisTemplate 을 `String` 과 `Object` 로 설정하여, 특정 채널의 이름을 String 타입으로 유동적으로 받기로 하고, 메세지를 Object 타입에 담아 전달하고자 하였습니다. 그리고 기본적으로 redisTemplate 에 Key 와 Value 를 설정해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그리고 메세지를 주고받기 위해서 redisMessageListenerContainer 설정을 해줍니다. 이 부분에서, Adapter 를 사용해서 특정한 채널을 명시해줄 수 있지만, 저의 경우에는 &lt;b&gt;order-status:member:{memberId}&lt;/b&gt; 형식으로 채널 명이 다르게 생성되어야 하기 때문에, 이곳에서 명시하지는 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1.2 Redis 메세지 발행&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;`OrderStatusPublisher` 클래스에서 Redis 채널로 메세지를 발행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734400658668&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderStatusPublisher {
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    public void publishOrderStatus(Long memberId, Long orderId, String message) {
        String channel = &quot;order-status:member:&quot; + memberId;
        OrderStatusMessage orderStatusMessage = new OrderStatusMessage(orderId, message);
        try {
            String jsonMessage = new ObjectMapper().writeValueAsString(orderStatusMessage);
            redisTemplate.convertAndSend(channel, jsonMessage);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(&quot;Redis 메시지 발행 실패&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이때, 메세지 발행이나 구독 등의 기능을 하나의 클래스에서 모두 묶어서 구현할 수 있지만 저는 유지보수나 확장성을 생각하여 가능한 개별적으로 분리해두었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1.3 Redis 메세지 구독&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1734400760106&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class RedisSubscriberService {
    private final WebSocketHandler webSocketHandler;

    public void subscribeMemberToChannel(Long memberId, String channel, WebSocketSession session) {
        MessageListenerAdapter listener = new MessageListenerAdapter((MessageListener)(message, pattern) -&amp;gt; {
            String payload = new String(message.getBody());
            webSocketHandler.sendMessageToUser(memberId, payload);
        });

        ChannelTopic topic = new ChannelTopic(channel);
        redisMessageListenerContainer().addMessageListener(listener, topic);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저는 API 가 동작할 때, `subscribeMemberToChannel 이라는 메서드가 호출 되어 특정한 채널을 구독하게 하고, &lt;b&gt;WebSocket&lt;/b&gt; 을 통해 사용자에게 메세지를 전달하여 보여주도록 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. WebSocket&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;WebSocket 은 &lt;b&gt;실시간으로 클라이언트와 서버 간의 양방향 통신&lt;/b&gt;을 할 수 있게 해주는 프로토콜 입니다. Redis 의 Pub/Sub 구조만 사용하면, 메세지 발행 및 수신은 충분히 가능하지만, 구독 상태가 지속적이지 않고 메세지는 단발성으로 처리됩니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이것을 실시간으로 연결을 유지시켜 위해, 그리고 지속적으로 메세지를 수신하도록 할 수 있게 하기 위해 WebSocket 을 사용하는 것을 채택하였습니다. 또한 WebSocket 을 이용하여 개별 사용자마다 메세지를 송수신 할 수 있게 되고, 실시간으로 사용자의 UI를 업데이트 시켜줄 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.1 WebSocket 기본 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;build.gradle 에 다음과 같이 의존성을 추가해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734401362090&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그리고 `WebSocketConfig` 클래스를 생성하여 기본적인 설정 및 `WebSocketHandler` 를 등록합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734401675993&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, &quot;/websocket&quot;).setAllowedOrigins(&quot;*&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이때, &lt;b&gt;registerWebSocketHandlers&lt;/b&gt; 메서드를 통해서 실제로 통신할 WebSocket의 URL 을 지정해줍니다. &lt;b&gt;setAllowedOrigins&lt;/b&gt; 부분은 와일드카드를 사용해서 WebSocket 의 어떤 URL 이든 요청을 허용하게 하지만, 실제로 사용할 때는 개별적으로 설정해주어야 합니다. 현재 addHandler 메서드에서 등록되어있는 &quot;/websocket&quot; 부분을 참고하면 실제로 테스트할 때 요청을 보내는 URL 은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ws://localhost:8080/websocket&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위의 URL 에 WebSocket 연결 요청을 보내면 정상적으로 연결 요청이 허용되어 연결이 유지됩니다. 하지만 저의 경우에는 회원이 자신의 주문에 관련된 채널만 구독하고, 메세지를 받게하기 위해서 실제로 요청하는 URL은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ws://localhost:8080/websocket?memberId=1&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위처럼 연결하면&lt;b&gt; order-status:member:1&lt;/b&gt; 이라는 채널을 memberId 1 인 회원이 구독을 하게 되고, 사장님이 &lt;b&gt;order-status:member:1&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt; 채널에 메세지를 발행하면 회원은 메세지를 받게됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.2 WebSocket 핸들러&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;우선 제가 설정한 전체적인 WebSocketHandler 를 보여드리도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734403510673&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class WebSocketHandler extends TextWebSocketHandler {
	private static final ConcurrentHashMap&amp;lt;Long, WebSocketSession&amp;gt; CLIENTS = new ConcurrentHashMap&amp;lt;&amp;gt;();
	private final ApplicationEventPublisher eventPublisher;

	@Override
	public void afterConnectionEstablished(WebSocketSession session) {
		// WebSocket 연결 시 memberId 추출
		Long memberId = extractMemberIdFromSession(session);
		if (memberId != null) {
			addClient(memberId, session);
			System.out.println(&quot;WebSocket 연결 성공: memberId=&quot; + memberId + &quot;, 세션 ID=&quot; + session.getId());
		} else {
			System.out.println(&quot;WebSocket 연결 실패: memberId가 전달되지 않음&quot;);
			try {
				session.close(CloseStatus.BAD_DATA);
			} catch (IOException e) {
				System.out.println(&quot;WebSocket 세션 종료 중 오류: &quot; + e.getMessage());
			}
		}
	}

	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
		// 연결 종료 시 세션 제거
		CLIENTS.entrySet().removeIf(entry -&amp;gt; entry.getValue().equals(session));
		System.out.println(&quot;WebSocket 연결 종료: 세션 ID = &quot; + session.getId());
	}

	public void addClient(Long memberId, WebSocketSession session) {
		CLIENTS.put(memberId, session);
		System.out.println(&quot;클라이언트 추가 성공: memberId=&quot; + memberId + &quot;, 세션 ID=&quot; + session.getId());
	}

	public void sendMessageToUser(Long memberId, String message) {
		WebSocketSession session = CLIENTS.get(memberId);
		if (session != null &amp;amp;&amp;amp; session.isOpen()) {
			try {
				session.sendMessage(new TextMessage(message));
				System.out.println(&quot;WebSocket 메시지 전송 성공: 사용자 ID=&quot; + memberId + &quot;, 메시지=&quot; + message);
			} catch (IOException e) {
				System.out.println(&quot;WebSocket 메시지 전송 실패: 사용자 ID=&quot; + memberId + &quot;, 예외=&quot; + e.getMessage());
			}
		} else {
			System.out.println(&quot;세션 없음: 사용자 ID=&quot; + memberId);
		}
	}

	public void subscribeUserToChannel(Long memberId, String channel) {
		WebSocketSession session = CLIENTS.get(memberId);
		if (session != null) {
			eventPublisher.publishEvent(new RedisSubscriptionEvent(this, memberId, channel, session));
			System.out.println(&quot;Redis 채널 구독 성공: memberId=&quot; + memberId + &quot;, 채널=&quot; + channel);
		} else {
			System.out.println(&quot;구독 실패: memberId=&quot; + memberId + &quot;, 세션이 존재하지 않음&quot;);
		}
	}

	private Long extractMemberIdFromSession(WebSocketSession session) {
		// WebSocket URL에서 memberId 추출
		Map&amp;lt;String, String&amp;gt; params = parseQueryParams(session.getUri().getQuery());
		if (params.containsKey(&quot;memberId&quot;)) {
			return Long.valueOf(params.get(&quot;memberId&quot;));
		}
		return null;
	}

	private Map&amp;lt;String, String&amp;gt; parseQueryParams(String query) {
		Map&amp;lt;String, String&amp;gt; params = new HashMap&amp;lt;&amp;gt;();
		if (query != null &amp;amp;&amp;amp; !query.isEmpty()) {
			String[] pairs = query.split(&quot;&amp;amp;&quot;);
			for (String pair : pairs) {
				String[] keyValue = pair.split(&quot;=&quot;);
				if (keyValue.length == 2) {
					params.put(keyValue[0], keyValue[1]);
				}
			}
		}
		return params;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ApplicationEventPublisher 나 extract 메서드 및 parseQueryParams 등의 메서드가 있지만, 이 부분들은 본인이 개발하는 요건에 맞게 설정가능한 부분이며 핵심은 @Override 된 부분에서 구현해야하는 내용입니다.(저는 Handler 와 Publisher 등에서 순환참조 문제가 발생하여 Handler 부분을 이벤트처리하여 컴파일 시점에 초기화하는게 아니라 메세지가 호출될때 수행되도록 설정하였습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 부분에는 `afterConnectionEstablished` 메서드와 `afterConnectionClosed` 가 있습니다. 저는 위에서 보여드린 연결 요청 URL 에 맞게 쿼리 파라미터에서 memberId 를 추출한 후 CLIENTS 라는 `ConcurrentHashMap` 에 회원의 ID 값과 회원이 연결한 WebSocket 의 세션 ID를 저장하여 연결을 완성합니다. (afterConnectionEstablished 내부의 로직)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저의 메서드와는 방식이 약간 다르지만 간략하게 요약하자면 아래와 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734404166452&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class WebSocketHandler extends TextWebSocketHandler {
    private static final ConcurrentHashMap&amp;lt;Long, WebSocketSession&amp;gt; CLIENTS = new ConcurrentHashMap&amp;lt;&amp;gt;();

    @Override // 이 메서드에서 위의 CLIENTS Map 에 회원ID 와 세션ID 를 넣음
    public void afterConnectionEstablished(WebSocketSession session) {
        Long memberId = extractMemberIdFromSession(session);
        if (memberId != null) {
            CLIENTS.put(memberId, session);
        } else {
            session.close(CloseStatus.BAD_DATA);
        }
    } 

	// 이 메서드를 통해 Redis 채널로 전송된 메세지를 WebSocket 통신으로 전송
    public void sendMessageToUser(Long memberId, String message) {
        WebSocketSession session = CLIENTS.get(memberId);
        if (session != null &amp;amp;&amp;amp; session.isOpen()) {
            session.sendMessage(new TextMessage(message));
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Override 한 메서드를 통해 WebSocket 과 회원간의 연결을 유지하고, Redis 채널에 발행되어 있는 메세지를 sendMessageToUser 메서드를 정의하여 보내주게 됩니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.3 주문 요청 처리 및 Redis 채널 구독&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;OrderService 에서 회원이 주문을 생성할 때, Redis 채널 구독을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734404518426&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public OrderResponse createOrder(Long storeId, Member member, OrderRequest orderRequest) {
	//주문 생성 로직 생략...
	Order order = Order.createOf( ... , ... , ...);
		
	// WebSocketHandler 에 사용자 등록 및 Redis 채널 구독
	String channel = &quot;order-status:member:&quot; + member.getId();
	webSocketHandler.subscribeUserToChannel(member.getId(), channel);

	// Redis 채널 메시지 발행
	String message = &quot;고객님의 주문 (주문 ID: &quot; + order.getId() + &quot;) 이 요청되었습니다.&quot;;
	orderStatusPublisher.publishOrderStatus(member.getId(), order.getId(), message);

	return new OrderResponse(order.getId(), order.getStatus().name());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;webSocketHandler 를 사용할 클래스에서 private final 로 초기화 하여 선언해준 다음, 사용할 비즈니스 로직의 내부에 subscribeUserToChannel 메서드를 호출하여 회원이 주문을 하는 시점에 본인이 메세지를 받을 채널을 구독할 수 있게 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또한 Redis 채널에 본인의 주문에 관련된 메세지를 발행하여 WebSocket으로 받아볼 수 있게 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;publishOrderStatus 라는 메서드는 아래와 같이 작성되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734404985726&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Slf4j
@RequiredArgsConstructor
public class OrderStatusPublisher {
	private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

	//특정 사용자(memberId)에게 Redis 채널로 메시지를 발행
	public void publishOrderStatus(Long memberId, Long orderId, String message) {
		// Redis 채널명 생성
		String channel = &quot;order-status:member:&quot; + memberId;

		// 메시지 객체 생성
		OrderStatusMessage orderStatusMessage = new OrderStatusMessage(orderId, message);

		// 메시지를 JSON 문자열로 변환하여 Redis 채널에 발행
		try {
			String jsonMessage = new ObjectMapper().writeValueAsString(orderStatusMessage);
			redisTemplate.convertAndSend(channel, jsonMessage);
			log.info(&quot;Redis 메시지 발행 성공: channel={}, message={}&quot;, channel, jsonMessage);
		} catch (JsonProcessingException e) {
			log.error(&quot;Redis 메시지 발행 실패: JSON 변환 중 오류 발생&quot;, e);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2.4 주문 상태 변경 및 채널에 메세지 발행&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;사장이 주문 상태를 변경할 때, 회원이 구독하고 있는 채널에 메세지를 발행합니다. 이 때 메세지는 Redis 의 특정 채널에 발행 된 후 WebSocket 을 통해 회원에게 전달됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1734405278619&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
	public OrderStatusUpdateResponse updateOrderStatus(Long storeId, Long orderId, OrderStatusUpdateRequest request,
		Member member) throws JsonProcessingException {
		// 유효성 검증 로직 생략...
		order.changeStatus(newStatus);
		orderRepository.save(order);

		// Redis로 메시지 발행
		String message = &quot;주문 상태가 업데이트되었습니다: &quot; + newStatus.name();
		orderStatusPublisher.publishOrderStatus(order.getMember().getId(), order.getId(), message);

		return OrderStatusUpdateResponse.builder()
			.orderId(order.getId())
			.orderStatus(order.getStatus().name())
			.build();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. WebSocket 테스트&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다양한 WebSocket 테스트 플랫폼이 있지만, 저는 크롬 브라우저의 확장 프로그램에 있는 WebSocket King Client 라는 테스터를 사용하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FvJMe/btsLjT90lgj/0fxxbTgjMVBIJSGsAvzru1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FvJMe/btsLjT90lgj/0fxxbTgjMVBIJSGsAvzru1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FvJMe/btsLjT90lgj/0fxxbTgjMVBIJSGsAvzru1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFvJMe%2FbtsLjT90lgj%2F0fxxbTgjMVBIJSGsAvzru1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1149&quot; height=&quot;627&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저의 조건에 맞게 URL 을 넣고 Connect 버튼을 누르면 다음과 같이 연결되었다는 메세지가 뜨게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;회원의 아이디는 memberId = 3 으로 설정되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Postman 을 사용하여 회원이 주문을 생성해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWFqkO/btsLltPDVce/CozJq3GDreTiKwZrm2gq0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWFqkO/btsLltPDVce/CozJq3GDreTiKwZrm2gq0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWFqkO/btsLltPDVce/CozJq3GDreTiKwZrm2gq0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWFqkO%2FbtsLltPDVce%2FCozJq3GDreTiKwZrm2gq0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;744&quot; height=&quot;530&quot; data-origin-width=&quot;744&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;주문이 생성되었고, orderId = 12 라는 주문이 생성되었습니다. 동시에 WebSocket King Client 를 보면 다음과 같은 메세지를 받게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4oAJ1/btsLlZgqBt2/kOJzzlKeMrvtP5D62bkPT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4oAJ1/btsLlZgqBt2/kOJzzlKeMrvtP5D62bkPT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4oAJ1/btsLlZgqBt2/kOJzzlKeMrvtP5D62bkPT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4oAJ1%2FbtsLlZgqBt2%2FkOJzzlKeMrvtP5D62bkPT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1146&quot; height=&quot;542&quot; data-origin-width=&quot;1146&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 유저는 order-status:member:3 이라는 채널을 구독하고 있고, 이 채널에 발행되는 메세지는 모두 WebSocket 으로 받아서 실시간으로 확인이 가능합니다. 콘솔에서도 다양한 정보를 볼 수 있게 처리해두었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;63&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvyxCn/btsLlf5dqlB/CTGLCtnjzXdrywvzxK4pHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvyxCn/btsLlf5dqlB/CTGLCtnjzXdrywvzxK4pHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvyxCn/btsLlf5dqlB/CTGLCtnjzXdrywvzxK4pHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvyxCn%2FbtsLlf5dqlB%2FCTGLCtnjzXdrywvzxK4pHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;63&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;63&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그럼 이제 사장이 회원의 주문 상태를 변경해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcP54a/btsLjGpvADW/d6fiEnYCgQtVCSELkiKXa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcP54a/btsLjGpvADW/d6fiEnYCgQtVCSELkiKXa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcP54a/btsLjGpvADW/d6fiEnYCgQtVCSELkiKXa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcP54a%2FbtsLjGpvADW%2Fd6fiEnYCgQtVCSELkiKXa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1150&quot; height=&quot;656&quot; data-origin-width=&quot;1150&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 환경에서 회원(memberId=3)이 11번과 12번 주문을 해둔 상태였기 때문에, 이 회원은 자신이 한 주문이라면 어떤 주문이든 변경했을 때 메세지를 받아볼 수 있고, 사장이 각각의 주문의 상태를 변경하면 WebSocket 으로 변경한 주문 상태를 수신할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;오늘 포스팅한 내용은, Redis 를 통해서 Pub/Sub 구조를 구현하고 메세지를 채널에 발행했을 때, 이것을 WebSocket 을 통해 수신하는 방식까지 구현하는 것을 목표로 작성하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis Pub/Sub 구조에서는 메세지 발행이 비동기처리되어 비즈니스 로직에서 메세지 처리하는 중에 문제가 발생되어도 실제 DB 에 상태를 변경하는 API 라던지, 주문을 생성하는 API 가 정상적으로 수행하고 메세지만 전송이 되지않는 일이 발생할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 이 말은 반대로, 비즈니스 로직상에서 DB에 저장하는 과정까지 제대로 도달하지 못해도 메세지는 전달되는 일이 발생할&amp;nbsp; 수 있고 그렇게 된다면 치명적인 문제가 아닐 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또한 메세지를 보내는 시점이 실제 DB가 업데이트 되는 시점보다 빠르기 때문에 이 시간적 차이를 synchronize 해주는 부분이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다음 포스팅에서는 이런 다양한 문제들을 개선하는 과정을 보여드리도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>it</category>
      <category>pub/sub</category>
      <category>pub/sub구현</category>
      <category>redis</category>
      <category>Spring</category>
      <category>websocket</category>
      <category>메세지기능</category>
      <category>알림기능</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/105</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Redis%EC%9D%98-PubSub-%EB%B0%8F-WebSocket-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#entry105comment</comments>
      <pubDate>Tue, 17 Dec 2024 12:29:49 +0900</pubDate>
    </item>
    <item>
      <title>[프로젝트] 식당 예약 및 웨이팅 플랫폼 개발 - 주제선정</title>
      <link>https://wanglan.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9D%EB%8B%B9-%EC%98%88%EC%95%BD-%EB%B0%8F-%EC%9B%A8%EC%9D%B4%ED%8C%85-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B0%9C%EB%B0%9C-%EC%A3%BC%EC%A0%9C%EC%84%A0%EC%A0%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이번에 팀원들과 새로운 프로젝트에 대한 개발을 시작하면서, 프로젝트 설계의 전반적인 과정과 저희 팀에서 오고 갔던 &lt;b&gt;기술적 의사결정의 과정들, 개선되는 점들&lt;/b&gt;에 대해서 정리하려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;지금은 프로젝트 주제가 식당 예약 및 웨이팅이 가능한 플랫폼을 만드는 것으로 확정되었지만, 주제 선정 및 이번 프로젝트에서 팀원이 각각 얻어가고 싶은 스킬이나 역량에 대한 부분 부터 고민이 많았던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;  주제 선정의 과정&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음 여러 주제를 던져보기 전, 팀원별로 각각 이번 프로젝트에서 해보고 싶은게 어떤 것인지에 대해서 말해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;본인: Kafka 나 RabbitMQ 같은 라이브러리를 사용해서 실시간 메세지 관리 기능을 구현하는 것&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;팀원1: ElasticSearch 를 활용한 조회 시스템 성능 개선&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;팀원2: MSA 개념을 따라 프로젝트의 구조를 설계하기&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;팀원3: WebSocket 등을 이용한 채팅 서비스 &lt;/span&gt;&lt;span&gt;등등..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이번에 진행하는 프로젝트는 개발 기간이 꽤나 길게 주어졌기 때문에, 꼭 해보고 내것으로 만들고 싶은 기술에 대해서 나열해보았습니다.&lt;/span&gt;&lt;span&gt;위에서 말한 기술들 이외에도 다양한 아이디어가 나왔지만, 기본적으로 &lt;b&gt;&quot;실시간&quot;, &quot;동시성 제어&quot;, &quot;MSA&quot;, &quot;알림&quot; &lt;/b&gt;등이 저희 주제 선정에 대한 키워드였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1️⃣ 첫번째 주제 - 여행 계획 공유 플랫폼&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;팀원들과 회의를 하며, 명확한 주제가 떠오르지 않아서, 휴대폰을 켜고 각자의 스마트폰에 설치되어있는 어플리케이션을 살펴봤습니다. 그 중에 눈에 띄고 저희가 적용하고 싶은 다양한 기술을 담을 수 있는 어플리케이션이&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1. 배달의 민족(및 기타 배달 관련 어플리케이션)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2. 캐치테이블(식당 예약 서비스)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3. 트리플(여행 계획 설계 및 공유)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정도가 눈에 띄었고, 이 중에 여행 계획 설계 및 공유가 가능한 &lt;b&gt;트리플&lt;/b&gt; 이라는 어플리케이션에 눈에 들어왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그리고 이런 플랫폼을 개발하기 위해서 설계를 시작하였고, 저희 프로젝트의 기본적인 소개와 핵심기술을 떠올려 보며 기본적인프로젝트 구상을 해보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; 주제: 실시간 여행 정보 공유 플랫폼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &amp;zwj;  프로젝트 소개: 1인, 2인 또는 여러명 등 여행하는 사람의 여행 계획, 일정 등의 정보를 등록하고 실시간으로 공유하는 플랫폼&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;✅ 프로젝트 핵심 기술:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;1. 여행정보에 대한 CRUD&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;2. 실시간 위치정보 사용(GPS)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;3. 등록된 여행 정보 수정 시 실시간 공유(메세지 큐)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;4. 근처 추천장소(캐싱을 통한 자주 조회되는 목록 조회)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;5. 끝난여행 기록 커뮤니티 공유&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;6. 채팅 기능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;7. 푸쉬 알림&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;8. OAuth 사용자 인증&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;9. 이벤트 등 할인 쿠폰(동시성 제어)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;등의 기능들이 떠올랐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발 기한은 기본적으로 5주정도였기 때문에, 3주 정도의 시간동안 개발하여 필수적으로 기능이 구현되는 선인 MVP 를 1번부터 5번 까지로 구상하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프로젝트를 진행하기에 앞서, 많은 조언과 도움을 주시는 시니어개발자 분들이 계셔서 이 주제에 대한 프로젝트 설계가 어떤 것 같은지 피드백을 받아보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;❔❕&lt;/span&gt; 피드백&lt;/span&gt;&lt;span&gt;시니어개발자1: 다양한 기술들을 사용하는 것도 좋은 경험이 될 수 있지만, 프로젝트의 요구 사항에 따라 개발을 진행 하다가 &lt;b&gt;더 나은 기술이 있을 때 채택하는 방식&lt;/b&gt;이 더 좋을 것 같다. 즉, &lt;b&gt;기술 선택의 타당성&lt;/b&gt;이 있어야 하는데 기술을 먼저 생각하고 그 기술에 해당하는 기능을 추가한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;처음 이 피드백을 들었을 때, 저희 프로젝트가 시작부터 약간 잘못 된 부분이 있다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;시니어 개발자분의 말 대로, 써보고 싶은 기술을 나열 한 뒤, 그 기술이 들어간 어플리케이션을 개발하는 이상한(?) 순서로 설계가 진행된 것 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;❔❕&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;피드백2&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;시니어개발자2: 사실 이 주제에 대해서 주니어개발자 들이 피드백을 받으러 왔을때, 이미 반려한 적이 있다. 기술을 가져다 쓰는 것은 디벨롭할 것이 없기 때문이다. 좀 오버엔지니어링이 아닌지 의문이 든다. 실시간 키워드에 집중을 해서 뭔가를 하더라도 그냥 Redis 를 이용해서 구현하는 것도 문제가 없는데 기술 활용의 난이도를 떠나서 기본기를 중요하게 생각해야할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째로 받은 피드백도 그저 현업에서 이런 기술적 역량을 가지고 있는 사람을 채용하는 공고를 많이봐서 이런 것에 대한 연습을 미리 하고싶었던 저희는 저희의 본분을 간과하고 프로젝트를 설계한 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 피드백들에서 &lt;b&gt;&quot;기슬적인&quot; &lt;/b&gt;부분이 아닌 프로젝트의 구현과 개선 과정에서 어떤 기술을 선택하는 &lt;b&gt;&quot;타당성&quot;&amp;nbsp;&lt;/b&gt;을 찾아가며 코드 한줄 한줄에 이유를 담아보면 어떨지에 대한 고민을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2️⃣ 두번째 주제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 주제를 선정하고 탄탄대로 같았던 설계과정이 한번 전부 바뀌고 난 뒤, 방향을 기술로 잡지 말고, 하나의 프로그램을 만들되, 멀티모듈을 구조를 통해 실제 어플리케이션들이 동작하듯 모듈간의 상호작용을 중점으로 잡고 구현해보자 라는 목표를 잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에 관련해서 MSA 에 대해서도 추가적으로 공부하고, 실제 멀티모듈로 프로그램을 개발했을 때, `USER`, `OWNER`, `ADMIN` 등 (배달의 민족 기준) 어떻게 하면 프로젝트를 유기적으로 연결되게 설계할 수 있을지에 대한 고민을 하면서 새로운 주제로 다시 고민해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; 주제: 식당 예약 및 웨이팅 플랫폼, &quot;체어링&quot;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &amp;zwj;  프로젝트 소개: 기다리지 않고 계획적인 방문 + 예약을 할 수 있는 플랫폼&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 프로젝트 핵심 기술:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 웨이팅 상태 변경 시 유저에게 알림 제공&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 웨이팅 시, 메세지 큐를 사용하여 실시간 대기자 목록 조회&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 쿠폰 발급 및 예약에서의 동시성 제어&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 유저, 사장님, 관리자 3개의 멀티모듈 프로젝트&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5. S3 를 활용한 이미지 등의 대용량 전용 서버 구축&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;6. CI/CD 를 통한 배포 자동화&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 기술들에서 MVP 로 1~4번까지의 내용을 선정하고, 배포에 관한 부분은 추가적으로 진행할 계획이었습니다. 우선 프로젝트 자체의 기능들(기본 CRUD)에서 기술적인 부분을 도입하는 것은, 개선하는 과정에서 진행하는 것으로 정했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 내용을 토대로 이번엔 다른 시니어분께 피드백을 받으러 가보았습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;❔❕&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;피드백3&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;시니어개발자3: 프로젝트의 난이도 자체는 크게 어렵지않고, 적당한 수준의 프로젝트가 될 것 같다. 하지만 코어의 역할이 너무 커질 수 있는데 이것을 라이브러리로 잘 쪼개는 것이 중요하다. 모듈 하나가 라이브러리라고 생각해야 할것이다. 의존성을 축소하여 어떻게 의존성을 활용할 것인가에 대한 내용이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;❔❕&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;피드백4&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;시니어개발자4: 멀티모듈로 프로젝트를 만드는 것에 대한 경험에 의의를 두는 것이라면 좋은 것 같다. 하지만 완성도를 생각하며 프로젝트의 결과물을 향해 달리는 것은 말리고 싶다. 왜냐하면 아직 주니어 단계에서 짧은 기간동안 모듈마다 아주 많은 충돌과 의존성 문제등이 발생할 텐데, 이런 문제는 너무 다양해서 해결하는 과정도 너무 까다로워 진다. 이걸 해결해서 특정한 멀티모듈 프로젝트를 만들어 낸다고 해도 완성도가 높기 어렵고, 이걸 해결하는 과정에서 의도된 문제해결과정을 겪는 것 까지만 좋은 것 같다. 오히려 주니어 개발자에게 바라는 것은 기본적인 구현 능력과, 구현하는 과정에 있어서 코드 한줄 한줄 의미를 이해하고 타당성을 가지는 것이 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;라는 피드백을 받았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;멀티모듈로 프로젝트를 설계하여 개발하는 것 자체가 어렵다 쉽다 의 문제가 아니라, 이 멀티 모듈 개발에서 얻을 수 있는게 긍정적인 경험이 될것 같지가 않다는 판단이 들었습니다. 그래서 팀원들과 다시 회의를 시작하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;여기서 저희들이 깨달은 바는 역시 &lt;b&gt;&quot;기술적 의사결정&quot;, &quot;타당성&quot;&lt;/b&gt; 등이었던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;그래서 마지막으로 주제는 그대로 하되, 프로젝트 개발 일정을 바꾸고 코드에 대한 안정성 등을 고려하며 개발을 진행한 뒤, 완성도 높은 하나의 기본적인 프로그램에서 어떻게하면 더 개선할 수 있고, 어떤 기술을 도입하면 좋을지 고민하는 과정을 경험하자 라는 것을 목표로 다음과 같이 설계했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; 주제: 식당 예약 및 웨이팅 플랫폼, &quot;체어링&quot;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; &amp;zwj;  프로젝트 소개: 기다리지 않고 계획적인 방문 + 예약을 할 수 있는 플랫폼&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;✅ 프로젝트 핵심 기술:&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 웨이팅 상태 변경 시 유저에게 알림 제공&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 웨이팅 시, 메세지 큐를 사용하여 실시간 대기자 목록 조회&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 쿠폰 발급 및 예약에서의 동시성 제어&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 캐시를 활용한 맛집 추천 및 리뷰 추천&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5. S3 를 활용한 이미지 등의 대용량 전용 서버 구축&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;6. CI/CD 를 통한 배포 자동화&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;좀더 프로젝트의 구현레벨에 있어서 간단해 졌습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1~4번까지의 항목이 3주동안 진행할 MVP 에 대한 내용이고, 동시성 제어나 실시간 대기자 목록 조회 기능은 3주차의 막바지에 넣을 계획입니다. 그리고 기본적인 구현이 완성도 있게 되고, 저희가 개발한 프로젝트의 코드에 신뢰를 가질 수 있게 &lt;b&gt;테스트코드 커버리지 100% 로,&lt;/b&gt; 안정성 있는 프로그램을 개발하려고 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 기본적인 와이어프레임, ERD, API 명세 등을 설계한 과정을 담아보도록 하겠습니다. 감사합니다.&lt;/p&gt;</description>
      <category>Project</category>
      <category>it</category>
      <category>java</category>
      <category>MVP</category>
      <category>springboot</category>
      <category>내일배움캠프</category>
      <category>백엔드</category>
      <category>주니어개발자</category>
      <category>프로젝트</category>
      <category>프로젝트설계</category>
      <category>회고</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/104</guid>
      <comments>https://wanglan.tistory.com/entry/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9D%EB%8B%B9-%EC%98%88%EC%95%BD-%EB%B0%8F-%EC%9B%A8%EC%9D%B4%ED%8C%85-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EA%B0%9C%EB%B0%9C-%EC%A3%BC%EC%A0%9C%EC%84%A0%EC%A0%95#entry104comment</comments>
      <pubDate>Wed, 11 Dec 2024 15:46:44 +0900</pubDate>
    </item>
    <item>
      <title>Spring - DB 조회의 최적화</title>
      <link>https://wanglan.tistory.com/entry/Spring-DB-%EC%A1%B0%ED%9A%8C%EC%9D%98-%EC%B5%9C%EC%A0%81%ED%99%94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드를 개발하는 사람이 고민하는 부분 중, 중요한 부분인 데이터베이스에 접근하여 원하는 값을 찾으려 할 때의 &lt;b&gt;성능 최적화&lt;/b&gt;가 있습니다. 오늘은 이런 DB 조회할 때의 성능 최적화에 대한 다양한 방법이나 아이디어에 대한 내용을 말해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. DB 조회 성능 최적화 방법  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 적절한 조회방식 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 API가 필요한 조건에 맞춰서 정확히 지정하여 조회하는 것이 효율적입니다. 다음은 여러가지 DB를 조회하는 기술들에 대한 간략한 설명입니다.&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;JPQL/Criteria API&lt;/b&gt;: JPA를 활용할 때 효율적인 쿼리 작성으로 필요한 데이터만 조회.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Native Query&lt;/b&gt;: 복잡한 쿼리나 대량 데이터 처리 시 SQL을 직접 사용하여 최적화.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;QueryDSL&lt;/b&gt;: 동적 쿼리 작성 시 성능과 코드 가독성 모두 확보.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 불필요한 데이터 조회 최소화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 데이터를 지정하고 조회를 하려고 해도, 부수적으로 조회될 수 밖에 없는 경우도 있습니다. 이런 부분을 방지하여 성능 최적화를 이룰수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Lazy Loading 사용&lt;/b&gt;: 연관된 엔티티를 필요할 때만 로딩 (@OneToMany, @ManyToOne 등).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Projection 사용&lt;/b&gt;: DTO나 특정 필드만 가져오는 Projection을 통해 필요한 데이터만 조회.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pagination&lt;/b&gt;: 대량 데이터를 처리할 때는 페이징 처리 (Pageable, Page).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 QueryDSL 을 사용하여 쿼리 작성시 Projection 기능을 사용하면 특정필드만 골라서 조회가능 하기 때문에 성능을 더 끌어올릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 캐싱 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 메모리에 자주 사용되는 데이터가 들어있게 하는 등의 방법을 통해 DB 까지 조회에 도달하지 않고 값을 불러오는 방법도 있습니다. 이 부분은 `캐싱` 이라는 키워드로 따로 블로그 포스팅을 다뤄볼 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1차 캐시&lt;/b&gt;: JPA에서 제공하는 기본 캐시를 활용 (영속성 컨텍스트).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2차 캐시&lt;/b&gt;: Ehcache, Redis 등을 사용하여 반복 조회 데이터 캐싱.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 쿼리 튜닝 및 인덱스 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인덱싱이랑 무엇인가?  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스(Index)&lt;/b&gt;는 DB에서 특정 컬럼에 대한 빠른 검색을 위해 사용되는 데이터 구조입니다. 책의 목차처럼 테이블에서 특정 데이터를 빠르게 찾아주는 역할을 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 종류&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Single Column Index&lt;/b&gt;: 단일 컬럼 기준의 인덱스.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Composite Index&lt;/b&gt;: 여러 컬럼 조합으로 생성된 인덱스.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Unique Index&lt;/b&gt;: 유일성을 보장하는 인덱스.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스 생성 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 예제&lt;/p&gt;
&lt;pre id=&quot;code_1732666395591&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE INDEX idx_column_name ON table_name(column_name);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 예제(Entity 에 설정)&lt;/p&gt;
&lt;pre id=&quot;code_1732666415989&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;table_name&quot;, indexes = @Index(name = &quot;idx_column_name&quot;, columnList = &quot;column_name&quot;))
public class ExampleEntity {
    @Id
    private Long id;
    private String columnName;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 성능 분석 및 최적화 확인 방법 &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 실행 시간 측정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 쿼리 실행 시간 측정을 위해 데이터베이스 프로파일러나 로깅 툴 사용&lt;/li&gt;
&lt;li&gt;Spring Boot 에서 Hibernate 쿼리 로깅(application.properties)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732666527674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.generate_statistics=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. `EXPLAIN` 키워드 사용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;EXPLAIN&amp;nbsp;&lt;/b&gt;을 쿼리문 앞에 붙이면 쿼리 실행 계획을 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;실행 계획은 쿼리가 테이블에서 데이터를 검색하는 방식(스캔 방식, 인덱스 사용여부 등)을 보여줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732666619924&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EXPLAIN SELECT * FROM table_name WHERE column_name = 'value';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주요 필드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: 쿼리 수행 방식 (e.g., ALL, index, ref, const 등).&lt;/li&gt;
&lt;li&gt;key: 사용된 인덱스 이름.&lt;/li&gt;
&lt;li&gt;rows: 예상 처리 행 수.&lt;/li&gt;
&lt;li&gt;extra: 추가 정보 (e.g., Using index, Using temporary 등).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 쿼리 성능 비교&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 추가 전/후로 EXPLAIN 결과에서 &lt;b&gt;rows 값 감소&lt;/b&gt; 및 &lt;b&gt;type 개선&lt;/b&gt; 확인.&lt;/li&gt;
&lt;li&gt;실행 시간을 직접 측정하여 최적화된 쿼리 실행 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. `EXPLAIN` 결과 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스가 없는 경우&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732666703180&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;id: 1
select_type: SIMPLE
table: table_name
type: ALL
possible_keys: NULL
key: NULL
rows: 10000
extra: Using where&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: ALL -&amp;gt; 풀 테이블 스캔 발생&lt;/li&gt;
&lt;li&gt;rows: 10,000 -&amp;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;pre id=&quot;code_1732666753776&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;id: 1
select_type: SIMPLE
table: table_name
type: ref
possible_keys: idx_column_name
key: idx_column_name
rows: 100
extra: Using index&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;type: ref -&amp;gt; 인덱스를 사용하여 효율적 조회&lt;/li&gt;
&lt;li&gt;rows: 100 -&amp;gt; 인덱스를 통해 대상 데이터만 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가적인 팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>db</category>
      <category>Spring</category>
      <category>개발자</category>
      <category>백엔드</category>
      <category>성능개선</category>
      <category>오블완</category>
      <category>인덱싱</category>
      <category>최적화</category>
      <category>캐싱</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/103</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-DB-%EC%A1%B0%ED%9A%8C%EC%9D%98-%EC%B5%9C%EC%A0%81%ED%99%94#entry103comment</comments>
      <pubDate>Wed, 27 Nov 2024 09:21:12 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Redis 적용 실습</title>
      <link>https://wanglan.tistory.com/entry/Spring-Redis-%EC%A0%81%EC%9A%A9-%EC%8B%A4%EC%8A%B5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난번 포스팅에서 `Redis` 에 대해서 간략하게 알아보았습니다. 이번 포스팅에서는 실제로 `Redis` 를 구현하여 `Lock`을 생성하고 동시성 제어하는 코드의 예제를 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Redis 의존성 추가&lt;/h2&gt;
&lt;pre id=&quot;code_1732667089880&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle 에 위의 의존성을 추가해줍니다. Spring boot 에서는 이렇게 redis 의존성이 추가되면 기본적으로 Lettuce 클라이언트를 사용하여 Redis를 구현할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Redis 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;appilcation.properties 에 다음과 같은 설정을 추가합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732667213794&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Redis Setting
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password=12345678
spring.data.redis.database=0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6379 라는 포트번호는 기본적으로 Redis 서버에서 사용하는 포트번호이고 비밀번호 등은 본인의 상황에 맞게 설정하셔도, 설정하지 않으셔도 괜찮습니다. database=0 부분은 레디스에서 기본 제공하는 데이터베이스중 0번 DB를 사용한다는 설정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RedisConfig&lt;/h2&gt;
&lt;div style=&quot;background-color: #263238; color: #c3cee3;&quot;&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
       return new LettuceConnectionFactory(&quot;localhost&quot;, 6379);
    }

    @Bean
    public RedisTemplate&amp;lt;String, Long&amp;gt; redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
       RedisTemplate&amp;lt;String, Long&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
       redisTemplate.setConnectionFactory(lettuceConnectionFactory);
       redisTemplate.setKeySerializer(new StringRedisSerializer());
       redisTemplate.setHashKeySerializer(new StringRedisSerializer());
       return redisTemplate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Redis 를 사용하여 Lock 을 구현할 때, Lock 은 기본적으로 Key-Value 형태의 구조로 Lock 을 설정합니다. 위의 코드에서도 RedisTemplate 에 있는 다양한 메서드들을 사용할 예정인데, 템플릿을 명시할 때는 RedisTemplate&amp;lt;K, V&amp;gt; 형식으로 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘서트의 티켓 예매하는 API 를 구현한다고 생각했을 때, 콘서트 예매에 접속하는 사람들의 트래픽이 몰리게 되고, 하나의 API 에 동시에 많은 요청이 쏟아질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, Lock 을 생성하고 Lock 을 얻지 못하면 API 의 접근을 제어 하여 하나의 API 에 쏠린 요청들을 순차적으로 처리하게 하고, 동시성 제어를 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 `concerId = 1` 인 콘서트와 `seat=A10` 인 좌석에 `memberId = 3` 인 유저가 예약을 하려는 요청을 한다고 가정하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 Lock 을 생성하는 구조는 다음과 같아집니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Lock
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;key: CONCERT-1-SEAT-A10&lt;/li&gt;
&lt;li&gt;value: 3&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Key-Value 의 의미는, &lt;b&gt;1번 콘서트의 A10번 좌석을 예약하는 사람은 현재 멤버 3 으로 Lock 을 걸어두겠다&amp;nbsp;&lt;/b&gt;라는 뜻입니다. 위의 경우에는 `key` 에 String 타입의 문자열이 들어가게 되고, `value` 에는 memberId 인 Long 타입 정수가 들어가기 때문에 위의 예제코드에서 `RedisTemplate&amp;lt;String, Long&amp;gt;` 이라고 설정해둔 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Service 에서의 적용&lt;/h2&gt;
&lt;pre id=&quot;code_1732667989216&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BookingService {
    private fianl RedisTemplate&amp;lt;String, Long&amp;gt; redisTemplate;
    private final BookingRepository bookingRepository;
    //기타 필요한 Repository ...
    
    //좌석 선택시 Lock 을 생성
    @Transactional
    public void selectSeatWithLock(Long concertId, String seatNumber, Long memberId) {
   	//CONCERT-123-SEAT-A10 / memberId
        String lockKey = RedisKeyUtil.generateSeatLockKey(concertId, seatNumber);
        try {
            //setIfAbsent 는 성공적으로 Lock 이 설정되었는지 True or False 반환
            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, memberId, 10, TimeUnit.MINUTES);
            if (lockAcquired == null || !lockAcquired) {
                throw new IllegalArgumentException(&quot;다른 유저가 해당 좌석을 예약중입니다.&quot;);
            }
            Seat seat = seatRepository.findByConcertIdAndSeatNumber(concertId, seatNumber)
                .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;공연 또는 좌석이 유효햐지 않습니다.&quot;));
            if (seat.isBooked()) {
                throw new IllegalArgumentException(&quot;좌석이 이미 예약되었습니다.&quot;);
            }

            seat.book(memberId);
            seatRepository.save(seat);
        } catch (IllegalArgumentException e) {
            //lock 을 얻지 못했으면
            throw new IllegalStateException(&quot;좌석을 예약할 수 없습니다. 다른 유저가 이미 예약중입니다.&quot;);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서는 제가 설정한 조건에 맞춰 Redis Key 를 만들기 위해 아래와 같은 RedisKeyUtil 을 따로 생성하여 Key 값을 설정하는 메서드를 사용했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732668095259&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RedisKeyUtil {
	public static String generateSeatLockKey(Long concertId, String seatNumber) {
		return String.format(&quot;CONCERT-%d-SEAT-%s&quot;, concertId, seatNumber);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 로직에서는 RedisTemplate 에 `SetIfAbsent` 메서드를 통해 키를 얻은 것에 대한 True, False Boolean 값을 반환 받습니다. Lock을 얻지못하면 예외가 던져지고, Lock을 얻으면 정상적으로 코드가 진행되어 좌석의 isBooked 필드의 값을 True 로 변경합니다. 10분의 Lock TTL 을 걸어두었고, Finally 를 통해서 Lock 을 Release 하여 해제하는 방법이 있지만, 저는 코드가 정상적으로 모두 수행되었을 때는 예매를 하려는 의지가 있다고 판단하여 Release 하는 로직은 제외시켰습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 오늘은 Redis 를 실제로 활용하여 Lock 을 설정하는 방법에 대해서 알아보았습니다. RedisConfig 에서 좀더 자세한 Redis 설정이 가능하고 Template 을 사용하는 것이 아니더라도 직접 Lock 생성하고 얻는 메서드를 조건에 맞게 제작하여 사용할 수도 있습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>it</category>
      <category>lock</category>
      <category>redis</category>
      <category>Spring</category>
      <category>백엔드</category>
      <category>분산락</category>
      <category>오블완</category>
      <category>주니어개발자</category>
      <category>트래픽</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/102</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Redis-%EC%A0%81%EC%9A%A9-%EC%8B%A4%EC%8A%B5#entry102comment</comments>
      <pubDate>Tue, 26 Nov 2024 21:06:08 +0900</pubDate>
    </item>
    <item>
      <title>Spring - 낙관적 락 &amp;amp; 비관적 락</title>
      <link>https://wanglan.tistory.com/entry/Spring-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9E%91%EC%84%B1-%EA%B3%84%ED%9A%8D</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;락은 동시성 문제를 제어하기 위한 중요한 개념입니다. 오늘의 포스팅에서는 &lt;b&gt;낙관적 락(Optimistic Lock)&lt;/b&gt; 과 &lt;b&gt;비관적 락(Pessimistic Lock)&lt;/b&gt;에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGjRIe/btsKZMILkVb/SP9Cp1CoWHaj4xC6kWVuN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGjRIe/btsKZMILkVb/SP9Cp1CoWHaj4xC6kWVuN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGjRIe/btsKZMILkVb/SP9Cp1CoWHaj4xC6kWVuN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGjRIe%2FbtsKZMILkVb%2FSP9Cp1CoWHaj4xC6kWVuN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;197&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 낙관적 락(Optimistic Lock)  &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락은 데이터 충돌이 드물 것이라고 가정하고 동작합니다.&lt;/li&gt;
&lt;li&gt;데이터를 읽은 후, 업데이트할 때 변경 충돌이 발생하지 않았는지 확인하는 방식입니다.&lt;/li&gt;
&lt;li&gt;일반적으로 &lt;b&gt;버전 번호(versioning)&lt;/b&gt;를 이용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작동 방식&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&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;예제코드로 어떻게 구현하고 동작하는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA에서의 낙관적 락&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732755760378&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Version;

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int stock;

    @Version
    private int version; // 버전 정보
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서비스 로직&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732755777210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void updateStock(Long productId, int quantity) {
    Product product = productRepository.findById(productId)
        .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;상품이 존재하지 않습니다.&quot;));

    if (product.getStock() &amp;lt; quantity) {
        throw new IllegalStateException(&quot;재고가 부족합니다.&quot;);
    }

    product.setStock(product.getStock() - quantity); // JPA가 버전 확인 후 업데이트
    productRepository.save(product); // 업데이트 시 버전이 자동으로 증가
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 Entity 부분에 `@Version` 어노테이션이 붙은 필드 `version` 이 있습니다. 로직이 실행될 때 조회되는 방식은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `product` 엔티티를 조회하면 @Version 필드 값도 포함되어 조회됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2.&amp;nbsp; &lt;b&gt;product.setStock(produck.getStock() - quantity)&lt;/b&gt; 를 호출한 후, &lt;b&gt;productRepository.save(product)&lt;/b&gt; 를 실행하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. JPA 는 `UPDATE` 쿼리를 생성할 때 &lt;b&gt;조건에 버전값을 추가&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732756088989&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE product 
SET stock = ?, version = version + 1 
WHERE id = ? AND version = ?;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 만약 `WHERE` 조건에 있는 `version` 의 값이 일치하지 않으면, JPA는 데이터가 이미 다른 트랙잭션에서 수정되었다고 판단하고 예외(`OptimisticLockExceptino`)를 던집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 관리의 중요성✅&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 충돌의 방지&lt;/b&gt;: 여러 사용자가 동시에 데이터를 변경하는 경우, 낙관적 락을 통해 충돌을 가지하고 적절히 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 효율성&lt;/b&gt;: 비관적 락처럼 트랜잭션 시작부터 락을 걸어두지 않기때문에, &lt;b&gt;락 오버헤드&lt;/b&gt;가 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 사용 사례&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;주식 거래 시스템&lt;/b&gt;: 주식 매수/매도 정보가 빈번히 조회되지만, 최종적으로 매수/매도를 확정할 때만 쓰기가 일어납니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쇼핑몰 재고 관리&lt;/b&gt;: 조회가 빈번하지만, 재고를 실제로 차감하는 경우는 상대적으로 적습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 비관적 락(Pessimistic Lock)  &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락은 데이터 충돌이 자주 발생할 것이라고 가정하고 동작합니다.&lt;/li&gt;
&lt;li&gt;데이터를 읽는 시점에 다른 트랜잭션이 데이터를 수정하지 못하도록 &lt;b&gt;락을 점유&lt;/b&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작동 방식&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 읽기 시 락을 설정하여 다른 트랜잭션이 데이터에 접근하지 못하도록 합니다.&lt;/li&gt;
&lt;li&gt;락이 해제되기 전까지 다른 트랜잭션은 대기하거나 예외를 발생시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락 또한 예제 코드로 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JPA에서의 비관적 락&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732756580657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.LockModeType;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductService {

    private final EntityManager entityManager;

    public ProductService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public void updateStock(Long productId, int quantity) {
        Product product = entityManager.find(Product.class, productId, LockModeType.PESSIMISTIC_WRITE);

        if (product.getStock() &amp;lt; quantity) {
            throw new IllegalStateException(&quot;재고가 부족합니다.&quot;);
        }

        product.setStock(product.getStock() - quantity);
        entityManager.persist(product); // 변경사항 저장
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서는 entityManager가 `find()` 메서드를 통해 DB에서 `product` 를 조회할 때, LockModeType.PESSIMISTIC_WRITE 옵션을 통해 조회하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 옵션은 &lt;b&gt;쓰기 락&lt;/b&gt;을 적용하는 방식으로, 충돌 가능성을 애초에 방지하려고 데이터 조회 시점에 바로 락을 걸어 다른 트랙잭션의 접근을 제한합니다. 따라서 호출 즉시 JPA 는 DB에서 `productId`를 조회하면서 즉시 락을 설정합니다. 이 락이 걸린 동안 다른 트랜잭션은 이 데이터에 대한 &lt;b&gt;수정(UPDATE)&lt;/b&gt;뿐 아니라 &lt;b&gt;읽기(SELECT) 락&lt;/b&gt;이 필요한 작업도 대기 상태가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 트랜잭션을 충돌을 감지하지 않고 &lt;b&gt;아예 예방&lt;/b&gt;을 하는 방식입니다. 예를 들어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 트랜잭션 A 가 `productId = 1` 에 대해 `PESSIMISTIC_WRITE` 락을 설정하고 작업 중입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 트랜잭션 B 가 동일한 `productId = 1` 에 대해 접근하려고 하면, 트랜잭션 A가 완료될 때까지 대기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 `PESSIMISTIC_WRITE` 옵션을 추가하면 JPA 는 다음과 같은 쿼리를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732757263667&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * 
FROM product 
WHERE id = 1 
FOR UPDATE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 `FOR UPDATE` 는 &lt;b&gt;행 수준 락(Row-level Lock)&lt;/b&gt; 을 설정하는 SQL 구문입니다. 이 구문은 다른 트랜잭션이 해당 행을 읽거나 수정하지 못하게 차단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 사용 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;은행 시스템&lt;/b&gt;: 계좌 간 금액 이체처럼 충돌이 치명적인 경우.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좌석 예매 시스템&lt;/b&gt;: 다수의 사용자가 동시에 동일한 좌석을 예약하려고 할 때 충돌 가능성이 매우 높습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 &lt;b&gt;낙관적 락&lt;/b&gt;과 &lt;b&gt;비관적 락&lt;/b&gt;에 대해서 알아보았습니다. 하지만 이렇게 생각하실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;애초에 충돌을 예방할 수 있으면 비관적 락을 사용하는게 무조건 효과적이지 않을까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락과 비관적 락은 시스템의 특성에 따라 사용됩니다. 낙관적 락은 장점은 성능과 유연성에 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;낙관적 락의 사용 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;낙관적 락은 &lt;b&gt;데이터 충돌이 드물거나, 충돌의 비용이 낮은 환경&lt;/b&gt;에서 더 효과적입니다. 아래에서 주요 이유를 살펴보겠습니다:&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;a. 락 점유 없이 동시성 처리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락은 데이터를 읽을 때 별도의 락을 걸지 않습니다.&lt;/li&gt;
&lt;li&gt;이로 인해 비관적 락처럼 락 점유와 해제 과정에서의 &lt;b&gt;추가 오버헤드&lt;/b&gt;가 없습니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 다수의 사용자가 읽기 작업을 수행하는 환경에서 낙관적 락은 비관적 락보다 훨씬 효율적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;b. 데드락 위험이 없음&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락은 락 점유 중에 데드락(교착 상태) 위험이 있습니다.&lt;/li&gt;
&lt;li&gt;낙관적 락은 충돌이 발생하더라도 데이터베이스 수준에서 단순히 실패를 알리기 때문에 데드락 위험이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;c. 쓰기 작업이 적은 환경에서 적합&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;낙관적 락은 데이터의 &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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;d. 충돌 발생 시 유연한 처리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;충돌이 발생하면 단순히 재시도하면 됩니다. 일부 비즈니스 로직에서는 이러한 재시도가 큰 부담이 아닙니다.&lt;/li&gt;
&lt;li&gt;예를 들어, 쇼핑몰에서 재고를 업데이트할 때 낙관적 락을 사용하면 충돌 시 다시 조회-재시도만 하면 되므로 쉽게 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 내용을 참고하여 자신이 개발하는 프로젝트에서 필요에 따라 낙관적 락과 비관적 락을 효율적으로 채택하여 사용하시면 됩니다. 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>낙관적락</category>
      <category>데드락</category>
      <category>동시성</category>
      <category>동시성제어</category>
      <category>백엔드</category>
      <category>분산락</category>
      <category>비관적락</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/100</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9E%91%EC%84%B1-%EA%B3%84%ED%9A%8D#entry100comment</comments>
      <pubDate>Sun, 24 Nov 2024 15:40:34 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Redis</title>
      <link>https://wanglan.tistory.com/entry/Spring-Redis</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Logo-redis.svg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHkUPl/btsKVyRc2R4/b2zmKuvpXvS8HZL2neOX4K/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHkUPl/btsKVyRc2R4/b2zmKuvpXvS8HZL2neOX4K/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHkUPl/btsKVyRc2R4/b2zmKuvpXvS8HZL2neOX4K/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHkUPl%2FbtsKVyRc2R4%2Fb2zmKuvpXvS8HZL2neOX4K%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;102&quot; data-filename=&quot;Logo-redis.svg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Redis에 대해 간략하게 알아보겠습니다. 자세한 내용은 추후에 분산락에 대한 포스팅을 하면서 Kafka 와 RabitMQ와 함께 깊게 다뤄볼 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 와 Lock 을 이용한 동시성 제어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis는 &lt;b&gt;Key-Value 기반의 In-Memory 데이터 저장소&lt;/b&gt;로, 빠른 읽기/쓰기 성능과 다양한 자료구조를 지원합니다.&lt;/li&gt;
&lt;li&gt;데이터를 메모리에 저장해 &lt;b&gt;속도가 매우 빠르며&lt;/b&gt;, 간단한 캐싱, 세션 관리, 실시간 데이터 처리 등에 사용됩니다.&lt;/li&gt;
&lt;li&gt;주요 특징:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NoSQL 데이터베이스로 스키마 없이 사용 가능&lt;/li&gt;
&lt;li&gt;다양한 자료구조 (String, Hash, List, Set, Sorted Set 등) 지원&lt;/li&gt;
&lt;li&gt;Pub/Sub, 트랜잭션, Lua 스크립트 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lock 이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Lock&lt;/b&gt;은 다수의 프로세스 또는 스레드가 공유 자원을 동시에 접근할 때 &lt;b&gt;경쟁 상태(Race Condition)&lt;/b&gt;를 방지하기 위해 사용됩니다.&lt;/li&gt;
&lt;li&gt;Lock을 사용하면 한 스레드가 자원을 사용하는 동안 다른 스레드가 접근하지 못하게 막을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Redis 로 Lock 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 사용한 Lock을 &lt;b&gt;분산 락&lt;/b&gt;이라고도 합니다. 이를 통해 여러 서버나 프로세스 간에 동시성 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;구현 원리:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 SET 명령어를 사용해 Lock을 설정합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1732493030065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SET lock_key &quot;value&quot; NX EX 5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;NX&lt;/b&gt;: Key가 존재하지 않을 때만 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;EX 5&lt;/b&gt;: TTL(Time-To-Live)을 5초로 설정 (Lock이 일정 시간이 지나면 자동 해제)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;Lock 획득 실패 시 처리:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재시도 로직을 구현하거나 실패 로그를 기록합니다.&lt;/li&gt;
&lt;li&gt;예:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1732493074446&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (lockAcquired) {
    // 처리 로직
} else {
    // 재시도 또는 실패 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;Redis Lock 의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 환경에서 Lock을 적용 가능&lt;/li&gt;
&lt;li&gt;높은 성능과 낮은 대기 시간&lt;/li&gt;
&lt;li&gt;TTL 설정을 통해 데드락 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lettuce 와 Redisson&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;Lettuce&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 클라이언트 라이브러리로, &lt;b&gt;비동기 및 반응형 프로그래밍&lt;/b&gt;을 지원합니다.&lt;/li&gt;
&lt;li&gt;Lightweight하며, 주로 Spring Data Redis에서 기본으로 사용됩니다.&lt;/li&gt;
&lt;li&gt;락 구현 시 직접 SET과 DEL 명령을 사용해 로직을 작성해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Redisson&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 클라이언트 라이브러리로, &lt;b&gt;분산 객체와 분산 서비스&lt;/b&gt;를 쉽게 구현할 수 있는 기능을 제공합니다.&lt;/li&gt;
&lt;li&gt;분산 락, 세마포어, 큐 등 고급 기능 내장&lt;/li&gt;
&lt;li&gt;설정이 Lettuce보다 간단&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Key 란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Lock의 Key는 &lt;b&gt;서버와 작업 간의 고유값&lt;/b&gt;이어야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예: order:123:lock&lt;/li&gt;
&lt;li&gt;Key를 특정 작업에 대한 고유값으로 설정해, 다른 Lock과 충돌하지 않도록 만듭니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/98</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Redis#entry98comment</comments>
      <pubDate>Fri, 22 Nov 2024 21:01:30 +0900</pubDate>
    </item>
    <item>
      <title>[개인 프로젝트] - 레거시 코드 리팩토링</title>
      <link>https://wanglan.tistory.com/entry/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 포스팅할 내용은 개인 프로젝트로 진행한 `레거시 코드 리팩토링` 이었습니다. 실무에서는 수년동안 많은 개발자들을 거쳐 작성된 코드들이 있고, 잘 작동되는 코드여도 오래된 기술을 사용하거나 현재는 사용되지 않는 코드도 많습니다. 이런 코드들을 `레거시 코드`라 하고 이 부분을 찾아 리팩토링 하는 과정을 통해 코드 가독성이나 유지보수성을 높이고, 최신 기술을 도입해서 향후 확장성을 높이거나 성능개선 부분에서도 이점을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트를 진행하면서 개선된 부분은 총 14가지 부분이고, 그 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. `@Transactional` 이 readOnly 옵션으로 지정되어 있어 쓰기 작업이 제대로 구동안된 점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 사용자가 회원가입 시, 닉네임을 지정할 수 있도록 `nickname` 옵션 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. `AOP` 가 제대로된 메서드에 동작하지 않고, 정상적인 시점에 동작하지 않는 부분 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 테스트코드가 제대로 동작하지 않던 부분을 목적에 맞게 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. JPA 를 활용하여 `Todo` 검색 시, 다양한 조건을 통해 검색할 수 있도록 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. `Cascade` 옵션을 지정해 `Todo` 등록 시, 작성자는 자동으로 담당자로 등록될 수 있도록 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7. `N+1` 문제 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8. `JPQL` 로 작성되어 있던 조회 메서드를 `QueryDSL` 을 사용해서 조회하는 방식으로 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9. 프로젝트에 `Spring Security` 도입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10. `QueryDSL` 을 사용하여 `5번` 항목의 내용과 비슷하게 다양한 조건으로 조회하는 기능 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;11. 특정 API 를 호출 시, 관련된 로그를 기록하는 `로그 테이블` 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;12. `AWS` 를 활용하여 `EC2`, `RDS`, `S3` 를 활용하여 프로젝트를 외부에서 접속할 수 있게 배포&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;13. 테스트코드를 사용하여 100만건의 데이터를 생성하고 그 데이터를 조회할 때 성능개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;14. `Kotlin` 문법을 적용하여 `Entity` 및 `Service` 로직 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 추가된 부분들, 수정된 부분들, 리팩토링 과 버그픽스의 개념이 같이 있는 프로젝트였다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 약간씩 수정하거나 추가하여 간단하게 해결할 수 있었지만, 문제가 발생했던 9번 `Spring Security` 에 대한 부분과 13번 `대량의 데이터 처리` 부분에 대해 해결 과정을 정리하고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring Security&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Spring-Security&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Security 란?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732161382683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring - Spring Security&quot; data-og-description=&quot;프로젝트를 구현하다 보면, 개별적으로 만든 &amp;#96;Filter&amp;#96; 에 &amp;#96;JWT&amp;#96; 토큰을 활용하여 인증과 인가를 진행하는 경우가 많습니다. 하지만 Spring Framework 에서는 강력한 보안 모듈로 &amp;#96;Spring Security&amp;#96; 를 제공하&quot; data-og-host=&quot;wanglan.tistory.com&quot; data-og-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Spring-Security&quot; data-og-url=&quot;https://wanglan.tistory.com/entry/Spring-Spring-Security&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bo24oZ/hyXDavYwI9/WwhWoKkOKAj2VMUF1Tt8K1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/hXJg0/hyXzSX3GWV/YHFgcW3YYiG8NIltc2fcAK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/nuYkW/hyXDkSTQNO/D1uFoAto9Y34zbCyQembd0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/Spring-Spring-Security&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wanglan.tistory.com/entry/Spring-Spring-Security&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bo24oZ/hyXDavYwI9/WwhWoKkOKAj2VMUF1Tt8K1/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/hXJg0/hyXzSX3GWV/YHFgcW3YYiG8NIltc2fcAK/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500,https://scrap.kakaocdn.net/dn/nuYkW/hyXDkSTQNO/D1uFoAto9Y34zbCyQembd0/img.png?width=500&amp;amp;height=500&amp;amp;face=0_0_500_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring - Spring Security&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 구현하다 보면, 개별적으로 만든 `Filter` 에 `JWT` 토큰을 활용하여 인증과 인가를 진행하는 경우가 많습니다. 하지만 Spring Framework 에서는 강력한 보안 모듈로 `Spring Security` 를 제공하&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wanglan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 에 대해선 위의 링크에 잘 정리되어 있습니다. 이 부분에서 처음 문제를 느꼈던 것은 기존의 HttpServletRequest 에서 Http에 담겨있던 토큰에서 JWT 유틸리티를 사용해 유저 정보를 추출하여 request 로 받아오던 방식에 갖혀 &quot;이 방식으로 Spring Security 도 동작한다&quot; 라고 생각해서 발생했던 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 를 도입하면 `AuthenticationManager` 가 SecurityContextHolder 에 있는 `SecurityContext` 에 자동으로 저장을 해고, 이것을 UserDetails(Impl) 로 받아 와서 사용하면 된다고 생각했었습니다. 하지만 authenticate 를 하면서 유저 정보를 Manager 가 저장은 하지만 실제로 저장되는 객체는 따로 만들어 주어야 한다는 사실을 간과하고 `UserDetails` 에 `SecurityContext` 에 저장되어 있는 정보를 저장해주지 않은 상태에서 UserDetailsImpl 를 호출하여 사용했었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 Spring Security 의 개념과 동작 방식에 대한 이해를 통해 쉽게 해결할 수 있었지만, 직접 Spring Security 를 정식으로 도입한 경우는 이번이 처음이라 적용하는데 미숙함이 있던 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 대량의 데이터 처리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 작성하던 CRUD 는 데이터를 하나씩 처리하는 방식이였고, 이렇게 대량의 데이터를 다루는 일은 거의 없었습니다. 이번에 주어진 업무는 100만개의 데이터를 생성하는 로직과 그렇게 생성된 데이터를 조회할 때 성능을 개선하여 조회하는 방법을 연구하는 것이였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 직면했던 문제는 말 그대로 100만개를 만드는 것 자체였습니다. 현재 사정상 개발을 6년된 노트북으로 진행하고 있고, 성능이 그렇게 나쁘지도 뛰어나지도 않은 컴퓨터였는데 처리가 가능할까 하는 걱정을 두고 코드를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 처음 작성했던 코드에 대해 설명하도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 첫번째 시도&lt;/h4&gt;
&lt;pre id=&quot;code_1732162360512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
class AuthControllerTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    @Transactional
    void signupManyPeople() { 
        int totalUsers = 1_000_000;
        int batchSize = 10_000; // 1만개씩 저장하고 List를 비울 예정
        List&amp;lt;User&amp;gt; userBatch = new ArrayList&amp;lt;&amp;gt;(batchSize);

        Set&amp;lt;String&amp;gt; emailSet = new HashSet&amp;lt;&amp;gt;();
        Set&amp;lt;String&amp;gt; nicknameSet = new HashSet&amp;lt;&amp;gt;();

        for (int i = 0; i &amp;lt; totalUsers; i++) {
            String email;
            do {
                email = generateRandomEmail();
            } while (!emailSet.add(email)); // 이메일 중복 확인하고 추가
            String nickname;
            do {
                nickname = generateRandomNickname();
            } while (!nicknameSet.add(nickname)); // 닉네임 중복 확인 후 추가

            String role = ThreadLocalRandom.current().nextBoolean() ? &quot;ADMIN&quot; : &quot;USER&quot;;
            UserRole userRole = UserRole.of(role); // role 랜덤 배정 후 변환

            User user = new User(email, &quot;password123&quot;, userRole, nickname);
            userBatch.add(user);

            if (userBatch.size() == batchSize) {
                userRepository.saveAll(userBatch);
                userBatch.clear(); // 배치 저장 후 초기화
            }
        }
        if (!userBatch.isEmpty()) {
            userRepository.saveAll(userBatch);
        }
    }

    private String generateRandomEmail() {
        return UUID.randomUUID().toString().substring(0, 8) + &quot;@example.com&quot;;
    }

    private String generateRandomNickname() {
        return &quot;User_&quot; + UUID.randomUUID().toString().substring(0, 8);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치사이즈를 설정하여 배치 구간별로 Insert 를 진행하여 데이터를 생성합니다. 이메일과 닉네임은 `UUID` 랜덤 메서드를 사용하여 생성했고, Set 을 사용하여 중복을 방지하는 방식으로 생성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방식대로 작성하고 프로그램을 가동해보니 인텔리제이에서 열심히 쿼리를 보내고 있는데, 5분, 10분이 지나도 끝날기미가 보이지 않았습니다. 시간이 지나고 나니 인텔리제이 내부의 힙메모리가 초과되어 프로그램이 중단되었고 첫번째 방식은 실패하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 두번째 시도&lt;/h4&gt;
&lt;pre id=&quot;code_1732162545805&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
public class UserPerformanceTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    @Transactional
    void createBatchUsers() {
        int totalUsers = 1_000_000;  // 생성할 총 유저 수
        int batchSize = 500;  // 한 번에 처리할 배치 크기
        List&amp;lt;User&amp;gt; userBatch = new ArrayList&amp;lt;&amp;gt;(batchSize);  // 배치 저장 리스트
        Set&amp;lt;String&amp;gt; emailSet = new HashSet&amp;lt;&amp;gt;();  // 이메일 중복 체크용
        Set&amp;lt;String&amp;gt; nicknameSet = new HashSet&amp;lt;&amp;gt;();  // 닉네임 중복 체크용

        for (int i = 0; i &amp;lt; totalUsers; i++) {
            String email;
            do {
                email = generateRandomEmail();
            } while (!emailSet.add(email));  // 이메일 중복 확인

            String nickname;
            do {
                nickname = generateRandomNickname();
            } while (!nicknameSet.add(nickname));  // 닉네임 중복 확인

            String role = ThreadLocalRandom.current().nextBoolean() ? &quot;ADMIN&quot; : &quot;USER&quot;;
            UserRole userRole = UserRole.of(role);  // 랜덤 역할 할당

            User user = new User(email, &quot;password123&quot;, userRole, nickname);
            userBatch.add(user);

            // 배치 크기만큼 쌓이면 저장하고 초기화
            if (userBatch.size() == batchSize) {
                saveBatch(userBatch);
                userBatch.clear();  // 리스트 초기화
            }
        }

        // 마지막에 남은 배치 저장
        if (!userBatch.isEmpty()) {
            saveBatch(userBatch);
        }
    }

    // 배치 저장을 위한 메서드
    private void saveBatch(List&amp;lt;User&amp;gt; userBatch) {
        for (int i = 0; i &amp;lt; userBatch.size(); i++) {
            entityManager.persist(userBatch.get(i));  // 유저 객체 저장
            if (i &amp;gt; 0 &amp;amp;&amp;amp; i % 50 == 0) {  // 일정 간격마다 flush
                entityManager.flush();
                entityManager.clear();  // 메모리 최적화를 위해 엔티티 매니저 클리어
            }
        }
        entityManager.flush();  // 남은 객체들을 한번에 flush
        entityManager.clear();  // 객체 매니저를 초기화
    }

    // 랜덤 이메일 생성
    private String generateRandomEmail() {
        return UUID.randomUUID().toString().substring(0, 8) + &quot;@example.com&quot;;
    }

    // 랜덤 닉네임 생성
    private String generateRandomNickname() {
        return &quot;User_&quot; + UUID.randomUUID().toString().substring(0, 8);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째는 배치사이즈가 너무 컸다고 생각이 들어 500정도로 수정하고, EntityManager 를 통해서 유저들을 Insert 하는 방식을 사용했고 EntityManager 도 특정한 구간마다 clear() 메서드로 초기화 해주는 과정으로 메모리를 절약해보자고 생각했습니다. 또한 Gradle 로 빌드를 진행하는 것 보다 인텔리제이 IDE 를 사용하는 것이 중간 과정이 간단하여 빠르고 효율적이다고 하여 설정 변경을 해주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kEjVO/btsKRlxKo55/UbnMS6Jl7NY1wNN7Xwmwbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kEjVO/btsKRlxKo55/UbnMS6Jl7NY1wNN7Xwmwbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kEjVO/btsKRlxKo55/UbnMS6Jl7NY1wNN7Xwmwbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkEjVO%2FbtsKRlxKo55%2FUbnMS6Jl7NY1wNN7Xwmwbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;284&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 해도 메모리가 올라가는 속도가 늦어질 뿐, 결국 힙메모리 초과에 도달하여 100만개 생성에는 실패했습니다. 그래서 하이버네이트에서 보내고있는 쿼리를 보니 끊어서 보내더라도 Insert 문이 결국 100만번 전송되어야 하는 구조라는것을 깨닫고 아예 한번의 Insert 에서 여러명의 유저정보를 넣는 방식으로 하자 라는 생각에 도달했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 세번째 시도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Native Query를 사용해서 String 타입으로 쿼리를 작성하여 하나의 Insert 문에서 여러명의 유저를 넣는 Bulk Insert 방식으로 변경하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732162839105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
public class UserPerformanceTest {
	@Autowired
	private EntityManager entityManager;

	@Test
	@Transactional
	void createBatchUsers() {
		int totalUsers = 1_000_000;  // 생성할 총 유저 수
		int batchSize = 500;  // 한 번에 처리할 배치 크기

		StringBuilder sql = new StringBuilder(&quot;INSERT INTO users (email, password, user_role, nickname) VALUES &quot;);
		Set&amp;lt;String&amp;gt; emailSet = new HashSet&amp;lt;&amp;gt;();  // 이메일 중복 체크용
		Set&amp;lt;String&amp;gt; nicknameSet = new HashSet&amp;lt;&amp;gt;();  // 닉네임 중복 체크용

		for (int i = 0; i &amp;lt; totalUsers; i++) {
			String email;
			do {
				email = generateRandomEmail();
			} while (!emailSet.add(email));  // 이메일 중복 확인

			String nickname;
			do {
				nickname = generateRandomNickname();
			} while (!nicknameSet.add(nickname));  // 닉네임 중복 확인

			String role = ThreadLocalRandom.current().nextBoolean() ? &quot;ADMIN&quot; : &quot;USER&quot;;

			// VALUES 부분을 생성
			sql.append(&quot;(&quot;)
				.append(&quot;'&quot;).append(email).append(&quot;', &quot;)
				.append(&quot;'password123', &quot;)
				.append(&quot;'&quot;).append(role).append(&quot;', &quot;)
				.append(&quot;'&quot;).append(nickname).append(&quot;'), &quot;);

			// 배치 크기마다 한 번에 INSERT 문을 실행
			if ((i + 1) % batchSize == 0 || i == totalUsers - 1) {
				// 마지막 값 뒤에 쉼표가 있으면 제거
				if (sql.charAt(sql.length() - 2) == ',') {
					sql.setLength(sql.length() - 2);  // 마지막 쉼표 제거
				}
				entityManager.createNativeQuery(sql.toString()).executeUpdate();
				sql.setLength(0);  // SQL 초기화
				sql.append(&quot;INSERT INTO users (email, password, user_role, nickname) VALUES &quot;);
			}
		}
	}

	// 랜덤 이메일 생성
	private String generateRandomEmail() {
		return UUID.randomUUID().toString().substring(0, 8) + &quot;@example.com&quot;;
	}

	// 랜덤 닉네임 생성
	private String generateRandomNickname() {
		return &quot;User_&quot; + UUID.randomUUID().toString().substring(0, 8);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같은 방식을 사용하여 하나의 Insert 쿼리문 안에 배치사이즈 500으로 한번에 500명의 데이터를 나눠서 100만명 까지 넣는 쿼리로 수정하였습니다. 결과는&amp;nbsp;전체 데이터를 생성하는데 40~50초의 시간이 걸렸고, 배치사이즈를 1000으로 늘려서 한번 더 테스트를 진행하니 37초까지 줄어드는 것을 확인할 수 있었습니다. &lt;br /&gt;이&amp;nbsp;방식으로&amp;nbsp;진행했을&amp;nbsp;때&amp;nbsp;4096mb&amp;nbsp;의&amp;nbsp;메모리를&amp;nbsp;초과하여&amp;nbsp;프로그램이&amp;nbsp;중단되던&amp;nbsp;문제가&amp;nbsp;500mb&amp;nbsp;의&amp;nbsp;힙메모리를&amp;nbsp;사용하면서&amp;nbsp;안정적으로&amp;nbsp;생성하는&amp;nbsp;것을&amp;nbsp;확인했습니다. &lt;br /&gt;배치사이즈를 조금씩 조절 해가면서 힙메모리 사용에 부담이 되지않으면서 쿼리를 보내는 주기를 수정해 나가면 시간을 더 단축할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 생성을 하는것은 성공적으로 끝냈고, 조회하는 로직을 작성했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732162963602&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void searchUserByNickname_in_million_data() throws Exception {
	if(savedNickname == null) {
		throw new Exception(&quot;닉네임이 생성되지 않았습니다.&quot;);
	}

	entityManager.clear();

	mvc.perform(get(&quot;/users/search-by-nickname&quot;)
		.param(&quot;nickname&quot;, savedNickname)
		.contentType(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andExpect(jsonPath(&quot;$.nickname&quot;).value(savedNickname));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회하는 로직과 생성 로직을 따로 작성한 뒤 통합 테스트로 진행하려고 했습니다. 조회하는 API 의 서비스 로직은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732163061705&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	public UserResponse findByNickname(String nickname) {
		User user = userRepository.findByNicknameUsingIndex(nickname)
			.orElseThrow(() -&amp;gt; new InvalidRequestException(&quot;User not found&quot;));
		return new UserResponse(user.getId(), user.getEmail(), user.getNickname());
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Repository&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732163096350&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
	// 기본 닉네임으로 조회하는 JPA 쿼리
	Optional&amp;lt;User&amp;gt; findByNickname(String nickname);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와같은 방식으로 조회하니 1차 캐시를 지우고 조회 했을때 최저 3.5초 까지 조회하는 시간이 걸렸습니다. 100만건 중에서 3.5초면 빠르지 않을까 생각할 수 있지만, 저희가 일반적으로 사용하는 조회하는 API 에서 1초 이상 걸리면 매우 느리다고 느끼는 것 처럼 3.5초면 상당한 시간이 걸리는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유는 기본적인 JPA&amp;nbsp; 쿼리를 통해 조회를 하면 전체 테이블 조회를 진행하여 원하는 값을 일찍 찾아도 테이블 크기만큼 전부 스캔을 진행하고 중복된 결과 등을 찾아서 반환하기 때문에 오래걸리게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 방식을 이용하여 개선할 수 있을까 하고 찾던 중 인덱스 방식이 있다는 것을 알게되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 방식을 활용하면 데이터를 조회하는 시간을 `log n` 만큼 줄이게 되어 100만건의 데이터를 조회하는데도 실제로 20번의 처리로 모두 조회를 완료하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 적용하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Entity 에 index 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1732163440132&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@NoArgsConstructor
@Table(name = &quot;users&quot;, indexes ={
    @Index(name = &quot;idx_nickname&quot;, columnList = &quot;nickname&quot;)
})
public class User extends Timestamped {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String email;
    private String password;
    //...
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 조회를 원하는 컬럼에 인덱스를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. JPQL 을 통해 인덱스를 명시적으로 사용&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티에 인덱스를 적용하고, 인덱스가 적용된 컬럼을 통해 조회를 하면 자동으로 Spring boot 에서 인덱스를 적용하여 조회를 하지만 더 확실히 인덱스를 사용하고 싶은 경우에는 JPA Repository 에서 사용해줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1732163546694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {

	// 기본 닉네임으로 조회하는 JPA 쿼리
	Optional&amp;lt;User&amp;gt; findByNickname(String nickname);

	// 인덱스를 사용해서 닉네임을 조회
	@Query(value = &quot;SELECT * FROM users USE INDEX (idx_nickname) WHERE nickname = :nickname&quot;, nativeQuery = true)
	Optional&amp;lt;User&amp;gt; findByNicknameUsingIndex(@Param(&quot;nickname&quot;) String nickname);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 추가된 `findByNicknameUsingIndex` 메서드를 사용하면 전체 데이터를 조회하는 시간이 매우 효율적으로 줄어듭니다. 저의 경우에선 100만건을 생성할떄 조회할 때 1.1 초가 걸리던 환경에서 0.3초까지 줄어든것을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;인덱스 적용전 데이터 생산 및 조회 시간.JPG&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SQ0Kg/btsKQtpO4ZL/Q7ZcRSUWnzNsSSk8OTgwG0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SQ0Kg/btsKQtpO4ZL/Q7ZcRSUWnzNsSSk8OTgwG0/img.jpg&quot; data-alt=&quot;인덱스 적용전 검색시간 1.12초&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SQ0Kg/btsKQtpO4ZL/Q7ZcRSUWnzNsSSk8OTgwG0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSQ0Kg%2FbtsKQtpO4ZL%2FQ7ZcRSUWnzNsSSk8OTgwG0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;394&quot; height=&quot;419&quot; data-filename=&quot;인덱스 적용전 데이터 생산 및 조회 시간.JPG&quot; data-origin-width=&quot;394&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인덱스 적용전 검색시간 1.12초&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;인덱스 적용후 생성 및 조회시간2.JPG&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCrOaL/btsKRkFK7H7/DlJhdrR0SVTYtrDydFVbG0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCrOaL/btsKRkFK7H7/DlJhdrR0SVTYtrDydFVbG0/img.jpg&quot; data-alt=&quot;인덱스 적용후 0.3초&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCrOaL/btsKRkFK7H7/DlJhdrR0SVTYtrDydFVbG0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCrOaL%2FbtsKRkFK7H7%2FDlJhdrR0SVTYtrDydFVbG0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;371&quot; height=&quot;297&quot; data-filename=&quot;인덱스 적용후 생성 및 조회시간2.JPG&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인덱스 적용후 0.3초&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 차이가 아니라고 생각이 들 수 있지만, 데이터 테이블의 전체 크기가 커지면 커질 수록 이 효과는 드라마틱하게 증가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇다고 해서 인덱스를 사용하는 방식을 반드시 사용해야 하는 것은 아닙니다. 지금과 같이 데이터를 조회하는 환경에서는 매우 효율적이지만, 쓰기 작업이 많은 서비스라면 인덱스부분이 추가적으로 들어가게 되는 것이기 때문에 DB에 부담이 될 수 있습니다. 따라서 주어진 조건에 맞게 골라서 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하는 방식 말고도 외부 라이브러리를 추가하여 전문검색 엔진을 사용하는 방법이나, 캐시에 자주 사용되는 닉네임등을 저장하여 사용하는 방식들도 있지만 이번 프로젝트에서는 인덱스를 다뤄보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 저의 개인 프로젝트는 마무리 하도록 하겠습니다. 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Project</category>
      <category>it</category>
      <category>개발자</category>
      <category>레거시코드</category>
      <category>리팩토링</category>
      <category>백엔드</category>
      <category>성능개선</category>
      <category>오블완</category>
      <category>트러블슈팅</category>
      <category>티스토리챌린지</category>
      <category>회고</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/97</guid>
      <comments>https://wanglan.tistory.com/entry/%EA%B0%9C%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%A0%88%EA%B1%B0%EC%8B%9C-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81#entry97comment</comments>
      <pubDate>Thu, 21 Nov 2024 13:38:27 +0900</pubDate>
    </item>
    <item>
      <title>동적 프로그래밍(Dynamic Programming)</title>
      <link>https://wanglan.tistory.com/entry/%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8DDynamic-Programming</link>
      <description>&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동적&amp;nbsp;프로그래밍(Dynamic&amp;nbsp;Programming)이란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동적 프로그래밍(DP)은 &lt;b&gt;복잡한 문제를&lt;/b&gt; &lt;b&gt;작은 부분 문제로 나누어 해결&lt;/b&gt;하는 &lt;b&gt;문제 해결 전략&lt;/b&gt;입니다. &lt;b&gt;부분 문제의 중복&lt;/b&gt;과 &lt;b&gt;최적 부분 구조&lt;/b&gt;라는 두 가지 속성을 이용하여 효율적으로 계산을 수행합니다. 이를 통해, 동일한 부분 문제를 반복적으로 계산하는 대신 &lt;b&gt;저장된 결과(메모이제이션)&lt;/b&gt;를 활용하여 실행 시간을 줄이는 방법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;napkin-selection.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r89Sd/btsKMLSa4rr/G2uY5AvXO1V63reBivu36k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r89Sd/btsKMLSa4rr/G2uY5AvXO1V63reBivu36k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r89Sd/btsKMLSa4rr/G2uY5AvXO1V63reBivu36k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr89Sd%2FbtsKMLSa4rr%2FG2uY5AvXO1V63reBivu36k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;720&quot; height=&quot;636&quot; data-filename=&quot;napkin-selection.png&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 동적 프로그래밍의 두 가지 핵심 속성&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최적 부분 구조 (Optimal Substructure) &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제를 여러 작은 문제로 나눌 수 있고, 작은 문제의 최적 결과를 조합하여 전체 문제의 최적 결과를 구할 수 있는 속성입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예: 피보나치 수열의&lt;i&gt; F(n) = F(n - 1) + F(n - 2)&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;부분 문제 중복 (Overlapping Subproblem)&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 동일한 부분 문제가 여러 번 반복해서 등장합니다. 이를 효율적으로 계산하기 위해 이전 계산 결과를 저장(메모이제이션)하거나, 점진적으로 값을 채워나가는 방법을 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예: 피보나치 수열에서 &lt;i&gt;F(3)&lt;/i&gt;&amp;nbsp;는 &lt;i&gt;F(4) &lt;/i&gt;와 &lt;i&gt;F(5) &lt;/i&gt;를 계산하는 과정에서 중복됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 동적 프로그래밍의 문제 풀이 전략&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1. 문제를 작게 나누기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주어진 문제를 작은 단위의 문제로 나눕니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예: 멀리뛰기 문제에서 nn&lt;span aria-hidden=&quot;true&quot;&gt;n&lt;/span&gt;개의 칸을 뛰는 경우, n&amp;minus;1 칸 또는 n&amp;minus;2 칸에 도달했을 때의 결과를 이용.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 점화식 세우기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;작은 문제 간의 관계를 정의하는 &lt;b&gt;수학적 공식&lt;/b&gt;(점화식)을 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예: 멀리뛰기 문제의 점화식은 &lt;i&gt;dp[n] = dp[n &amp;minus; 1] + dp[n &amp;minus; 2] &lt;/i&gt;입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 기저 조건 정의하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 작은 문제를 해결하기 위한 기본 조건을 정의합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예: 멀리뛰기 문제에서 &lt;i&gt;dp[1] = 1, dp[2] = 2&lt;/i&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 배열/테이블로 문제 해결하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;탑다운(재귀+메모이제이션)&lt;/b&gt; 방식: 재귀적으로 계산하면서 이전 계산 결과를 저장.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;바텀업(반복문)&lt;/b&gt; 방식: 작은 문제부터 시작해 차례로 값을 채워나감.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5. 결과 도출&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막으로 구한 dp[n] 이 주어진 문제의 답이 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 점화식이란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;점화식(Recurrence Relation)&lt;/b&gt;은 &lt;b&gt;현재 단계의 문제를 이전 단계의 문제와의 관계로 정의한 식&lt;/b&gt;입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동적 프로그래밍에서는 &lt;b&gt;문제를 풀기 위한 규칙&lt;/b&gt;을 수학적으로 표현한 것으로, 프로그래밍 구현의 기반이 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;점화식 예제: 피보나치 수열&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정의: F(n) = F(n&amp;minus;1) + F(n&amp;minus;2)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기저 조건: F(1) = 1, F(2) = 1&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;점화식 예제: 멀리뛰기 문제&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정의: dp[n] = dp[n&amp;minus;1] + dp[n&amp;minus;2]&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기저 조건: dp[1] = 1, dp[2] = 2&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4.&amp;nbsp; DP 문제 풀이 예시&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 피보나치 수열 계산&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731979110911&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Fibonacci {
    public int fibonacci(int n) {
        if (n &amp;lt;= 2) return 1; // 기저 조건
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 1;
        for (int i = 3; i &amp;lt;= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2]; // 점화식
        }
        return dp[n];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 멀리뛰기 문제&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731979122760&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JumpingSteps {
    public int solution(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1; // 기저 조건
        dp[2] = 2; // 기저 조건
        for (int i = 3; i &amp;lt;= n; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % 1234567; // 점화식과 모듈로 연산
        }
        return dp[n];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. DP 의 장단점과 문제 유형&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;장점&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;효율성&lt;/b&gt;: 중복 계산을 제거하여 실행 시간을 크게 줄일 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;직관적 구현&lt;/b&gt;: 명확한 문제 구조와 점화식이 있다면 구현이 비교적 간단합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단점&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;: 배열이나 테이블에 모든 부분 문제를 저장하므로 메모리 사용량이 높아질 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;구현 난이도&lt;/b&gt;: 문제를 잘게 나누고 점화식을 세우는 과정이 까다로울 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제 유형&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;최적 경로 탐색&lt;/b&gt;: 다익스트라, 플로이드-워셜 알고리즘.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;조합 및 부분 집합 문제&lt;/b&gt;: 배낭 문제, 동전 교환 문제.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;문자열 문제&lt;/b&gt;: 최장 공통 부분 문자열, 편집 거리 계산.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;게임 이론&lt;/b&gt;: 타일 채우기 문제, 계단 오르기 문제.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 실전 예제를 보도록 하겠습니다. 문제는 프로그래머스의 &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12914&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;멀리 뛰기&lt;/a&gt; 문제를 참고하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731979454195&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12914&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/KoUCk/hyXzRK7zTX/DE4wKHL7XAjkkdKcAw1TX1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cXF7Lr/hyXzPmd4nN/MukbIvSiC5Ayx8ozJD4p41/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12914&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12914&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/KoUCk/hyXzRK7zTX/DE4wKHL7XAjkkdKcAw1TX1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cXF7Lr/hyXzPmd4nN/MukbIvSiC5Ayx8ozJD4p41/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;멀리 뛰기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;문제&amp;nbsp;설명 &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;효진이는&amp;nbsp;멀리&amp;nbsp;뛰기를&amp;nbsp;연습하고&amp;nbsp;있습니다.&amp;nbsp;효진이는&amp;nbsp;한번에&amp;nbsp;1칸,&amp;nbsp;또는&amp;nbsp;2칸을&amp;nbsp;뛸&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;칸이&amp;nbsp;총&amp;nbsp;4개&amp;nbsp;있을&amp;nbsp;때,&amp;nbsp;효진이는 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(1칸,&amp;nbsp;1칸,&amp;nbsp;1칸,&amp;nbsp;1칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(1칸,&amp;nbsp;2칸,&amp;nbsp;1칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(1칸,&amp;nbsp;1칸,&amp;nbsp;2칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(2칸,&amp;nbsp;1칸,&amp;nbsp;1칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(2칸,&amp;nbsp;2칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;의&amp;nbsp;5가지&amp;nbsp;방법으로&amp;nbsp;맨&amp;nbsp;끝&amp;nbsp;칸에&amp;nbsp;도달할&amp;nbsp;수&amp;nbsp;있습니다.&amp;nbsp;멀리뛰기에&amp;nbsp;사용될&amp;nbsp;칸의&amp;nbsp;수&amp;nbsp;n이&amp;nbsp;주어질&amp;nbsp;때,&amp;nbsp;효진이가&amp;nbsp;끝에&amp;nbsp;도달하는&amp;nbsp;방법이&amp;nbsp;몇&amp;nbsp;가지인지&amp;nbsp;알아내,&amp;nbsp;여기에&amp;nbsp;1234567를&amp;nbsp;나눈&amp;nbsp;나머지를&amp;nbsp;리턴하는&amp;nbsp;함수,&amp;nbsp;solution을&amp;nbsp;완성하세요.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;4가&amp;nbsp;입력된다면,&amp;nbsp;5를&amp;nbsp;return하면&amp;nbsp;됩니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;제한&amp;nbsp;사항&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;n은 1 이상, 2000 이하인 정수입니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 31.9767%; height: 132px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;n&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;result&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;입출력&amp;nbsp;예&amp;nbsp;설명&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;입출력&amp;nbsp;예&amp;nbsp;#1 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위에서&amp;nbsp;설명한&amp;nbsp;내용과&amp;nbsp;같습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;입출력&amp;nbsp;예&amp;nbsp;#2 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(2칸,&amp;nbsp;1칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(1칸,&amp;nbsp;2칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(1칸,&amp;nbsp;1칸,&amp;nbsp;1칸) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;총&amp;nbsp;3가지&amp;nbsp;방법으로&amp;nbsp;멀리&amp;nbsp;뛸&amp;nbsp;수&amp;nbsp;있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 문제는 DP 문제이므로 문제 성질을 분석한 뒤 점화식을 도출하여 해결하면 됩니다. 점화식을 유도하는 과정을 보도록 하겠습니다.&lt;/span&gt;&lt;span&gt;1. 문제 성질 분석&lt;/span&gt;&lt;span&gt;효진이는 한 번에 1칸 또는 2칸을 뛸 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;n 번째 칸에 도달하려면:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;마지막에 1칸&lt;/b&gt;을 뛰는 경우: n-1 번째 칸에서 도달해야합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;마지막에 2칸&lt;/b&gt;을 뛰는 경우: n-2 번째 칸에서 도달해야합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 부분 문제로 나누기&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;dp&lt;/i&gt;[n] : n 칸에 도달하는 방법의 총 경우의 수 이므로&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;dp&lt;/i&gt;[n] 은&lt;i&gt; dp&lt;/i&gt;[n - 1] (마지막에 1칸 뛴 경우) 과 &lt;i&gt;dp&lt;/i&gt;[n - 2] (마지막에 2칸 뛴 경우) 의 합으로 표현됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 점화식 도출&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 내용을 수학적으로 표현하면:&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;dp&lt;/i&gt;[n] = &lt;i&gt;dp&lt;/i&gt;[n - 1] + &lt;i&gt;dp&lt;/i&gt;[n - 2]&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기저 조건: &lt;i&gt;dp&lt;/i&gt;[1] = 1, &lt;i&gt;dp&lt;/i&gt;[2] = 2.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 점화식은 피보나치 수열과 동일합니다. 이 점화식을 문장으로 풀어 설명하자면,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;총 4칸이 있는 경우:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막에 1칸 뛰는 경우 = 총 3칸이 있는 경우 3칸인 마지막에 도달 후 한번 더 뛰어서 4번째 칸에 오게됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막에 2칸 뛰는 경우 = 총 2칸이 있는 경우 2칸인 마지막에 도달 후 한번 더 뛰어서 4번째 칸에 오게됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;따라서 총 2칸이 있을 때의 경우의 수와, 총 3칸이 있을 때의 경우의 수의 &lt;b&gt;합&lt;/b&gt;이 총 4칸이 있을때의 경우의 수가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 내용대로 Java 로 작성해보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731980370934&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Solution {
    public int solution(int n) {
        if (n == 1) return 1; // n = 1일 때
        if (n == 2) return 2; // n = 2일 때

        int[] dp = new int[n + 1];
        dp[1] = 1; // 기저 조건
        dp[2] = 2; // 기저 조건

        for (int i = 3; i &amp;lt;= n; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % 1234567; // 점화식과 모듈러 연산
        }

        return dp[n];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;계산중 값이 커질 수 있으므로 1234567 로 나눈 나머지를 저장합니다.(모듈로 연산)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최종적으로 마지막 항인 n일 때의 dp[n] 을 반환하면 정답을 얻을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 방식의 코드를 배열을 사용하지 않고 공간 복잡도를 O(1) 로 줄이는 방법도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731980492923&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;

        int mod = 1234567;
        int a = 1, b = 2;

        for (int i = 3; i &amp;lt;= n; i++) {
            int temp = (a + b) % mod;
            a = b;
            b = temp;
        }

        return b;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DP 문제는 점화식을 도출하는 과정이 까다롭지만, 점화식을 도출 하기만 한다면 코드 작성에서의 논리가 매우 간단하고 효율적이게 됩니다. 따라서 다양한 문제를 많이 풀어보고 점화식을 도출하는 실력을 기르는 것이 좋다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;읽어주셔서 감사합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>DP</category>
      <category>it</category>
      <category>동적프로그래밍</category>
      <category>멀리뛰기</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>주니어개발자</category>
      <category>코딩테스트</category>
      <category>티스토리챌린지</category>
      <category>프로그래머스</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/95</guid>
      <comments>https://wanglan.tistory.com/entry/%EB%8F%99%EC%A0%81-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8DDynamic-Programming#entry95comment</comments>
      <pubDate>Tue, 19 Nov 2024 10:44:08 +0900</pubDate>
    </item>
    <item>
      <title>Spring - AWS 로 애플리케이션 배포하기</title>
      <link>https://wanglan.tistory.com/entry/Spring-AWS-%EB%A1%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;애플리케이션 배포.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oYyCv/btsKOxqXSQs/5GUPVRl6t42u9mkI7TDRU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oYyCv/btsKOxqXSQs/5GUPVRl6t42u9mkI7TDRU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oYyCv/btsKOxqXSQs/5GUPVRl6t42u9mkI7TDRU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoYyCv%2FbtsKOxqXSQs%2F5GUPVRl6t42u9mkI7TDRU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2021&quot; height=&quot;1161&quot; data-filename=&quot;애플리케이션 배포.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 통해 개발 툴을 통해 애플리케이션을 개발을 진행하고 완료하면 본인의 로컬 컴퓨터에서 `Postman` 등의 API 플랫폼을 이용하여 테스트가 가능합니다. 내부적인 테스트가 끝나고 나서 완성된 애플리케이션을 배포하려면 어떤 방식으로 배포를 진행하여야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷플릭스 등의 회사가 가상 서버를 이용하기 전까지만 해도, 많은 기업에서 프로그램을 배포하기 위해, 그리고 서버를 가동하기 위해 데이터센터와 같은 서버 컴퓨터가 구비되어 있는 시설을 설치해야 했습니다. 하지만 스타트업 기업이나, 소규모 개발팀이 이런것들을 이루기 위해 서버전용 시설을 구축하는 것은 비용적인 측면에서&lt;/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;</description>
      <category>Spring</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/94</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-AWS-%EB%A1%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#entry94comment</comments>
      <pubDate>Mon, 18 Nov 2024 20:43:44 +0900</pubDate>
    </item>
    <item>
      <title>Spring - AWS</title>
      <link>https://wanglan.tistory.com/entry/Spring-AWS</link>
      <description>&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;AWS.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ztYBB/btsKL0Vzm7r/RUAsmpcMDqiIPeyaaj01E1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ztYBB/btsKL0Vzm7r/RUAsmpcMDqiIPeyaaj01E1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ztYBB/btsKL0Vzm7r/RUAsmpcMDqiIPeyaaj01E1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FztYBB%2FbtsKL0Vzm7r%2FRUAsmpcMDqiIPeyaaj01E1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2021&quot; height=&quot;1161&quot; data-filename=&quot;AWS.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AWS 란?&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;9a6d1134-ac5d-42cb-88d7-bd3d3aaa2252&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AWS (Amazon Web Services)&lt;/b&gt;는 Amazon에서 제공하는 &lt;b&gt;클라우드 컴퓨팅 플랫폼&lt;/b&gt;으로, 기업과 개인에게 다양한 IT 리소스를 인터넷을 통해 제공하는 서비스입니다.&lt;br /&gt;이를 통해 사용자는 물리적 서버를 구매하거나 직접 데이터센터를 운영할 필요 없이, 필요할 때만 컴퓨팅 파워, 스토리지, 데이터베이스, 네트워크 등 다양한 리소스를 빌려서 사용할 수 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 클라우드 컴퓨팅이란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;클라우드 컴퓨팅&lt;/b&gt;은 인터넷(클라우드)을 통해 &lt;b&gt;서버, 스토리지, 데이터베이스, 네트워크, 소프트웨어 등&lt;/b&gt; IT 리소스를 제공하는 기술입니다.&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;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;On-Demand:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;필요할 때만 리소스를 사용.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;유연성(Flexibility):&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;리소스 사용량을 늘리거나 줄일 수 있음.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;비용 절감:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하드웨어 구매와 유지보수 비용이 없음.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;글로벌 가용성:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전 세계에서 동일한 품질로 서비스 제공 가능.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 클라우드 서비스 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드는 제공 방식에 따라 &lt;b&gt;IaaS&lt;/b&gt;, &lt;b&gt;PaaS&lt;/b&gt;, &lt;b&gt;SaaS&lt;/b&gt;로 나뉩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;IaaS (Infrastructure as a Service)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&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;AWS 서비스 예:&lt;/b&gt; &lt;b&gt;EC2&lt;/b&gt;, &lt;b&gt;EBS&lt;/b&gt;, &lt;b&gt;VPC&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시:&lt;/b&gt; AWS EC2에서 가상 서버를 빌려 운영체제와 필요한 소프트웨어를 직접 설치.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;PaaS (Platform as a Service)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&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;AWS 서비스 예:&lt;/b&gt; &lt;b&gt;Elastic Beanstalk&lt;/b&gt;, &lt;b&gt;RDS&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시:&lt;/b&gt; 웹 애플리케이션을 Elastic Beanstalk에 배포하여 실행.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;SaaS (Software as a Service)&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&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;AWS 서비스 예:&lt;/b&gt; &lt;b&gt;WorkSpaces&lt;/b&gt;, &lt;b&gt;Amazon Chime&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예시:&lt;/b&gt; AWS WorkSpaces를 통해 원격으로 데스크톱 환경 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AWS&amp;nbsp; 주요 개념&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;루트 계정  &amp;zwj; &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS 계정을 처음 생성할 때 사용하는 &lt;b&gt;최고 관리자 계정&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;모든 리소스에 대한 완전한 액세스 권한이 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주의사항:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일상 작업에는 사용하지 않음.&lt;/li&gt;
&lt;li&gt;대신 &lt;b&gt;IAM 계정&lt;/b&gt;을 생성하고 권한을 위임.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;IAM (Identity and Access Management)  &amp;zwj; &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS 리소스의 &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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자에게 EC2 인스턴스 접근 권한만 부여.&lt;/li&gt;
&lt;li&gt;S3 버킷에 대한 읽기/쓰기 권한 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;리전 (Region)  &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS가 전 세계에 배포한 &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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;us-east-1 (미국 동부), ap-northeast-2 (한국).&lt;/li&gt;
&lt;li&gt;서비스 사용 시 가까운 리전을 선택.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;가용 영역 (Availability Zone, AZ)  &lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 리전에 있는 &lt;b&gt;독립적인 데이터센터 그룹&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;한 리전에 여러 AZ가 있으며, &lt;b&gt;고가용성과 장애 복구&lt;/b&gt;를 지원.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예:&lt;/b&gt; 서울 리전 (ap-northeast-2)에는 4개의 가용 영역이 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. AWS EC2 (Elastic Compute Cloud)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EC2&lt;/b&gt;는 AWS의 가상 서비스로, 컴퓨팅 파워를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;확장성:&lt;/b&gt; 인스턴스를 필요에 따라 늘리거나 줄일 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;유연성:&lt;/b&gt; 다양한 운영체제(Windows, Linux)와 사양 선택 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ &lt;b&gt;사용 예:&lt;/b&gt; 웹 서버, 데이터베이스 서버, 머신러닝 모델 학습 등.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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. AMI 선택: &lt;/b&gt;Amazon Machine Image(운영체제 및 소프트웨어 이미지).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 인스턴스 유형:&lt;/b&gt; CPU, 메모리, 스토리지 등 사양 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 보안 그룹:&lt;/b&gt; 네트워크 접근 권한 설정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 키 페어:&lt;/b&gt; SSH를 통한 원격 접속 설정.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. AWS 의 장점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유연성:&lt;/b&gt; 다양한 서비스를 필요에 맞게 조합 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비용 절감:&lt;/b&gt; 사용한 만큼만 비용 지불 (Pay-as-You-Go).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성:&lt;/b&gt; 필요에 따라 리소스 추가/삭제 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안:&lt;/b&gt; 데이터 암호화, IAM, DDoS 방어 등 강력한 보안 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS는 클라우드 컴퓨팅의 선두주자로, 다양한 서비스와 기능을 통해 현대 IT 인프라의 핵심 역할을 하고 있습니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>AWS</category>
      <category>db</category>
      <category>it</category>
      <category>개발자</category>
      <category>무료db</category>
      <category>백엔드</category>
      <category>서버구축</category>
      <category>아마존</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/93</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-AWS#entry93comment</comments>
      <pubDate>Sun, 17 Nov 2024 18:13:20 +0900</pubDate>
    </item>
    <item>
      <title>Spring - DB 와의 상호작용</title>
      <link>https://wanglan.tistory.com/entry/Spring-DB-%EC%99%80%EC%9D%98-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;DB 정리.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAmqBa/btsKNdMWapS/NU0oTnRnjerIKWSuazhdoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAmqBa/btsKNdMWapS/NU0oTnRnjerIKWSuazhdoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAmqBa/btsKNdMWapS/NU0oTnRnjerIKWSuazhdoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAmqBa%2FbtsKNdMWapS%2FNU0oTnRnjerIKWSuazhdoK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2021&quot; height=&quot;1161&quot; data-filename=&quot;DB 정리.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 여러 기업들에서 백엔드 부분에서 데이터베이스를 조회할 때, 다양한 방식들을 사용하고 있습니다. JPA 또는 MyBatis 등을 기업의 조건에 맞게 골라서 사용하고 있는데, 본 포스팅에서는 각 라이브러리 또는 프레임워크가 개발된 배경과 장단점, 특징 등에 대한 내용을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-author-role=&quot;assistant&quot; data-message-id=&quot;18e97207-b3c5-4cc2-a172-621b6178716e&quot; data-message-model-slug=&quot;gpt-4o&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 데이터베이스와의 상호작용을 위한 다양한 라이브러리와 프레임워크가 등장한 배경은 개발 환경의 변화와 복잡한 요구사항에 의해 발전해왔습니다. 각 라이브러리는 이전 기술의 단점을 보완하거나 새로운 요구를 충족시키기 위해 개발되었습니다. 아래는 주요 라이브러리들의 발전 순서와 특징에 대한 설명입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. JDBC (Java Database Connectivity)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;   개발 시기:&lt;/b&gt; 1997년&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 개념:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDBC는 Java가 데이터베이스와 상호작용할 수 있는 최초의 API로, SQL 쿼리를 직접 작성하고 실행하여 데이터를 처리합니다.&lt;/li&gt;
&lt;li&gt;Java 애플리케이션과 관계형 데이터베이스 사이를 연결하는 표준 인터페이스입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;b&gt;✅ &lt;/b&gt;DBMS에 독립적인 API로 다양한 데이터베이스에 연결 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✅ &lt;/b&gt;저수준에서 데이터베이스와 직접 상호작용하여 높은 유연성과 성능 제공.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;❗ SQL 쿼리를 코드에서 직접 작성하므로 가독성이 떨어지고 유지보수성이 낮음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗&lt;span&gt; 반복 코드가 많고 트랜잭션 관리 및 연결 해제 같은 부수적인 작업이 필요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗&lt;span&gt; 객체 지향적인 Java 코드와 관계형 데이터베이스 간의 패러다임 불일치(Impersistence Impedance Mismatch) 문제.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/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;JDBC는 기본적이지만 개발자가 수동으로 많은 작업을 처리해야 했음. 이를 개선하기 위해 프레임워크가 개발되기 시작.&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;pre id=&quot;code_1731887827857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class JdbcExample {
    public static void main(String[] args) throws Exception {
        String url = &quot;jdbc:mysql://localhost:3306/testdb&quot;;
        String user = &quot;root&quot;;
        String password = &quot;password&quot;;

        Connection connection = DriverManager.getConnection(url, user, password);
        String query = &quot;SELECT id, name FROM users WHERE id = ?&quot;;
        
        PreparedStatement statement = connection.prepareStatement(query);
        statement.setInt(1, 1); // Set the parameter for id
        
        ResultSet resultSet = statement.executeQuery();
        while (resultSet.next()) {
            System.out.println(&quot;ID: &quot; + resultSet.getInt(&quot;id&quot;) + &quot;, Name: &quot; + resultSet.getString(&quot;name&quot;));
        }

        resultSet.close();
        statement.close();
        connection.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MyBatis (이전 이름: iBatis)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;개발 시기:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 2001&lt;/span&gt;년 (iBatis로 시작, 2010년에 MyBatis로 재탄생)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 개념:&lt;/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;SQL을 XML 또는 주석(annotation)으로 관리하고 Java 객체와 매핑(mapping)하여 JDBC의 반복 작업을 줄이고 SQL 관리를 체계화.&lt;/li&gt;
&lt;li&gt;SQL 중심 접근 방식으로, 개발자가 직접 쿼리를 제어하면서도 매핑 작업은 자동화.&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;p data-ke-size=&quot;size16&quot;&gt;✅ SQL을 명시적으로 작성하므로 복잡한 쿼리에 강점.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ XML 파일을 사용하여 SQL과 Java 코드를 분리, 유지보수성을 개선.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ JDBC의 반복 작업(PreparedStatement, ResultSet 처리 등)을 자동화.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 여전히 SQL 작성에 의존적이므로 대규모 프로젝트에서는 SQL 코드가 비대해질 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 객체와 테이블 간의 매핑을 수동으로 정의해야 하므로 개발자의 작업 부담이 남아있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/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;MyBatis는 SQL 중심적인 애플리케이션에 적합하지만, 대규모 애플리케이션에서 객체 지향 설계를 효율적으로 지원하기엔 부족.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML Mapper(`UserMapper.xml`)&lt;/p&gt;
&lt;pre id=&quot;code_1731887885932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;mapper namespace=&quot;UserMapper&quot;&amp;gt;
    &amp;lt;select id=&quot;findUserById&quot; parameterType=&quot;int&quot; resultType=&quot;User&quot;&amp;gt;
        SELECT id, name FROM users WHERE id = #{id}
    &amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 코드&lt;/p&gt;
&lt;pre id=&quot;code_1731887916669&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

public class MyBatisExample {
    public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory = MyBatisUtils.getSqlSessionFactory(); // Utility method to get factory
        try (SqlSession session = sqlSessionFactory.openSession()) {
            User user = session.selectOne(&quot;UserMapper.findUserById&quot;, 1);
            System.out.println(&quot;ID: &quot; + user.getId() + &quot;, Name: &quot; + user.getName());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. JPA (Java Persistence API)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;개발 시기:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; 2006년 (Java EE 5에서 표준으로 소개)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 개념:&lt;/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;JPA는 Java 객체와 데이터베이스 테이블 간의 매핑을 자동화하여 객체 지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치를 해결.&lt;/li&gt;
&lt;li&gt;Hibernate, EclipseLink, OpenJPA 등 다양한 구현체가 존재하며, Hibernate가 가장 널리 사용됨.&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;p data-ke-size=&quot;size16&quot;&gt;✅ 객체 중심으로 데이터를 관리하며, SQL 대신 메서드 호출로 데이터를 처리할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 엔티티(Entity)를 통해 객체와 데이터베이스 간 매핑을 정의하여 코드 가독성과 생산성을 높임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 트랜잭션 관리, 캐싱, 지연 로딩(Lazy Loading) 등 고급 기능 지원.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ JPQL(Java Persistence Query Language)을 통해 객체 중심의 쿼리 작성 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 학습 곡선이 높고 설정이 복잡할 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 복잡한 쿼리나 SQL 튜닝이 필요한 상황에서는 JPQL의 표현력이 제한될 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JPA는 객체 지향 설계를 기반으로 한 애플리케이션에서 개발 생산성과 유지보수성을 크게 향상시켰지만, SQL을 더 정교하게 제어해야 하는 상황에서는 여전히 한계가 있음.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티 클래스 (`User.java`)&lt;/p&gt;
&lt;pre id=&quot;code_1731887994376&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String name;

    // Getters and setters omitted for brevity
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 코드&lt;/p&gt;
&lt;pre id=&quot;code_1731888012932&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

public class JpaExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;testPU&quot;);
        EntityManager em = emf.createEntityManager();
        
        em.getTransaction().begin();
        User user = em.find(User.class, 1L);
        System.out.println(&quot;ID: &quot; + user.getId() + &quot;, Name: &quot; + user.getName());
        em.getTransaction().commit();
        
        em.close();
        emf.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. JPQL (Java Persistence Query Language)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;개발 시기:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; JPA 와 함께 등장(2006년)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 개념:&lt;/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;JPQL은 객체 지향적인 방식으로 데이터를 조회할 수 있도록 설계된 쿼리 언어.&lt;/li&gt;
&lt;li&gt;SQL과 문법적으로 유사하지만, 데이터베이스 테이블 대신 JPA 엔티티와 그 속성을 기반으로 작동.&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;p data-ke-size=&quot;size16&quot;&gt;✅ 객체 모델과 밀접하게 연결되어 있으므로 SQL보다 직관적.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 데이터베이스 테이블 변경 시에도 JPQL은 엔티티를 기준으로 작성되므로 코드 변경이 최소화됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 복잡한 SQL(특히 특정 DBMS의 고유 기능)이 필요한 경우 사용이 어려움.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ SQL보다 실행 효율이 떨어질 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731888042401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.util.List;

public class JpqlExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;testPU&quot;);
        EntityManager em = emf.createEntityManager();
        
        em.getTransaction().begin();
        List&amp;lt;User&amp;gt; users = em.createQuery(&quot;SELECT u FROM User u WHERE u.name LIKE :name&quot;, User.class)
                             .setParameter(&quot;name&quot;, &quot;%John%&quot;)
                             .getResultList();
        users.forEach(user -&amp;gt; System.out.println(&quot;ID: &quot; + user.getId() + &quot;, Name: &quot; + user.getName()));
        em.getTransaction().commit();
        
        em.close();
        emf.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. QueryDSL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;개발 시기:&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;2009년&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 개념:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QueryDSL은 JPA와 함께 사용되며, 메서드 체인 방식으로 타입 안전(type-safe)하고 동적 쿼리를 작성 가능.&lt;/li&gt;
&lt;li&gt;JPQL의 복잡한 표현을 Java 코드로 간결하고 안전하게 표현.&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;p data-ke-size=&quot;size16&quot;&gt;✅ 컴파일 시점에 쿼리 검증이 가능하므로 실행 시점의 오류를 줄임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 메서드 체인 방식으로 가독성과 유지보수성이 뛰어남.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 동적 쿼리를 생성하기에 적합하며, SQL, MongoDB, Lucene 등 다양한 데이터 소스를 지원.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 설정과 학습이 필요하며, Q타입 클래스 생성이 추가 작업으로 요구됨 (컴파일시 generated 패키지에 자동생성 되지만, 별도의 선언이 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;QueryDSL은 JPQL의 단점을 보완하고 동적 쿼리 및 복잡한 쿼리를 안전하고 간결하게 작성할 수 있게 해줌. JPA를 사용하는 프로젝트에서 특히 유용.&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q 클래스 생성: &lt;/b&gt;QueryDSL 은 `QUser` 라는 클래스(컴파일 시 자동 생성) 를 사용하여 쿼리를 작성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`build.gradle`에 추가&lt;/p&gt;
&lt;pre id=&quot;code_1731888114752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;QueryDSL 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731888142142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.querydsl.jpa.impl.JPAQueryFactory;

public class QueryDslExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory(&quot;testPU&quot;);
        EntityManager em = emf.createEntityManager();
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QUser qUser = QUser.user;

        List&amp;lt;User&amp;gt; users = queryFactory.selectFrom(qUser)
                                       .where(qUser.name.contains(&quot;John&quot;))
                                       .fetch();

        users.forEach(user -&amp;gt; System.out.println(&quot;ID: &quot; + user.getId() + &quot;, Name: &quot; + user.getName()));
        em.close();
        emf.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기업마다 개발환경이 다르고, 요구조건에 따라 `MyBatis` 를 쓰는경우도, `JPA`를 쓰는경우도, `QueryDSL` 을 사용하는 경우도 있습니다. 개발자로서 DB를 다루는 학습을 할 때, 자신이 필요한 기술만 공부하기 보다는, `JDBC` 부터 학습을 시작하여 어떻게 DB와의 상호작용을 만들어 나가는지 파악하고 각각의 기술들을 단계별로 공부해 보며 장점과 단점을 직접 느끼고 이해하면 위의 기술들을 활용할 때 더 도움이 될 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상 DB와의 상호작용 포스팅을 마치도록 하겠습니다. 감사합니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>db</category>
      <category>jdbc</category>
      <category>JPA</category>
      <category>JPQL</category>
      <category>querydsl</category>
      <category>Spring</category>
      <category>백엔드</category>
      <category>서버</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/92</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-DB-%EC%99%80%EC%9D%98-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9#entry92comment</comments>
      <pubDate>Sat, 16 Nov 2024 13:45:23 +0900</pubDate>
    </item>
    <item>
      <title>Spring - QueryDSL</title>
      <link>https://wanglan.tistory.com/entry/Spring-QueryDSL</link>
      <description>&lt;h2 style=&quot;text-align: center;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;free-icon-sql-5486490.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/92ZNe/btsKMF3LjWE/zteKKuZZ3xVXLT9sGbwJJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/92ZNe/btsKMF3LjWE/zteKKuZZ3xVXLT9sGbwJJ1/img.png&quot; data-alt=&quot;&amp;amp;lt;a href=&amp;quot;https://www.flaticon.com/kr/free-icons/sql&amp;quot; title=&amp;quot;sql 아이콘&amp;quot;&amp;amp;gt;Sql 아이콘 제작자: Freepik - Flaticon&amp;amp;lt;/a&amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/92ZNe/btsKMF3LjWE/zteKKuZZ3xVXLT9sGbwJJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F92ZNe%2FbtsKMF3LjWE%2FzteKKuZZ3xVXLT9sGbwJJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;512&quot; height=&quot;512&quot; data-filename=&quot;free-icon-sql-5486490.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;a href=&quot;https://www.flaticon.com/kr/free-icons/sql&quot; title=&quot;sql 아이콘&quot;&amp;gt;Sql 아이콘 제작자: Freepik - Flaticon&amp;lt;/a&amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&lt;span style=&quot;letter-spacing: 0px; color: #000000;&quot;&gt;QueryDSL은 타입 안전성과 코드 가독성을 제공하여 JPA와 함께 사용할 수 있는 강력한 쿼리 빌더 라이브러리입니다. 이를 통해 SQL, JPQL, 그리고 HQL 같은 쿼리를 타입 안전하게 작성할 수 있습니다. 자세히 알아보겠습니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL 을 말하기전에 우선적으로 알아야 될 것들이 있습니다. Spring framework 에서는 DB 에 데이터를 조회하는 방식으로 JDBC 부터 시작하여, `Mybatis` 나 `JPA` 같은 ORM 등을 추가적으로 개발하였고, `JPA` 에서 좀 더 확장하여 `JPQL` 과 `QueryDSL` 이라는 라이브러리까지 개발하게 되었습니다. 사실 이렇게 개발된 순서에 대한 내용은 확실하지 않으나, 라이브러리를 사용하는 방식을 보면 어떤식으로 편의성이 개선되고 업그레이드 되어왔는지 이해할 수 있던 것 같습니다. 이번 포스팅에서는 QueryDSL 에 대해서만 다룰 예정이나, 추후에 `JDBC` 와 `JPA`, `JPQL`, `QueryDSL` 을 연관지어 전체적인 흐름에 대해서 알아보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. &lt;b&gt;QueryDSL의 장점&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;타입 안전성&lt;/b&gt;: QueryDSL은 코드 작성 시점에 타입 검사를 제공하여, 잘못된 쿼리로 인한 오류를 컴파일 시점에 잡아낼 수 있습니다. 이는 JPQL이나 SQL처럼 문자열을 사용하는 방식보다 안전하며, 런타임 오류를 줄여줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;가독성&lt;/b&gt;: SQL이나 JPQL 문자열을 사용하는 대신, 자바 메서드 체이닝 방식으로 쿼리를 작성합니다. 덕분에 코드의 가독성이 높아지고, 리팩토링이 용이합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;유연성&lt;/b&gt;: 복잡한 조건문, 동적 쿼리, 페이징 처리 등을 구현할 때 유용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;재사용 가능성&lt;/b&gt;: 쿼리를 객체화하여 쉽게 재사용할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. &lt;b&gt;QueryDSL의 구동 방식&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL은 데이터베이스 엔티티 클래스를 기반으로 &amp;ldquo;Q타입&amp;rdquo;이라는 동적 쿼리 객체를 생성합니다. 이 Q타입을 통해 엔티티의 필드에 접근하여 쿼리를 생성하게 됩니다. Spring과의 연동이 가능하며, QueryDSL은 JPAQueryFactory를 통해 쿼리를 빌드하고 실행하는 방식으로 동작합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 3. &lt;b&gt;QueryDSL의 사용법 및 구조&lt;/b&gt; &lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.1 &lt;b&gt;Q타입과 Q클래스&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL을 사용할 때, 엔티티 클래스마다 `Q타입`이라 불리는 클래스를 생성하게 됩니다. 이 Q타입 클래스는 엔티티의 각 필드에 대한 동적 쿼리를 쉽게 작성할 수 있도록 지원합니다. 예를 들어, `Todo`라는 엔티티가 있다면 `QTodo`라는 이름의 클래스가 생성됩니다. 이를 통해 `QTodo.todo.title` 처럼 엔티티의 필드를 참조하여 조건을 설정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Q클래스 예시&lt;/b&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. `QTodo` 클래스는 `title`, `user`, `createdAt`과 같은 `Todo` 엔티티의 필드를 포함하며, 쿼리 작성 시 타입 안전성을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 기본적으로 `Q타입` 클래스는 `target/generated-sources` 폴더에 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.2 &lt;b&gt;JPQL과 QueryDSL의 차이&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JPQL&lt;/b&gt;: JPA에서 제공하는 객체 지향 쿼리 언어로, 쿼리를 문자열로 작성해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;QueryDSL&lt;/b&gt;: 메서드 체인 방식으로 타입 안전한 쿼리를 작성합니다. QueryDSL은 select, from, where 등의 메서드를 제공하여 SQL 스타일로 쿼리를 작성하지만, JPQL이나 SQL과 다르게 문자열이 아닌 객체 기반으로 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.3 &lt;b&gt;QueryFactory와 쿼리 빌드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JPAQueryFactory&lt;/b&gt;: QueryDSL 쿼리를 작성하기 위한 팩토리 클래스입니다. 보통 Spring에서 Bean으로 등록하여 사용합니다. 주입받은 EntityManager를 통해 인스턴스가 생성됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿼리 작성 시 jpaQueryFactory를 통해 select, from, where 등을 호출하여 쿼리를 빌드합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. &lt;b&gt;QueryDSL Custom 클래스 구현&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;QueryDSL을 통해 커스텀 쿼리를 작성하려면 Custom Repository 인터페이스와 그 구현체가 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4.1 Custom Repository 인터페이스 생성&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 기존 리포지토리 인터페이스에 커스텀 메서드를 정의할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 예를 들어, `TodoRepositoryCustom` 인터페이스를 생성하여 메서드를 선언합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731671973030&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface TodoRepositoryCustom {
    Page&amp;lt;TodoSummaryResponse&amp;gt; findTodosByKeywords(String titleKeyword, String nicknameKeyword, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4.2 Custom Repository 구현체&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인터페이스에서 정의한 메서드를 구현하려면 `TodoRepositoryCustomImpl` 이라는 클래스를 작성하여 실제 쿼리 로직을 넣습니다. 이 클래스는 `TodoRepositoryCustom` 을 구현하고, `JPAQueryFactory`를 주입받아 QueryDSL 쿼리를 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731672010779&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class TodoRepositoryCustomImpl implements TodoRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public Page&amp;lt;TodoSummaryResponse&amp;gt; findTodosByKeywords(
            String titleKeyword, String nicknameKeyword, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {

        QTodo todo = QTodo.todo;
        QManager manager = QManager.manager;
        QUser user = QUser.user;

        // QueryDSL을 사용한 쿼리 작성
        JPQLQuery&amp;lt;TodoSummaryResponse&amp;gt; query = queryFactory
            .select(Projections.constructor(
                TodoSummaryResponse.class,
                todo.title,
                todo.managers.size().castToNum(Long.class),
                todo.comments.size().castToNum(Long.class)
            ))
            .from(todo)
            .leftJoin(todo.managers, manager)
            .leftJoin(manager.user, user)
            .where(
                titleContains(titleKeyword),
                managerNicknameContains(nicknameKeyword),
                createdDateBetween(startDate, endDate)
            )
            .groupBy(todo.id)
            .orderBy(todo.createdAt.desc());

        long total = query.fetchCount();
        List&amp;lt;TodoSummaryResponse&amp;gt; results = query
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        return new PageImpl&amp;lt;&amp;gt;(results, pageable, total);
    }

    private BooleanExpression titleContains(String titleKeyword) {
        return titleKeyword != null ? QTodo.todo.title.contains(titleKeyword) : null;
    }

    private BooleanExpression managerNicknameContains(String nicknameKeyword) {
        return nicknameKeyword != null ? QUser.user.nickname.contains(nicknameKeyword) : null;
    }

    private BooleanExpression createdDateBetween(LocalDateTime startDate, LocalDateTime endDate) {
        return QTodo.todo.createdAt.between(startDate, endDate);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. QueryDSL을 사용한 쿼리 작성 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 TodoRepositoryCustomImpl 클래스에서 다음과 같은 방식으로 QueryDSL 쿼리를 작성했습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JPAQueryFactory를 통해 쿼리를 시작합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;select 절에 반환할 필드를 정의합니다. 여기서는 Projections.constructor를 사용하여 TodoSummaryResponse 객체에 필요한 필드를 선택했습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;from 절에서 기준이 되는 엔티티(todo)를 설정하고 필요한 경우 leftJoin으로 연관된 엔티티를 조인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;조건이 필요한 경우 where 절에서 BooleanExpression으로 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;groupBy와 orderBy로 그룹화와 정렬을 설정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;offset, limit을 통해 페이지네이션을 적용하고 fetch()로 결과를 가져옵니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 방식으로 QueryDSL을 사용하면 문자열을 사용하는 JPQL보다 실수할 가능성이 줄어들고, 코드 가독성도 높아집니다. QueryDSL은 복잡한 조건을 적용하고 동적 쿼리를 사용할 때 특히 유리합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>it</category>
      <category>jdbc</category>
      <category>JPA</category>
      <category>JPQL</category>
      <category>querydsl</category>
      <category>SQL</category>
      <category>til</category>
      <category>병아리</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/91</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-QueryDSL#entry91comment</comments>
      <pubDate>Fri, 15 Nov 2024 21:02:52 +0900</pubDate>
    </item>
    <item>
      <title>[구현] - N개의 최소공배수</title>
      <link>https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-N%EA%B0%9C%EC%9D%98-%EC%B5%9C%EC%86%8C%EA%B3%B5%EB%B0%B0%EC%88%98</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zynsI/btsKJ5hfw74/vr4k8ZLipo5CO3d2EnQOK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zynsI/btsKJ5hfw74/vr4k8ZLipo5CO3d2EnQOK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zynsI/btsKJ5hfw74/vr4k8ZLipo5CO3d2EnQOK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzynsI%2FbtsKJ5hfw74%2Fvr4k8ZLipo5CO3d2EnQOK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 다뤄볼 문제는 알고리즘에 자주 나오는 `구현` 유형 문제입니다. `구현` 유형의 문제에 대한 정리는 아래 블로깅에서 잘 정리해 두었으니 참고하시길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-%EB%91%98%EB%A7%8C%EC%9D%98-%EC%95%94%ED%98%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;알고리즘의 구현 이란?&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1731572249924&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[구현] - 둘만의 암호&quot; data-og-description=&quot;구현(Implementation) 유형의 알고리즘 문제는 주어진 조건을 그대로 구현하고 해결하는 것을 주 목적으로 하는 문제 유형입니다. 구현 문제는 대체로 문제에서 요구하는 규칙이나 절차를 코드로 정&quot; data-og-host=&quot;wanglan.tistory.com&quot; data-og-source-url=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-%EB%91%98%EB%A7%8C%EC%9D%98-%EC%95%94%ED%98%B8&quot; data-og-url=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-%EB%91%98%EB%A7%8C%EC%9D%98-%EC%95%94%ED%98%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2EecP/hyXwkmN5VU/oTRu2jkaG8PzO5kUu8849k/img.png?width=284&amp;amp;height=177&amp;amp;face=0_0_284_177,https://scrap.kakaocdn.net/dn/A1igZ/hyXzT8UGmY/6ZuP36E2b3SuQbOgpTtc31/img.png?width=284&amp;amp;height=177&amp;amp;face=0_0_284_177,https://scrap.kakaocdn.net/dn/bZ6jI8/hyXzI0Dr8U/tatIsWTgFqOtR484BQj6dk/img.jpg?width=419&amp;amp;height=259&amp;amp;face=0_0_419_259&quot;&gt;&lt;a href=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-%EB%91%98%EB%A7%8C%EC%9D%98-%EC%95%94%ED%98%B8&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-%EB%91%98%EB%A7%8C%EC%9D%98-%EC%95%94%ED%98%B8&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2EecP/hyXwkmN5VU/oTRu2jkaG8PzO5kUu8849k/img.png?width=284&amp;amp;height=177&amp;amp;face=0_0_284_177,https://scrap.kakaocdn.net/dn/A1igZ/hyXzT8UGmY/6ZuP36E2b3SuQbOgpTtc31/img.png?width=284&amp;amp;height=177&amp;amp;face=0_0_284_177,https://scrap.kakaocdn.net/dn/bZ6jI8/hyXzI0Dr8U/tatIsWTgFqOtR484BQj6dk/img.jpg?width=419&amp;amp;height=259&amp;amp;face=0_0_419_259');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[구현] - 둘만의 암호&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;구현(Implementation) 유형의 알고리즘 문제는 주어진 조건을 그대로 구현하고 해결하는 것을 주 목적으로 하는 문제 유형입니다. 구현 문제는 대체로 문제에서 요구하는 규칙이나 절차를 코드로 정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wanglan.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 프로그래머스에서 참고하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12953&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] - N개의 최소공배수&lt;/a&gt;&lt;/h2&gt;
&lt;figure id=&quot;og_1731572358534&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12953&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/SIkBS/hyXzMWcb6g/0BldQwgTmhUHdLidGK4yrK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cctOkf/hyXwkNOHXr/IlyytWnkBsw5nuMK07a2nK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12953&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12953&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/SIkBS/hyXzMWcb6g/0BldQwgTmhUHdLidGK4yrK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cctOkf/hyXwkNOHXr/IlyytWnkBsw5nuMK07a2nK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&amp;nbsp;설명&lt;/b&gt; &lt;br /&gt;두&amp;nbsp;수의&amp;nbsp;최소공배수(Least&amp;nbsp;Common&amp;nbsp;Multiple)란&amp;nbsp;입력된&amp;nbsp;두&amp;nbsp;수의&amp;nbsp;배수&amp;nbsp;중&amp;nbsp;공통이&amp;nbsp;되는&amp;nbsp;가장&amp;nbsp;작은&amp;nbsp;숫자를&amp;nbsp;의미합니다.&amp;nbsp;예를&amp;nbsp;들어&amp;nbsp;2와&amp;nbsp;7의&amp;nbsp;최소공배수는&amp;nbsp;14가&amp;nbsp;됩니다.&amp;nbsp;정의를&amp;nbsp;확장해서,&amp;nbsp;n개의&amp;nbsp;수의&amp;nbsp;최소공배수는&amp;nbsp;n&amp;nbsp;개의&amp;nbsp;수들의&amp;nbsp;배수&amp;nbsp;중&amp;nbsp;공통이&amp;nbsp;되는&amp;nbsp;가장&amp;nbsp;작은&amp;nbsp;숫자가&amp;nbsp;됩니다.&amp;nbsp;n개의&amp;nbsp;숫자를&amp;nbsp;담은&amp;nbsp;배열&amp;nbsp;arr이&amp;nbsp;입력되었을&amp;nbsp;때&amp;nbsp;이&amp;nbsp;수들의&amp;nbsp;최소공배수를&amp;nbsp;반환하는&amp;nbsp;함수,&amp;nbsp;solution을&amp;nbsp;완성해&amp;nbsp;주세요. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;제한&amp;nbsp;사항&lt;/b&gt; &lt;br /&gt;arr은&amp;nbsp;길이&amp;nbsp;1이상,&amp;nbsp;15이하인&amp;nbsp;배열입니다. &lt;br /&gt;arr의&amp;nbsp;원소는&amp;nbsp;100&amp;nbsp;이하인&amp;nbsp;자연수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;179&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMFsyb/btsKJLQMJbF/jWaAD9St4wSfP9bIATABFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMFsyb/btsKJLQMJbF/jWaAD9St4wSfP9bIATABFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMFsyb/btsKJLQMJbF/jWaAD9St4wSfP9bIATABFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMFsyb%2FbtsKJLQMJbF%2FjWaAD9St4wSfP9bIATABFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;179&quot; height=&quot;154&quot; data-origin-width=&quot;179&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 &lt;b&gt;수학적 알고리즘&lt;/b&gt;을 잘 알고 있다면 간단히 풀 수 있는 문제입니다. 최대공약수(GCD)는 &lt;b&gt;유클리드 호제법&lt;/b&gt;을 통해 구하고, 이것을 통해서 최대공약수인 LCM 을 구하면 되는 문제입니다. LCM 은 `n*m/GCD(n*m)` 의 공식으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731572914554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int[] arr) {
        int lcm = arr[0];
        
        for (int i = 1; i&amp;lt;arr.length; i++) {
            lcm = calculateLCM(lcm,arr[i]);
        }
        
        return lcm;
    }
    
    
    private int calculateGCD(int a, int b) {
        if (b == 0) return a;
        return calculateGCD(b, a % b);
    }
    
    private int calculateLCM(int a, int b) {
        return a*(b/calculateGCD(a,b));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 `calculateGCD()` 메서드에서 재귀적으로 파라미터 b 가 0이 될떄 까지 반복하여 최소공배수를 구한 뒤, `calculateLCM()` 을 입력받은 `arr` 의 인자 수 만큼 반복하여 두 수씩 차례로 계산하여 해결하도록 하는 코드입니다.&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>백주</category>
      <category>병아리</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>유클리드호제법</category>
      <category>최대공약수</category>
      <category>최소공배수</category>
      <category>코딩테스트</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/90</guid>
      <comments>https://wanglan.tistory.com/entry/%EA%B5%AC%ED%98%84-N%EA%B0%9C%EC%9D%98-%EC%B5%9C%EC%86%8C%EA%B3%B5%EB%B0%B0%EC%88%98#entry90comment</comments>
      <pubDate>Thu, 14 Nov 2024 17:32:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring - Spring Security</title>
      <link>https://wanglan.tistory.com/entry/Spring-Spring-Security</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxTD1B/btsKIRRAdQI/LuBZ08dcIDHg8t2bIHNPf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxTD1B/btsKIRRAdQI/LuBZ08dcIDHg8t2bIHNPf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxTD1B/btsKIRRAdQI/LuBZ08dcIDHg8t2bIHNPf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxTD1B%2FbtsKIRRAdQI%2FLuBZ08dcIDHg8t2bIHNPf0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;500&quot; data-filename=&quot;java.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;프로젝트를 구현하다 보면, 개별적으로 만든 `Filter` 에 `JWT` 토큰을 활용하여 인증과 인가를 진행하는 경우가 많습니다. 하지만 Spring Framework 에서는 강력한 보안 모듈로 `Spring Security` 를 제공하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본적으로 Spring Security 는 애플리케이션의 &lt;b&gt;인증&lt;/b&gt;(Authentication)과 &lt;b&gt;인가&lt;/b&gt;(Authorization) 기능을 제공합니다. 사용자 인증, 권한 부여, CSRF 방지, 세션 관리 등의 보안 기능을 제공하며, Spring 애플리케이션에서 손쉽게 설정하고 사용할 수 있도록 설계되었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Spring Security란?&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인증(Authentication)&lt;/b&gt;: 사용자가 누구인지 확인하는 과정입니다. 로그인 과정에서 사용자의 자격 증명을 검증하여 신원을 확인하는 절차입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;인가(Authorization)&lt;/b&gt;: 인증된 사용자가 애플리케이션의 특정 리소스나 기능에 접근할 권한이 있는지 검증하는 과정입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security는 인증과 인가를 처리할 수 있는 다양한 메커니즘을 제공하며, 특히 REST API에서 사용하는 JWT(Json Web Token) 기반 인증과도 쉽게 연동할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Spring Security의 동작 방식&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`Spring Security`는 `필터 체인(Filter Chain)`을 통해 모든 요청을 처리합니다. 요청이 들어오면 여러 가지 보안 필터가 순차적으로 실행되며, 각 필터가 인증과 인가에 필요한 다양한 작업을 수행합니다. 주요 필터는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;UsernamePasswordAuthenticationFilter&lt;/b&gt;: 기본적인 사용자 이름과 비밀번호를 사용한 인증 필터.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;BasicAuthenticationFilter&lt;/b&gt;: 기본 인증 헤더를 이용한 인증 필터.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JwtFilter&lt;/b&gt;(커스텀 필터): JWT 토큰을 이용한 인증을 위해 추가하는 필터.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;: 인증이나 인가 과정에서 발생한 예외를 처리하는 필터.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;FilterSecurityInterceptor&lt;/b&gt;: 인가 작업을 담당하는 최종 필터로, 요청의 URI나 메서드에 대한 접근 권한을 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 필터들은 Spring Security의 필터 체인에 의해 실행되며, 각 요청이 애플리케이션에 접근하기 전에 인증과 인가 과정을 거치게 됩니다. 그리고 실제 API 에서는 `SecurityContext`라는 객체를 이용하여 &lt;b&gt;애플리케이션의 현재 인증된 사용자 정보&lt;/b&gt;를 저장하고 다룰 수 있습니다. 이 인증된 객체를 이용하여 API에서 사용하는 방식은, `@AuthenticationPrincipal` 어노테이션을 사용하여, API 의 파라미터로 전달 한 뒤 비즈니스 로직에서 사용하는 형태입니다. 이 부분에 대한 내용은 7번 항목에서 설명하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 회원 방식으로 관리되는 사이트에서는, 각 기능을 요청할 때 마다 필터와 JWT 등을 통해서 로그인되어 있는 사용자의 정보를 가져온 뒤, 사용자의 요청에 대한 권한이 있는지를 확인하는 방식을 사용하고 있습니다. 그렇다면 굳이 Spring Security 를 사용하는 이유는 무엇일까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 단순히 필터와 JWT만으로 구현할 수 있는 기본적인 인증 기능 외에도, &lt;b&gt;보안 관리를 체계적으로 하고 확장성을 높여주는 다양한 기능&lt;/b&gt;을 제공합니다. Spring Security를 사용해야 하는 주요 이유를 설명하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Spring Security 의 장점 및 특징&lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 표준화된 보안 프레임워크 제공&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 수많은 애플리케이션에서 검증된 표준 보안 프레임워크로, 여러 가지 보안 요구사항을 다룰 수 있는 다양한 기능과 설정을 갖추고 있습니다. 이를 통해 애플리케이션마다 보안 구조를 새롭게 설계하지 않고도 안정적이고 표준화된 방식으로 인증, 인가, 세션 관리 등을 구현할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;정형화된 인증 구조&lt;/b&gt;: Spring Security는 `UserDetails`, `UserDetailsService`, `AuthenticationProvider` 같은 표준 인터페이스를 제공하여 인증 구조를 일관되게 설계할 수 있게 합니다. 이러한 구조는 인증 로직을 표준화하여 코드의 재사용성을 높이고, 유지보수를 용이하게 합니다.&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;다양한 보안 기능 내장&lt;/b&gt;: Spring Security는 CSRF 방지, 세션 관리, 동시 접속 제한, 비밀번호 암호화 등 다양한 보안 기능을 내장하고 있어 설정만으로 손쉽게 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 세분화된 보안 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; JWT와 필터만으로 인증 기능을 구현할 경우, 모든 보안 규칙을 직접 처리해야 하며, 세분화된 접근 제어를 구현하기 어렵습니다. Spring Security는 URI 패턴별, 메서드별 권한 설정을 지원하고, 요청에 따른 접근 제어를 유연하게 설정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt; &lt;span style=&quot;text-align: left;&quot;&gt;URL 기반 권한 설정&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;:&lt;/b&gt; `HttpSecurity` 에서 URI별 접근 제어가 가능하며, `hasRole`, `hasAuthority` 같은 메서드로 요청마다 권한을 설정할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;메서드 보안&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;:&lt;/b&gt; `&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;@PreAuthorize`&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;,`&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;@Secured`&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;와 같은 어노테이션으로 서비스 메서드 레벨에서 권한을 제어할 수 있습니다.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;3. 필터 체인 구조로 손쉬운 인증 확장&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security의 필터 체인은 다양한 인증 방식이나 인가 절차를 손쉽게 추가할 수 있도록 설계되어 있습니다. `addFilterBefore`, `addFilterAfter`와 같은 메서드로 필터를 원하는 위치에 배치할 수 있어, JWT 기반 인증과 추가적인 인증 절차를 손쉽게 조합할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;커스텀 필터 추가 용이&lt;/b&gt;: Spring Security는 JWT 외에도 OIDC, OAuth, SAML과 같은 다양한 인증 방식을 추가하기 용이합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;다양한 인증 공급자와 통합&lt;/b&gt;: `AuthenticationProvider`를 통해 인증 공급자를 확장하거나 여러 개의 인증 방식을 혼합하여 사용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 통합된 예외 처리 및 오류 관리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 예외 처리와 오류 관리를 중앙화하여, 다양한 인증 및 인가 예외를 일관성 있게 처리할 수 있도록 돕습니다. 이를 통해 각종 예외 상황에서 일관된 응답을 제공할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;ExceptionTranslationFilter&lt;/b&gt;: 인증이 필요한 리소스에 접근할 때 예외가 발생하면 이 필터가 적절한 응답을 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;DataAccessException&lt;/b&gt;과 같은 예외 변환: Spring Security는 데이터베이스와 관련된 예외를 `DataAccessException` 계층으로 통일하여 일관된 예외 처리가 가능하게 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 확장성과 재사용성을 높이는 구조&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 인터페이스 기반으로 설계되어 있어, 커스터마이징이 쉽고, 프로젝트에 따라 다양한 요구사항을 충족할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;UserDetailsService와 AuthenticationProvider&lt;/b&gt;: 사용자 인증 로직을 표준 인터페이스로 구현하면, 인증 방식을 변경하더라도 코드를 최소한으로 수정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; &lt;b&gt;PasswordEncoder&lt;/b&gt;: 비밀번호 인코딩을 다양한 방식으로 처리할 수 있으며, 보안 수준에 맞게 쉽게 변경할 수 있습니다. `BCryptPasswordEncoder` 같은 보안 수준이 높은 인코딩 기법을 기본적으로 지원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. JWT 와의 쉬운 통합&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Spring Security는 커스텀 필터와 보안 설정을 통해 JWT 기반 인증을 쉽게 추가할 수 있도록 돕습니다. JwtFilter를 사용하여 JWT 토큰을 검증하는 로직을 추가한 후, Spring Security의 `SecurityContextHolder`에 인증된 사용자의 정보를 설정하면, 이후의 모든 요청에서 이를 참조하여 인증과 인가를 처리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security와 JWT 통합 예시&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Security를 사용한 JWT 통합은 다음과 같은 구조로 이루어집니다:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. &lt;b&gt;로그인 요청 처리&lt;/b&gt;: 클라이언트가 로그인 요청을 보내면 `AuthController`가 이를 받아 사용자 정보를 확인하고, 인증에 성공하면 JWT 토큰을 생성해 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. &lt;b&gt;JwtFilter 추가&lt;/b&gt;: `JwtFilter`를 생성하여, 모든 요청에 대해 JWT 토큰을 확인하고 검증하는 필터를 추가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. &lt;b&gt;SecurityConfig에 JwtFilter 등록&lt;/b&gt;: SecurityConfig에서 필터 체인에 JwtFilter를 등록하여 인증 절차에 포함시킵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. Spring Security 와 JWT 연동&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; JWT는 세션/쿠키 기반 인증과 달리, 로그인 시 클라이언트에게 암호화된 토큰을 발급하여 이후의 요청에서 이 토큰을 인증 수단으로 사용하는 방식입니다. 이 토큰을 클라이언트가 요청 헤더에 포함하여 서버로 보내면, 서버는 토큰을 검증하여 사용자의 인증 상태를 확인합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JWT 기반 인증을 위한 구현 단계&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;로그인 요청 처리&lt;/b&gt;: 클라이언트가 ID와 비밀번호로 로그인 요청을 보내면 서버는 자격 증명을 확인하고, 인증에 성공하면 JWT 토큰을 생성해 응답으로 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;토큰 저장 및 사용&lt;/b&gt;: 클라이언트는 이 JWT 토큰을 저장하고, 이후의 모든 요청에 Authorization 헤더에 이 토큰을 포함해 서버에 요청합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;JWT 검증&lt;/b&gt;: 서버는 요청이 들어올 때마다 JWT 필터(JwtFilter 등)를 통해 토큰을 검증하고, 유효한 토큰이라면 인증된 사용자로 처리합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. SecurityConfig 설정하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; JWT 기반 인증을 구현하려면 Spring Security 설정 클래스인 `SecurityConfig`에서 여러 가지 설정을 해야 합니다. `SecurityConfig`는 `@Configuration`을 붙여 설정 클래스로 만들고, `@EnableWebSecurity` 를 사용하여 Spring Security를 활성화합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731569108836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {...}

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
	private final JwtUtil jwtUtil;
	private final UserDetailsServiceImpl userDetailsServiceImpl;
	private final AuthenticationConfiguration authenticationConfiguration;

	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws
		Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}

	@Bean
	public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
		JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
		filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
		return filter;
	}

	@Bean
	public JwtAuthorizationFilter jwtAuthorizationFilter() {
		return new JwtAuthorizationFilter(jwtUtil, userDetailsServiceImpl);
	}

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http.csrf(csrf -&amp;gt; csrf.disable());

		http.authorizeHttpRequests((authorizeHttpRequests) -&amp;gt;
			authorizeHttpRequests
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
				.requestMatchers(&quot;/auth/**&quot;).permitAll() // '/auth/'로 시작하는 요청 모두 접근 허가
				.anyRequest().authenticated() // 그 외 모든 요청 인증처리
		);

		http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
		http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

		return http.build();
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;1. JwtUtil:&lt;/b&gt; `JwtUtil` 은 Spring Security 에서 사용될 `JwtAuthenticationFilter` 를 생성하기 위해 주입해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. UserDetailsServiceImpl:&lt;/b&gt; Spring Security 에서 제공하는 UserDetailsService 를 구현하기 위한 구현체로, 사용자의 정보를 조회하는 역할을 하고 DB에서 사용자의 정보를 로드하여 인증에 사용합니다. 위의 예제 코드에서는 `AuthenticationFilter` 와 `AuthorizationFilter` 를 나누어 인증과 인가를 진행하고 있고, `AuthorizationFilter` 를 구현하기 위해 파라미터로 대입될 때 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. PasswordEncoder: &lt;/b&gt;비밀번호를 암호화하기 위한 `PasswordEncoder`를 빈으로 등록합니다. `BCryptPasswordEncoder`는 보안 수준이 높은 암호화 방식입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;4. permitAll() 설정:&lt;/b&gt; 회원가입 및 로그인 API 는 인증이 필요 없도록 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;5. addFilterBefore 설정:&lt;/b&gt; 이 부분에서 Filter 를 어느 순서대로 거치게 될지 설정합니다. addFilterBefore 은 첫 번째 파라미터에 있는 필터를, 두번째 파라미터에 있는 필터보다 먼저 실행시킨다는 의미를 가지고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. UserDetails 와 UserDetailsService 구현&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;`UserDetails`와 `UserDetailsService`는 Spring Security에서 사용자 정보를 다루는 표준 인터페이스입니다. Spring Security 에서 `AuthenticationManager` 는 `UserDetailsService` 에서 `UserDetails` 를 반환하게 되는데, 그 `UserDetails` 는 사용자의 이름으로 데이터베이스에서 조회를 요청하여 `UserDetails` 객체를 생성합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 이 두 인터페이스를 구현함으로써 Spring Security는 DB에서 조회한 사용자 정보를 활용하여 인증을 처리할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예제는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731569822771&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ...

public class UserDetailsImpl implements UserDetails {

	private final User user;

	public UserDetailsImpl(User user) {
		this.user = user;
	}

	public User getUser() {
		return user;
	}

	@Override
	public String getPassword() {
		return user.getPassword();
	}

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

	public Long getId() {
		return user.getId();
	}

	@Override
	public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
		UserRole role = user.getUserRole();
		String authority = &quot;ROLE_&quot; + role.name();

		SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
		return List.of(simpleGrantedAuthority);
	}

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

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

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

	@Override
	public boolean isEnabled() {
		return true;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때의 상속받은 UserDetails 인터페이스의 내용은 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731569856391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserDetails extends Serializable {
    Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities();

    String getPassword();

    String getUsername();

    default boolean isAccountNonExpired() {
        return true;
    }

    default boolean isAccountNonLocked() {
        return true;
    }

    default boolean isCredentialsNonExpired() {
        return true;
    }

    default boolean isEnabled() {
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 Spring Security 에서 제공하는 이 인터페이스를 상속 받아서, 실제로 구현을 하고 메서드들을 Override 하여 확장시켜 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`UserDetailsServiceImpl` 을 예제로 보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731569958161&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ...

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
		User user = userRepository.findByEmail(email)
			.orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;존재하지 않는 유저 이메일입니다.&quot;));

		return new UserDetailsImpl(user);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService 인터페이스를 추가적으로 보면,&lt;/p&gt;
&lt;pre id=&quot;code_1731569989754&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 작성되어 있고, 이것을 실제로 구현 후 Override 하여 사용하게 됩니다. 위의 예제에서는 사용자의 요청에서 `Email` 로 로그인하는 조건이었기 때문에 파라미터로 `Email` 을 받고, `UserRepository` 에서 `Email` 을 통해 유저가 존재하는지 조회하는 예제입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;왜 UserDetails 와 UserDetailsService 를 사용하는가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security는 모든 애플리케이션에서 일관된 방식으로 사용자 정보를 다룰 수 있도록 `UserDetails`와 `UserDetailsService` 인터페이스를 정의하였습니다. 이를 통해 인증 메커니즘이 변경되더라도 사용자 정보 조회 및 검증 로직을 쉽게 재사용하고 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Authentication, Authorization Filter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 보여드릴 예제에서는 인증을 하는 필터와, 인가를 하는 필터를 따로 구분하여 진행하도록 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731570315236&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j(topic = &quot;로그인 및 JWT 생성&quot;)
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
	private final JwtUtil jwtUtil;

	public JwtAuthenticationFilter(JwtUtil jwtUtil) {
		this.jwtUtil = jwtUtil;
		setFilterProcessesUrl(&quot;/auth/signin&quot;);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		log.info(&quot;로그인 시도&quot;);
		try {
			SigninRequest requestDto = new ObjectMapper().readValue(request.getInputStream(), SigninRequest.class);

			return getAuthenticationManager().authenticate(
				new UsernamePasswordAuthenticationToken(
					requestDto.getEmail(),
					requestDto.getPassword(),
					null
				)
			);
		} catch (IOException e) {
			log.error(e.getMessage());
			throw new RuntimeException(e.getMessage());
		}
	}

	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
		log.info(&quot;로그인 성공 및 JWT 생성&quot;);
		UserRole role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getUserRole();

		String token = jwtUtil.createToken(
			((UserDetailsImpl)authResult.getPrincipal()).getUser().getId(),
			((UserDetailsImpl)authResult.getPrincipal()).getUser().getEmail(),
			role,
			((UserDetailsImpl)authResult.getPrincipal()).getUser().getNickname());
		jwtUtil.addJwtToCookie(token, response);
	}

	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
		log.info(&quot;로그인 실패&quot;);
		response.setStatus(401);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 요청인 `/auth/signin` URL 에 요청이 들어오게 되면 이 필터가 동작하게 되고, 인증을 시작하게 됩니다. 요청이 잘 시행되면&lt;/p&gt;
&lt;div style=&quot;background-color: #263238; color: #c3cee3;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;return getAuthenticationManager().authenticate()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 `Username` 과 `Password` 로 객체가 생성되고, 이 객체는 인증 전 자격 증명의 객체로 사용됩니다. 이후 `attemptAuthentication()` 메서드에서 반환받은 객체로 JWT 토큰을 생성하고 `addJwtToCookie` 메서드를 이용해 로그인 후 헤더의 쿠키 부분에 토큰을 반환하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 API 요청이 들어오게 되면 아래의 Authorization 필터를 통해 권한이 있는지 확인하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731570694613&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j(topic = &quot;JWT 검증 및 인가&quot;)
public class JwtAuthorizationFilter extends OncePerRequestFilter {

	private final JwtUtil jwtUtil;
	private final UserDetailsServiceImpl userDetailsService;

	public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
		this.jwtUtil = jwtUtil;
		this.userDetailsService = userDetailsService;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

		String tokenValue = jwtUtil.getTokenFromRequest(req);

		if (StringUtils.hasText(tokenValue)) {
			// JWT 토큰 substring
			tokenValue = jwtUtil.substringToken(tokenValue);
			log.info(tokenValue);

			if (!jwtUtil.validateToken(tokenValue)) {
				log.error(&quot;Token Error&quot;);
				return;
			}

			Claims info = jwtUtil.extractClaims(tokenValue);
			String email = info.get(&quot;email&quot;, String.class);

			try {
				setAuthentication(email);
			} catch (Exception e) {
				log.error(e.getMessage());
				return;
			}
		}

		filterChain.doFilter(req, res);
	}

	// 인증 처리
	public void setAuthentication(String username) {
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		Authentication authentication = createAuthentication(username);
		context.setAuthentication(authentication);

		SecurityContextHolder.setContext(context);
	}

	// 인증 객체 생성
	private Authentication createAuthentication(String username) {
		UserDetails userDetails = userDetailsService.loadUserByUsername(username);
		return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Http의 쿠키에 등록되어 있는 JWT 토큰을 가져와서 검증할 수 있도록 가공한 뒤 토큰을 이용하여 토큰이 가지고 있는 유저 정보가 들어있는 `Claims` 객체를 생성하여 로그인한 유저의 Email 정보를 가져옵니다. 이 Email 정보를 토대로 `setAuthentication()` 와 `createAuthentication()` 메서드를 통해 `UserDetailsService` 에서 유저 정보가 있는지 확인 후 가져와서 인증객체를 생성한 뒤, 인증처리를 하게 됩니다. 인증처리가 되면 이 객체를 `setAuthentication()` 메서드에서 `SecurityContext` 부분에 저장을 하고 이때 `SecurityContext` 가 가지고 있는 정보를 API 에서 @AuthenticationPrincipal 어노테이션을 활용해 가져올 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731571070567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	@PostMapping(&quot;/todos&quot;)
	public ResponseEntity&amp;lt;TodoSaveResponse&amp;gt; saveTodo(
		@AuthenticationPrincipal UserDetailsImpl authUser,
		@Valid @RequestBody TodoSaveRequest todoSaveRequest
	) {
		return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest));
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 일정을 등록하는 역할을 하는 `saveTodo()` 라는 API 가 이렇게 정의되어 있다면, `UserDetailsImpl` 로 구현한 authUser 를 비즈니스 로직에 전달하여, 요청하는 로그인된 사용자와 일정에 관련된 정보를 담고있는 `todoSaveRequest`를 같이 Service 에 전달하여 Todo 객체를 생성하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅으로 Spring Security&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>it</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>til</category>
      <category>백엔드</category>
      <category>병아리</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <category>필터</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/89</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-Spring-Security#entry89comment</comments>
      <pubDate>Wed, 13 Nov 2024 20:47:44 +0900</pubDate>
    </item>
    <item>
      <title>[완전탐색] - 카펫</title>
      <link>https://wanglan.tistory.com/entry/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89-%EC%B9%B4%ED%8E%AB</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;알고리즘 완전탐색.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbvl3z/btsKFgwP97H/UxfdSrMMkataZseWi0jpPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbvl3z/btsKFgwP97H/UxfdSrMMkataZseWi0jpPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbvl3z/btsKFgwP97H/UxfdSrMMkataZseWi0jpPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdbvl3z%2FbtsKFgwP97H%2FUxfdSrMMkataZseWi0jpPK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2021&quot; height=&quot;1161&quot; data-filename=&quot;알고리즘 완전탐색.png&quot; data-origin-width=&quot;2021&quot; data-origin-height=&quot;1161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완전탐색(Brute Force)&lt;/b&gt;은 가능한 모든 경우를 하나씩 시도해보며 정답을 찾는 알고리즘 기법입니다. 완전탐색은 효율성보다는 정확성을 우선시하며, 문제의 모든 가능한 해를 다 찾아보고 비교하기 때문에 &lt;b&gt;답을 반드시 찾을 수 있는 보장된 방법&lt;/b&gt;입니다. 이 방법은 &lt;b&gt;경우의 수가 작은 문제&lt;/b&gt;에 적합하며, 보통 다음과 같은 유형의 문제에 사용됩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;순열&lt;/b&gt;과 &lt;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;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;장점&lt;/b&gt;: 간단하게 구현 가능하고 모든 경우를 다 탐색하므로 정확한 결과를 보장.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;: 경우의 수가 많아지면 수행 시간이 기하급수적으로 증가해 비효율적.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 숫자 배열에서 두 수의 합이 특정 값이 되는 모든 쌍을 찾을 때, 모든 쌍을 비교하는 방식으로 구현할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Tip&lt;/b&gt;: `완전탐색`을 사용할 때는 `문제의 입력 크기`를 보고 효율적인 알고리즘이 필요한지 먼저 판단하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전 탐색의 기법에는 다양한 방식이 있는데, 각 기법마다 접근 방식과 활용 방식이 조금씩 다릅니다. 아래에서 각 기법을 예시와 함께 설명하겠습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Brute Force(단순 완전 탐색)&lt;/li&gt;
&lt;li&gt;비트마스크&lt;/li&gt;
&lt;li&gt;재귀함수&lt;/li&gt;
&lt;li&gt;순열&lt;/li&gt;
&lt;li&gt;BFS/DFS&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;27e5f7dc-22a8-4037-95a5-9e4e356dee8d&quot; data-message-author-role=&quot;assistant&quot;&gt;&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. &lt;b&gt;Brute Force (단순 완전 탐색)&lt;/b&gt;&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;b&gt;예시 문제&lt;/b&gt;: 1부터 100까지의 숫자 중 두 숫자의 합이 50이 되는 모든 쌍을 찾기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731371602720&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for (int i = 1; i &amp;lt;= 100; i++) {
    for (int j = i + 1; j &amp;lt;= 100; j++) {
        if (i + j == 50) {
            System.out.println(i + &quot;, &quot; + j);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 설명&lt;/b&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;2. &lt;b&gt;비트마스크 (Bitmask)&lt;/b&gt;&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;b&gt;예시 문제&lt;/b&gt;: 배열 [1, 2, 3]의 모든 부분집합을 구하기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731371689748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int[] arr = {1, 2, 3};
int n = arr.length;
for (int i = 0; i &amp;lt; (1 &amp;lt;&amp;lt; n); i++) {  // 2^n 개의 부분집합
    System.out.print(&quot;{ &quot;);
    for (int j = 0; j &amp;lt; n; j++) {
        if ((i &amp;amp; (1 &amp;lt;&amp;lt; j)) != 0) {  // j번째 비트가 1인지 확인
            System.out.print(arr[j] + &quot; &quot;);
        }
    }
    System.out.println(&quot;}&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 설명&lt;/b&gt;: 모든 비트 조합을 이용해 부분집합을 탐색합니다. 비트 1이 켜진 위치의 원소만 포함시킵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;b&gt;재귀 함수 (Recursion)&lt;/b&gt;&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;b&gt;예시 문제&lt;/b&gt;: 숫자 n부터 1까지의 합을 구하기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731371762198&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public int sumToN(int n) {
    if (n == 1) return 1;       // 종료 조건
    return n + sumToN(n - 1);   // 자기 자신을 호출
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 설명&lt;/b&gt;: sumToN 함수가 재귀적으로 호출되며 n부터 1까지의 합을 구합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. &lt;b&gt;순열 (Permutation)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 원소의 순서를 바꾸어 가능한 모든 경우를 나열하는 방법입니다. 예를 들어 n개의 원소로 만들 수 있는 모든 순서를 구하는데 사용됩니다.&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;: 숫자 [1, 2, 3]로 만들 수 있는 모든 순열을 구하기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731371800097&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void permute(int[] arr, int l, int r) {
    if (l == r) {
        System.out.println(Arrays.toString(arr));
    } else {
        for (int i = l; i &amp;lt;= r; i++) {
            swap(arr, l, i);
            permute(arr, l + 1, r);
            swap(arr, l, i);  // 백트래킹
        }
    }
}

public void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설명&lt;/b&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;5. &lt;b&gt;BFS/DFS (너비 우선 탐색 / 깊이 우선 탐색)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프 탐색 기법으로, 모든 노드를 방문하여 최단 경로, 연결 요소 등을 탐색하는 데 사용됩니다. BFS는 주로 최단 경로 탐색에, DFS는 깊이 우선으로 탐색이 필요할 때 유용합니다.&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;: 2차원 배열에서 시작점에서 끝점까지의 경로를 찾기 (벽이 없는 미로)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예제 코드&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1731371943862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean dfs(int[][] maze, int x, int y, boolean[][] visited) {
    if (x &amp;lt; 0 || x &amp;gt;= maze.length || y &amp;lt; 0 || y &amp;gt;= maze[0].length || visited[x][y]) return false;
    visited[x][y] = true;
    if (maze[x][y] == 9) return true; // 목표 지점 도달
    
    return dfs(maze, x + 1, y, visited) || dfs(maze, x - 1, y, visited) ||
           dfs(maze, x, y + 1, visited) || dfs(maze, x, y - 1, visited);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; 설명&lt;/b&gt;: DFS는 재귀적으로 동서남북으로 이동해 목표 지점에 도달할 수 있는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;653157b1-2d54-4b43-b696-89cea2ea9a03&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`&lt;b&gt;BFS(너비 우선 탐색)&lt;/b&gt;`은 `DFS`와 다르게 `큐(queue)`를 사용하여 한 레벨씩 넓게 탐색합니다. 아래는 위와 동일한 미로 예시를 BFS로 구현한 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1731372093739&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.LinkedList;
import java.util.Queue;

public boolean bfs(int[][] maze, int startX, int startY) {
    int[] dx = {1, -1, 0, 0};
    int[] dy = {0, 0, 1, -1};
    boolean[][] visited = new boolean[maze.length][maze[0].length];
    Queue&amp;lt;int[]&amp;gt; queue = new LinkedList&amp;lt;&amp;gt;();
    
    queue.add(new int[]{startX, startY});
    visited[startX][startY] = true;

    while (!queue.isEmpty()) {
        int[] pos = queue.poll();
        int x = pos[0];
        int y = pos[1];

        if (maze[x][y] == 9) return true; // 목표 지점 도달

        for (int i = 0; i &amp;lt; 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];
            if (nx &amp;gt;= 0 &amp;amp;&amp;amp; nx &amp;lt; maze.length &amp;amp;&amp;amp; ny &amp;gt;= 0 &amp;amp;&amp;amp; ny &amp;lt; maze[0].length &amp;amp;&amp;amp; !visited[nx][ny]) {
                visited[nx][ny] = true;
                queue.add(new int[]{nx, ny});
            }
        }
    }
    return false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설명:&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. ` queue`에 시작점을 추가하고, 한 레벨씩 인접한 노드들을 탐색합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. `목표 지점(예: 값이 9인 지점)`을 찾으면 `true` 를 반환하고, 모든 가능한 위치를 방문해도 찾지 못하면 `false` 를 반환합니다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 완전 탐색의 `Brute Force` 의 접근방식에 해당하는 카펫 문제를 예시로 글을 마무리 하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42842&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] - 카펫&lt;/a&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1731372303425&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42842&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/doKikk/hyXwhpxawQ/dRFVuBHRmB1pObYNqSbL11/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ivHmV/hyXwuCmBGO/c8qfBCMjlvTbmSWRO7k9EK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42842&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42842&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/doKikk/hyXwhpxawQ/dRFVuBHRmB1pObYNqSbL11/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ivHmV/hyXwuCmBGO/c8qfBCMjlvTbmSWRO7k9EK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제&amp;nbsp;설명&lt;/b&gt; &lt;br /&gt;Leo는&amp;nbsp;카펫을&amp;nbsp;사러&amp;nbsp;갔다가&amp;nbsp;아래&amp;nbsp;그림과&amp;nbsp;같이&amp;nbsp;중앙에는&amp;nbsp;노란색으로&amp;nbsp;칠해져&amp;nbsp;있고&amp;nbsp;테두리&amp;nbsp;1줄은&amp;nbsp;갈색으로&amp;nbsp;칠해져&amp;nbsp;있는&amp;nbsp;격자&amp;nbsp;모양&amp;nbsp;카펫을&amp;nbsp;봤습니다. &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdTEt2/btsKFCTSa1f/JqGedQBlFJcwKBxbuR2nI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdTEt2/btsKFCTSa1f/JqGedQBlFJcwKBxbuR2nI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdTEt2/btsKFCTSa1f/JqGedQBlFJcwKBxbuR2nI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdTEt2%2FbtsKFCTSa1f%2FJqGedQBlFJcwKBxbuR2nI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;430&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Leo는&amp;nbsp;집으로&amp;nbsp;돌아와서&amp;nbsp;아까&amp;nbsp;본&amp;nbsp;카펫의&amp;nbsp;노란색과&amp;nbsp;갈색으로&amp;nbsp;색칠된&amp;nbsp;격자의&amp;nbsp;개수는&amp;nbsp;기억했지만,&amp;nbsp;전체&amp;nbsp;카펫의&amp;nbsp;크기는&amp;nbsp;기억하지&amp;nbsp;못했습니다. &lt;br /&gt;&lt;br /&gt;Leo가&amp;nbsp;본&amp;nbsp;카펫에서&amp;nbsp;갈색&amp;nbsp;격자의&amp;nbsp;수&amp;nbsp;brown,&amp;nbsp;노란색&amp;nbsp;격자의&amp;nbsp;수&amp;nbsp;yellow가&amp;nbsp;매개변수로&amp;nbsp;주어질&amp;nbsp;때&amp;nbsp;카펫의&amp;nbsp;가로,&amp;nbsp;세로&amp;nbsp;크기를&amp;nbsp;순서대로&amp;nbsp;배열에&amp;nbsp;담아&amp;nbsp;return&amp;nbsp;하도록&amp;nbsp;solution&amp;nbsp;함수를&amp;nbsp;작성해주세요. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;제한사항&lt;/b&gt; &lt;br /&gt;갈색&amp;nbsp;격자의&amp;nbsp;수&amp;nbsp;brown은&amp;nbsp;8&amp;nbsp;이상&amp;nbsp;5,000&amp;nbsp;이하인&amp;nbsp;자연수입니다. &lt;br /&gt;노란색&amp;nbsp;격자의&amp;nbsp;수&amp;nbsp;yellow는&amp;nbsp;1&amp;nbsp;이상&amp;nbsp;2,000,000&amp;nbsp;이하인&amp;nbsp;자연수입니다. &lt;br /&gt;카펫의&amp;nbsp;가로&amp;nbsp;길이는&amp;nbsp;세로&amp;nbsp;길이와&amp;nbsp;같거나,&amp;nbsp;세로&amp;nbsp;길이보다&amp;nbsp;깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입출력 예&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 33.0233%; height: 126px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;brown&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;yellow&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;return&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;[4, 3]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;[3, 3]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;24&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;24&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;[8, 6]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나의 풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731372516085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int[] solution(int brown, int yellow) {
        int area = brown + yellow;
        for(int height = 3; height &amp;lt;= Math.sqrt(area); height++) {
            if(area%height == 0) {
                int width = area/height;
                if((width-2)*(height-2)==yellow){
                    return new int[] {width, height};
                }
            }
        }
        return new int[0];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 주어진 갈색과 노란색 격자 수를 바탕으로 카펫의 가로와 세로 길이를 찾는 문제입니다. 이때, &lt;b&gt;갈색 격자는 전체 카펫의 테두리&lt;/b&gt;를 이루고, &lt;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;1.&lt;b&gt; 카펫 전체의 넓이&lt;/b&gt;는 `&lt;span&gt;&lt;span&gt;brown`+ `yellow`&lt;/span&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 가능한 &lt;b&gt;카펫의 가로와 세로 길이&lt;/b&gt;를 찾기 위해서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가로와 세로의 곱이 전체 넓이와 같아야 하고, 가로는 세로 이상이어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 각 경우에 대해 &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;(가로 - 2) * (세로 - 2) == yellow&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 포스팅 마무리 하겠습니다. 읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>BFS</category>
      <category>백준</category>
      <category>병아리</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>완전탐색</category>
      <category>주니어개발자</category>
      <category>코딩테스트</category>
      <category>티스토리챌린지</category>
      <category>프로그래머스</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/88</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%99%84%EC%A0%84%ED%83%90%EC%83%89-%EC%B9%B4%ED%8E%AB#entry88comment</comments>
      <pubDate>Tue, 12 Nov 2024 09:54:06 +0900</pubDate>
    </item>
    <item>
      <title>[오버플로우] - 피보나치 수</title>
      <link>https://wanglan.tistory.com/entry/%EC%98%A4%EB%B2%84%ED%94%8C%EB%A1%9C%EC%9A%B0-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘을 작성하다 보면, 수가 급격하게 커지면서 특정한 경우에 런타임에러가 발생하는 경우를 볼 수 있습니다. 이는 피보나치 수열과 같이 값이 급격하게 상승하는 특정한 수열들에서 에러가 발생하고 이런 경우를 `오버플로우`라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;`오버플로우(overflow)` 는 컴퓨터에서 정수나 실수를 표현할 수 있는 범위를 초과하는 상황을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 자바의 int 자료형은 약 -21억부터 +21억까지의 범위를 갖는데, 계산 중 이 범위를 넘어서면 잘못된 값이 나옵니다. 예를 들어, 2147483647 + 1을 하면 가장 작은 값인 -2147483648로 돌아가죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피보나치 수를 계산하는 문제로 예를 들어 보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12945&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[프로그래머스] - 피보나치 수&lt;/a&gt;&lt;/h3&gt;
&lt;figure id=&quot;og_1731285980544&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12945&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dg2biD/hyXwgjrwxR/VJ1PCEnKEmN2ZsBkEAztVK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/btoDiN/hyXwihhlOb/2LFUJjMxh5NxH48K82rP7K/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12945&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12945&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dg2biD/hyXwgjrwxR/VJ1PCEnKEmN2ZsBkEAztVK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/btoDiN/hyXwihhlOb/2LFUJjMxh5NxH48K82rP7K/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제&amp;nbsp;설명 &lt;br /&gt;피보나치&amp;nbsp;수는&amp;nbsp;F(0)&amp;nbsp;=&amp;nbsp;0,&amp;nbsp;F(1)&amp;nbsp;=&amp;nbsp;1일&amp;nbsp;때,&amp;nbsp;1&amp;nbsp;이상의&amp;nbsp;n에&amp;nbsp;대하여&amp;nbsp;F(n)&amp;nbsp;=&amp;nbsp;F(n-1)&amp;nbsp;+&amp;nbsp;F(n-2)&amp;nbsp;가&amp;nbsp;적용되는&amp;nbsp;수&amp;nbsp;입니다. &lt;br /&gt;&lt;br /&gt;예를들어 &lt;br /&gt;&lt;br /&gt;F(2)&amp;nbsp;=&amp;nbsp;F(0)&amp;nbsp;+&amp;nbsp;F(1)&amp;nbsp;=&amp;nbsp;0&amp;nbsp;+&amp;nbsp;1&amp;nbsp;=&amp;nbsp;1 &lt;br /&gt;F(3)&amp;nbsp;=&amp;nbsp;F(1)&amp;nbsp;+&amp;nbsp;F(2)&amp;nbsp;=&amp;nbsp;1&amp;nbsp;+&amp;nbsp;1&amp;nbsp;=&amp;nbsp;2 &lt;br /&gt;F(4)&amp;nbsp;=&amp;nbsp;F(2)&amp;nbsp;+&amp;nbsp;F(3)&amp;nbsp;=&amp;nbsp;1&amp;nbsp;+&amp;nbsp;2&amp;nbsp;=&amp;nbsp;3 &lt;br /&gt;F(5)&amp;nbsp;=&amp;nbsp;F(3)&amp;nbsp;+&amp;nbsp;F(4)&amp;nbsp;=&amp;nbsp;2&amp;nbsp;+&amp;nbsp;3&amp;nbsp;=&amp;nbsp;5 &lt;br /&gt;와&amp;nbsp;같이&amp;nbsp;이어집니다. &lt;br /&gt;&lt;br /&gt;2&amp;nbsp;이상의&amp;nbsp;n이&amp;nbsp;입력되었을&amp;nbsp;때,&amp;nbsp;n번째&amp;nbsp;피보나치&amp;nbsp;수를&amp;nbsp;1234567으로&amp;nbsp;나눈&amp;nbsp;나머지를&amp;nbsp;리턴하는&amp;nbsp;함수,&amp;nbsp;solution을&amp;nbsp;완성해&amp;nbsp;주세요. &lt;br /&gt;&lt;br /&gt;제한&amp;nbsp;사항 &lt;br /&gt;n은&amp;nbsp;2&amp;nbsp;이상&amp;nbsp;100,000&amp;nbsp;이하인&amp;nbsp;자연수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wnqjj/btsKCOac0ot/ruwoPxKcC5kH502hAqEPjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wnqjj/btsKCOac0ot/ruwoPxKcC5kH502hAqEPjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wnqjj/btsKCOac0ot/ruwoPxKcC5kH502hAqEPjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwnqjj%2FbtsKCOac0ot%2FruwoPxKcC5kH502hAqEPjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;319&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 조건에 1234567로 나눈 나머지를 찾는 이유는, 단순히 어려운 수로 나누기 위함이 아니라 모듈로 연산을 활용하기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피보나치 수열에서는 값이 빠르게 커지기 때문에, 수가 너무 커져서 메모리를 초과하거나 계산이 부정확해질 수 있습니다. 모듈로 연산을 사용하여 값이 1234567을 넘지 않도록 제한함으로써, &lt;b&gt;정확도와 효율성을 유지&lt;/b&gt;할 수 있습니다. 코딩 테스트 문제에서도 일반적으로 큰 수 연산을 방지하려고 모듈로를 적용하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나의 풀이&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731286273870&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int n) {
        long[] result = fibo(n);
        int answer = (int) result[n];
        return answer;
    }
    
    public long[] fibo(int n) {
        long[] nacci = new long[n+1];
        nacci[0] = 0;
        nacci[1] = 1;
        for(int i = 2; i &amp;lt;= n; i++){
            nacci[i] = (nacci[i-1]+nacci[i-2])%1234567;
        }
        return nacci;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피보나치 수를 찾아주는 메서드를 만듭니다. 배열을 사용하며, 초기값 n=0, n=1 일때 n=2는 1이 되야하므로 초기 배열의 길이는 n+1 로 설정합니다. 그리고 조건에 맞게 반복문을 시행하면 n 까지의 피보나치 수가 배열로 만들어집니다. 계산 과정 중, 1234567 로 모듈로 연산을 진행하기 때문에, 수는 충분히 작은 수의 범위에서 벗어나지 않고 문제의 조건에도 부합하게 됩니다.&lt;/p&gt;</description>
      <category>Algorithm</category>
      <category>병아리</category>
      <category>알고리즘</category>
      <category>오버플로우</category>
      <category>오블완</category>
      <category>코딩테스트</category>
      <category>코테</category>
      <category>티스토리챌린지</category>
      <category>프로그래머스</category>
      <category>피보나치</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/87</guid>
      <comments>https://wanglan.tistory.com/entry/%EC%98%A4%EB%B2%84%ED%94%8C%EB%A1%9C%EC%9A%B0-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%EC%88%98#entry87comment</comments>
      <pubDate>Mon, 11 Nov 2024 09:55:45 +0900</pubDate>
    </item>
    <item>
      <title>Spring - JDBC Template</title>
      <link>https://wanglan.tistory.com/entry/Spring-JDBC-Template-1</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;`JdbcTemplate`&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;은 Spring에서 제공하는 데이터베이스 접근 도구로, JDBC 작업을 단순화하고 오류 처리를 일관되게 해줍니다. 기본적인 SQL 쿼리 실행, 데이터 조회, 업데이트 등을 간단하게 수행할 수 있으며, 커넥션 관리, 자원 해제 등의 작업을 자동으로 처리합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. JdbcTemplate의 주요 기능과 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커넥션 및 리소스 관리 자동화&lt;/b&gt;: JdbcTemplate은 데이터베이스 연결을 자동으로 관리하고 SQL 실행 후 자원을 해제합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;간결한 코드 작성&lt;/b&gt;: SQL 실행을 위한 복잡한 Connection, Statement, ResultSet 처리가 필요 없어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관된 예외 처리&lt;/b&gt;: SQL 예외를 Spring의 DataAccessException 계층으로 변환하여 일관성 있는 예외 처리가 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체 매핑 기능 지원&lt;/b&gt;: 쿼리 결과를 RowMapper나 BeanPropertyRowMapper 등을 사용하여 객체에 쉽게 매핑할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. JdbcTemplate 사용법&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;`JdbcTemplate`을 사용하려면 Spring에서 `JdbcTemplate` 객체를 빈으로 정의하고, 데이터베이스와의 연결 정보를 설정해야 합니다. 주로 Spring Boot에서는 데이터베이스 설정만 추가하면 자동으로 `JdbcTemplate` 빈을 주입받을 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 설정 및 기본 사용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 의존성 추가 (build.gradle)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731206626268&quot; class=&quot;nginx&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. JdbcTemplate 빈 주입 및 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731206626268&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Autowired
private JdbcTemplate jdbcTemplate;

public int insertUser(String name, int age) {
    String sql = &quot;INSERT INTO users (name, age) VALUES (?, ?)&quot;;
    return jdbcTemplate.update(sql, name, age);
}

public List&amp;lt;User&amp;gt; getAllUsers() {
    String sql = &quot;SELECT * FROM users&quot;;
    return jdbcTemplate.query(sql, new UserRowMapper());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3. RowMapper와 ResultSetExtractor&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JdbcTemplate에서 쿼리 결과를 객체로 변환하기 위해 `&lt;b&gt;RowMapper`&lt;/b&gt;와 `&lt;b&gt;ResultSetExtractor&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;RowMapper&lt;/b&gt;: SQL 결과의 각 행을 객체로 매핑할 때 사용합니다. RowMapper는 주로 조회 쿼리에서 사용되며, 각 결과 행을 지정된 객체로 변환합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ResultSetExtractor&lt;/b&gt;: RowMapper와 유사하지만, 보다 복잡한 결과 처리가 필요할 때 사용됩니다. 전체 ResultSet을 조작하여 변환을 수행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RowMapper 예제&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RowMapper 인터페이스를 사용하여, SQL 결과를 Java 객체로 매핑합니다. 예를 들어, User 객체에 매핑하는 RowMapper를 작성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731206626270&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class User {
    private int id;
    private String name;
    private int age;

    // Getters and setters
}

public class UserRowMapper implements RowMapper&amp;lt;User&amp;gt; {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt(&quot;id&quot;));
        user.setName(rs.getString(&quot;name&quot;));
        user.setAge(rs.getInt(&quot;age&quot;));
        return user;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위의 `UserRowMapper`는 SQL 쿼리로 반환된 `ResultSet`의 각 행을 `User` 객체로 변환합니다. 이후 `JdbcTemplate.query()` 메서드에서 이 `UserRowMapper`를 사용하여 SQL 결과를 `List&amp;lt;User&amp;gt;`로 반환할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JdbcTemplate와 RowMapper 사용 예제&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731206626272&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;User&amp;gt; getUsersByAge(int age) {
    String sql = &quot;SELECT * FROM users WHERE age = ?&quot;;
    return jdbcTemplate.query(sql, new UserRowMapper(), age);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 메서드는 `age` 값으로 필터링된 `User` 객체 목록을 반환합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;4. BeanPropertyRowMapper&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 BeanPropertyRowMapper라는 유틸리티 클래스를 제공하여, RowMapper 없이도 자동으로 필드 이름을 기준으로 매핑할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731206626272&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;User&amp;gt; getAllUsers() {
    String sql = &quot;SELECT * FROM users&quot;;
    return jdbcTemplate.query(sql, new BeanPropertyRowMapper&amp;lt;&amp;gt;(User.class));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;`BeanPropertyRowMapper`는 SQL 결과의 컬럼 이름과 객체의 필드 이름이 동일하다면 자동으로 값을 매핑해줍니다. 하지만, 필드명과 컬럼명이 일치하지 않으면 오류가 발생할 수 있으므로 주의가 필요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;5. queryForObject 메서드&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;`JdbcTemplate`의 `queryForObject` 메서드는 SQL 쿼리 결과가 한 행일 때 사용하며, 단일 객체를 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731206626273&quot; class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public User getUserById(int id) {
    String sql = &quot;SELECT * FROM users WHERE id = ?&quot;;
    return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;6. update 메서드&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 수정하거나 삽입, 삭제할 때는 `update` 메서드를 사용합니다. 이 메서드는 실행 후 영향을 받은 행(row)의 수를 반환합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1731206626274&quot; class=&quot;pgsql&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public int updateUser(int id, String name, int age) {
    String sql = &quot;UPDATE users SET name = ?, age = ? WHERE id = ?&quot;;
    return jdbcTemplate.update(sql, name, age, id);
}

public int deleteUser(int id) {
    String sql = &quot;DELETE FROM users WHERE id = ?&quot;;
    return jdbcTemplate.update(sql, id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;7. JdbcTemplate와 RowMapper의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가독성 및 코드 간결화&lt;/b&gt;: JdbcTemplate은 SQL 실행에 필요한 기본 작업들을 추상화하여 코드 양을 줄이고 가독성을 높여줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정적인 자원 관리&lt;/b&gt;: 데이터베이스 연결 및 자원 해제 처리를 자동으로 해주기 때문에 리소스 누수 가능성이 줄어듭니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;객체 매핑 용이&lt;/b&gt;: RowMapper 또는 BeanPropertyRowMapper를 통해 SQL 결과를 쉽게 Java 객체로 매핑할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;`JdbcTemplate`는 직접 JDBC를 사용하는 것보다 훨씬 간단하게 데이터베이스 작업을 수행할 수 있도록 도와주며, 특히 Spring 프로젝트에서 매우 유용하게 쓰입니다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>java</category>
      <category>jdbc</category>
      <category>Spring</category>
      <category>til</category>
      <category>백엔드</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>Wanglan</author>
      <guid isPermaLink="true">https://wanglan.tistory.com/86</guid>
      <comments>https://wanglan.tistory.com/entry/Spring-JDBC-Template-1#entry86comment</comments>
      <pubDate>Sun, 10 Nov 2024 11:44:40 +0900</pubDate>
    </item>
  </channel>
</rss>