<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>평범한 개발자 노트</title>
    <link>https://cornswrold.tistory.com/</link>
    <description>초보 개발자의 지식 공유의 장
</description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 23:11:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>깡냉쓰</managingEditor>
    <image>
      <title>평범한 개발자 노트</title>
      <url>https://tistory1.daumcdn.net/tistory/2881762/attach/b2435ad0fe554bc2a27dafb350ad39ec</url>
      <link>https://cornswrold.tistory.com</link>
    </image>
    <item>
      <title>노무비 대장 / 작업 일보</title>
      <link>https://cornswrold.tistory.com/649</link>
      <description>&lt;p&gt;데이터는 있다. 이제 엑셀을 받아서 어떻게 만들지에 대한 고민이 필요하다.&lt;/p&gt;
&lt;h1&gt;아키텍처와 기술 스택&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;라이브러리 선택 : Apache POI &lt;ul&gt;
&lt;li&gt;대용량 처리에 특화된 SCSSF(Streaming Usermode API)&lt;/li&gt;
&lt;li&gt;복잡한 서식(병합된 셀, 스타일, 수식)은 Apache POI에서 세밀한 제어가 가능 &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대용량 처리 및 성능 최적화 전략&lt;ul&gt;
&lt;li&gt;Paging 처리 Slice / Page 1,00건으로 끊어서 엑셀을 사용&lt;/li&gt;
&lt;li&gt;Style 재사용 : 스타일 (폰트, 배경색, 테두리 등)은 미리 생성해두고 재사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기 생성 및 응답 프로세스&lt;ul&gt;
&lt;li&gt;요청 접수: 사용자가 엑셀 생성을 요청하면 서버는 즉시 작업 ID(Task ID)를 응답하고 요청을 큐(Redis, RabbitMQ 등)에 넣는다.&lt;/li&gt;
&lt;li&gt;백그라운드 처리: 별도의 스레드나 Worker가 큐에서 요청을 꺼내 엑셀을 생성&lt;/li&gt;
&lt;li&gt;파일 저장: 생성이 완료된 파일을 서버 메모리가 아닌 AWS S3 같은 오브젝트 스토리지에 업로드&lt;/li&gt;
&lt;li&gt;알림 및 다운로드:&lt;ul&gt;
&lt;li&gt;방법 A (Polling): 클라이언트가 주기적으로 Task ID 상태를 확인하여 완료되면 다운로드 링크를 노출&lt;/li&gt;
&lt;li&gt;방법 B (Push): 완료 시 WebSocket이나 알림을 통해 사용자에게 알린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;수식(Formula) 활용 및 자동 계산&lt;ul&gt;
&lt;li&gt;수식 적용: cell.setCellFormula(&amp;quot;SUM(A1:A10)&amp;quot;)와 같이 직접 엑셀 함수를 주입할 수 있음&lt;/li&gt;
&lt;li&gt;장점: 백엔드에서 복잡한 합계 로직을 일일이 계산하지 않아도 엑셀이 열릴 때 자동으로 계산됨&lt;/li&gt;
&lt;li&gt;주의사항: * 라이브러리에서 수식을 설정해도, 엑셀 파일 생성 직후에는 값이 비어있을 수 있음&lt;ul&gt;
&lt;li&gt;workbook.getCreationHelper().createFormulaEvaluator().evaluateAll()을 호출하면 생성 시점에 미리 계산된 값을 채울 수 있지만, 데이터가 너무 많으면 성능 저하의 원인이 됨. 보통은 사용자가 파일을 열 때 계산되도록 둠&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;엑셀 관리는 어떻게 할 것 인가?&lt;/h1&gt;
&lt;h2&gt;템플릿 방식 (Template-based)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;디자인(폰트, 색상, 테두리, 고정 텍스트)을 코드로 다 짜지 말고, &lt;strong&gt;미리 만들어둔 엑셀 파일(.xlsx)&lt;/strong&gt;을 템플릿으로 사용하는 방식&lt;/li&gt;
&lt;li&gt;방법: 리소스 폴더(src/main/resources/templates/excel)에 표준 양식 파일을 저장&lt;/li&gt;
&lt;li&gt;프로세스:&lt;ul&gt;
&lt;li&gt;템플릿 파일을 읽어 메모리(SXSSF)에 로드&lt;/li&gt;
&lt;li&gt;데이터가 들어갈 시작 셀(Row, Column) 위치를 지정&lt;/li&gt;
&lt;li&gt;데이터를 순회하며 해당 셀에 값만 채워 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;추천 도구 (Jxls): 만약 Apache POI로 위치를 지정하는 것조차 번거롭다면 Jxls 라이브러리를 고려. 엑셀 파일 안에 ${worker.name} 같은 태그를 적어두면 데이터 객체와 매핑하여 자동으로 그려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;전체 프로세스 흐름&lt;/h2&gt;
&lt;p&gt;전체 프로세스 흐름 (Architecture)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client: 월별 엑셀 생성 API 호출.&lt;/li&gt;
&lt;li&gt;Server (Controller): excel_export_task 테이블에 QUEUED 상태로 레코드 생성 후 즉시 task_id 반환.&lt;/li&gt;
&lt;li&gt;Server (Background/Worker): * 비동기(@Async 또는 Event)로 작업 시작.&lt;ul&gt;
&lt;li&gt;상태를 PROCESSING으로 변경.&lt;/li&gt;
&lt;li&gt;DB에서 월별 데이터 조회 (Paging 처리).&lt;/li&gt;
&lt;li&gt;엑셀 템플릿 로드 후 데이터 기입.&lt;/li&gt;
&lt;li&gt;S3 등 스토리지에 업로드.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Server (Finish): DB 상태를 COMPLETED로 변경하고 file_url 업데이트.&lt;/li&gt;
&lt;li&gt;Client (Polling/Notice): 사용자는 목록 화면에서 &amp;quot;진행 중&amp;quot; 확인 후, 완료되면 &amp;quot;다운로드&amp;quot; 버튼 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;파일 보관 기간: 생성된 엑셀 파일은 무한정 쌓아두지 말고, S3 Lifecycle 정책을 통해 7일이나 30일 뒤 자동 삭제되도록 설정하는 것이 효율적&lt;/li&gt;
&lt;li&gt;중복 요청 방지: 동일한 사용자가 같은 월의 엑셀을 짧은 시간에 여러 번 요청하면 서버 부하가 커집니다. status가 PROCESSING인 동일 조건의 작업이 있다면 추가 요청을 막거나 기존 작업을 안내하는 로직이 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;생각&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;POI 를 사용하여 세밀한 엑셀을 만들고자 했지만, 템플릿 방식을 활용하는게 관리자에게도 편할 것 같다.&lt;/li&gt;
&lt;li&gt;Jxls 에 대해 알아본다.&lt;/li&gt;
&lt;li&gt;이와 별개로 비동기로 엑셀을 생성하는 로직을 작성한다. 큐가 없으니 일단 DB를 활용하도록 한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로젝트 관리</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/649</guid>
      <comments>https://cornswrold.tistory.com/649#entry649comment</comments>
      <pubDate>Fri, 13 Mar 2026 18:21:09 +0900</pubDate>
    </item>
    <item>
      <title>AccessToken, RefreshToken</title>
      <link>https://cornswrold.tistory.com/648</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;왜&amp;nbsp;Access&amp;nbsp;Token은&amp;nbsp;수명이&amp;nbsp;짧아야&amp;nbsp;할까요?&lt;br /&gt;가장&amp;nbsp;큰&amp;nbsp;이유는&amp;nbsp;탈취의&amp;nbsp;위험&amp;nbsp;때문입니다.&lt;br /&gt;Access&amp;nbsp;Token은&amp;nbsp;API를&amp;nbsp;호출할&amp;nbsp;때마다&amp;nbsp;HTTP&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;API를&amp;nbsp;호출할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&amp;bull; 만료&amp;nbsp;시간이&amp;nbsp;1년이라면?&amp;nbsp;-&amp;gt;&amp;nbsp;해커는&amp;nbsp;1년&amp;nbsp;동안&amp;nbsp;사용자&amp;nbsp;계정으로&amp;nbsp;모든&amp;nbsp;것을&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있습니다.&lt;br /&gt;&amp;bull; 만료&amp;nbsp;시간이&amp;nbsp;30분이라면?&amp;nbsp;-&amp;gt;&amp;nbsp;해커가&amp;nbsp;토큰을&amp;nbsp;훔쳐도&amp;nbsp;최대&amp;nbsp;30분만&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;2.&amp;nbsp;불편한&amp;nbsp;UX는&amp;nbsp;어떻게&amp;nbsp;해결하나요?&amp;nbsp;(Refresh&amp;nbsp;Token의&amp;nbsp;등장)&lt;br /&gt;&quot;그럼&amp;nbsp;30분마다&amp;nbsp;다시&amp;nbsp;로그인해야&amp;nbsp;하나요?&quot;&amp;nbsp;라는&amp;nbsp;문제를&amp;nbsp;해결하기&amp;nbsp;위해&amp;nbsp;Refresh&amp;nbsp;Token이&amp;nbsp;등장합니다.&lt;br /&gt;&amp;bull; Access&amp;nbsp;Token&amp;nbsp;(액세스&amp;nbsp;토큰):&lt;br /&gt;&amp;nbsp; ◦ 역할:&amp;nbsp;API&amp;nbsp;호출&amp;nbsp;시&amp;nbsp;사용하는&amp;nbsp;&quot;실제&amp;nbsp;출입증&quot;&lt;br /&gt;&amp;nbsp; ◦ 수명:&amp;nbsp;매우&amp;nbsp;짧음&amp;nbsp;(30분&amp;nbsp;~&amp;nbsp;1시간)&lt;br /&gt;&amp;nbsp; ◦ 특징:&amp;nbsp;탈취되어도&amp;nbsp;피해가&amp;nbsp;적음.&lt;br /&gt;&amp;bull; Refresh&amp;nbsp;Token&amp;nbsp;(리프레시&amp;nbsp;토큰):&lt;br /&gt;&amp;nbsp; ◦ 역할:&amp;nbsp;만료된&amp;nbsp;Access&amp;nbsp;Token을&amp;nbsp;새로&amp;nbsp;발급받기&amp;nbsp;위한&amp;nbsp;&quot;교환권&quot;&lt;br /&gt;&amp;nbsp; ◦ 수명:&amp;nbsp;매우&amp;nbsp;김&amp;nbsp;(7일&amp;nbsp;~&amp;nbsp;30일)&lt;br /&gt;&amp;nbsp; ◦ 특징:&amp;nbsp;DB에&amp;nbsp;저장하여&amp;nbsp;관리하며,&amp;nbsp;오직&amp;nbsp;Access&amp;nbsp;Token을&amp;nbsp;재발급받는&amp;nbsp;용도로만&amp;nbsp;사용됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp;호텔&amp;nbsp;키카드&amp;nbsp;비유&lt;br /&gt;이&amp;nbsp;구조는&amp;nbsp;호텔&amp;nbsp;시스템과&amp;nbsp;매우&amp;nbsp;유사합니다.&lt;br /&gt;1. 체크인&amp;nbsp;(로그인):&lt;br /&gt;&amp;nbsp; ◦ 프론트&amp;nbsp;데스크에서&amp;nbsp;신분&amp;nbsp;확인&amp;nbsp;후&amp;nbsp;**객실&amp;nbsp;키카드(Access&amp;nbsp;Token)**와&amp;nbsp;**예약&amp;nbsp;확인증(Refresh&amp;nbsp;Token)**을&amp;nbsp;받습니다.&lt;br /&gt;2. 객실&amp;nbsp;이용&amp;nbsp;(API&amp;nbsp;호출):&lt;br /&gt;&amp;nbsp; ◦ 객실&amp;nbsp;문을&amp;nbsp;열&amp;nbsp;때마다&amp;nbsp;**키카드(Access&amp;nbsp;Token)**를&amp;nbsp;사용합니다.&lt;br /&gt;3. 키카드&amp;nbsp;만료&amp;nbsp;(Access&amp;nbsp;Token&amp;nbsp;만료):&lt;br /&gt;&amp;nbsp; ◦ 키카드가&amp;nbsp;갑자기&amp;nbsp;작동하지&amp;nbsp;않습니다.&amp;nbsp;(API&amp;nbsp;호출&amp;nbsp;시&amp;nbsp;401&amp;nbsp;에러&amp;nbsp;발생)&lt;br /&gt;4. 키카드&amp;nbsp;재발급&amp;nbsp;(토큰&amp;nbsp;재발급):&lt;br /&gt;&amp;nbsp; ◦ 다시&amp;nbsp;프론트&amp;nbsp;데스크로&amp;nbsp;가서&amp;nbsp;**예약&amp;nbsp;확인증(Refresh&amp;nbsp;Token)**을&amp;nbsp;보여주고,&amp;nbsp;새로운&amp;nbsp;**키카드(Access&amp;nbsp;Token)**를&amp;nbsp;발급받습니다.&lt;br /&gt;&amp;nbsp; ◦ 이&amp;nbsp;과정이&amp;nbsp;프론트엔드의&amp;nbsp;Axios&amp;nbsp;Interceptor를&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;5. 체크아웃&amp;nbsp;(로그아웃):&lt;br /&gt;&amp;nbsp; ◦ 프론트&amp;nbsp;데스크에서&amp;nbsp;체크아웃을&amp;nbsp;하면,&amp;nbsp;호텔&amp;nbsp;시스템은&amp;nbsp;**예약&amp;nbsp;확인증(Refresh&amp;nbsp;Token)**을&amp;nbsp;무효화(DB에서&amp;nbsp;삭제)합니다.&amp;nbsp;이제&amp;nbsp;더&amp;nbsp;이상&amp;nbsp;새로운&amp;nbsp;키카드를&amp;nbsp;발급받을&amp;nbsp;수&amp;nbsp;없습니다.&lt;/p&gt;</description>
      <category>그 외 ... (정리해야함)/그 외</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/648</guid>
      <comments>https://cornswrold.tistory.com/648#entry648comment</comments>
      <pubDate>Mon, 9 Mar 2026 21:45:08 +0900</pubDate>
    </item>
    <item>
      <title>70%를 넘어서: 인간 역할의 극대화</title>
      <link>https://cornswrold.tistory.com/647</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;30%의 작업
