<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>junhkang의 개발 블로그</title>
    <link>https://junhkang.tistory.com/</link>
    <description>  산업의 역군에서 백앤드 개발과 DevOps 전략을 이끌고 있습니다. 새로운 기술을 탐색하고 개발팀 문화를 강화하는 데 큰 열정을 가지고 있습니다.</description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 05:50:49 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>junhkang</managingEditor>
    <image>
      <title>junhkang의 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/6616983/attach/2a870f4c803a4926882f300eff69b60a</url>
      <link>https://junhkang.tistory.com</link>
    </image>
    <item>
      <title>[Spring] 스프링부트와 HikariCP를 활용한 Connection Pool 설정 및 최적화</title>
      <link>https://junhkang.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;몇 년 전 운영 프로젝트 설정 시 자세하게 봤던 내용이지만, 트래픽이 대폭 증가하고, DBMS에 연결된 프로젝트와 모듈이 늘어남에 따라 재설정을 위해 개념을 다시 정리하게 되었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기본적인 데이터베이스 연결과정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DB Connection 열기&lt;/b&gt;- 데이터베이스 드라이버를 사용하여 데이터베이스 서버와의 연결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP 소켓 열기&lt;/b&gt; - 데이터베이스 전송을 위해 TCP 소켓을 생성하고 데이터베이스 서버와 통신채널을 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 통신 수행&lt;/b&gt; - 생성된 소켓을 통해 SQL 쿼리를 전송하고 데이터를 Read / Write&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB연결 닫기&lt;/b&gt; - 데이터 통신이 완료되면 데이터베이스와의 연결을 종료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP 소켓 닫기&lt;/b&gt; - 사용한 TCP 소켓을 닫아 통신 채널 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 어플리케이션은 클라이언트의 HTTP 요청이 들어오면 스레드를 생성한다. 각 요청 시 DB서버로부터 데이터를 얻기 위해서 DB에 지속적으로 접근하는 작업이 필요하다. 스프링부트를 예로 들면, DB에 직접 연결하는 경우, JDBC 드라이버는 애플리케이션 시작 시 한번 로드되고, 사용자 요청 시마다 새로운 connection 객체 생성하여 데이터베이스에 연결한 후 종료되어야 한다. 이렇게 사용자 요청 시 매번 connection 객체를 생성/연결/종료해야 한다면 굉장히 비효율적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Connection Pool&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. Connection Pool이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정량의 컨넥션 객체를 미리 생성하여 pool에 저장하고,&amp;nbsp;클라이언트에서 요청이 오면 connection 객체를 빌려주고, 임무가 완료되면 다시 객체를 반납 받아서 pool에 다시 저장하는 기법이다. 일반적인 pool 패턴에서 사용되는 방식으로, 자원을 효율적으로 재사용함으로써 성능 최적화할 수 있는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링부트에선 컨테이너 구동시 일정 양의 connection 객체를 생성하여, 클라이언트 요청에 따라 데이터베이스 접근이 필요하면, connection pool에서 connection객체를 받아와 작업 후, 컨넥션풀에 다시 객체를 반납한다.&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;1255&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3YUL/btsL31kewUk/32ai746EpSQSsLxg0HdflK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3YUL/btsL31kewUk/32ai746EpSQSsLxg0HdflK/img.png&quot; data-alt=&quot;출처 : https://steady-coding.tistory.com/564&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3YUL/btsL31kewUk/32ai746EpSQSsLxg0HdflK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft3YUL%2FbtsL31kewUk%2F32ai746EpSQSsLxg0HdflK%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;1255&quot; height=&quot;648&quot; data-origin-width=&quot;1255&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://steady-coding.tistory.com/564&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. HikariCP Connection Pool 획득 과정&lt;/h4&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;li&gt;전체 컨넥션 목록 중 사용가능한 컨넥션 존재여부 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hikariCP에선 Thread가 연결요청시 컨넥션풀에 이전에 사용했던 컨넥션을 확인하고(3) 이를 우선반환하는 특징이 있다. 사용가능한 컨넥션이 없을 경우(4) HandOffQueue를 폴링 하면서 다른 Thread가 컨넥션을 반납하는 것을 기다린다. 지정 타임아웃 시간까지 대기하다가 시간이 만료되면 예외를 던진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 획득해서 사용완료하면, 해당 객체를 컨넥션 풀에 반납한다. 컨넥션 풀은 사용내용을 관리하고, HandOffQueue에 반납된 컨넥션을 객체를 삽입한다. 해당 큐를 폴링하던 스레드는 컨넥션을 획득하고 작업을 처리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Connection Pool 장점&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;/li&gt;