&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;생성현 AI는 코드를 생성할 수 있지만, 엔지니어링에서는 종종 어려움을 격음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동화는 프로그래밍 방식에 변화를 가져왔지만, 숙련된 개발자가 필요한 &lt;code&gt;이유&lt;/code&gt;는 변하지 않았음&lt;/li&gt;
&lt;li&gt;AI가 가장 잘하는 70%를 활용하면서, 나머지 30%에 필요한 핵심 역량과 통찰력을 강화하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시니어 개발자 : AI를 활용한 경험의 극대화&lt;/h2&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;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;AI와 함께 효율적인 페어 프로그래밍을 하자&lt;/li&gt;
&lt;li&gt;기준을 높게 잡아, 코드가 조직이 원하는 품질, 보안, 성능 기준을 충족하는지 확인해야 함&lt;/li&gt;
&lt;li&gt;주니어 개발자가 생성형 AI 출력을 그대로 가져오지 않고 먼저 확인해 보도록 지시해야 함&lt;/li&gt;
&lt;/ul&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;경험이 적은 팀원들에게 효과적인 AI 사용법과 변치않는 모범 사례를 코칭하는 것&lt;/li&gt;
&lt;li&gt;주니어 개발자가 직접 코드를 검토하고 테스트하는 방법을 가르치는 것이 중요. AI의 코드를 철저히 테스트하는 방법을 시연하여 모범을 보이고, 출력된 코드를 의심하고 검증하는 문화를 장려해야 함&lt;/li&gt;
&lt;li&gt;시니어 개발자라면 팀에서 이러한 규범을 앞장서서 지켜야 함.&lt;/li&gt;
&lt;/ul&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;문제 도메인을 깊이 이해하도록 지속적으로 투자해야 함.&lt;/li&gt;
&lt;/ul&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;AI가 일부 코딩을 담당하면, 시니어 개발자는 엔지니어링의 인간적인 측면에 더 많은 에너지를 쏟을 수 있음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이해관계자와의 소통, 설계 회의 리드, 기술과 비즈니스 전략을 조율하는 판단 등 핵심 업무에 집중할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아키텍처 로드맵 작성, 채택할 툴의 평가, 조직의 AI 코딩 가이드라인 정의 같은 작업에 참여. 이는 AI가 수행하지 못하는 작업
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리더로서의 존재감을 키워 당신이 (다른 도구로 대체 가능한) 코드 생성기가 아니라 팀을 이끄는 데 없어서는 안 될 기술 리더임을 어필하는 편이 좋음중금 개발자: 적응과 전문화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;엣지 케이스와 모호한 부분을 처리하는 방법을 배워야 함.&lt;/li&gt;
&lt;li&gt;지속가능한 기술은 비판적 사고와 통찰력. 엣지 케이스를 나열하고, 실패를 예상 하며, 이를 코드나 디자인에서 해결하는 것이 필요&lt;/li&gt;
&lt;/ul&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;인간의 이해가 여전히 중요한 복잡한 도메인에서 전문성을 키워야 함
&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;/li&gt;
&lt;/ul&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;모니터링 및 가시성&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;/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;AI가 생성하는 코드가 늘어날 수록 코드를 철저히 리뷰하고 테스트하는 능력이 더욱 중요해짐
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모두가 코드 테스트와 리뷰에 대해 훨씬 더 중요하게 생각해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 중요한 논리 경로를 검증하고 정적 분석이나 린터를 사용해야 함.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;품질 기준에 맞지 않는 AI 제공 코드는 주저하지 말고 재작성해야 함&lt;/li&gt;
&lt;li&gt;품질 보증은 AI에게 맡겨서는 안됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;비즈니스 요구사항을 기술적 솔루션으로 번역하는 능력은 구현 시간이 줄어들수록 가치가 높아짐&lt;/li&gt;
&lt;li&gt;프로덕트 매니저, 디자이너 및 기타 이해관계자와 효과적으로 소통할 수 있는 개발자는 점점 더 좋은 평가를 받을 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 설계 및 아키텍처 전문성 강화&lt;/li&gt;
&lt;li&gt;시스템 사고를 연습하고 전체적인 배경 정보에 대한 이해 유지&lt;/li&gt;
&lt;li&gt;비판적 사고, 문제 해결 능력, 미래 예측 능력을 연마&lt;/li&gt;
&lt;li&gt;전문 도메인에서의 전문성 구축&lt;/li&gt;
&lt;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;AI 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI 노트/AI 끄적끄적</category>
      <category>30%</category>
      <category>AI</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/647</guid>
      <comments>https://cornswrold.tistory.com/647#entry647comment</comments>
      <pubDate>Mon, 9 Feb 2026 15:10:17 +0900</pubDate>
    </item>
    <item>
      <title>효과적인 AI 보조 워크플로</title>
      <link>https://cornswrold.tistory.com/646</link>
      <description>&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;30% 정도는 결국 인간의 심층적인 전문 지식 필요. (70%문제)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생산성이 뛰어난 주니어 개발자? 취급 당함 ㅜ. 속도와 열정이 뛰어난 한편 '약물에 취한 듯'&lt;/li&gt;
&lt;li&gt;경험이 부족한 개발자는 '문제가 없어 보이네!' 라는 안일한 판단으로 그대로 사용하면 심각한 재앙을 일으킬 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;무엇&lt;/code&gt;을 만들고, &lt;code&gt;어떻게&lt;/code&gt; 구성할지, &lt;code&gt;왜&lt;/code&gt; 그렇게 해야 하는지 결정하는 창의적이고 분석적인 사고는 여전히 인간의 몫&lt;/li&gt;
&lt;li&gt;시니어 개발자는 AI 제안을 비판 없이 수용하지 않고, 포괄적인 오류 처리와 AI가 놓친 엣지 케이스 처리를 추가하면서 AI의 출력을 조정하고 제안해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 보조 워크플로 안티패턴&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 결과물을 쉽게 받아들이다보면, 같으로는 완성된 것처럼 보이지만 실제 환경에서는 쉽게 무너지는 '모래성 코드'가 만들어질 수 있음&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;모든지 과도한 의존은 좋지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실용적인 AI 보조 워크플로 패턴&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI를 효과적으로 사용하기 위해선
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI 초안 작성 : AI 모델이 초기 코드를 생성 후, 개발자가 이를 정제하고 리팩터링 및 테스트 함&lt;/li&gt;
&lt;li&gt;AI와 페어 프로그래밍 : 개발자와 AI가 끊임없이 대화하며 긴밀한 피드백과 빈번한 코드 리뷰, 그리고 최소한의 컨텍스트만을 주고받음&lt;/li&gt;
&lt;li&gt;AI를 사용한 검증 : 개발자가 초기 코드를 직접 작성한 후 AI를 활용해 코드의 검증, 테스트 및 개선 작업을 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI의 초안 작성&lt;/h3&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;일관된 스타일(린팅 규칙과 프로젝트 관례)를 정하고, 그 가이드라인을 AI 툴에 반영&lt;/li&gt;
&lt;li&gt;스타일 선호도나 예시 코드를 제공하여 출력 결과를 유도하도록 허용&lt;/li&gt;
&lt;li&gt;README에 'AI 활용 팁'섹션을 만드는 것도 좋아 보임&lt;/li&gt;
&lt;/ul&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;프롬프트가 좋은 결과를 냈다면, 해당 프롬프트를 이슈 트래커나 팀 채팅 등을 통해 다른 개발자와 공유하면 시간 절약과 일관성 유지에 도움이 됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;agents.md 같은 것인가? xxx.md (AI가 context를 유지할 수 있도록?)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;팀에서는 AI 사용을 개발 방향 설정에 있어 자연스러운 부분으로 취급해야 함. 팀에서 AI를 잘 활용할 수 있도록 개발자들의 사용 사례, 효과적인 기법, 주의 사항(예: '코파일럿이 X 작업에 구식 라이브러리 사용을 제안하니 조심') 등을 공유하는 것이 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI와 페어 프로그래밍&lt;/h3&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;인간-AI : 코드를 빠르게 만들어내고 지루한 작업들을 효율적으로 처리&lt;/li&gt;
&lt;li&gt;인간-인간 : 복잡한 문제를 풀 때 진가를 발휘. 미묘한 부분까지 이해하고 함께 아이디어를 내야하는 상황에서 유용(코드에 대한 오너쉽과 팀 차원의 이해도를 높여줌)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI 페어 프로그래밍 모범 사례
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업별로 AI 세션 시작 (컨텍스트가 명료해짐)&lt;/li&gt;
&lt;li&gt;명확하고 간결한 프롬프트&lt;/li&gt;
&lt;li&gt;많은 확인과 커밋 (AI가 만든 코드를 주기적으로 화인하고 테스트하면 문제를 빨리 찾을 수 있음)&lt;/li&gt;
&lt;li&gt;긴밀한 피드백 루프 유지 (잘못된 부분을 고쳐주고 가이드)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;빠르고 직관적인 AI 보조 엔지니어링은 창의적인 실험과 엔지니어링 원칙 사이의 균형을 잡아주는 확실한 가이드가 없으면 쉽게 혼돈에 빠질 수 있음&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;원하는 내용을 구체적이고 명확하게 전달&lt;/li&gt;
&lt;li&gt;항상 AI의 출력을 원래 의도와 맞는지 검증&lt;/li&gt;
&lt;li&gt;AI를 주니어 개발자처럼 감독&lt;/li&gt;
&lt;li&gt;AI에 생각을 맡기지 않는다. 자신의 능력을 확장하는 데 사용&lt;/li&gt;
&lt;li&gt;코드를 생성하기 전에 미리 규칙을 정한다.&lt;/li&gt;
&lt;li&gt;AI 사용을 개발 관련 소통의 자연스러운 부분으로 생각한다.&lt;/li&gt;
&lt;li&gt;AI 변경 사항을 분리할 수 있도록 별도의 커밋을 수행한다.&lt;/li&gt;
&lt;li&gt;인간이 작성했든 AI가 생성했든 모든 코드는 반드시 리뷰를 거친다.&lt;/li&gt;
&lt;li&gt;이해하지 못하는 코드는 머지하지 않는다.&lt;/li&gt;
&lt;li&gt;문서, 주석, 아키텍처 결정 기록(Architecture decision record, ADR)을 우선시 한다.&lt;/li&gt;
&lt;li&gt;효과적인 프롬프트를 공유하고 재사용 한다.&lt;/li&gt;
&lt;li&gt;정기적으로 성찰하고 이터레이터를 개선한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI 노트/AI 끄적끄적</category>
      <category>AI</category>
      <category>실용적 AI</category>
      <category>워크플로</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/646</guid>
      <comments>https://cornswrold.tistory.com/646#entry646comment</comments>
      <pubDate>Mon, 2 Feb 2026 15:48:25 +0900</pubDate>
    </item>
    <item>
      <title>AI와의 효과적인 소통</title>
      <link>https://cornswrold.tistory.com/645</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI에 의도를 어떻게 전달하느냐에 따라 코드 품질이 달라짐&lt;/li&gt;