&lt;li&gt;연결이 끝난 컨넥션 객체를 재사용해서 객체 생성비용 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Connection Pool 설정시 주의사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1. Connection Pool 증가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-1-1. 과도한 데이터베이스 접속 발생 시 대기 문제&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;gt; 결국, 컨넥션 풀을 크게설정하면, 메모리 소모가 큰 대신 사용자 대기시간이 줄어들지만 컨넥션 풀이 작으면 그만큼 대기시간이 길어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-1-2. 컨넥션 풀 증가의 한계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 컨넥션풀이 늘어난다고 무조건 성능이 좋아지는건아니다. 컨넥션의 생성 주체는 결국 Thread이기 때문에 같이 고려해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2. Connection Pool &amp;amp; Thread Pool&lt;/h4&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;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-3. Connection Pool, Thread Pool의 동시 증가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-1. 메모리 소모 및 자원 비효율성&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;size18&quot;&gt;&lt;b&gt;4-3-2. 콘텍스트 스위칭 오버헤드&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 수가 증가함에 따라 운영체제가 스레드 간 콘텍스트 스위칭을 더 자주 하게 된다. CPU 리소스를 소모하게 됨으로 스레드 수가 과도하게 많아지면 전체 시스템 성능이 저하될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4-3-3. 데이터베이스 서버의 스레드 관리 부담&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;h3 data-ke-size=&quot;size23&quot;&gt;5. 적정 Connection Pool&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-1. HikariCP 공식문서 적정 Connection Pool&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히카리 CP 기준 공식 문서에 따르면 다음과 같이 적정 컨넥션 풀을 정의하고 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1 connections = ((core_count) * 2) + effective_spindle_count)&lt;/blockquote&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;core_count -&lt;/b&gt; 현재 사용하는 서버 환경에서의 CPU 개수, CPU코어 수는 동시에 처리할 수 있는 스레드의 기본 수용능력을 나타냄
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;core_count * 2 : 공식 문서에서는 스레드 풀의 스레드 수를 CPU코어 수의 2배로 설정하는 것을 권장, I/O 작업으로 인한 블로킹 시간 동안 다른 스레드가 CPU를 활용할 수 있게 하기 위함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;effective_spindle_count -&lt;/b&gt;&amp;nbsp; 기본적으로 디비 서버가 관리할 수 있는 동시 I/O요청수
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;하드 디스크 하나는 spindle 하나를 갖는다 spindle은 디스크의 물리적 회전축을 의미&lt;/li&gt;
&lt;li&gt;디스크가 16개인 경우, 시스템은 동시에 16개의 I/O처리 가능&lt;/li&gt;
&lt;li&gt;이는 데이터베이스 서버가 효율적인 디스크 사용을 위해, 동시에 처리할 수 있는 I/O 작업의 수를 제한한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5-2. 서버 환경 예시&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 코어 수 : 8개&lt;/li&gt;
&lt;li&gt;디스크 스핀들 수 : 16개 (디스크 16개 기준)&lt;/li&gt;
&lt;li&gt;적정 컨넥션 풀 크기 계산 :&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(8 &amp;times;2)+16=32&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 서버의 적정 컨넥션 풀의 크기는 32이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. HikariCP 옵션&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;auto-commit (default: true)&lt;/b&gt;&lt;br /&gt;커넥션 종료 또는 풀에 반환 시 트랜잭션을 자동으로 커밋할지 결정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;connection-timeout (default: 30000ms)&lt;/b&gt;&lt;br /&gt;풀에서 커넥션을 얻기 위해 기다리는 최대 시간을 설정, 초과 시 SQLException 던짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;idle-timeout (default: 600000 ms)&lt;/b&gt;&lt;br /&gt;풀에 미사용 커넥션을 유지하는 시간을 지정, minimum-idle보다 작게 설정 시에만 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max-lifetime (default: 1800000 ms)&lt;/b&gt;&lt;br /&gt;풀에서 커넥션이 유지될 수 있는 최대 수명 시간을 설정, 사용 중이지 않을 때만 제거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;minimum-idle (default: maximum-pool-size)&lt;/b&gt;&lt;br /&gt;풀에서 항상 유지할 최소 커넥션 수를 설정, 최적의 성능을 위해 maximum-pool-size와 동일하게 설정 권장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;maximum-pool-size (default: 10)&lt;/b&gt;&lt;br /&gt;풀에서 유지할 수 있는 최대 커넥션 수를 설정, 데이터베이스 부하에 따라 조정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pool-name (default: auto-generated)&lt;/b&gt;&lt;br /&gt;풀의 이름을 지정하여 로깅이나 JMX 관리 콘솔에 표시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;initialization-fail-timeout (default: 1ms)&lt;/b&gt;&lt;br /&gt;초기 커넥션 생성 실패 시 풀 초기화를 빠르게 실패하도록 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;validation-timeout (default: 5000ms)&lt;/b&gt;&lt;br /&gt;커넥션 유효성 검사를 수행할 때 사용할 타임아웃 시간 지정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;connection-test-query (default: none)&lt;/b&gt;&lt;br /&gt;JDBC4 드라이버 미지원 시 유효한 커넥션인지 확인하기 위한 쿼리 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;read-only (default: false)&lt;/b&gt;&lt;br /&gt;풀에서 커넥션을 획득할 때 읽기 전용 모드로 설정하여 최적화 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;transaction-isolation (default: none)&lt;/b&gt;&lt;br /&gt;java.sql.Connection에 지정할 트랜잭션 격리 수준 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;category (default: none)&lt;/b&gt;&lt;br /&gt;커넥션에서 사용할 카테고리를 결정, 설정 없을 시 JDBC 드라이버 기본값 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;leak-detection-threshold (default: 0ms)&lt;/b&gt;&lt;br /&gt;커넥션 누수 감지를 위해 커넥션 풀에 반환되기 전 허용할 최대 시간 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Statement Cache&lt;/b&gt;&lt;br /&gt;각 커넥션마다 PreparedStatement 캐싱, 데이터베이스 드라이버 설정을 통해 관리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;driver-class-name (default: none)&lt;/b&gt;&lt;br /&gt;특정 드라이버를 명시적으로 설정 시 사용, 지정 시 jdbc-url 반드시 설정정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;registerMbeans (default: false)&lt;/b&gt;&lt;br /&gt;JMX 관리 Beans에 풀을 등록할지 여부 설정&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;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;br /&gt;&lt;a href=&quot;https://steady-coding.tistory.com/564&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://steady-coding.tistory.com/564&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://1-7171771.tistory.com/119&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://1-7171771.tistory.com/119&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://backtony.tistory.com/58&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://backtony.tistory.com/58&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/brettwooldridge/HikariCP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>Connection Pool</category>
      <category>HikariCP</category>
      <category>spring</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/125</guid>
      <comments>https://junhkang.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 30 Jan 2025 16:50:24 +0900</pubDate>
    </item>
    <item>
      <title>[LINUX] SFTP 초기화 오류 : Failed to connect and initialize SSH connection. Message: [Failed to connect SFTP channel</title>
      <link>https://junhkang.tistory.com/124</link>
      <description>&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;jenkins.plugins.publish_over.BapPublisherException:&amp;nbsp;Failed&amp;nbsp;to&amp;nbsp;connect&amp;nbsp;and&amp;nbsp;initialize&amp;nbsp;SSH&amp;nbsp;connection.&amp;nbsp;Message:&amp;nbsp;[Failed&amp;nbsp;to&amp;nbsp;connect&amp;nbsp;SFTP&amp;nbsp;channel.&amp;nbsp;Message&amp;nbsp;[4:&amp;nbsp;Received&amp;nbsp;message&amp;nbsp;is&amp;nbsp;too&amp;nbsp;long:&amp;nbsp;458961709]]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 문제상황&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-01-03 오후 3.32.21.png&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd97sv/btsLCKxGhJQ/KT5KKUDHkmvuupq7nXuh80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd97sv/btsLCKxGhJQ/KT5KKUDHkmvuupq7nXuh80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd97sv/btsLCKxGhJQ/KT5KKUDHkmvuupq7nXuh80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd97sv%2FbtsLCKxGhJQ%2FKT5KKUDHkmvuupq7nXuh80%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;835&quot; height=&quot;100&quot; data-filename=&quot;스크린샷 2025-01-03 오후 3.32.21.png&quot; data-origin-width=&quot;835&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 잘 작동하던 SFTP를 활용한 CI/CD가 갑자기 작동하지 않는다. 에러 메시지에 따르면 약 438mb의 메시지를 받은 상황인데,&amp;nbsp;파일을 실제로 업로드하는 시점이 아닌 SFTP연결 시도를 하는 순간(SSH연결을 설정하고 초기화하는 부분)에도 이렇게 큰 응답을 받는 상황이었다.&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;SSH 접속시, 서버 간 구분을 주기 위해 ~/. bashrc의 설정을 통해 웰컴메시지를 출력하는 부분이 문제였다. (다음 포스트에서 진행한 서버별 웰컴 메시지 등록 부분에서 작업과정 확인 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://junhkang.tistory.com/123&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2024.12.31 - [AWS] - [AWS] Bastion 서버 설정 및 서버 접속 상태 한눈에 구분하기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1735885355933&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;[AWS] Bastion 서버 설정 및 서버 접속 상태 한눈에 구분하기&quot; data-og-description=&quot;1. Bastion 서버란?Bastion&amp;nbsp;서버란&amp;nbsp;클라우드&amp;nbsp;환경,&amp;nbsp;네트워크 환경에서&amp;nbsp;보안&amp;nbsp;게이트웨이&amp;nbsp;역할을&amp;nbsp;하는&amp;nbsp;서버로,&amp;nbsp;외부&amp;nbsp;네트워크에서&amp;nbsp;private&amp;nbsp;서버로&amp;nbsp;접속할 때&amp;nbsp;보안을&amp;nbsp;강화해 주고&amp;nbsp;접근제어를&amp;nbsp;&quot; data-og-host=&quot;junhkang.tistory.com&quot; data-og-source-url=&quot;https://junhkang.tistory.com/123&quot; data-og-url=&quot;https://junhkang.tistory.com/123&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/0h1U1/hyXSqAEx4K/iaGRvkAYaVbe7IP0aJ9KZK/img.png?width=800&amp;amp;height=238&amp;amp;face=0_0_800_238,https://scrap.kakaocdn.net/dn/bjrHXC/hyXWyqeZYA/mlnDutIidAsbBifoKoXpWK/img.png?width=800&amp;amp;height=238&amp;amp;face=0_0_800_238&quot;&gt;&lt;a href=&quot;https://junhkang.tistory.com/123&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://junhkang.tistory.com/123&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/0h1U1/hyXSqAEx4K/iaGRvkAYaVbe7IP0aJ9KZK/img.png?width=800&amp;amp;height=238&amp;amp;face=0_0_800_238,https://scrap.kakaocdn.net/dn/bjrHXC/hyXWyqeZYA/mlnDutIidAsbBifoKoXpWK/img.png?width=800&amp;amp;height=238&amp;amp;face=0_0_800_238');&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;[AWS] Bastion 서버 설정 및 서버 접속 상태 한눈에 구분하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. Bastion 서버란?Bastion&amp;nbsp;서버란&amp;nbsp;클라우드&amp;nbsp;환경,&amp;nbsp;네트워크 환경에서&amp;nbsp;보안&amp;nbsp;게이트웨이&amp;nbsp;역할을&amp;nbsp;하는&amp;nbsp;서버로,&amp;nbsp;외부&amp;nbsp;네트워크에서&amp;nbsp;private&amp;nbsp;서버로&amp;nbsp;접속할 때&amp;nbsp;보안을&amp;nbsp;강화해 주고&amp;nbsp;접근제어를&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;junhkang.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SFTP클라이언트는 SSH 세션이 시작될떄 특정 초기화 메시지를 받아야 한다. 웹컴메시지가 삽입되면서 클라이언트가 메세저리를 SFTP데이터로 잘못 해석될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 메세지 크기의 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성한 웰컴 메시지는 고작 한 줄이고, kb이하의 단위인데 어떻게 저렇게 큰 메시지 크기를 응답받았을까?&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. 데이터 첫부분의 잘못된 해석&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH 세션 시작될때, 서버로부터 잘못된 형식의 데이터를 받아서 올바르게 해석하지 못하고 잘못된 패킷크기로 계산되는 경우가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 패킷이 0x1 B6 C8 FDD(16진수)로 시작하면, 이를 10진수로 변환한 값이 458961709가 된다. 웰컴 메시지가 이런 종류의 데이터를 포함하거나 데이터의 첫 부분을 잘못 해석한 경우 이런 식으로 계산될 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2. 버퍼오염&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웰컴 메세지와 SFTP초기화 데이터가 섞이면서 클라이언트 버퍼가 오염될 수 있다. 이 경우 클라이언트는 비정상적인 크기의 메시지 읽었다고 판단한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근본적으로는 의미없는 웰컴 메시지, 서버 첫 접속 시의 자동화된 문구 출력 자체를 없애는 것이 좋겠지만, 꼭 써야 한다면&lt;/p&gt;
&lt;pre id=&quot;code_1735885062888&quot; class=&quot;lua&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;if [[ &quot;$SSH_TTY&quot; ]]; then
    echo &quot;Welcome to the server!&quot;
fi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 웰컴 메세지가 비인터렉티브 세션 (SFTP)에서는 출력되지 않도록 수정하여 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Linux</category>
      <category>SFTP</category>
      <category>ssh</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/124</guid>
      <comments>https://junhkang.tistory.com/124#entry124comment</comments>
      <pubDate>Fri, 3 Jan 2025 15:38:03 +0900</pubDate>
    </item>
    <item>
      <title>[AWS] Bastion 서버 설정 및 서버 접속 상태 한눈에 구분하기</title>
      <link>https://junhkang.tistory.com/123</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Bastion 서버란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bastion&amp;nbsp;서버란&amp;nbsp;클라우드&amp;nbsp;환경,&amp;nbsp;네트워크 환경에서&amp;nbsp;보안&amp;nbsp;게이트웨이&amp;nbsp;역할을&amp;nbsp;하는&amp;nbsp;서버로,&amp;nbsp;외부&amp;nbsp;네트워크에서&amp;nbsp;private&amp;nbsp;서버로&amp;nbsp;접속할 때&amp;nbsp;보안을&amp;nbsp;강화해 주고&amp;nbsp;접근제어를&amp;nbsp;구현해 준다.&amp;nbsp;외부&amp;nbsp;사용자가&amp;nbsp;특정&amp;nbsp;IP에서만&amp;nbsp;Bastion&amp;nbsp;서버에&amp;nbsp;접속하도록&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있으며,&amp;nbsp;private&amp;nbsp;서버들은&amp;nbsp;Bastion&amp;nbsp;서버를&amp;nbsp;통해서&amp;nbsp;트래픽을&amp;nbsp;철저하게&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;이번&amp;nbsp;포스트에선&amp;nbsp;Bastion서버의&amp;nbsp;설정&amp;nbsp;방법과,&amp;nbsp;다중&amp;nbsp;서버를&amp;nbsp;Bastion&amp;nbsp;서버에서&amp;nbsp;관리할&amp;nbsp;시&amp;nbsp;접속상태를&amp;nbsp;한눈에&amp;nbsp;구분할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방법에&amp;nbsp;대해&amp;nbsp;알아보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Bastion 서버 설정 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1.aws 인스턴스 생성&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;t2 micro~t3 micro - 소규모 개발팀&lt;/li&gt;
&lt;li&gt;t3 small&amp;nbsp; - 중간 규모팀&lt;/li&gt;
&lt;li&gt;T3.medium, M5.large - 대규모팀 (다수 개발자, 운영팀)&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-12-31 오후 12.14.14.png&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MMCoM/btsLBLbQoKv/WURQTRJELy3maVIFyHbyG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MMCoM/btsLBLbQoKv/WURQTRJELy3maVIFyHbyG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MMCoM/btsLBLbQoKv/WURQTRJELy3maVIFyHbyG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMMCoM%2FbtsLBLbQoKv%2FWURQTRJELy3maVIFyHbyG0%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;569&quot; height=&quot;170&quot; data-filename=&quot;스크린샷 2024-12-31 오후 12.14.14.png&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 생성 후 bastion 서버로 접속하고자하는 ip를 보안그룹에 등록해 준다. (ssh : 22 port)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 키 파일 포워딩&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bastion 서버를 구성하는 방식은 크게 두가지가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bastion 서버 내에 실제 접속 서버들의 키파일을 보관하여, 각 서버로 접속 시에 사용
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Bastion 서버가 해킹될 경우 모든 운영 서버에 접근 가능&lt;/li&gt;
&lt;li&gt;키파일의 업로드 및 관리 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로컬의 키파일을 forwarding하여 접속 시에 사용 (권장)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 개발자 개개인의 로컬 키파일을 forwarding 하는 사용하는 방식으로 적용해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1735615309088&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eval &quot;$(ssh-agent -s)&quot; &amp;amp;&amp;amp; ssh-add &quot;키파일&quot; &amp;amp;&amp;amp; sudo ssh -A -i &quot;키파일&quot; ec2-user@&quot;IP&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;이렇게 Bastion서버에 접속하면, Bastion 서버에서 로컬 키파일을 forwarding 받아 바로 다른 서버로 ssh 접속이 가능하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 호스트 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bastian 서버에서 매번 ssh 커맨드와 여러 개의 ip를 직접 입력하는 것은 불편하고 위험하다.&lt;br /&gt;그럴 때 Bastion 서버에서 ~/. ssh/config 파일에 호스트를 등록하고 간편하게 사용 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-31 오후 12.23.58.png&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;72&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSQ20T/btsLBug4v4R/SXjXRZqCtxKLk3Qky2LkfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSQ20T/btsLBug4v4R/SXjXRZqCtxKLk3Qky2LkfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSQ20T/btsLBug4v4R/SXjXRZqCtxKLk3Qky2LkfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSQ20T%2FbtsLBug4v4R%2FSXjXRZqCtxKLk3Qky2LkfK%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;183&quot; height=&quot;72&quot; data-filename=&quot;스크린샷 2024-12-31 오후 12.23.58.png&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;72&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;pre id=&quot;code_1735615469761&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh &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;으로 각 서버에 바로 ssh 접속이 가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 서버별 접속상태 구분하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 이렇게 bastion 서버에서 여러 서버로의 접근은 이제 가능하지만, 불편한 점이 있다. bastion에서 수십 개의 서버를 접속한다고 하면, 자동등록된 호스트명으로 접속하더라도 접속이 잘되었는지, ssh 연결이 끊어졌는지 구분이 잘 되지 않아 불편하고 사고로 이어질 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1. PS1환경변수 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔에서 서버 간의 구분을 명확히 하기 위해 PS1 환경변수를 사용해 프롬프트를 변경하거나 표시되는 정보를 추가할 수 있다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bastion 서버의 `~/. bashrc` 또는 `~/. zshrc` 파일에 다음을 추가해 보자&lt;/p&gt;
&lt;pre id=&quot;code_1735615573237&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export PS1=&quot;\[\e[33m\][Bastion: \u@\h \W]\$\[\e[0m\] &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;**`[Bastion:...]`**: 프롬프트에 &quot;Bastion&quot;이라는 표시를 추가.&lt;/li&gt;
&lt;li&gt;**`\u`**: 사용자 이름.&lt;/li&gt;
&lt;li&gt;**`\h`**: 호스트 이름.&lt;/li&gt;
&lt;li&gt;**`\W`**: 현재 디렉터리.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후 source ~/. bashrc로 적용을 하면, bastion 서버에 연결된 상태일 때는 다음과 같이 표시된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-31 오후 12.27.26.png&quot; data-origin-width=&quot;128&quot; data-origin-height=&quot;28&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doGBJm/btsLBwewusL/MCxqfEvifHx44XVX7FMQ3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doGBJm/btsLBwewusL/MCxqfEvifHx44XVX7FMQ3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doGBJm/btsLBwewusL/MCxqfEvifHx44XVX7FMQ3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoGBJm%2FbtsLBwewusL%2FMCxqfEvifHx44XVX7FMQ3k%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;128&quot; height=&quot;28&quot; data-filename=&quot;스크린샷 2024-12-31 오후 12.27.26.png&quot; data-origin-width=&quot;128&quot; data-origin-height=&quot;28&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 각 서버에 PS1환경변수로 그에 맞는 헤더를 설정하면, 쉽게 구분된 상태로 서버 간 접속을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AWS</category>
      <category>AWS</category>
      <category>Bastion</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/123</guid>
      <comments>https://junhkang.tistory.com/123#entry123comment</comments>
      <pubDate>Tue, 31 Dec 2024 12:30:05 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] Quota discoveryengine.googleapis.com/documents exceeded.</title>
      <link>https://junhkang.tistory.com/122</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-17 오후 10.49.42.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5ZwcG/btsLlRjrm9W/eqanDmHW9xEtGiQrvoBVT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5ZwcG/btsLlRjrm9W/eqanDmHW9xEtGiQrvoBVT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5ZwcG/btsLlRjrm9W/eqanDmHW9xEtGiQrvoBVT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5ZwcG%2FbtsLlRjrm9W%2FeqanDmHW9xEtGiQrvoBVT1%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;788&quot; height=&quot;130&quot; data-filename=&quot;스크린샷 2024-12-17 오후 10.49.42.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 문제 발생&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Quota&amp;nbsp;discoveryengine.googleapis.com/documents&amp;nbsp;exceeded.&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP에서 MLOps를 구축 중, RAG를 위한 Discovery engine의 데이터 최대 수량이 초과되었다는 경고를 받게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 리밋은 100만건이지만 더 많은 데이터를 저장하기 위해서는 할당량 수정 요청이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 할당량 수정 요청&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. IAM &amp;amp; 관리자 -&amp;gt; 할당량 및 시스템 한도&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;998&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Hd6zk/btsLmi17EJQ/cK3LRlbPDLO14oKlASZ7K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Hd6zk/btsLmi17EJQ/cK3LRlbPDLO14oKlASZ7K1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Hd6zk/btsLmi17EJQ/cK3LRlbPDLO14oKlASZ7K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHd6zk%2FbtsLmi17EJQ%2FcK3LRlbPDLO14oKlASZ7K1%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;998&quot; data-origin-width=&quot;908&quot; data-origin-height=&quot;998&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 초과된 항목 선택&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당량 수정을 원하는 항목의 맨 오른쪽 메뉴 탭에서 할당량 변경을 선택할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4S0kD/btsLnnuAykZ/hfZ6DpwO4lkHYjdJk8dGS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4S0kD/btsLnnuAykZ/hfZ6DpwO4lkHYjdJk8dGS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4S0kD/btsLnnuAykZ/hfZ6DpwO4lkHYjdJk8dGS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4S0kD%2FbtsLnnuAykZ%2FhfZ6DpwO4lkHYjdJk8dGS1%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;2028&quot; height=&quot;168&quot; data-origin-width=&quot;2028&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. 할당량 변경 신청&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-17 오후 10.51.33.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;1124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tv0pT/btsLmhvkIcK/iVIGQcuM86Xj2PNjonipQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tv0pT/btsLmhvkIcK/iVIGQcuM86Xj2PNjonipQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tv0pT/btsLmhvkIcK/iVIGQcuM86Xj2PNjonipQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftv0pT%2FbtsLmhvkIcK%2FiVIGQcuM86Xj2PNjonipQK%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;1108&quot; height=&quot;1124&quot; data-filename=&quot;스크린샷 2024-12-17 오후 10.51.33.png&quot; data-origin-width=&quot;1108&quot; data-origin-height=&quot;1124&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할당량 변경 신청을 요청하면, 2~3일 이내에 회신을 받을 수 있다. (내 경우 1일 만에 회신을 받았으나, GCP 계정을 만든 지 얼마 되지 않아 승인이 어렵다는 답변을 받아서, 재심사 요청한 상태이다, 최종 결과가 나오면 포스트를 업데이트할 예정)&lt;/p&gt;</description>
      <category>LLM</category>
      <category>Dataflow</category>
      <category>GCP</category>
      <category>LLM</category>
      <category>quota</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/122</guid>
      <comments>https://junhkang.tistory.com/122#entry122comment</comments>
      <pubDate>Tue, 17 Dec 2024 23:02:31 +0900</pubDate>
    </item>
    <item>
      <title>[LLM] Google Cloud Discovery Engine 데이터 스토어 업로드 포맷</title>
      <link>https://junhkang.tistory.com/121</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Google Cloud Discovery Engine이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Cloud Discovery Engine은 구글 클라우드 플랫폼에서 제공하는 검색 및 추천 서비스로, 웹사이트나 앱 내에서 사용자가 원하는 정보를 쉽고 빠르게 찾을 수 있도록 도와주는 서비스로 다음과 같은 특징을 가진다.&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; : 단순 키워드 검색이 아닌, 사용자의 의도에 맞춰 의미를 파악하고 관련 콘텐츠를 제안하는 자연어 처리(NLP) 기반 검색을 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인화된 추천&lt;/b&gt; : 머신러닝 기반 추천엔진을 활용, 취향과 행동 패턴에 맞춘 추천 콘텐츠 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장 가능성&lt;/b&gt; : 다양한 규모와 범위의 콘텐츠에 대해 빠른 검색 및 추천 성능유지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문제상황&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google cloud discovery engine을 활용해 rag를 고도화 시도하던 중, 기존 데이터베이스의 단순 question, answer 필드구조를 csv형태로 업로드하던 방식을 벗어나 metadata, description 필드를 구성하고자 하였다. content필드를 만족하는 데이터 구조를 구성하기 위해 기존 데이터셋을 discovery engine이 요구하는 특정 struct_value, string_value 타입의 jsonl 포맷으로 변환하는 과정에서 여러 에러를 만나게 되었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 기존 question, answer를 유지한 채 content를 변경 (실패)&lt;/h4&gt;
&lt;pre id=&quot;code_1734355965439&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;id&quot;:&quot;doc1&quot;,&quot;content&quot;:{&quot;question&quot;:&quot;어떤 기능에 대한 질문&quot;,&quot;answer&quot;:&quot;기능에 대한 자세한 설명&quot;, &quot;metadata&quot;:&quot;메타데이터&quot;}}&lt;/code&gt;&lt;/pre&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;invalid&amp;nbsp;JSON&amp;nbsp;in&amp;nbsp;google.cloud.discoveryengine.v1 main.Document&amp;nbsp;@&amp;nbsp;content:...&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;near&amp;nbsp;1:42&amp;nbsp;(offset&amp;nbsp;41):&amp;nbsp;no&amp;nbsp;such&amp;nbsp;field:&amp;nbsp;'question'&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;* 그 외 title, text 같은 여러 필드로도 시도해 보았지만, 여전히 제공하지 않는 필드 에러 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2-2.  content를 json 객체가 아닌 문자열로 변경 (실패)&lt;/h4&gt;
&lt;pre id=&quot;code_1734356150852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;id&quot;:&quot;doc1&quot;,&quot;content&quot;:&quot;내용&quot;, &quot;question&quot;:&quot;질문&quot;, &quot;answer&quot;:&quot;답변&quot;}&lt;/code&gt;&lt;/pre&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;invalid JSON in google.cloud.discoveryengine.v1 main.Document, near 1:1 (offset 0): unexpected character: 'j'; expected '{'&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-12-16 오후 10.27.02.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZOKoQ/btsLlrYsvw1/SvcPuxyFLOB13oDP9YPs0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZOKoQ/btsLlrYsvw1/SvcPuxyFLOB13oDP9YPs0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZOKoQ/btsLlrYsvw1/SvcPuxyFLOB13oDP9YPs0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZOKoQ%2FbtsLlrYsvw1%2FSvcPuxyFLOB13oDP9YPs0K%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;1018&quot; height=&quot;138&quot; data-filename=&quot;스크린샷 2024-12-16 오후 10.27.02.png&quot; data-origin-width=&quot;1018&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 정상적인 데이터 포맷&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2-1, 2-2 외에도 기존에 사용하던 document 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;struct_value&lt;/b&gt; : json 객체를 저장하는 경우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;string_value&lt;/b&gt; : 단순 문자열로 저장하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(두 가지 중 한 가지만 사용 가능하다. )&lt;/p&gt;
&lt;pre id=&quot;code_1734356363498&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;id&quot;: &quot;1&quot;, &quot;struct_data&quot;: {&quot;name&quot;: &quot;이름입니다.&quot;, &quot;description&quot;: &quot;설명&quot;, &quot;feature1&quot;: &quot;특징1&quot;, &quot;features2&quot;: [&quot;feature2-1&quot;, &quot;feature2-2&quot;]}}
{&quot;id&quot;: &quot;2&quot;, &quot;struct_data&quot;: {&quot;name&quot;: &quot;이름2입니다.&quot;, &quot;description&quot;: &quot;설명2.&quot;, &quot;feature1&quot;: &quot;특징1&quot;, &quot;features2&quot;: [&quot;feature2-1&quot;, &quot;feature2-2&quot;]}}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;다음과 같이 struct_data로 규격화된 데이터를 명시해 주면 정상적으로 업로드가 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1734356318136&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;Google Cloud Discovery Engine V1 Client - Class Content (0.4.0)&quot; data-og-description=&quot;Google Cloud Discovery Engine V1 Client - Class Content (0.4.0) Stay organized with collections Save and categorize content based on your preferences. Reference documentation and code samples for the Google Cloud Discovery Engine V1 Client class Content.Un&quot; data-og-host=&quot;cloud.google.com&quot; data-og-source-url=&quot;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&quot; data-og-url=&quot;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/niKu7/hyXOd7pTGQ/ATax3hzwPrv7ohhnkWn8kK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cloud.google.com/php/docs/reference/cloud-discoveryengine/0.4.0/V1.Document.Content&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/niKu7/hyXOd7pTGQ/ATax3hzwPrv7ohhnkWn8kK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Google Cloud Discovery Engine V1 Client - Class Content (0.4.0)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google Cloud Discovery Engine V1 Client - Class Content (0.4.0) Stay organized with collections Save and categorize content based on your preferences. Reference documentation and code samples for the Google Cloud Discovery Engine V1 Client class Content.Un&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cloud.google.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 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>LLM</category>
      <category>discovery engine</category>
      <category>GCP</category>
      <category>LLM</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/121</guid>
      <comments>https://junhkang.tistory.com/121#entry121comment</comments>
      <pubDate>Mon, 16 Dec 2024 22:47:00 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 테스트 4 - 얼마나 깊게 테스트 코드를 작성해야 하는가?</title>
      <link>https://junhkang.tistory.com/99</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. HOW DEEP - 얼마나 깊게 테스트 코드를 작성해야 하는가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 테스트 깊이를 결정하는 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 깊이를 설정할 때는 다음과 같은 기준을 고려해야 합니다:&lt;/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;테스트 피라미드(Test Pyramid)&lt;/b&gt;: 테스트 피라미드는 테스트 종류에 따른 계층 구조를 보여줍니다. 일반적으로 단위 테스트가 가장 많고, 그다음으로 통합 테스트, 시스템 또는 E2E(End-to-End) 테스트가 위치합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단위 테스트(Unit Tests)&lt;/b&gt;: 가장 많은 비중을 차지하며, 작은 코드 단위를 독립적으로 테스트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통합 테스트(Integration Tests)&lt;/b&gt;: 여러 모듈이 상호작용하는지 테스트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;E2E 테스트(End-to-End Tests)&lt;/b&gt;: 실제 사용자 관점에서 전체 시스템이 잘 작동하는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;위험 기반 테스트(Risk-Based Testing)&lt;/b&gt;: 비즈니스 중요도와 잠재적 위험 요소에 따라 테스트 우선순위를 설정합니다. 비즈니스에 중요한 기능이나 리스크가 높은 부분에 대한 테스트는 더 깊이 있게 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유스 케이스 기반 테스트&lt;/b&gt;: 핵심 사용자 흐름과 엣지 케이스를 기반으로 테스트를 작성합니다. 실제로 사용자가 자주 사용하는 기능이나 예외적인 상황에서의 동작을 검증하는 것이 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현실적인 제약과 팀 역량 고려&lt;/b&gt;: 모든 부분을 깊이 테스트하는 것은 시간과 리소스 측면에서 비효율적일 수 있습니다. 팀의 역량과 프로젝트 일정 등을 고려하여 테스트 깊이를 조정하는 것이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;526&quot; data-origin-height=&quot;541&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OQEO8/btsJIVTYr2m/UtaI3KLAGXaVKy7jF4QbbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OQEO8/btsJIVTYr2m/UtaI3KLAGXaVKy7jF4QbbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OQEO8/btsJIVTYr2m/UtaI3KLAGXaVKy7jF4QbbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOQEO8%2FbtsJIVTYr2m%2FUtaI3KLAGXaVKy7jF4QbbK%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;526&quot; height=&quot;541&quot; data-origin-width=&quot;526&quot; data-origin-height=&quot;541&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;sub&gt;이미지 출처: &lt;a href=&quot;https://www.headspin.io/blog/the-testing-pyramid-simplified-for-one-and-all&quot;&gt;Mastering the Test Pyramid&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 테스트 커버리지 및 품질 지표 활용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;코드 커버리지&lt;/b&gt;: 코드 커버리지는 테스트가 소스 코드의 얼마나 많은 부분을 실행하는지를 나타내는 지표입니다. 일반적으로 라인 커버리지(Line Coverage)와 브랜치 커버리지(Branch Coverage)를 측정합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라인 커버리지(Line Coverage)&lt;/b&gt;: 테스트가 실행된 코드 라인의 비율.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브랜치 커버리지(Branch Coverage)&lt;/b&gt;: 분기문(예: if/else)의 각 분기를 테스트했는지 확인하는 비율.&lt;/li&gt;
&lt;li&gt;다음은 본 리포지토리 샘플 소스의 테스트 커버리지입니다.&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-origin-width=&quot;702&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ShmaT/btsJGNQS68R/BsDWlbkcKlYuFg3cVqbTZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ShmaT/btsJGNQS68R/BsDWlbkcKlYuFg3cVqbTZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ShmaT/btsJGNQS68R/BsDWlbkcKlYuFg3cVqbTZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FShmaT%2FbtsJGNQS68R%2FBsDWlbkcKlYuFg3cVqbTZk%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;534&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;534&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;&lt;b&gt;커버리지 목표 설정&lt;/b&gt;: 높은 커버리지는 중요하지만, 무조건 100% 커버리지를 목표로 하는 것은 오히려 비효율적일 수 있습니다. 핵심 비즈니스 로직이나 복잡한 부분에 집중하여 테스트 깊이를 설정하는 것이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 오버테스팅의 문제점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;유지보수 비용 증가&lt;/b&gt;: 불필요하게 많은 테스트는 유지보수 부담을 가중시킬 수 있습니다. 코드가 변경될 때마다 테스트도 함께 수정해야 할 수 있으며, 이는 오히려 생산성을 저해할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개발 속도 저하&lt;/b&gt;: 모든 기능을 테스트하려다 보면 개발 속도가 느려질 수 있습니다. 핵심적인 부분에 집중하는 것이 효율적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리소스 낭비&lt;/b&gt;: 지나치게 많은 테스트는 리소스를 낭비하게 만듭니다. 테스트를 효율적으로 유지하는 것이 중요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 효율적인 테스트 범위 설정을 위한 체크리스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심 로직에 대한 집중적인 테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주요 비즈니스 로직을 검증합니다.&lt;/li&gt;
&lt;li&gt;경계 값 및 예외 상황을 철저히 테스트합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 기능 및 사용자 시나리오 검증&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핵심 사용자 흐름을 테스트하여 애플리케이션의 주요 사용 시나리오가 올바르게 작동하는지 확인합니다.&lt;/li&gt;
&lt;li&gt;다양한 사용자 역할 및 권한에 따른 테스트를 수행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 및 예외 처리에 대한 테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 메시지 및 예외 처리 로직을 검증합니다.&lt;/li&gt;
&lt;li&gt;인증 및 권한 부여와 관련된 로직에 대한 테스트를 강화합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 시스템 및 통합 부분 테스트&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API 연동 및 응답 처리에 대한 테스트를 포함합니다.&lt;/li&gt;
&lt;li&gt;데이터베이스 트랜잭션 일관성 검증 등을 고려합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.5 리팩토링과 테스트의 균형 잡기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;필요한 부분에 집중하여 테스트 작성&lt;/b&gt;: 모든 부분을 테스트하기보다는 중요한 부분에 집중하여 테스트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;불필요하거나 중복된 테스트 코드 제거&lt;/b&gt;: 리팩토링 과정에서 불필요하거나 중복된 테스트 코드는 제거해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 코드의 유지보수성과 가독성 확보&lt;/b&gt;: 테스트 코드도 프로덕션 코드처럼 유지보수성과 가독성을 확보해야 합니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>test</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/99</guid>
      <comments>https://junhkang.tistory.com/99#entry99comment</comments>
      <pubDate>Sun, 22 Sep 2024 17:59:07 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 테스트 3 - 언제 테스트 코드를 적용해야 하는가?</title>
      <link>https://junhkang.tistory.com/98</link>
      <description>&lt;h1&gt;3. WHEN - 언제 테스트 코드를 적용해야 하는가?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.1 TDD와 BDD의 개념 및 적용 시점&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;TDD (Test-Driven Development)&lt;/b&gt;: TDD는 테스트를 먼저 작성하고, 그 테스트를 통과할 수 있는 최소한의 코드를 작성하며 개발을 진행하는 방법론입니다. TDD는 테스트를 통해 명확한 요구사항을 확인하고 코드 품질을 보장하는 방법으로 활용됩니다.