&lt;li&gt;좋은 프로픔프트를 작성하는 것을 &lt;code&gt;프롬프트 엔지니어링(prompt engineering)&lt;/code&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;바이브 코딩 : 사용자와 AI 모델의 대화&lt;/li&gt;
&lt;li&gt;프롬프트 엔지니어링 : AI의 언어를 완벽하게 구사하는 기술
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI를 두고 자연어로 프로그래밍하는 것&lt;/li&gt;
&lt;li&gt;컴파일러의 출력물이 입력된 소스 코드의 품질에 따라 다르듯이, AI의 출력물도 프롬프트의 품질에 따라 달라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;&quot;웹 사이트를 만들어주세요&quot; -&amp;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;프로그래밍 언어&lt;/li&gt;
&lt;li&gt;프레임워크&lt;/li&gt;
&lt;li&gt;라이브러리&lt;/li&gt;
&lt;/ul&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;고객 기록 목록을 받아 sort_by_lastname(customers)라는 파이썬 함수를 작성하세요. 각 고객기록엔 first_name과 last_name필드가 있으며, 이 함수는 last_name을 ..블라블라&lt;/li&gt;
&lt;/ul&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;사용 언어 또는 환경에 대한 언급&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;엣지 케이스나 제약 조건을 생각하고 프롬프트에 포함하세요.&lt;/li&gt;
&lt;/ul&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;'그거'라는 단어 사용하지 않기&lt;/li&gt;
&lt;/ul&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;코드만 제공하세요. 설명은 필요하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;소설 쓰기&lt;/li&gt;
&lt;li&gt;AI가 세부사항을 제대로 채울것이라는 가정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중요한 사항(쓰레드 안전성, 특수 문자 처리)가 있다면 반드시 언급. 언급되지 않은 문제를 처리하지 못할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;개방적인 '창의적'프롬프트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 분석하는 코드를 작성하세요. -&amp;gt; 숫자 목록의 평균과 표준편차를 계산하는 코드를 작성하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반복적인 정제: AI와의 피드백 반복&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;프롬프트 입력 -&amp;gt; AI 출력 -&amp;gt; 검토 -&amp;gt; 프롬프트 수정 -&amp;gt; AI 출력 -&amp;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;사고의 연쇄(Chain-of-thought)프롬프트
&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;12개 중에서 4개를 선택하는 방법의 수를 단계별로 알려주세요.&lt;/li&gt;
&lt;/ul&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;AI가 특정한 정체성이나 역할을 맡도록 요청해 AI 응답 방식에 영향을 미침&lt;/li&gt;
&lt;li&gt;당신은 파이썬 강사입니다. 다음 코드를 설명한 후, 더이 파이썬답게 수정하세요.&lt;/li&gt;
&lt;/ul&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;필요한 작업 설명 외에 추가적인 컨텍스트나 정보를 제공하는 것을 의미&lt;/li&gt;
&lt;/ul&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;Cot를 시연하는 퓨샷 프롬프트 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시니어 엔지니어의 입장에서 문제를 단계별로 생각한 다음, 코드를 제공하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'작동하지 않습니다. 수정해 주세요' 또는 'X를 하는 A를 작성하세요'같은 프롬프트는 세부 사항이 부족한 표현&lt;/li&gt;
&lt;li&gt;컨텍스트와 구체적인 내용을 추가해야 함. 질문을 했는데 AI가 너무 뻔함 답('X를 확인해 보셨나요?')을 내놓는다면, 더 많은 세부 사항 (오류 메시지, 코드 발췌,ㅡ 예상 결과 vs 실제 결과 등)을 포함해서 다시 질문해 보는게 좋다.&lt;/li&gt;
&lt;/ul&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;AI가 한번에 너무 많은 일을 하도록 지시하는 경우, AI는 과부하에 빠진다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AI가 시킨대로 시도할 수 있지만 결과가 엉망이거나 불완전할 가능성이 높고 시킨 일 전체를 못할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사용자 인증이 포함된 완전한 노드JS 애플리케이션을 생성하고, 프론트엔드는 리액트로 구성하며, 배포 스크립트를 작성하세요.&lt;/li&gt;
&lt;li&gt;해결책은 작업을 분할하는 것. 한 번에 한 가지 일만 해야 함.&lt;/li&gt;
&lt;/ul&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;많은 정보를 제공하면서 질문이나 필요한 내용을 구체적으로 제시하지 않는 경우&lt;/li&gt;
&lt;li&gt;요청을 명확하게 작성해야함. 텍스트만 제공하고 질문이나 지시가 없으면, AI는 잘못된 가정을 하게 됨&lt;/li&gt;
&lt;/ul&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;이 함수를 더 빠르게 만드세요. -&amp;gt; 이 함수를 최적화하여 선형 시간에 실행되도록 하세요. (현재 시간복잡도는 n^2 입니다.)&lt;/li&gt;
&lt;li&gt;코드를 깔끔하게 작성하세요. -&amp;gt; 전역 변수를 제거하고 대신 클래스를 사용하도록 리팩터링하세요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AI의 설명이나 출력 무시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'리액트 클래스 컴포넌트를 사용하고 계신가요. 아니면 함수형 컴포넌트를 사용하고 계신가요?' 이런 응답을 무시하지 않고 답변하도록 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일관성 부족
&lt;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;'나는'(1인칭)과 '사용자는'(3인칭)을 번걸아 쓰거나, 가상 코드와 실제 코드를 구분 없이 섞어서 제시하는 경우 프롬프트마다 다른 방법을 제안할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;'위의 함수'나 '이전 출력'이라고 입력한다면, 잘못된 접근을 할 수 있음. 코드를 다시 붙여 넣거나 리팩터링할 메서드를 직접 언급하는 편이 안전&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI 노트/AI 끄적끄적</category>
      <category>AI</category>
      <category>프롬프트</category>
      <category>프롬프트 엔지니어링</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/645</guid>
      <comments>https://cornswrold.tistory.com/645#entry645comment</comments>
      <pubDate>Sun, 25 Jan 2026 21:46:37 +0900</pubDate>
    </item>
    <item>
      <title>[Ubuntu] Asia/Seoul 타임존 설정</title>
      <link>https://cornswrold.tistory.com/644</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ec2인스턴스를 실행했더니 UTC +0000 으로 설정되어 있어서 timezone 설정이 필요하였음&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;$ timedatectl # 현재 설정된 시간대 확인
               Local time: Sat 2025-01-25 12:10:36 UTC
           Universal time: Sat 2025-01-25 12:10:36 UTC
                 RTC time: Sat 2025-01-25 12:10:35
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

$ timedatectl list-timezones # Asia/Seoul 시간대 목록에서 찾기
$ sudo timedatectl set-timezone Asia/Seoul
               Local time: Sat 2025-01-25 21:11:00 KST
           Universal time: Sat 2025-01-25 12:11:00 UTC
                 RTC time: Sat 2025-01-25 12:11:00
                Time zone: Asia/Seoul (KST, +0900)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no
$ date
Sat Jan 25 21:11:21 KST 2025&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;시스템에 설정된 timezone을 생각 못하고 LocalDateTime.now()를 사용하면 생각했던 시간이 안나올 수도 있다.&lt;/li&gt;
&lt;li&gt;시스템 timezone 을 변경하지 않고, jdk를 설치했다면 jvm은 자동적으로 시스템 timezone을 따라가게 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우, log 시간도 timezone을 따라가기 때문에 시간확인이 불편할 수 있음&lt;/li&gt;
&lt;li&gt;JDK 시스템 전역 설정 파일로 timezone을 변경할 수도 있고, application 실행시 -Duser.timezone=Asia/Seoul을 이용하여 시간대를 설정할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍 노트/인프라</category>
      <category>EC2</category>
      <category>ubuntu</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/644</guid>
      <comments>https://cornswrold.tistory.com/644#entry644comment</comments>
      <pubDate>Sat, 25 Jan 2025 21:53:33 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Embedded Kafka Broker를 활용한 Kafka 로컬 테스트</title>
      <link>https://cornswrold.tistory.com/643</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Embedded Kafka 를 사용하게 된 이유&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 Kafka 서버에 의존하지 않고 안정적이고 독립적으로 테스트 하는 방법이 필요하였음&lt;/li&gt;