&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;: TDD의 기본 개발 사이클은 &lt;code&gt;레드 단계&lt;/code&gt; (실패하는 테스트 작성) &amp;rarr; &lt;code&gt;그린 단계&lt;/code&gt; (테스트를 통과하기 위한 코드 작성) &amp;rarr; &lt;code&gt;리팩터 단계&lt;/code&gt; (코드 정리 및 최적화)로 이루어집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 시점&lt;/b&gt;: 새로운 기능을 개발하거나 기존 코드를 리팩터링 할 때, TDD를 통해 코드의 안정성과 유지보수성을 높일 수 있습니다.&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-origin-width=&quot;720&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8ZSfY/btsJGI3hPak/hn7XUtJ4IEMzRjgs4TFkLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8ZSfY/btsJGI3hPak/hn7XUtJ4IEMzRjgs4TFkLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ZSfY/btsJGI3hPak/hn7XUtJ4IEMzRjgs4TFkLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8ZSfY%2FbtsJGI3hPak%2Fhn7XUtJ4IEMzRjgs4TFkLK%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;478&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;sub&gt;이미지 출처: &lt;a href=&quot;https://medium.com/pilar-2020/applying-test-driven-development-6d6d3af186cb&quot;&gt;Applying Test-Driven Development&lt;/a&gt;&lt;/sub&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;BDD (Behavior-Driven Development)&lt;/b&gt;: BDD는 사용자의 관점에서 시스템의 동작(Behavior)을 기술하고, 그에 맞는 테스트를 작성하여 개발을 진행하는 방법론입니다. BDD는 테스트를 통해 요구사항을 명확히 하고, 기능적인 동작을 검증합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Given-When-Then 패턴&lt;/b&gt;: BDD의 테스트는 &lt;code&gt;Given&lt;/code&gt; (어떤 상황이 주어졌을 때), &lt;code&gt;When&lt;/code&gt; (어떤 동작이 수행되었을 때), &lt;code&gt;Then&lt;/code&gt; (그 결과로 어떤 일이 발생해야 한다)의 패턴을 따릅니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 시점&lt;/b&gt;: 새로운 요구사항이 정의될 때, BDD를 통해 고객의 요구사항을 명확히 이해하고 구현할 수 있습니다.&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-origin-width=&quot;720&quot; data-origin-height=&quot;532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byvFBa/btsJIncaRpr/dskN1DZROKurxNpfukeVDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byvFBa/btsJIncaRpr/dskN1DZROKurxNpfukeVDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byvFBa/btsJIncaRpr/dskN1DZROKurxNpfukeVDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyvFBa%2FbtsJIncaRpr%2FdskN1DZROKurxNpfukeVDk%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;532&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;sub&gt;이미지 출처: &lt;a href=&quot;https://medium.com/@dineshrajdhanapathy/writing-human-readable-tests-a-guide-to-effective-bdd-practices-75a7ab7888bb&quot;&gt;Writing Human-Readable Tests: A Guide to Effective BDD Practices&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.2 기존 코드베이스에 테스트 추가하기&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;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;: 레거시 코드가 테스트하기 어렵다면, 코드의 모듈화 또는 의존성 분리(Dependency Injection) 등을 통해 테스트 가능성을 높이는 작업이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리팩토링 후 테스트 작성&lt;/b&gt;: 테스트를 추가하기 전에, 코드가 너무 복잡하거나 결합도가 높다면 리팩토링을 먼저 진행한 후 테스트를 작성하는 것이 좋습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.3 새로운 기능 개발 시 테스트 작성 시점&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;: TDD 원칙에 따라 새로운 기능을 개발할 때, 테스트를 먼저 작성하고 그 테스트를 통과할 수 있는 최소한의 프로덕션 코드를 작성하는 방식입니다.
&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;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.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;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;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3.5 테스트 작성의 우선순위와 체크리스트&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;핵심 로직 및 비즈니스 규칙&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;</description>
      <category>Spring</category>
      <category>test</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/98</guid>
      <comments>https://junhkang.tistory.com/98#entry98comment</comments>
      <pubDate>Sun, 22 Sep 2024 17:57:13 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 테스트 2 - 테스트 코드를 어떻게 작성해야 하는가?</title>
      <link>https://junhkang.tistory.com/97</link>
      <description>&lt;h1&gt;2. HOW - 테스트 코드를 어떻게 작성해야 하는가?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1 테스트 케이스 선택 방법&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;: 쉬운 테스트부터 시작해 점차 복잡한 테스트로 나아가면서 시스템의 안정성을 검증합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2 TDD (Test-Driven Development) 방법론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD는 테스트 주도 개발 방식으로, 테스트 코드를 먼저 작성하고 이를 기반으로 프로덕션 코드를 작성하는 방식입니다. TDD는 다음과 같은 세 단계를 따릅니다:&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;/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;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3 다양한 테스트 종류와 계층 구조 이해&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;: 여러 모듈이 함께 작동하는지를 테스트하는 방식입니다. 단위 테스트보다 더 복잡한 시나리오를 검증할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시스템 테스트&lt;/b&gt;: 전체 시스템이 의도한 대로 작동하는지를 검증하는 테스트로, 사용자의 관점에서 테스트를 진행합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.4 JUnit5 활용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit5는 자바 테스트를 위한 표준 프레임워크입니다. 다음과 같은 주요 구성 요소와 기능을 갖추고 있습니다:&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@Test&lt;/code&gt;: 테스트 메서드를 나타냅니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@BeforeAll&lt;/code&gt;, &lt;code&gt;@BeforeEach&lt;/code&gt;: 각각 전체 테스트 전, 개별 테스트 전 실행될 메서드를 정의합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@AfterEach&lt;/code&gt;, &lt;code&gt;@AfterAll&lt;/code&gt;: 각각 개별 테스트 후, 전체 테스트 후 실행될 메서드를 정의합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 어서션 메서드&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;assertEquals(expected, actual)&lt;/code&gt;: 기대값과 실제값이 일치하는지 확인합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assertNull(object)&lt;/code&gt;: 객체가 null인지 검증합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;assertThrows()&lt;/code&gt;: 예외가 발생하는지 검증합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 코드: &lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot;&gt;JUnit 기본 테스트 예시 - UserServiceImplTest&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726995091172&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot; data-og-description=&quot;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot; data-og-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnOQ8P/hyW6BuD2k7/UdddSWM5G3W2h0HDTWRviK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnOQ8P/hyW6BuD2k7/UdddSWM5G3W2h0HDTWRviK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaUserServiceImplTest.java&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.5 Mockito와 같은 Mocking 프레임워크 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mocking은 외부 의존성을 모방하여 테스트하는 방법입니다. &lt;code&gt;Mockito&lt;/code&gt;와 같은 프레임워크를 사용하여 쉽게 Mock 객체를 생성하고 테스트할 수 있습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Dummy&lt;/b&gt;: 사용되지 않는 매개변수에 전달되는 객체입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stub&lt;/b&gt;: 미리 정의된 결과를 반환하는 객체입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Mock&lt;/b&gt;: 동작을 검증할 수 있는 객체로, 메서드 호출 여부 등을 검증합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spy&lt;/b&gt;: 실제 객체의 동작을 일부 모니터링하거나 수정하는 객체입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fake&lt;/b&gt;: 실제 동작을 구현하지만, 단순하게 동작하는 테스트 객체입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 코드: &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot;&gt;Mockito를 활용한 UserControllerTest에서 Mock 객체 활용 예시&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726995117218&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java at ma&quot; data-og-description=&quot;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; data-og-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vuOaY/hyW6HuQ6OC/PTO1sfcHKK0TuLACQSwMN0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vuOaY/hyW6HuQ6OC/PTO1sfcHKK0TuLACQSwMN0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java at ma&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;1098&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EuyQ6/btsJGzrM7CC/khRZrWU7k5b72937KQxtU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EuyQ6/btsJGzrM7CC/khRZrWU7k5b72937KQxtU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EuyQ6/btsJGzrM7CC/khRZrWU7k5b72937KQxtU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEuyQ6%2FbtsJGzrM7CC%2FkhRZrWU7k5b72937KQxtU1%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;1084&quot; height=&quot;1098&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;1098&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;sub&gt;이미지 출처: &lt;a href=&quot;https://www.linkedin.com/posts/danielmoka_mocks-are-one-of-the-most-misunderstood-and-activity-7241327748164571136-hGZn/?utm_source=share&amp;amp;utm_medium=member_ios&quot;&gt;linkedin : Daniel Moka&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.6 다양한 테스트 어노테이션 및 도구 활용&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;@ParameterizedTest&lt;/b&gt;: 여러 파라미터를 전달하여 같은 테스트를 반복 실행합니다. &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java#L103&quot;&gt;참조코드&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;@ValueSource&lt;/code&gt;: 다양한 값을 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@EnumSource&lt;/code&gt;: Enum 타입의 파라미터를 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@MethodSource&lt;/code&gt;: 메서드를 통해 테스트 데이터를 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Nested&lt;/b&gt;: 테스트를 그룹화하여 계층적으로 관리할 수 있습니다. &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java#L81&quot;&gt;참조코드&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Nested&lt;/b&gt;를 사용하면 테스트를 논리적인 그룹으로 분리할 수 있으며, 이를 통해 &lt;b&gt;특정 도메인, 기능, 시나리오&lt;/b&gt;에 대한 테스트를 더욱 체계적으로 관리할 수 있습니다. 특히, 여러 테스트 메서드를 계층적으로 정리하여 가독성을 높이고, 테스트 목적이 더 명확해지도록 도와줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@DisplayName&lt;/b&gt;: 테스트의 설명을 추가하여 가독성을 높일 수 있습니다. &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java#L82&quot;&gt;참조코드&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DisplayName&lt;/b&gt;을 통해 각 테스트 메서드에 대해 직관적인 설명을 부여할 수 있으며, 이는 테스트 결과 리포트에서도 그대로 반영되어 가독성을 크게 향상합니다. &lt;b&gt;테스트 트리&lt;/b&gt;를 시각화할 때 각 테스트의 목적과 역할을 쉽게 이해할 수 있도록 도와줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@Timeout&lt;/b&gt;: 테스트 실행 시간에 제한을 두어, 성능을 테스트할 수 있습니다. &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java#L236&quot;&gt;참조코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@RepeatedTest&lt;/b&gt;: 동일한 테스트를 여러 번 반복 실행합니다. &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java#L357&quot;&gt;참조코드&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RepeatedTest&lt;/b&gt;는 동일한 테스트를 여러 번 반복 실행하여, 특정 코드가 여러 실행 환경에서도 일관되게 동작하는지 확인하는 데 유용합니다. &lt;b&gt;성능 테스트&lt;/b&gt;나 &lt;b&gt;동시성 이슈&lt;/b&gt;를 확인할 때 자주 사용됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;@SpringBootTest&lt;/b&gt;: 전체 스프링 컨텍스트를 로드하여 통합 테스트를 수행합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Testcontainers&lt;/b&gt;: 컨테이너 환경을 활용하여 통합 테스트를 진행할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가독성 향상된 테스트 트리 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Nested&lt;/code&gt;와 &lt;code&gt;@DisplayName&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-origin-width=&quot;652&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6mTnw/btsJIhJ1KpQ/K7Fk21pPbx5Pn6UcsymY2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6mTnw/btsJIhJ1KpQ/K7Fk21pPbx5Pn6UcsymY2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6mTnw/btsJIhJ1KpQ/K7Fk21pPbx5Pn6UcsymY2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6mTnw%2FbtsJIhJ1KpQ%2FK7Fk21pPbx5Pn6UcsymY2K%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;652&quot; height=&quot;401&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;401&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://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java&quot;&gt;SpringBootTest와 다양한 어노테이션 활용 예시 - OrderControllerTest&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726995136405&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.jav&quot; data-og-description=&quot;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java&quot; data-og-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ymuQS/hyW6Mwdfwx/fpkT9sbr9tkVff5Pqp9RhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ymuQS/hyW6Mwdfwx/fpkT9sbr9tkVff5Pqp9RhK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/service/impl/JpaOrderServiceImplTest.jav&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.7 MockMvc와 WebTestClient를 사용한 웹 레이어 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 레이어 테스트를 위해 &lt;code&gt;MockMvc&lt;/code&gt;와 &lt;code&gt;WebTestClient&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MockMvc&lt;/b&gt;: Spring MVC를 모킹 하여 웹 애플리케이션의 HTTP 요청 및 응답을 테스트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WebTestClient&lt;/b&gt;: WebFlux를 지원하는 비동기식 테스트 클라이언트로, 웹 애플리케이션의 반응형 동작을 검증할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 코드: &lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot;&gt;MockMvc를 사용한 Web Layer 테스트 예시 - UserControllerTest&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1726995136426&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java at ma&quot; data-og-description=&quot;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; data-og-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vuOaY/hyW6HuQ6OC/PTO1sfcHKK0TuLACQSwMN0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/blob/main/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vuOaY/hyW6HuQ6OC/PTO1sfcHKK0TuLACQSwMN0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;springboot-testing-from-zero-to-hero/src/test/java/io/github/junhkang/springboottesting/controller/UserControllerTest.java at ma&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Contribute to junhkang/springboot-testing-from-zero-to-hero development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>test</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/97</guid>
      <comments>https://junhkang.tistory.com/97#entry97comment</comments>
      <pubDate>Sun, 22 Sep 2024 17:55:00 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 테스트 1 - 왜 테스트 코드를 작성해야 하는가?</title>
      <link>https://junhkang.tistory.com/96</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t4fjZ/btsJHqt9JuX/UGdPqNXswF3KM3mSzyBErk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t4fjZ/btsJHqt9JuX/UGdPqNXswF3KM3mSzyBErk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t4fjZ/btsJHqt9JuX/UGdPqNXswF3KM3mSzyBErk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft4fjZ%2FbtsJHqt9JuX%2FUGdPqNXswF3KM3mSzyBErk%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;400&quot; height=&quot;279&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;279&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;1. WHY - 왜 테스트를 작성해야 하는가?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.1 테스트 코드의 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 소프트웨어 개발에서 매우 중요한 역할을 합니다. 기능을 수정하거나 새로운 기능을 추가할 때 코드가 안정적으로 작동하는지 확인할 수 있는 수단이 바로 테스트 코드입니다. 이를 통해 예상하지 못한 버그를 방지하고, 코드 품질을 높일 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.2 테스트 코드 작성의 장점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.1 안정적인 개발 환경 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 코드의 변경이 다른 기능에 미치는 영향을 최소화하는 데 도움을 줍니다. 개발자는 자신 있게 코드를 수정하거나 리팩터링 할 수 있으며, 기존 기능이 예상대로 작동하는지 검증할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.2 버그 감소 및 코드 품질 향상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 통해 코드 내 버그를 사전에 발견하고 해결할 수 있습니다. 이를 통해 운영 환경에서 발생할 수 있는 문제를 줄이고, 최종 사용자에게 더 나은 품질의 소프트웨어를 제공할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.3 리팩토링의 용이성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드가 있는 경우, 코드의 리팩토링을 안전하게 수행할 수 있습니다. 테스트는 코드 변경 후에도 기능이 정상적으로 작동하는지 확인해주므로, 리팩토링 과정에서 발생할 수 있는 예기치 않은 문제를 방지할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2.4 단일 책임 원칙(SOLID) 준수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성하다 보면 자연스럽게 단일 책임 원칙(Single Responsibility Principle)을 준수하게 됩니다. 이는 각 클래스와 메서드가 하나의 책임만을 가지도록 하며, 유지보수가 용이한 코드를 작성하는 데 도움을 줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.3 테스트를 작성하지 않았을 때의 문제점&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;디버깅 시간 증가&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;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;h2 data-ke-size=&quot;size26&quot;&gt;1.4 좋은 테스트 코드 - FIRST 원칙&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 테스트 코드는 다음의 &lt;b&gt;FIRST&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;F - Fast (빠르게 실행되어야 함)&lt;/b&gt;: 테스트는 빠르게 실행되어야 하며, 개발 중 자주 실행해도 부담이 없어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;I - Isolated (독립적으로 실행될 수 있어야 함)&lt;/b&gt;: 각 테스트는 서로 의존하지 않고 독립적으로 실행될 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;R - Repeatable (반복 실행 가능해야 함)&lt;/b&gt;: 테스트는 언제 실행하더라도 동일한 결과를 보장해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;S - Self-Validating (스스로 결과를 검증할 수 있어야 함)&lt;/b&gt;: 테스트는 기대값과 실제값을 스스로 비교하여 성공 또는 실패 여부를 판단할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;T - Timely (적시에 작성되어야 함)&lt;/b&gt;: 테스트는 프로덕션 코드 작성 직전에 작성되어야 하며, TDD(Test-Driven Development) 방식과 잘 맞아떨어집니다.&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;개발 생산성을 높이고, 유지보수 비용을 줄이며, 리팩토링에 대한 자신감을 부여&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/junhkang/springboot-testing-from-zero-to-hero/tree/main&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/junhkang/springboot-testing-from-zero-to-hero/tree/main&lt;/a&gt;&lt;/p&gt;</description>
      <category>Spring</category>
      <category>SpringBoot</category>
      <category>test</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/96</guid>
      <comments>https://junhkang.tistory.com/96#entry96comment</comments>
      <pubDate>Sun, 22 Sep 2024 17:50:22 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Caused by: java.lang.IllegalArgumentException: 이름이 {fragment}인, 둘 이상의 fragment들이 발견되었습니다.</title>
      <link>https://junhkang.tistory.com/95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dUiXJF/btsI7GroQks/TEQ85BSh5YHrMjCTUbkrO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dUiXJF/btsI7GroQks/TEQ85BSh5YHrMjCTUbkrO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dUiXJF/btsI7GroQks/TEQ85BSh5YHrMjCTUbkrO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdUiXJF%2FbtsI7GroQks%2FTEQ85BSh5YHrMjCTUbkrO0%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;400&quot; height=&quot;279&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;279&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;Caused by: java.lang.IllegalArgumentException: 이름이 {fragment}인, 둘 이상의 fragment들이 발견되었습니다. 이는 상대적 순서배열에서 불허됩니다. 상세 정보는 서블릿 스펙 8.2.2 2c 장을 참조하십시오. 절대적 순서배열을 사용하는 것을 고려해 보십시오.&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;Spring MVC가 포함되어 있는 상태에서 중복된 디펜던시를 추가하면서 발생한 에러이다. 라이브러리 버전업, 혹은 신규 라이브러리 추가 시 주로 발생하는 현상으로, 메이븐 클린을 통해 메이븐 리포지토리를 정리하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메이븐 클린(프로젝트 우클릭 &amp;gt; maven &amp;gt; maven clean)&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring framework의 경우 web.xml에 &amp;lt;display-name&amp;gt;에 &amp;lt;absolute-ordering /&amp;gt; 추가하여 절대순서로 정렬&lt;/li&gt;
&lt;li&gt;SpringBoot의 경우 중복된 메이븐 디펜던시 정리&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>SpringBoot</category>
      <author>junhkang</author>
      <guid isPermaLink="true">https://junhkang.tistory.com/95</guid>
      <comments>https://junhkang.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 20 Aug 2024 10:41:57 +0900</pubDate>
    </item>
  </channel>
</rss>