&lt;li&gt;타부서에서 발행하는 message를 consume하여 처리하기로 하였는데, 개발계에 카프카 서버가 셋팅되지 않음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가만든 @KafkaListener 가 정상 동작하는지 검증필요&lt;/li&gt;
&lt;/ul&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;@KafkaListener를 통하여 consume &amp;amp; 비즈니스 로직 실행 코드는 완성된 상태&lt;/li&gt;
&lt;li&gt;일정상 개발계 kafka 셋팅이 완료되었을 때 테스트 지원이 힘든 상황이여 테스트 코드로 검증을 대신해야함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역직렬화(deserializer)가 정상 동작 하는지&lt;/li&gt;
&lt;li&gt;여러 consumer configuration이 정상 동작 하는지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Embedded Kafka를 활용한 간단 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고) &lt;a href=&quot;https://docs.spring.io/spring-kafka/reference/3.1/testing.html&quot;&gt;https://docs.spring.io/spring-kafka/reference/3.1/testing.html&lt;/a&gt;&lt;br /&gt;참고) &lt;a href=&quot;https://www.baeldung.com/spring-boot-kafka-testing&quot;&gt;https://www.baeldung.com/spring-boot-kafka-testing&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 추가&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;testImplementation(&quot;org.springframework.kafka:spring-kafka-test&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@EmbeddedKafka설정 및 테스트 코드 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AccountDeletionListener 간단 설명 : 계정 탈회 Message가 발행되면, 계정에 등록된 카드들을 모두 삭제&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AccountDeletionListenerTest.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EmbeddedKafka 정의한 주소로 in-memory kafka가 실행되며 해당 kafka를 이용하여 produce/consume 테스트가 가능&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootTest(
    classes = [MockTestKafkaConfig::class, KafkaConfig::class], // 테스트에 필요한 bean들만 등록하기 위해 지정
)
@EmbeddedKafka(
    partitions = 1,
    brokerProperties = [&quot;listeners=PLAINTEXT://\${kafka.bootstrap-servers}&quot;], // spring properties 활용 가능
    kraft = false,
)
@DirtiesContext
class DeletionListenerTest(
  private val kafkaTemplate: KafkaTemplate&amp;lt;String, Any&amp;gt;,
  private val broker: EmbeddedKafkaBroker, // EmbeddedKafkaBroker 생성 후 주입된다.
  @Value(&quot;\${topics.account-deletion.topic}&quot;) private val topic: String
): FreeSpec() {

  override fun extensions(): List&amp;lt;Extension&amp;gt; = listOf(SpringExtension)

  @Autowired
  lateinit var cardLoadService: CardLoadService

  @Autowired
  lateinit var cardDeletionService: CardDeletionService

  @Autowired
  lateinit var retryService: CardRetryService

  val logger = KotlinLogging.logger {}

  init { 
    beforeSpec {
      // mocking 에 필요한 로직을 추가한다.
      // 생략
    }

    &quot;메시지를 consume 하면, 등록된 카드를 모두 삭제한다. 만약 카드 삭제에 실패하면 retry 테이블에 적재한다.&quot; {
      // given
      val servers = broker.brokersAsString
      logger.debug { &quot;Kafka Broker is running at : $servers &quot; } // Kafka Broker is running at : 127.0.0.1:9092

      every { cardLoadService.selectList(any()) } answers {
        lisfOf(
          Card(&quot;key1&quot;, &quot;사용자Seq&quot;), // 삭제 성공할 카드
          Card(&quot;key2&quot;, &quot;사용자Seq&quot;)  // 삭제 실패할 카드
        )
      }

      val deletionRequestCard = slot&amp;lt;Card&amp;gt;()
      every { deletionService.delete(capture(card)) } answers {
        if (&quot;key1&quot; == deletionRequestCard.captured.key) {
          throw IllegalStateException(&quot;카드 삭제 실패&quot;)
        }
        &quot;사용자Seq&quot;
      }

      // when
      val payload = DeletionPayload(&quot;사용자Seq&quot;, ....)
      kafkaTemplate.send(topic, payload)


      // then
      await().atMost(3, TimeUnit.SECONDS).untilAsserted {
        verify(exactly = 1) {
          retryService.saveDeletionRetryTarget(
            // 삭제 실패된 카드에 대한 retry 호출 검증
            SaveRequestDeletionRetryTarget(Card(&quot;key2&quot;, &quot;사용자Seq&quot;), &quot;카드 삭제 실패&quot;)
          )
        }
      }
      verify(exactly = 2) { cardDeletionService.delete(any()) } // 카드 삭제 요청 2번 호출
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;KafkaConfig.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaListener 설정(Consumer 관련)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
@EnableKafka
class KafkaConfig {
  @Bean
  fun kafkaListenerContainerFactory(
    @Value(&quot;\${kafka.bootstrap-servers}&quot;) bootStrapServers: String,
    kafkaConsumerProperties: KafkaConsumerProperties // consumer 설정(생략 함)
  ): KafkaListenerContainerFactory&amp;lt;ConcurrentMessageListenerContainer&amp;lt;String, Any&amp;gt;&amp;gt; =
    ConcurrentKafkaListenerContainerFactory&amp;lt;String, Any&amp;gt;().apply {
      consumerFactory =
        DefaultKafkaConsumerFactory(
            nPayInternal2kafkaConsumerProperties.toConfigMap(bootStrapServers),
            StringDeserializer(),
            JsonDeserializer(Any::class.java).apply {
                addTrustedPackages(&quot;*&quot;)
                ignoreTypeHeaders()
            },
        )
      containerProperties.ackMode = ContainerProperties.AckMode.MANUAL_IMMEDIATE // Acknowledgement.acknowledge() 호출시 즉시 커밋
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MockTestKafkaConfig.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Message Produce, Mocking에 필요한 bean 정의&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class MockTestKafkaConfig {

  @Bean
  fun producerFactory(
    @Value(&quot;\${kafka.bootstrap-servers}&quot;) bootStrapServers: String,
  ): ProducerFactory&amp;lt;String, Any&amp;gt; = 
    DefaultKafkaProducerFactory(
      mapOf(
        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootStrapServers,
        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java,
        ProducerConfig.ACKS_CONFIG to &quot;all&quot;,
        ProducerConfig.LINGER_MS_CONFIG to 0, // 메시지 전송하기 전 기다리는 시간
      ),
    )

  @Bean
  fun kafkaTemplate(factory: ProducerFactory&amp;lt;String, Any&amp;gt;): KafkaTemplate&amp;lt;String, Any&amp;gt; =
      KafkaTemplate(factory).apply {
        setProducerListener(produceListener())
      }

  ...

  @Bean
  fun cardLoadService(): CardLoadService = mockk()

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트러블 슈팅&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EmbeddedKafka 실행시 지정된 port로 실행이 안된다?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@EmbeddedKafka 설정 시, borkerProperties를 설정하면 해당 port로 borker가 실행되어야 하는데 random한 포트로 실행이 된다.&lt;br /&gt;KRaft mode시에는 port 지정이 안된다고 하여 &lt;code&gt;kraft = false&lt;/code&gt; 옵션을 추가하여 broker를 실행했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EmbeddedKafkaKraftBroker -&amp;gt; random port&lt;/li&gt;
&lt;li&gt;EmbeddedKafkaZKBroker -&amp;gt; port 지정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 이슈 : &lt;a href=&quot;https://github.com/spring-projects/spring-kafka/issues/2936&quot;&gt;https://github.com/spring-projects/spring-kafka/issues/2936&lt;/a&gt;&lt;br /&gt;kraft란 무엇인가? : &lt;a href=&quot;https://devocean.sk.com/blog/techBoardDetail.do?ID=165711&amp;amp;boardType=techBlog&quot;&gt;https://devocean.sk.com/blog/techBoardDetail.do?ID=165711&amp;amp;boardType=techBlog&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;test코드를 실행했는데, consume이 되지 않고 테스트가 종료된다?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 consume이 되기전, 테스트 코드가 끝나는 현상이 있어 delay를 시키는 로직이 필요했다.&lt;br /&gt;awaitility 라이브러리를 사용하면 비동기 코드를 테스트할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;await().atMost(3, TimeUnit.SECONDS).untilAsserted { // 최대 3초 동안 해당 로직이 실행됬는지 검증
    // listener 처리 대기
    verify(exactly = 1) {
        mockRetryService.saveDeletionRetryTarget(
            // 삭제 실패된 카드에 대한 retry 저장
            SaveRequestDeletionRetryTarget(
                deletionFailureCard,
                &quot;카드 삭제 실패 ${deletionFailureCard.key}&quot;,
            ),
        )
    }
    verify(exactly = 2) { mockDeletionService.delete(any()) } // 카드 삭제 요청 2번 호출
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 대기 시간(atMost)를 지정하고 polling을 통해서 untilAsserted 에 지정한 코드가 호출됬는지 검증이 가능하다.&lt;br /&gt;내부적으로 polling 시간을 지정할 수 있는 것으로 보인다.&lt;br /&gt;awaitility : &lt;a href=&quot;http://www.awaitility.org/&quot;&gt;http://www.awaitility.org/&lt;/a&gt;&lt;/p&gt;</description>
      <category>프로그래밍 노트/kafka</category>
      <category>embeddedkafka</category>
      <category>Kafka</category>
      <category>local</category>
      <category>카프카</category>
      <category>카프카 테스트</category>
      <category>테스트</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/643</guid>
      <comments>https://cornswrold.tistory.com/643#entry643comment</comments>
      <pubDate>Wed, 18 Dec 2024 17:32:20 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 가비지 컬렉터 (Garbage Collector), 두 번째 이야기</title>
      <link>https://cornswrold.tistory.com/642</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cornswrold.tistory.com/506&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2021.01.22 - [프로그래밍 노트/JAVA] - [JAVA] 가비지 컬렉터(GarabageCollector,GC)&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GC(Garbage Collector) 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC는 메모리 관리 기법 중 하나로 시스템에 있는 모든 객체의 수명을 정확히는 몰라도 런타임에 대신 객체를 추적하여 쓸모없는 객체를 알아서 제거해준다. 이렇게 자동 회수한 메모리는 깨끗이 비우고 재활용할 수 있게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, c나 c++ 처럼 개발자가 직접 메모리를 해제하는 것이 아닌 JVM 에서는 GC가 메모리를 자동 해제해 준다.&lt;/li&gt;
&lt;li&gt;프로그래머가 저수준 세부를 일일이 신경쓰지 않는 대가로 저수준 제어권을 포기한다는 사상이 바로 자바 관리 방식의 핵심이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;동적으로 할당했던 메모리 영역(Heap 영역)에서 필요 없게 된 메모리 객체(참조가 없는 객체)를 알아서 제거해준다.&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++) GC는 다음 두 가진 기본 원칙을 준수해야 한다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++) GC의 단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GC가 실행되는 동안 다른 동작을 멈추기 때문에 오버헤드가 발생한다. G(STW - Stop-The-World)&lt;/li&gt;
&lt;li&gt;언제 GC가 메모리를 해제하는 정확히 알 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GC 알고리즘 - Mark And Sweep&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;마크 앤 스위프&lt;/code&gt;는 가비지 수집에 사용되는 기초적인 알고리즘이다. 사용되지 않는 객체를 &lt;code&gt;식별(Mark)&lt;/code&gt;하고 &lt;code&gt;제거(Sweep)&lt;/code&gt;하는 동작을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;MakrAndSweep.gif&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAbJoo/btsKmpUlQpi/dOIpKWZisOMuA7Xl7BH1ck/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAbJoo/btsKmpUlQpi/dOIpKWZisOMuA7Xl7BH1ck/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAbJoo/btsKmpUlQpi/dOIpKWZisOMuA7Xl7BH1ck/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bAbJoo/btsKmpUlQpi/dOIpKWZisOMuA7Xl7BH1ck/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;600&quot; height=&quot;317&quot; data-filename=&quot;MakrAndSweep.gif&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GC Root로부터 살아 있는 객체를 찾는다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;살아있는 객체는 대부분 깊이-우선(detpth-first)방식으로 찾는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이렇게 찾은 객체마다 마크 비트를 세팅한다. (Mark)&lt;/li&gt;
&lt;li&gt;마크 비트가 세팅되지 않은 객체를 찾아 메모리를 회수한다.(객체 삭제) (Sweep)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC Root란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Mark And Sweep&lt;/code&gt; 은 루트로 부터 해당 객체에 접근이 가능한지가 해제의 기준이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runtime data areas 보면 GC의 Root는 Heap 메모리 영역을 참조하는 &lt;code&gt;stack&lt;/code&gt;, &lt;code&gt;native method stack&lt;/code&gt; , &lt;code&gt;method area&lt;/code&gt; 영역이 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-10-22 오후 7.52.37.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxgFim/btsKlZPdIcM/8IiZQtCS0YxTfMTQ4QpwGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxgFim/btsKlZPdIcM/8IiZQtCS0YxTfMTQ4QpwGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxgFim/btsKlZPdIcM/8IiZQtCS0YxTfMTQ4QpwGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxgFim%2FbtsKlZPdIcM%2F8IiZQtCS0YxTfMTQ4QpwGK%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;600&quot; height=&quot;376&quot; data-filename=&quot;스크린샷 2024-10-22 오후 7.52.37.png&quot; data-origin-width=&quot;1088&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;b&gt;GC 루트 종류&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;스택 프레임(stack frame)&lt;/li&gt;
&lt;li&gt;JNI (Native Method Stack)&lt;/li&gt;
&lt;li&gt;레지스터(hoisted된 변수)&lt;/li&gt;
&lt;li&gt;(JVM 코드 캐시에서) 코드 루트&lt;/li&gt;
&lt;li&gt;전역 객체&lt;/li&gt;
&lt;li&gt;로드된 클래스의 메타데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;약한 세대별 가설&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약한 세대별 가설(Weak Generational Hypothesis)은 JVM메모리 관리의 이론적 근간을 형성한다.&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;대부분의 객체는 금방 접근 불가 상태(Unreachable)이 된다.&lt;/li&gt;
&lt;li&gt;오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.&lt;/li&gt;
&lt;li&gt;즉, 객체는 대부분 일회성으로 생성되며 메모리에 오랫동안 남아있는 경우는 드물다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;lsquo;세대 카운트(generational count) - 객체가 지금까지 무사 통과한 가비지 수집 횟수&amp;rsquo;를 센다&lt;/li&gt;
&lt;li&gt;큰 객체를 제외한 나머지 객체는 에덴(Eden)공간에 생성한다. 여기서 살아남은 객체는 다른 곳으로 옮긴다.&lt;/li&gt;
&lt;li&gt;장수했다고 할 정도로 충분히 오래 살아남은 객체들은 별도의 메모리 영역(올드(old))에 보관한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;가비지를 수집하는 힙은, 단명 객체를 쉽고 빠르게 수집할 수 있게 설계해야 하며, 장수 객체와 단명 객체를 완전히 떼어놓는게 가장 좋다&lt;/code&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가비지 컬렉션 종류&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Parallel GC&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK 8의 Default GC&lt;/li&gt;
&lt;li&gt;Serial GC의 알고리즘과 동일하지만 멀티 스레드로 작업이 수행되어 GC 수행 시간이 짧음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Old 영역에서는 싱글 스레드로 수행되며, Youing 영역에서만 멀티 스레드로 수행&lt;/li&gt;
&lt;/ul&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;스크린샷 2024-10-26 오후 9.22.32.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HnkWQ/btsKlmK9hP8/Aw6ayXli0DmK1CpdHRJ1qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HnkWQ/btsKlmK9hP8/Aw6ayXli0DmK1CpdHRJ1qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HnkWQ/btsKlmK9hP8/Aw6ayXli0DmK1CpdHRJ1qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHnkWQ%2FbtsKlmK9hP8%2FAw6ayXli0DmK1CpdHRJ1qk%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;550&quot; height=&quot;375&quot; data-filename=&quot;스크린샷 2024-10-26 오후 9.22.32.png&quot; data-origin-width=&quot;730&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java -XX:+UseParallelGC -jar api.jar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;G1 GC (Garbage First)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK7 부터 정식 포함되어 사용 가능해졌고, JDK9 부터 디폴트 GC로 지정&lt;/li&gt;
&lt;li&gt;기존 Heap 구조와는 다르게 Young/Old 영역을 명확하게 구분하지 않고 논리적 단위인 Region 이라는 개념을 도입해서 사용.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Humonogous : Region 크기의 50%를 초과하는 큰 객체를 저장히기 위한 공간&lt;/li&gt;
&lt;li&gt;Available/Unused : 아직 사용되지 않은 Region&lt;/li&gt;
&lt;/ul&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;스크린샷 2024-10-26 오후 9.34.56.png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m0KKk/btsKlHHYAcT/gowzTWthtEVO2etKWZrd0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m0KKk/btsKlHHYAcT/gowzTWthtEVO2etKWZrd0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m0KKk/btsKlHHYAcT/gowzTWthtEVO2etKWZrd0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm0KKk%2FbtsKlHHYAcT%2FgowzTWthtEVO2etKWZrd0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;427&quot; data-filename=&quot;스크린샷 2024-10-26 오후 9.34.56.png&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리가 많이 차 있는 Region을 우선적으로 GC. 전체가 아닌 Region을 나눠 탐색하고 Region별로 GC가 실행됨&lt;/li&gt;
&lt;li&gt;Heap크기가 클수록 잘 동작하며 Gabage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로 GC 빈도가 줄어듬&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java -XX:+UseG1GC -jar api.jar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ZGC (Z Garbage Collector)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JDK15에서 release&lt;/li&gt;
&lt;li&gt;G1의 Region 영역처럼, ZPage라는 영역으로 메모리를 나눠 관리하는 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Zpage는 2MB 배수로 동적 운영됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Heap 크기가 증가하여도 STW시간이 10초를 넘지 않음&lt;/code&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;스크린샷 2024-10-26 오후 9.50.43.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v0gZV/btsKj12nArB/TrD4G3BWMY3eJjHcRQy3D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v0gZV/btsKj12nArB/TrD4G3BWMY3eJjHcRQy3D1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v0gZV/btsKj12nArB/TrD4G3BWMY3eJjHcRQy3D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv0gZV%2FbtsKj12nArB%2FTrD4G3BWMY3eJjHcRQy3D1%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;550&quot; height=&quot;345&quot; data-filename=&quot;스크린샷 2024-10-26 오후 9.50.43.png&quot; data-origin-width=&quot;1060&quot; data-origin-height=&quot;664&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;low-latency가 필요한 애플리케이션에 적당하며 STW 시간을 최소화하려는 특성상 CPU 사용량이 증가할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar api.java&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GC 로깅 및 튜닝&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC 로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 로깅은 오버헤드가 거의 없는 것이나 다름없으니 주요 JVM 프로세스는 항상 로깅을 켜놓아야 한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;필수 GC 플래그
-Xloggc:gc.log // GC 이벤트에 로깅할 파일을 지정
-XX:+PrintGCDetails // GC 이벤트 세부 정보를 로깅
-XX:+PrintTenuringDistribution // 툴링에 꼭 필요한, 부가적인 GC 이벤트 세부 정보를 추가
-XX:+PrintGCTimeStamps // GC 이벤트 발생 시간을 (VM 시작 이후 경과한 시간을 초 단위로) 출력
-XX:+PrintGCDateStamps // GC 이벤트 발생 시간을 (벽시계 시간 기준으로) 출력

GC 로그 순환 플래그 (데브옵스를 비롯한 운영팀과 협의해서 수립)
-XX:+UseGCLogFileRotation // 로그 순환 기능을 켠다.
-XX:+NumberOfGCLogFiles=&amp;lt;n&amp;gt; // 보관 가능한 최대 로그파일 개수를 설정
-XX:+GCLogFileSize=&amp;lt;size&amp;gt; // 순환 직전 각 파일의 최대 크기를 설정&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC 기본 튜닝&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;-Xms&amp;lt;size&amp;gt; // 힙 메모리의 최소 크기를 설정
-Xmx&amp;lt;size&amp;gt; // 힙 메모리의 최대 크기를 설정
-XX:MaxPermSize=&amp;lt;size&amp;gt; // 펌젠 메모리 최대 크기를 설정 (자바 7 이전)
-XX:MaxMetaspaceSize=&amp;lt;size&amp;gt; // 메타스페이스 메모리의 최대 크기를 설정 (자바 8 이후)&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;일반적으로 시스템 성능을 최대한 활용하기 위해 전체 물리 메모리의 50-75% 정도를 JVM 힙에 할당하는 것이 권장됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OS와 다른 프로세스가 안정적 동작을 하기 위해 약 4GB 정도는 남기는 것이 좋음&lt;/li&gt;
&lt;li&gt;만약 16GB 머신이라면,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;50% 할당(8GB) - 적은 힙 메모리로도 충분한 경우&lt;/li&gt;
&lt;li&gt;75% 할당(12GB) - 메모리 집약적인 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;너무 큰 힙은 GC에 부하를 주어 오히려 성능 저하를 유발하며, 반대로 너무 작은 힙은 자주 GC가 발생하여 애플리케이션 성능에 영향을 줌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC&quot;&gt;https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘- -총정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://catsbi.oopy.io/56acd9f4-4331-4887-8bc3-e3e50b2f3ea5&quot;&gt;https://catsbi.oopy.io/56acd9f4-4331-4887-8bc3-e3e50b2f3ea5&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로그래밍 노트/JAVA</category>
      <category>garabe</category>
      <category>GarbageCollector</category>
      <category>GC</category>
      <category>java</category>
      <category>jdk</category>
      <category>가비지컬렉터</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/642</guid>
      <comments>https://cornswrold.tistory.com/642#entry642comment</comments>
      <pubDate>Sat, 26 Oct 2024 22:36:05 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] RedisCluster 연동 &amp;amp; RedisTemplate 적용 (feat. AutoConfiguration)</title>
      <link>https://cornswrold.tistory.com/641</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis 의존성 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring-boot-starter-data-redis 의존성 추가&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;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;SpringBoot에서 외부 라이브러리 버전을 관리하기에, spring-boot-starter-data-redis 를 추가하면 해당 SpringBoot버전에서 관리하는 외부라이브러리가 자동으로 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 현재 SpringBoot 3.2.4 버전을 사용하여 lettuce 6.3.2.RELEASE 버전이 자동 추가된 것을 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-10-22 오전 10.44.28.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIn9YW/btsKe68HLPc/V7FvM9KlYUvND1u27EK4Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIn9YW/btsKe68HLPc/V7FvM9KlYUvND1u27EK4Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIn9YW/btsKe68HLPc/V7FvM9KlYUvND1u27EK4Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIn9YW%2FbtsKe68HLPc%2FV7FvM9KlYUvND1u27EK4Ak%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;700&quot; height=&quot;143&quot; data-filename=&quot;edited_스크린샷 2024-10-22 오전 10.44.28.png&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리되는 버전은 &lt;a href=&quot;https://docs.spring.io/spring-boot/docs/3.2.4/reference/html/dependency-versions.html&quot;&gt;https://docs.spring.io/spring-boot/docs/3.2.4/reference/html/dependency-versions.html&lt;/a&gt; 에서 확인 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RedisAutoConfiguration&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; Autoconfigure 에서 Spring Data's Redis를 support 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisAutoConfiguration.class 를 살펴보면 Bean이 자동 설정되는 조건들을 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(RedisConnectionDetails.class)
    PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties) {
        return new PropertiesRedisConnectionDetails(properties);
    }

    @Bean
    @ConditionalOnMissingBean(name = &quot;redisTemplate&quot;)
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate&amp;lt;Object, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate&amp;lt;Object, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }

}&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;RestTemplate은 RedisConnectionFactory Bean이 존재하면 자동 등록된다.&lt;/li&gt;
&lt;li&gt;RedisConnectionFactory는 RedisAutoConfiguration이 @Import하고 있는 LettuceConnectionConfiguration.class 에서 자동 생성해주지만 우리는 RedisCluster관련한 설정이 추가적으로 필요하므로 재정의하도록 한다.&lt;/li&gt;
&lt;li&gt;LettuceConnectionFactory(RedisConnectionFactory 구현체)를 custom bean으로 등록하면 RedisTemplate은 자동등록 되므로 해당 설정만 Configuration에 추가해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LettuceConnectionFactory Bean 정의(feat. RedisCluster)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisCluster 연동시, fail-over를 위한 설정들이 몇 가지 필요하다. 설정 관련해서는 아래 글들을 참고했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://meetup.nhncloud.com/posts/379&quot;&gt;https://meetup.nhncloud.com/posts/379&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redis/lettuce/wiki/Client-options&quot;&gt;https://github.com/redis/lettuce/wiki/Client-options&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class RedisConfig {
    @Bean
    fun redisConnectionFactory(
        @Value(&quot;\${redis.node-list}&quot;) redisNodeList: String,
        @Value(&quot;\${redis.connection-timeout-millis}&quot;) connectionTimeoutMillis: Long,
        @Value(&quot;\${redis.operation-timeout-millis}&quot;) operationTimeoutMillis: Long,
        @Value(&quot;\${redis.request-queue-size}&quot;) requestQueueSize: Int,
    ): RedisConnectionFactory {
        val nodeList: List&amp;lt;String&amp;gt; = redisNodeList.split(&quot;,&quot;).toList()

        // (1) Socket Option
        val socketOptions =
            SocketOptions
                .builder()
                .connectTimeout(Duration.ofMillis(connectionTimeoutMillis))
                .keepAlive(true)
                .build()

        // (2) Cluster topology refresh
        val clusterTopologyRefreshOptions =
            ClusterTopologyRefreshOptions
                .builder()
                .dynamicRefreshSources(true) // 모든 Redis 노드로부터 topology 정보 획득
                .enableAllAdaptiveRefreshTriggers() // Redis 클러스터 모든 이벤트(MOVE, ACK)등에 대해 topology 갱신
                .enablePeriodicRefresh(Duration.ofSeconds(30)) // 토폴로지 갱신 텀
                .refreshTriggersReconnectAttempts(1) // 한 번이라도 실패가 일어나면 토폴로지 갱신
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)) // 토폴로지 업데이터가 일어난 경우 30초 이내 재갱신 x
                .build()

        // (3) Cluster client
        val clusterClientOptions =
            ClusterClientOptions
                .builder()
                .pingBeforeActivateConnection(true) // 커넥션을 사용하기 위하여 PING 명령어를 사용하여 검증 default : true
                .autoReconnect(true) // 자동 재접속 옵션 default : true
                .socketOptions(socketOptions)
                .topologyRefreshOptions(clusterTopologyRefreshOptions)
                .maxRedirects(nodeList.size)
                .requestQueueSize(requestQueueSize)
                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
                .build()

        // (4) Lettuce Client
        val lettuceClientConfig =
            LettuceClientConfiguration
                .builder()
                .commandTimeout(Duration.ofMillis(operationTimeoutMillis))
                .clientOptions(clusterClientOptions)
                .build()

        val clusterConfig =
            RedisClusterConfiguration(nodeList).apply {
                this.maxRedirects = nodeList.size
            }

        // (5) LettuceConnectionFactory
        return LettuceConnectionFactory(clusterConfig, lettuceClientConfig).apply {
            this.validateConnection = false // 연결 검증 비활성화 default : false
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RedisTemplate Bean이 자동 등록됬는지 테스트&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootTest(
    classes = [RedisConfig::class],
)
@ImportAutoConfiguration(RedisAutoConfiguration::class)
class RedisConfigTest(
    private val applicationContext: ApplicationContext,
) : FreeSpec({
        extensions(SpringExtension)

        &quot;RedisTemplate이 AutoConfiguration에 의해 자동 등록 된다.&quot; {
            applicationContext.getBean(&quot;redisTemplate&quot;) shouldNotBe null
            applicationContext.getBean(&quot;stringRedisTemplate&quot;) shouldNotBe null
        }
    })&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RedisTemplate이 정상 동작하는지 테스트&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@SpringBootTest(
    classes = [RedisConfig::class],
)
@ImportAutoConfiguration(RedisAutoConfiguration::class)
class RedisTemplateTest(
    private val redisTemplate: RedisTemplate&amp;lt;Any, Any&amp;gt;,
) : FreeSpec({
        extensions(SpringExtension)

        beforeTest {
            redisTemplate.delete(&quot;key&quot;)
        }

        &quot;redisTemplate Test&quot; - {
            &quot;get() - key 미존재시 null 반환&quot; {
                redisTemplate.opsForValue().get(&quot;key&quot;) shouldBe null
            }

            &quot;delete() - key 존재시 true 반환&quot; {
                redisTemplate.opsForValue().set(&quot;key&quot;, &quot;value&quot;)

                redisTemplate.delete(&quot;key&quot;) shouldBe true
            }

            &quot;delete() - key 미존재시 false 반환&quot; {
                redisTemplate.delete(&quot;key&quot;) shouldBe false
            }

            &quot;setIfAbsent() - key 존재시 false(이미 존재) 반환&quot; {
                redisTemplate.opsForValue().set(&quot;key&quot;, &quot;value&quot;)
                val result = redisTemplate.opsForValue().setIfAbsent(&quot;key&quot;, &quot;newValue&quot;)

                result shouldBe false
                redisTemplate.opsForValue().get(&quot;key&quot;) shouldBe &quot;value&quot;
            }

            &quot;setIfAbsent() - key 미존재시 true 반환&quot; {
                val result = redisTemplate.opsForValue().setIfAbsent(&quot;key&quot;, &quot;value&quot;)

                result shouldBe true
                redisTemplate.opsForValue().get(&quot;key&quot;) shouldBe &quot;value&quot;
            }
        }
    })&lt;/code&gt;&lt;/pre&gt;</description>
      <category>프로그래밍 노트/SPRING BOOT</category>
      <category>autocnfiguration</category>
      <category>redis cluster</category>
      <category>redis template</category>
      <category>springboot</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/641</guid>
      <comments>https://cornswrold.tistory.com/641#entry641comment</comments>
      <pubDate>Tue, 22 Oct 2024 11:34:25 +0900</pubDate>
    </item>
    <item>
      <title>테스트 코드 작성 이유</title>
      <link>https://cornswrold.tistory.com/640</link>
      <description>&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;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동화된 테스트? 회귀 테스트?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크고 복잡한 시스템에서는 수정한 코드가 어디에 어떤 영향을 줄지 완벽하게 분석하기란 &lt;code&gt;불가능&lt;/code&gt;에 가깝다.&lt;br /&gt;테스트 코드가 없는 조직에서는 코드를 수정한 후 일부 기능을 &lt;code&gt;수동 테스트&lt;/code&gt;로 진행하게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안하는 것보다는 낫겠지만, 다양한 경우의 수를 확인하기 어렵다.&lt;/li&gt;
&lt;li&gt;현실적으로 모든 경우의 테스트를 할 수 없기에 버그를 놓칠 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 자동으로 실행할 수 있는 테스트 코드가 있다면 수정한 코드가 발생시키는 문제를 빨리 찾을 수 있다. 이렇게 되면 QA 담당자를 기다릴 필요도 없고 개발자가 직접 실행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 커버리지가 높다고 하여 모든 문제를 없앨 수 없지만 테스트를 통과한 코드는 문제가 없다는 것을 &lt;code&gt;확신&lt;/code&gt;할 수 있게 된다. 테스트 검증하는 범위가 넓을수록 코드를 수정하는데 자신감이 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커버리지를 지나치게 높이기 위해 가치 없는 코드를 만드느라 시간을 낭비하지 말자. 70~80% 수준이면 적당한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;회귀 테스트(regression test)&lt;/code&gt;는 수정한 기능 외에 다른 기능에 영향이 없는지 검증하는 테스트를 말한다. 자동화된 테스트가 있으면 회귀 테스트를 쉽게 진행할 수 있으며, QA 담당자는 신규 기능 및 몇 가지 필수 기능만 점검하면 된다. (자동화된 테스트가 많은 범위를 검증하기 때문)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 주도 개발(TDD)?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD는 테스트 코드를 먼저 만드는 개발 방식이다. 구현할 대상에 대한 테스트 코드를 먼저 만들고 테스트 코드르 통과시킬 만큼 구현을 진행한다.&lt;br /&gt;TDD를 하면 기능을 설계하는 데 도움을 준다. 또한 테스트 코드를 작성하면 업무에 대한 이해도 함께 높일 수 있다. 테스트를 만들려면 테스트할 대상의 기능을 실행할 수 있어야하기에 아래와 같은 것들을 미리 정해야 하는데, 이는 기능을 설계하는 과정과 동일하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스 이름&lt;/li&gt;
&lt;li&gt;메서드 이름&lt;/li&gt;
&lt;li&gt;메서드 파라미터 타입&lt;/li&gt;
&lt;li&gt;리턴 타입, 익셉션 타입(예외)&lt;/li&gt;
&lt;li&gt;의존 대상과 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 주도 개발과 생산성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성하는 자체가 생산성이 떨어지는 일이라 생각할 수 있지만 전체적인 개발 관점에서 보면 테스트가 주는 이점은 상당히 크다. 하여 테스트 코드 작성 능력을 키워둬야 한다.&lt;br /&gt;&lt;code&gt;개발 시간 = 코드를 작성하는 시간 + 테스트하는 시간 + 디버깅하는 시간&lt;/code&gt;&lt;br /&gt;개발을 완료할 때까지 개발, 테스트, 디버깅을 반복한다. 개발 시간을 줄이려면 코딩하는 시간뿐 아니라 테스트, 디버깅 시간을 줄여야하는데 TDD를 적용하면 이 시간들을 줄일 수 있다.&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;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;수동으로 테스트할 때는 대부분 데이터베이스와 외부 API를 직접 연동한다. 따라서 원하는 상황을 만들려면 데이터베이스에 테스트 데이터를 구성해야하거나 외부 API를 사용할 때는 API 제공 부서에 연락해서 도움을 요청해야 한다. 이런 번거로운 과정은 개발 시간을 증가시키고 개발 자체를 힘겹게 만든다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 코드를 자동화 한다면? TDD를 사용한다면?&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;코드를 작성하는 시점과 테스트 시점 간의 차이가 벌어질수록 문제가 발생했을 때 원인을 찾는데 더 긴 시간이 걸린다. (코드를 다시 읽고 분석해야 하기 때문) TDD는 기능을 구현하자마자 테스트를 실행하기때문에 테스트 직전에 코드를 작성했기 때문에 실해 원인을 빨리 찾을 수 있다. (디버깅 시간 축소)&lt;/li&gt;
&lt;li&gt;TDD는 리팩터링을 포함하기에 코드 구조와 가독성을 개선하는 작업을 함께 진행한다. 이는 미래의 디버깅 시간과 코딩 시간을 줄여주는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;TDD를 진행하면 전체가 아닌 일부 코드만 검증 할 수 있다. 하여 컨트롤러-서비스-모델-리포지터리처럼 범위를 좁혀서 한 번에 하나만 집중할 수 있도록 도와준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테스트 가능성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 코드를 TDD로 진행할 필요는 없다. 개발을 먼저 하고 테스트 코드를 작성해도 되고 TDD처럼 테스트 코드를 먼저 작성해도 된다. 궁극적으로는 &lt;code&gt;테스트 가능성(testability)&lt;/code&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 가능성을 염두해둔다면 구현을 분리해서 코드를 작성하게 되는데, 외부 연동 구현같은 경우 손쉽게 mocking 처리할 수 있게 된다.(외부 환경에 대한 의존이 줄어 전체가 아닌 일부만 빠르게 테스트할 수 있다.)&lt;br /&gt;=&amp;gt; 테스트 가능성을 높이는 과정에서 자연스럽게 역할이 분리되게 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고) 육각형 개발자&lt;/p&gt;</description>
      <category>프로그래밍 노트/TEST를 해보자</category>
      <category>TDD</category>
      <category>Test</category>
      <category>Test Code</category>
      <category>testability</category>
      <category>테스트</category>
      <category>테스트 코드</category>
      <author>깡냉쓰</author>
      <guid isPermaLink="true">https://cornswrold.tistory.com/640</guid>
      <comments>https://cornswrold.tistory.com/640#entry640comment</comments>
      <pubDate>Mon, 14 Oct 2024 22:46:12 +0900</pubDate>
    </item>
  </channel>
</rss>